class DT(DateTimeDataType):
    """
    Class for DT base datatype. It extends DatetimeDatatype and it represents a time value with
    year, month and day. Parameters are the same of the superclass.

    The :attr:`allowed_formats` tuple is ``('%Y', '%Y%m', '%Y%m%d')``
    """

    allowed_formats = ('%Y', '%Y%m', '%Y%m%d')

    def __init__(self, value=None, out_format='%Y%m%d'):
        super(DT, self).__init__(value, out_format) class TM(DateTimeDataType):
    """
    Class for TM base datatype. It extends DateTimeDatatype and it represents a time value with
    hours, minutes, seconds and microseconds. Parameters are the same of the superclass plus ``offset``.
    Since HL7 supports only four digits for microseconds, and Python datetime uses 6 digits, the wanted
    precision must be specified.

    The :attr:`allowed_formats` tuple is ``('%H', '%H%M', '%H%M%S', '%H%M%S.%f')``.
    It needs also the ``offset`` parameter which represents the UTC offset

    :type offset: ``str``
    :param offset: the UTC offset. By default it is ''. It must be in the form ``'+/-HHMM'``

    :type microsec_precision: ``int``
    :param microsec_precision: Number of digit of the microseconds part of the value.
        It must be between 1 and 4
    """

    allowed_formats = ('%H', '%H%M', '%H%M%S', '%H%M%S.%f')

    def __init__(self, value=None, out_format='%H%M%S.%f', offset='', microsec_precision=4):
        super(TM, self).__init__(value, out_format)

        if not (1 <= microsec_precision <= 4):
            raise InvalidMicrosecondsPrecision()

        self.microsec_precision = microsec_precision

        if offset and len(offset) != 5:
            raise InvalidDateOffset(offset)
        try:
            d = datetime.strptime(offset[1:], '%H%M')
            if offset[0] == '+' and d.hour > 14 or offset[0] == '-' and d.hour > 12:
                raise ValueError
        except ValueError:
            if offset:
                raise InvalidDateOffset(offset)

        if offset and offset[0] not in ('+', '-'):
            raise InvalidDateOffset(offset)

        self.offset = offset

    def to_er7(self, encoding_chars=None):
        date_value = super(TM, self).to_er7()
        if self.format.find('%f') != -1:
            index = 6 - self.microsec_precision
            date_value = date_value[:-index]
        return '{0}{1}'.format(date_value, self.offset) class DTM(TM):
    """
    Class for DTM base datatype. It extends TM and it represents classes DT and DTM combined.
    Thus it represents year, month, day, hours, minutes, seconds and microseconds.
    Parameters are the same of the superclass.

    The :attr:`allowed_formats` tuple is
    ``('%Y', '%Y%m', '%Y%m%d', '%Y%m%d%H', '%Y%m%d%H%M', '%Y%m%d%H%M%S', '%Y%m%d%H%M%S.%f')``
    """

    allowed_formats = ('%Y', '%Y%m', '%Y%m%d', '%Y%m%d%H', '%Y%m%d%H%M',
                       '%Y%m%d%H%M%S', '%Y%m%d%H%M%S.%f')

    def __init__(self, value=None, out_format='%Y%m%d%H%M%S.%f', offset='', microsec_precision=4):
        super(DTM, self).__init__(value, out_format, offset, microsec_precision) class ST(TextualDataType):
    """
    Class for ST datatype. It extends :class:`hl7apy.base_datatypes.TextualDatatype` and the parameters are
    the same of the superclass

    :attr:`max_length` is 199
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(ST, self).__init__(value, 199, highlights, validation_level) class FT(TextualDataType):
    """
    Class for FT datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 65536
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(FT, self).__init__(value, 65536, highlights, validation_level) class ID(TextualDataType):
    """
    Class for ID datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` None
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        # max_length is None bacause it depends from the HL7 table
        super(ID, self).__init__(value, None, highlights, validation_level)
        # TODO: check for tables of allowed values: are we strict or not? class IS(TextualDataType):
    """
    Class for IS datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 20
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(IS, self).__init__(value, 20, highlights, validation_level)
        # TODO: check for tables of allowed values (also defined on site): are we strict or not? class TX(TextualDataType):
    """
    Class for TX datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 65536
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(TX, self).__init__(value, 65536, highlights, validation_level) class GTS(TextualDataType):
    """
    Class for GTS datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 199
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(GTS, self).__init__(value, 199, highlights, validation_level) class NM(NumericDataType):
    """
    Class for NM datatype. It extends :class:`hl7apy.base_datatypes.NumericDatatype` and the parameters are
    the same of the superclass

    :attr:`max_length` is 16.

    The type of ``value`` must be :class:`decimal.Decimal` or :class:`Real <numbers.Real>`

    :raise: :exc:`ValueError` raised when the value is not of one of the correct type
    """

    def __init__(self, value=None,
                 validation_level=None):
        if value is not None and isinstance(value, numbers.Real):
            value = Decimal(value)
        elif value is not None and not isinstance(value, Decimal):
            raise ValueError('Invalid value for a NM data')
        super(NM, self).__init__(value, 16, validation_level) class SI(NumericDataType):
    """
    Class for NM datatype. It extends NumericDatatype and the parameters are the same of the superclass

    :attr:`max_length` is 4.

    The type of ``value`` must be `int` or :class:`numbers.Integral`

    :raise: :exc:`ValueError` raised when the value is not of one of the correct type
    """
    def __init__(self, value=None,
                 validation_level=None):
        if value is not None and not isinstance(value, numbers.Integral):
            raise ValueError('Invalid value for a SI data')

        super(SI, self).__init__(value, 4, validation_level) class TN(TextualDataType):
    """
    Class for TN datatype. It extends TextualDatatype and the parameters are the same of the superclass

    :attr:`max_length` is 199.

    The type of ``value`` must be `str` and should match the format
    [NN] [(999)]999-9999[X99999][B99999][C any text]

    :raise: :exc:`ValueError` raised when the value does not match the expected format
    """
    def __init__(self, value, validation_level=None):

        regexp = r'(\d\d\s)?(\(\d+\))?(\d+-?\d+)(X\d+)?(B\d+)?(C.+)?'
        if not re.match(regexp, value):
            raise ValueError('Invalid value for TN data')

        super(TN, self).__init__(value, 199, None, validation_level) class ST(TextualDataType):
    """
    Class for ST datatype. It extends :class:`hl7apy.base_datatypes.TextualDatatype` and the parameters are
    the same of the superclass

    :attr:`max_length` is 199
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(ST, self).__init__(value, 199, highlights, validation_level) class FT(TextualDataType):
    """
    Class for FT datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 65536
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(FT, self).__init__(value, 65536, highlights, validation_level) class ID(TextualDataType):
    """
    Class for ID datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` None
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        # max_length is None bacause it depends from the HL7 table
        super(ID, self).__init__(value, None, highlights, validation_level)
        # TODO: check for tables of allowed values: are we strict or not? class IS(TextualDataType):
    """
    Class for IS datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 20
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(IS, self).__init__(value, 20, highlights, validation_level)
        # TODO: check for tables of allowed values (also defined on site): are we strict or not? class TX(TextualDataType):
    """
    Class for TX datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 65536
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(TX, self).__init__(value, 65536, highlights, validation_level) class GTS(TextualDataType):
    """
    Class for GTS datatype. It extends :class:`hl7apy.base_datatypes.TextualDataType` and the parameters are
    the same of the superclass

    :attr:`max_length` is 199
    """
    def __init__(self, value, highlights=None,
                 validation_level=None):
        super(GTS, self).__init__(value, 199, highlights, validation_level) class SNM(TextualDataType):
    """
    :attr:`max_length` is 199
    """
    def __init__(self, value, highlights=None, validation_level=None):
        super(SNM, self).__init__(value, None, highlights, validation_level) def datatype_factory(datatype, value, version=None, validation_level=None):
    """
    Factory function for both base and complex datatypes. It generates the correct object according
    to the datatype in input.
    It should be noted that if you use the factory it is not possible to specify
    some parameters for the datatype (e.g. the format for datetime base datatypes)
    If the value is not valid for the datatype specified if the ``validation_level`` is
    :attr:`hl7apy.consts.VALIDATION_LEVEL.TOLERANT` it generates an :class:`hl7apy.base_datatypes.ST` object

    :type datatype: ``str``
    :param datatype: The datatype to be generated

    :param value: The value of the datatype

    :type version: ``str``
    :param version: A valid HL7 version. It must be one of
        :attr:`SUPPRTED_LIBRARIES <hl7apy.SUPPORTED_LIBRARIES>`

    :type validation_level: ``int``
    :param validation_level: It must be a value from class :attr:`validation_level`
        :class:`VALIDATION_LEVEL hl7apy.consts.VALIDATION_LEVEL` or ``None`` to use the default value

    :rtype: The type specified in datatype

    :raises :exc:`ValueError`: If the ``validation_level`` is
        :attr:`VALIDATION_LEVEL.STRICT <hl7apy.consts.VALIDATION_LEVEL.STRICT>`
     and the value is not valid for the specified datatype

    :raises :exc:`InvalidDatatype <hl7apy.exceptions.InvalidDatatype>`: If the ``datatype`` specified is not
        valid for the given ``version``

    """

    from hl7apy.validation import Validator

    if validation_level is None:
        validation_level = get_default_validation_level()

    if version is None:
        version = get_default_version()

    lib = load_library(version)

    base_datatypes = lib.get_base_datatypes()

    factories = base_datatypes.copy()

    if 'DT' in factories:
        factories['DT'] = date_factory
    if 'TM' in factories:
        factories['TM'] = timestamp_factory
    if 'DTM' in factories:
        factories['DTM'] = datetime_factory
    if 'NM' in factories:
        factories['NM'] = numeric_factory
    if 'SI' in factories:
        factories['SI'] = sequence_id_factory

    try:
        factory = factories[datatype]
        if isinstance(factory, FunctionType):
            return factory(value, base_datatypes[datatype], validation_level=validation_level)
        return factory(value, validation_level=validation_level)
    except KeyError:
        raise InvalidDataType(datatype)
    except ValueError as e:
        if Validator.is_strict(validation_level):
            raise e
        # TODO: Do we really want this? In that case the parent's datatype must be changed accordingly
        return factories['ST'](value) class InvalidDateFormat(HL7apyException):
    """
    Raised when the output format for a :class:`hl7apy.base_datatypes.DateTimeDataType` is not valid

    >>> from hl7apy.v2_5 import DTM
    >>> DTM(value='10102013', out_format="%d%m%Y")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidDateFormat: Invalid date format: %d%m%Y
    """
    def __init__(self, out_format):
        self.format = out_format

    def __str__(self):
        return 'Invalid date format: {0}'.format(self.format) class InvalidDateOffset(HL7apyException):
    """
    Raised when the offset for a :class:`TM` or :class:`hl7apy.base_datatypes.DTM` is not valid

    >>> from hl7apy.v2_5 import DTM
    >>> DTM(value='20131010', out_format="%Y%m%d", offset='+1500')  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidDateOffset: Invalid date offset: +1500
    """
    def __init__(self, offset):
        self.offset = offset

    def __str__(self):
        return 'Invalid date offset: {0}'.format(self.offset) class MaxLengthReached(HL7apyException):
    """
    Value length exceeds its datatype :attr:`max_length`.

    >>> from hl7apy.v2_5 import get_base_datatypes
    >>> from hl7apy.consts import VALIDATION_LEVEL
    >>> SI = get_base_datatypes()['SI']
    >>> st = SI(value=11111, validation_level=VALIDATION_LEVEL.STRICT)  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    MaxLengthReached: The value 11111 exceed the max length: 4
    """
    def __init__(self, value, limit):
        self.value = value
        self.limit = limit

    def __str__(self):
        return 'The value {0} exceed the max length: {1}'.format(self.value, self.limit) class InvalidHighlightRange(HL7apyException):
    """
    Raised when the specified highlight range is not valid

    For a description of highlight range see :class:`hl7apy.base_datatypes.TextualDataType`

    >>> from hl7apy.v2_5 import ST
    >>> s = ST(value='some useful information', highlights=((5, 3),))
    >>> s.to_er7()  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidHighlightRange: Invalid highlight range: 5 - 3
    """
    def __init__(self, lower_bound, upper_bound):
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound

    def __str__(self):
        return 'Invalid highlight range: {0} - {1}'.format(self.lower_bound, self.upper_bound) class InvalidDataType(HL7apyException):
    """
    Raised when the currently used HL7 version does not support the given datatype

    >>> from hl7apy.factories import datatype_factory
    >>> datatype_factory('TN', '11 123456', version="2.4") #doctest: +ELLIPSIS
    <hl7apy.base_datatypes.TN object at 0x...>
    >>> datatype_factory('GTS', '11 123456', version="2.4")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidDataType: The datatype GTS is not available for the given HL7 version
    """
    def __init__(self, datatype):
        self.datatype = datatype

    def __str__(self):
        return "The datatype {0} is not available for the given HL7 version".format(self.datatype) class InvalidMicrosecondsPrecision(HL7apyException):
    """
    Raised when the microseconds precision of a TM or DTM oject is not between 1 and 4

    >>> from hl7apy.v2_5 import get_base_datatypes
    >>> DTM = get_base_datatypes()['DTM']
    >>> DTM(value='20131010', microsec_precision=5)  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidMicrosecondsPrecision: Invalid microseconds precision. It must be between 1 and 4
    """

    def __str__(self):
        return 'Invalid microseconds precision. It must be between 1 and 4' class Validator(object):
    def __init__(self, level):
    def validate(element, reference=None, report_file=None):
        def _check_z_element(el, errs, warns):
        def _check_repetitions(el, children, cardinality, child_name, errs):
        def _check_table_compliance(el, ref, warns):
        def _check_length(el, ref, warns):
        def _check_datatype(el, ref, errs):
        def _get_valid_children_info(ref):
        def _get_child_reference_info(ref):
        def _check_known_element(el, ref, errs, warns):
        def _is_valid(el, ref, errs, warns):
    def is_strict(level):
    def is_tolerant(level):
    def is_quiet(level):
import unittest
from datetime import datetime
from hl7apy.base_datatypes import DT, TM, DTM, ST, FT, ID, IS, TX, GTS, NM, SI, TN
from hl7apy.v2_7 import ST as ST27, FT as FT27, ID as ID27, IS as IS27, TX as TX27, GTS as GTS27, SNM
from hl7apy.factories import datatype_factory
from hl7apy.exceptions import InvalidDateFormat, InvalidDateOffset, MaxLengthReached, \
    InvalidHighlightRange, InvalidDataType, InvalidMicrosecondsPrecision
from hl7apy.validation import VALIDATION_LEVEL
from decimal import Decimal
    def test_datatype_not_allowed_value_creation_strict(self):
        self.assertRaises(ValueError, datatype_factory, 'TM', '999999', validation_level=VALIDATION_LEVEL.STRICT)

    def test_datatype_not_allowed_value_creation(self):
        datatype_factory('TM', '9999')

    def test_create_timestamp_with_offset_by_factory(self):
        tm = datatype_factory('TM', '120000+0000')
        self.assertEqual(tm.classname, 'TM')
        self.assertEqual(tm.offset, '+0000')
        self.assertEqual(tm.to_er7(), '120000+0000')

    def test_create_year_month_date_by_factory(self):
        dt = datatype_factory('DT', '201201')
        self.assertEqual(dt.classname, 'DT')
        self.assertEqual(dt.to_er7(), '201201')

    def test_create_year_month_day_date_by_factory(self):
        dt = datatype_factory('DT', '20120101')
        self.assertEqual(dt.classname, 'DT')
        self.assertEqual(dt.to_er7(), '20120101')

    def test_create_hour_time_by_factory(self):
        tm = datatype_factory('TM', '14')
        self.assertEqual(tm.classname, 'TM')
        self.assertEqual(tm.to_er7(), '14')

    def test_create_datetime_by_factory(self):
        dtm = datatype_factory('DTM', '20130726120252.0590+0200')
        self.assertEqual(dtm.classname, 'DTM')
        self.assertEqual(dtm.to_er7(), '20130726120252.0590+0200')

        dtm = datatype_factory('DTM', '20130726120252.059+0200')
        self.assertEqual(dtm.classname, 'DTM')
        self.assertEqual(dtm.to_er7(), '20130726120252.059+0200')

        dtm = datatype_factory('DTM', '20130726120252.05+0200')
        self.assertEqual(dtm.classname, 'DTM')
        self.assertEqual(dtm.to_er7(), '20130726120252.05+0200')

        dtm = datatype_factory('DTM', '20130726120252.5+0200')
        self.assertEqual(dtm.classname, 'DTM')
        self.assertEqual(dtm.to_er7(), '20130726120252.5+0200')

        dtm = datatype_factory('DTM', '20130726120252+0200')
        self.assertEqual(dtm.classname, 'DTM')
        self.assertEqual(dtm.to_er7(), '20130726120252+0200')

    def test_create_timestamp_by_factory(self):
        dtm = datatype_factory('TM', '120252.0590+0200')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252.0590+0200')

        dtm = datatype_factory('TM', '120252.059+0200')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252.059+0200')

        dtm = datatype_factory('TM', '120252.05-0200')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252.05-0200')

        dtm = datatype_factory('TM', '120252.05+1400')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252.05+1400')

        dtm = datatype_factory('TM', '120252.5+0200')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252.5+0200')

        dtm = datatype_factory('TM', '120252-1200')
        self.assertEqual(dtm.classname, 'TM')
        self.assertEqual(dtm.to_er7(), '120252-1200')

    def test_create_nm_by_factory(self):
        nm = datatype_factory('NM', 100000)
        self.assertEqual(nm.classname, 'NM')
        self.assertEqual(nm.to_er7(), '100000')

    def test_create_invalid_nm_by_factory_strict(self):
        self.assertRaises(ValueError, datatype_factory, 'NM', 'aaaaa', validation_level=VALIDATION_LEVEL.STRICT)


class TestDT(unittest.TestCase):

    def test_DT(self):
        date = datetime.strptime('2013', '%Y')
        date1 = datetime.strptime('201307', '%Y%m')
        date2 = datetime.strptime('20130715', '%Y%m%d')
        dt = DT(date, out_format='%Y')
        dt1 = DT(date1, out_format='%Y%m')
        dt2 = DT(date2, out_format='%Y%m%d')
        self.assertEqual(dt.classname, 'DT')
        self.assertEqual(dt.to_er7(), datetime.strftime(date, dt.format))
        self.assertEqual(dt1.to_er7(), datetime.strftime(date1, dt1.format))
        self.assertEqual(dt2.to_er7(), datetime.strftime(date2, dt2.format))

    def test_DT_wrong_format(self):
        self.assertRaises(InvalidDateFormat, DT, datetime.strptime('13', '%y'), out_format='%y')
        self.assertRaises(InvalidDateFormat, DT, datetime.strptime('12', '%m'), out_format='%m')
        self.assertRaises(InvalidDateFormat, DT, datetime.strptime('1202', '%m%d'), out_format='%m%d')
        self.assertRaises(InvalidDateFormat, DT, datetime.strptime('02', '%d'), out_format='%d')

    def test_DT_default_format(self):
        date = datetime.strptime('20130715', '%Y%m%d')
        dt = DT(date)
        self.assertEqual(dt.to_er7(), datetime.strftime(date, dt.format))

    def test_DT_wrong_default_format_strict(self):
        date = datetime.strptime('20', '%H')
        self.assertRaises(InvalidDateFormat, DT, date, out_format='%H', )


class TestTM(unittest.TestCase):

    def test_TM(self):

        time = datetime.strptime('01', '%H')
        time1 = datetime.strptime('0101', '%H%M')
        time2 = datetime.strptime('010111', '%H%M%S')
        time3 = datetime.strptime('010111.1110', '%H%M%S.%f')

        tm = TM(time, out_format='%H')