From 7a7b28bee58fc57def49fa3b6156a26afb36f779 Mon Sep 17 00:00:00 2001 From: Hoss Date: Wed, 27 Apr 2022 18:48:32 +0800 Subject: [PATCH] Start by extracting the code from a private repo --- pyx12lib/__init__.py | 0 pyx12lib/common/__init__.py | 0 pyx12lib/common/interchange/__init__.py | 5 + .../common/interchange/functional_group.py | 54 ++++ .../common/interchange/grammar/__init__.py | 3 + .../interchange/grammar/functional_group.py | 100 +++++++ .../common/interchange/grammar/interchange.py | 164 ++++++++++++ .../interchange/grammar/transaction_set.py | 52 ++++ pyx12lib/common/interchange/interchange.py | 68 +++++ .../common/interchange/transaction_set.py | 30 +++ pyx12lib/core/__init__.py | 0 pyx12lib/core/exceptions.py | 125 +++++++++ pyx12lib/core/grammar/__init__.py | 2 + pyx12lib/core/grammar/element.py | 56 ++++ pyx12lib/core/grammar/segment.py | 18 ++ pyx12lib/core/renderer.py | 246 ++++++++++++++++++ requirements.txt | 2 + 17 files changed, 925 insertions(+) create mode 100644 pyx12lib/__init__.py create mode 100644 pyx12lib/common/__init__.py create mode 100644 pyx12lib/common/interchange/__init__.py create mode 100644 pyx12lib/common/interchange/functional_group.py create mode 100644 pyx12lib/common/interchange/grammar/__init__.py create mode 100644 pyx12lib/common/interchange/grammar/functional_group.py create mode 100644 pyx12lib/common/interchange/grammar/interchange.py create mode 100644 pyx12lib/common/interchange/grammar/transaction_set.py create mode 100644 pyx12lib/common/interchange/interchange.py create mode 100644 pyx12lib/common/interchange/transaction_set.py create mode 100644 pyx12lib/core/__init__.py create mode 100644 pyx12lib/core/exceptions.py create mode 100644 pyx12lib/core/grammar/__init__.py create mode 100644 pyx12lib/core/grammar/element.py create mode 100644 pyx12lib/core/grammar/segment.py create mode 100644 pyx12lib/core/renderer.py create mode 100644 requirements.txt diff --git a/pyx12lib/__init__.py b/pyx12lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyx12lib/common/__init__.py b/pyx12lib/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyx12lib/common/interchange/__init__.py b/pyx12lib/common/interchange/__init__.py new file mode 100644 index 0000000..6685d79 --- /dev/null +++ b/pyx12lib/common/interchange/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import + +from .functional_group import GeRenderer, GsRenderer +from .interchange import IeaRenderer, IsaRenderer +from .transaction_set import SeRenderer, StRenderer diff --git a/pyx12lib/common/interchange/functional_group.py b/pyx12lib/common/interchange/functional_group.py new file mode 100644 index 0000000..88ab5de --- /dev/null +++ b/pyx12lib/common/interchange/functional_group.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from builtins import str + +import dateutil.parser + +from pyx12lib.core.renderer import SegmentRenderer + +from .grammar import GeSegment, GsSegment + + +class GsRenderer(SegmentRenderer): + grammar = GsSegment + + @property + def element_value_getters(self): + return { + 'GS01': lambda ele, data, stat: 'SO', # Shipping Instruction + 'GS02': lambda ele, data, stat: data.sender_id, + 'GS03': lambda ele, data, stat: data.vendor_id, + 'GS04': self.gs04, + 'GS05': self.gs05, + 'GS06': lambda ele, data, stat: '{:d}'.format(data.functional_group_no), + 'GS07': lambda ele, data, stat: 'X', # ANSI X12 + 'GS08': lambda ele, data, stat: '004010', # Version 4010 + } + + @staticmethod + def gs04(ele, data, stat): + if data.submit_datetime: + datetime = dateutil.parser.parse(data.submit_datetime) + return datetime.strftime("%Y%m%d") + + return '' + + @staticmethod + def gs05(ele, data, stat): + if data.submit_datetime: + datetime = dateutil.parser.parse(data.submit_datetime) + return datetime.strftime('%H%M') + + return '' + + +class GeRenderer(SegmentRenderer): + grammar = GeSegment + + @property + def element_value_getters(self): + return { + 'GE01': lambda ele, data, stat: str(data.transaction_count), + 'GE02': lambda ele, data, stat: '{:d}'.format(data.functional_group_no), + } diff --git a/pyx12lib/common/interchange/grammar/__init__.py b/pyx12lib/common/interchange/grammar/__init__.py new file mode 100644 index 0000000..a7f5cad --- /dev/null +++ b/pyx12lib/common/interchange/grammar/__init__.py @@ -0,0 +1,3 @@ +from .functional_group import GeSegment, GsSegment +from .interchange import IeaSegment, IsaSegment +from .transaction_set import SeSegment, StSegment diff --git a/pyx12lib/common/interchange/grammar/functional_group.py b/pyx12lib/common/interchange/grammar/functional_group.py new file mode 100644 index 0000000..cf18363 --- /dev/null +++ b/pyx12lib/common/interchange/grammar/functional_group.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pyx12lib.core.grammar import Element +from pyx12lib.core.grammar.segment import USAGE_MANDATORY, BaseSegment + + +class GsSegment(BaseSegment): + segment_id = 'GS' + usage = 'M' + max_use = 1 + elements = ( + Element( + reference_designator='GS01', + name='Functional Identifier Code', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=2, + maximum=2, + ), + Element( + reference_designator='GS02', + name='Application Sender\'s Code', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=2, + maximum=15, + ), + Element( + reference_designator='GS03', + name='Application Receiver\'s Code', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=2, + maximum=15, + ), + Element( + reference_designator='GS04', + name='Date', + usage=USAGE_MANDATORY, + element_type='DT', + minimum=8, + maximum=8, + ), + Element( + reference_designator='GS05', + name='Time', + usage=USAGE_MANDATORY, + element_type='TM', + minimum=4, + maximum=8, + ), + Element( + reference_designator='GS06', + name='Group Control Number', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=1, + maximum=9, + ), + Element( + reference_designator='GS07', + name='Responsible Agency Code', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=1, + maximum=2, + ), + Element( + reference_designator='GS08', + name='Version / Release / Industry Identifier Code', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=1, + maximum=12, + ), + ) + + +class GeSegment(BaseSegment): + segment_id = 'GE' + max_use = 1 + elements = ( + Element( + reference_designator='GE01', + name='VNumber of Transaction Sets Included', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=1, + maximum=6, + ), + Element( + reference_designator='GE02', + name='Group Control Number', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=1, + maximum=9, + ), + ) diff --git a/pyx12lib/common/interchange/grammar/interchange.py b/pyx12lib/common/interchange/grammar/interchange.py new file mode 100644 index 0000000..44c17b7 --- /dev/null +++ b/pyx12lib/common/interchange/grammar/interchange.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pyx12lib.core.grammar import Element +from pyx12lib.core.grammar.segment import USAGE_MANDATORY, BaseSegment + + +class IsaSegment(BaseSegment): + segment_id = 'ISA' + usage = 'M' + max_use = 1 + elements = ( + Element( + reference_designator='ISA01', + name='Authorization Information Qualifier', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=2, + maximum=2, + ), + Element( + reference_designator='ISA02', + name='Authorization Information', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=10, + maximum=10, + ), + Element( + reference_designator='ISA03', + name='Security Information Qualifier', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=2, + maximum=2, + ), + Element( + reference_designator='ISA04', + name='Security Information', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=10, + maximum=10, + ), + Element( + reference_designator='ISA05', + name='Interchange ID Qualifier', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=2, + maximum=2, + ), + Element( + reference_designator='ISA06', + name='Interchange Sender ID', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=15, + maximum=15, + ), + Element( + reference_designator='ISA07', + name='Interchange ID Qualifier', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=2, + maximum=2, + ), + Element( + reference_designator='ISA08', + name='Interchange Receiver ID', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=15, + maximum=15, + ), + Element( + reference_designator='ISA09', + name='Interchange Date', + usage=USAGE_MANDATORY, + element_type='DT', + minimum=6, + maximum=6, + ), + Element( + reference_designator='ISA10', + name='Interchange Time', + usage=USAGE_MANDATORY, + element_type='TM', + minimum=4, + maximum=4, + ), + Element( + reference_designator='ISA11', + name='Interchange Control Standards Identifier', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=1, + maximum=1, + ), + Element( + reference_designator='ISA12', + name='Interchange Control Version Number', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=5, + maximum=5, + ), + Element( + reference_designator='ISA13', + name='Interchange Control Number', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=9, + maximum=9, + ), + Element( + reference_designator='ISA14', + name='Acknowledgment Requested', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=1, + maximum=1, + ), + Element( + reference_designator='ISA15', + name='Usage Indicator', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=1, + maximum=1, + ), + Element( + reference_designator='ISA16', + name='Component Element Separator', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=1, + maximum=1, + ), + ) + + +class IeaSegment(BaseSegment): + segment_id = 'IEA' + max_use = 1 + elements = ( + Element( + reference_designator='IEA01', + name='Number of Included Functional Groups', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=1, + maximum=5, + ), + Element( + reference_designator='IEA02', + name='Interchange Control Number', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=9, + maximum=9, + ), + ) diff --git a/pyx12lib/common/interchange/grammar/transaction_set.py b/pyx12lib/common/interchange/grammar/transaction_set.py new file mode 100644 index 0000000..cb38481 --- /dev/null +++ b/pyx12lib/common/interchange/grammar/transaction_set.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pyx12lib.core.grammar import Element +from pyx12lib.core.grammar.segment import USAGE_MANDATORY, BaseSegment + + +class StSegment(BaseSegment): + segment_id = 'ST' + usage = 'M' + max_use = 1 + elements = ( + Element( + reference_designator='ST01', + name='Transaction Set Identifier Code', + usage=USAGE_MANDATORY, + element_type='ID', + minimum=3, + maximum=3, + ), + Element( + reference_designator='ST02', + name='Transaction Set Control Number', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=4, + maximum=9, + ), + ) + + +class SeSegment(BaseSegment): + segment_id = 'SE' + max_use = 1 + elements = ( + Element( + reference_designator='SE01', + name='Number of Included Segments', + usage=USAGE_MANDATORY, + element_type='N0', + minimum=1, + maximum=10, + ), + Element( + reference_designator='SE02', + name='Transaction Set Control Number', + usage=USAGE_MANDATORY, + element_type='AN', + minimum=4, + maximum=9, + ), + ) diff --git a/pyx12lib/common/interchange/interchange.py b/pyx12lib/common/interchange/interchange.py new file mode 100644 index 0000000..6b4c0bf --- /dev/null +++ b/pyx12lib/common/interchange/interchange.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from builtins import str + +import dateutil.parser + +from pyx12lib.core.grammar.element import COMPONENT_DELIMITER +from pyx12lib.core.renderer import SegmentRenderer + +from .grammar import IeaSegment, IsaSegment + + +class IsaRenderer(SegmentRenderer): + grammar = IsaSegment + + @property + def element_value_getters(self): + return { + 'ISA01': lambda ele, data, stat: '00', # No Authorization Info Present + 'ISA02': lambda ele, data, stat: '{: <10}'.format(' '), + 'ISA03': lambda ele, data, stat: '00', # No Security Info Present + 'ISA04': lambda ele, data, stat: '{: <10}'.format(' '), + 'ISA05': lambda ele, data, stat: 'ZZ', # Sender ID Mutually Defined + 'ISA06': lambda ele, data, stat: '{: <{width}}'.format(data.sender_id, width=ele.minimum), + 'ISA07': lambda ele, data, stat: 'ZZ', # Receiver ID Mutually Defined + 'ISA08': lambda ele, data, stat: '{: <{width}}'.format(data.vendor_id, width=ele.minimum), + 'ISA09': self.isa09, + 'ISA10': self.isa10, + 'ISA11': lambda ele, data, stat: 'U', + 'ISA12': lambda ele, data, stat: '00401', + 'ISA13': self.isa13, + 'ISA14': lambda ele, data, stat: '1', # to request an interchange ack + 'ISA15': lambda ele, data, stat: 'P', # Production Data + 'ISA16': lambda ele, data, stat: COMPONENT_DELIMITER, + } + + @staticmethod + def isa09(ele, data, stat): + if data.submit_datetime: + datetime = dateutil.parser.parse(data.submit_datetime) + return datetime.strftime("%y%m%d") + + return '' + + @staticmethod + def isa10(ele, data, stat): + if data.submit_datetime: + datetime = dateutil.parser.parse(data.submit_datetime) + return datetime.strftime("%H%M") + + return '' + + def isa13(self, ele, data, stat): + div, mod = divmod(data.interchange_no, 999999999) # ISA Control Number is a 9 digits integer + control_no = mod if not div else mod + 1 + return '{:09d}'.format(control_no) + + +class IeaRenderer(SegmentRenderer): + grammar = IeaSegment + + @property + def element_value_getters(self): + return { + 'IEA01': lambda ele, data, stat: str(data.functional_group_count), + 'IEA02': lambda ele, data, stat: '{:09d}'.format(data.interchange_no), + } diff --git a/pyx12lib/common/interchange/transaction_set.py b/pyx12lib/common/interchange/transaction_set.py new file mode 100644 index 0000000..fcbaaed --- /dev/null +++ b/pyx12lib/common/interchange/transaction_set.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from builtins import str + +from pyx12lib.core.renderer import SegmentRenderer + +from .grammar import SeSegment, StSegment + + +class StRenderer(SegmentRenderer): + grammar = StSegment + + @property + def element_value_getters(self): + return { + 'ST01': lambda ele, data, stat: '304', # Shipping Instruction + 'ST02': lambda ele, data, stat: '{:04d}'.format(data.transaction_set_no), + } + + +class SeRenderer(SegmentRenderer): + grammar = SeSegment + + @property + def element_value_getters(self): + return { + 'SE01': lambda ele, data, stat: str(data.segment_counts), + 'SE02': lambda ele, data, stat: '{:04d}'.format(data.transaction_set_no), + } diff --git a/pyx12lib/core/__init__.py b/pyx12lib/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyx12lib/core/exceptions.py b/pyx12lib/core/exceptions.py new file mode 100644 index 0000000..2fca9ad --- /dev/null +++ b/pyx12lib/core/exceptions.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + + +class BaseX12Exception(ValueError): + pass + + +class MandatorySegmentException(BaseX12Exception): + """ + Exception raised when a mandatory segment does not have any values + """ + + def __init__(self, segment): + message = '{segment_id}: mandatory segment has no values'.format( + segment_id=segment.segment_id, + ) + super(MandatorySegmentException, self).__init__(message) + + +class MandatoryCompositeElementException(BaseX12Exception): + """ + Exception raised when a mandatory composite element does not have any values + """ + + def __init__(self, element): + message = '{segment_id}: mandatory composite element has no values'.format( + segment_id=element.reference_designator, + ) + super(MandatoryCompositeElementException, self).__init__(message) + + +class MandatoryElementException(BaseX12Exception): + """ + Exception raised when a mandatory element is not filled with value + """ + + def __init__(self, element, value): + message = '{ref_des} {name}'.format( + ref_des=element.reference_designator, + name=element.name, + ) + super(MandatoryElementException, self).__init__(message) + + +class MandatoryComponentException(MandatoryElementException): + """ + Exception raised when a mandatory component is not filled with value + """ + + +class LengthException(BaseX12Exception): + """ + Exception raised when a element is not match its width limit + """ + + def __init__(self, element, value): + message = '{ref_des} {name}: {value} not match {min}/{max}'.format( + ref_des=element.reference_designator, + name=element.name, + value='~empty str~' if value == '' else value, + min=element.minimum, + max=element.maximum, + ) + super(LengthException, self).__init__(message) + + +class NotStringException(BaseX12Exception): + """ + Exception raised when a element is not in string type + """ + + def __init__(self, element, value): + message = '{ref_des} {name}: {value} => {type}'.format( + ref_des=element.reference_designator, + name=element.name, + value=value, + type=type(value), + ) + super(NotStringException, self).__init__(message) + + +class NotDecimalError(BaseX12Exception): + """ + Exception raised when a element is not in string type + """ + + def __init__(self, element, value): + message = '{ref_des} {name}: expect: {ele_type}, get: {value} => {type}'.format( + ref_des=element.reference_designator, + name=element.name, + ele_type=element.type, + value=value, + type=type(value), + ) + super(NotDecimalError, self).__init__(message) + + +class DecimalPlaceNotMatchError(BaseX12Exception): + """ + Exception raised when a element is not in string type + """ + + def __init__(self, element, value): + message = '{ref_des} {name}: expect: {ele_type}, get: {value} => {type}'.format( + ref_des=element.reference_designator, + name=element.name, + ele_type=element.type, + value=value, + type=type(value), + ) + super(DecimalPlaceNotMatchError, self).__init__(message) + + +class NotUsedElementException(BaseX12Exception): + """ + Exception raised when NotUsedElement has value + """ + + def __init__(self, element, value): + message = '{ref_des}: {value} NotUsedElement should not have values'.format( + ref_des=element.reference_designator, + value=value, + ) + super(NotUsedElementException, self).__init__(message) diff --git a/pyx12lib/core/grammar/__init__.py b/pyx12lib/core/grammar/__init__.py new file mode 100644 index 0000000..9c0f9f2 --- /dev/null +++ b/pyx12lib/core/grammar/__init__.py @@ -0,0 +1,2 @@ +from .element import Component, CompositeElement, Element, NotUsedElement +from .segment import BaseSegment diff --git a/pyx12lib/core/grammar/element.py b/pyx12lib/core/grammar/element.py new file mode 100644 index 0000000..34a498b --- /dev/null +++ b/pyx12lib/core/grammar/element.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from builtins import object + +COMPONENT_DELIMITER = '^' + +USAGE_MANDATORY = 'M' +USAGE_OPTIONAL = 'O' +USAGE_CONDITIONAL = 'C' + +ELEMENT_TYPE_ID = 'ID' +ELEMENT_TYPE_STRING = 'AN' +ELEMENT_TYPE_DATE = 'DT' +ELEMENT_TYPE_TIME = 'TM' +ELEMENT_TYPE_NUMERIC = 'N' +ELEMENT_TYPE_DECIMAL = 'R' + + +def get_numeric_type(max_digits): + return ELEMENT_TYPE_NUMERIC + str(max_digits) + + +class BaseElement(object): + def __init__(self, reference_designator): + self.reference_designator = reference_designator + + +class NotUsedElement(BaseElement): + @staticmethod + def value_getter(ele, data, stat): + return '' + + +class Element(BaseElement): + def __init__(self, reference_designator, name, usage, element_type, minimum, maximum): + super(Element, self).__init__(reference_designator) + + self.name = name + self.usage = usage + self.type = element_type + self.minimum = minimum + self.maximum = maximum + + +class Component(Element): + pass + + +class CompositeElement(Element): + definition = None + + def __init__(self, reference_designator, name, usage, element_type, minimum, maximum, components): + super(CompositeElement, self).__init__(reference_designator, name, usage, element_type, minimum, maximum) + + self.components = components diff --git a/pyx12lib/core/grammar/segment.py b/pyx12lib/core/grammar/segment.py new file mode 100644 index 0000000..dbfb95a --- /dev/null +++ b/pyx12lib/core/grammar/segment.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from builtins import object + +ELEMENT_DELIMITER = '*' +SEGMENT_TERMINATOR = '~' + +USAGE_MANDATORY = 'M' +USAGE_OPTIONAL = 'O' +USAGE_CONDITIONAL = 'X' + + +class BaseSegment(object): + segment_id = None + usage = None + max_use = None + elements = None diff --git a/pyx12lib/core/renderer.py b/pyx12lib/core/renderer.py new file mode 100644 index 0000000..08bdc70 --- /dev/null +++ b/pyx12lib/core/renderer.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import collections +import copy +from builtins import object, zip + +import six + +from pyx12lib.core import exceptions +from pyx12lib.core.grammar.element import ( + COMPONENT_DELIMITER, + ELEMENT_TYPE_DECIMAL, + ELEMENT_TYPE_NUMERIC, + NotUsedElement, +) +from pyx12lib.core.grammar.segment import ( + ELEMENT_DELIMITER, + SEGMENT_TERMINATOR, + USAGE_CONDITIONAL, + USAGE_MANDATORY, + USAGE_OPTIONAL, +) + +WEIGHT_MAX_DIGITS = 3 +MEASURE_MAX_DIGITS = 4 + + +class BaseSegmentRenderer(object): + def __init__( + self, + segment_terminator=SEGMENT_TERMINATOR, + element_delimiter=ELEMENT_DELIMITER, + component_delimiter=COMPONENT_DELIMITER, + ): + self._segment_terminator = segment_terminator + self._element_delimiter = element_delimiter + self._component_delimiter = component_delimiter + + def count(self): + raise NotImplementedError + + def render(self): + raise NotImplementedError + + +class SegmentRenderer(BaseSegmentRenderer): + grammar = None + element_value_getters = None + + _element_values = None + + def __init__(self, data, **kwargs): + super(SegmentRenderer, self).__init__(**kwargs) + + self._data = data + + def get_element_value_getter(self, ref_des): + if self.element_value_getters is None: + raise NotImplementedError('element_value_getters should be defined.') + return self.element_value_getters.get(ref_des, NotUsedElement.value_getter) + + @staticmethod + def _is_element_valid(element, value): + # Skip NotUsedElement + if isinstance(element, NotUsedElement): + if value != '': + raise exceptions.NotUsedElementException(element, value) + return True + + # Check string type + if not isinstance(value, six.string_types): + raise exceptions.NotStringException(element, value) + + # Check mandatory + if element.usage == USAGE_MANDATORY and value == '': + raise exceptions.MandatoryElementException(element, value) + + if value != '': + # Check width + if not (element.minimum <= len(value) <= element.maximum): + raise exceptions.LengthException(element, value) + + # Check type + if element.type == ELEMENT_TYPE_DECIMAL or element.type.startswith(ELEMENT_TYPE_NUMERIC): + try: + float(value) + except ValueError: + raise exceptions.NotDecimalError(element, value) + + if element.type.startswith(ELEMENT_TYPE_NUMERIC): + decimal_places = int(element.type[1]) + try: + if len(value.split('.')[1]) != decimal_places: + raise exceptions.DecimalPlaceNotMatchError(element, value) + except IndexError: + if decimal_places > 0: + raise exceptions.DecimalPlaceNotMatchError(element, value) + + return True + + def is_valid(self): + # Check empty segment + if not any(self._element_values.values()): + if self.grammar.usage == USAGE_MANDATORY: + raise exceptions.MandatorySegmentException(self.grammar) + if self.grammar.usage in (USAGE_OPTIONAL, USAGE_CONDITIONAL): + return True + + # Check each elements + return all( + self._is_element_valid(ele, self._element_values[ele.reference_designator]) for ele in self.grammar.elements + ) + + def count(self): + return 1 if any(self._element_values.values()) else 0 + + def build(self): + self._element_values = collections.OrderedDict() + + for ele in self.grammar.elements: + ref_des = ele.reference_designator + value_getter = self.get_element_value_getter(ref_des) + self._element_values[ref_des] = value_getter(ele, self._data, self._element_values) + + return self._element_values + + def render(self): + ele_values_dict = self._element_values if self._element_values is not None else self.build() + + self.is_valid() + + if not any(ele_values_dict.values()): + return '' + + ele_values_list = list(ele_values_dict.values()) + ele_values_list.insert(0, self.grammar.segment_id) + segment_value = ( + self._element_delimiter.join(ele_values_list).rstrip(self._element_delimiter) + self._segment_terminator + ) + + return segment_value + + +class ComponentSegmentRenderer(SegmentRenderer): + def is_composite_element_valid(self, element, comp_values_tuple): + # Check empty composite element + if not any(val for ele, val in comp_values_tuple): + if element.usage == USAGE_MANDATORY: + raise exceptions.MandatoryCompositeElementException(element) + if element.usage in (USAGE_OPTIONAL, USAGE_CONDITIONAL): + return True + + # Check each components + return all(self.is_component_valid(element, value) for element, value in comp_values_tuple) + + @staticmethod + def is_component_valid(component, value): + # Skip NotUsedElement + if isinstance(component, NotUsedElement): + if value != '': + raise exceptions.NotUsedElementException(component, value) + return True + + # Check data type + if not isinstance(value, six.string_types): + raise exceptions.NotStringException(component, value) + + # Check mandatory + if component.usage == USAGE_MANDATORY and value == '': + raise exceptions.MandatoryComponentException(component, value) + + if value != '': + # Check width + if not (component.minimum <= len(value) <= component.maximum): + raise exceptions.LengthException(component, value) + + return True + + def get_component_values(self, ele, data, stat, value_getters): + local_stat = copy.deepcopy(stat) + + comp_values = {} + for comp in ele.components: + ref_des = comp.reference_designator + value_getter = value_getters.get(ref_des, NotUsedElement.value_getter) + comp_values[ref_des] = value_getter(comp, data, local_stat) + + local_stat.update(comp_values) + + comp_values_list = [comp_values[comp.reference_designator] for comp in ele.components] + self.is_composite_element_valid(ele, list(zip(ele.components, comp_values_list))) + + return self._component_delimiter.join(comp_values_list).rstrip(self._component_delimiter) + + +class SegmentRendererLoop(BaseSegmentRenderer): + loop_id = None + renderer_class_list = None + + _count = 0 + _renderer_list = None + + def __init__(self, data, **kwargs): + super(SegmentRendererLoop, self).__init__(**kwargs) + + self._data_list = self.preprocess_data(data) + + assert isinstance(self._data_list, list) + assert all( + issubclass(renderer, (SegmentRenderer, SegmentRendererLoop)) for renderer in self.renderer_class_list + ) + + def build_data(self, **kwargs): + return collections.namedtuple(self.loop_id + 'LoopData', list(kwargs.keys()))(**kwargs) + + def preprocess_data(self, data): + """ + :param data: data from outer loop + :return: list of processed data + """ + return [data] + + def count(self): + return self._count + + def build(self): + self._renderer_list = [] + for data in self._data_list: + for renderer_class in self.renderer_class_list: + renderer = renderer_class(data) + renderer.build() + self._renderer_list.append(renderer) + + self._count = sum(r.count() for r in self._renderer_list) + + return self._renderer_list + + def render(self): + """ + traverse the loop to get data from each Segments, validation is done by each SegmentRenderer + :return: values of all segments inside the loop + """ + renderer_list = self._renderer_list if self._renderer_list is not None else self.build() + + return ''.join(r.render() for r in renderer_list if r.count()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4d23dd7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-dateutil>=2.6.1 +six>=1.11.0