From fbb8fa36dec4a748db7d08b7f6e15e95b773db6f Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 27 Jan 2021 15:52:38 +0000 Subject: [PATCH 01/14] WIP --- wip.py | 256 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 wip.py diff --git a/wip.py b/wip.py new file mode 100644 index 0000000..01970be --- /dev/null +++ b/wip.py @@ -0,0 +1,256 @@ +from typing import Callable, List, Optional, Tuple, Union + + +from pydifact.segmentcollection import Interchange +from pydifact.segments import Segment, SegmentProvider + + + +interchange = Interchange.from_str("""UNA:+.?*' +UNB+UNOA:4+5021376940009:14+1111111111111:14+200421:1000+0001+ORDERS' +UNH+1+ORDERS:D:01B:UN:EAN010' +BGM+220+123456+9' +DTM+137:20150410:102' +DTM+2:20150710:102' +FTX+DIN+++DELIVERY INSTRUCTIONS DESCRIPTION' +NAD+BY+5021376940009::9' +NAD+SU+1111111111111::9' +RFF+IA:123456' +NAD+ST+++Mr John Smith+The Bungalow:Hastings Road+Preston+:::Lancashire+SW1A 1AA' +CTA+LB+:Mr John Smith' +COM+01772 999999:TE' +COM+johnsmith@gmail.com:EM' +CUX+2:GBP:9' +TDT+20+++++SD' +LIN+1++121354654:BP' +IMD+F++:::TPRG item description' +QTY+21:2' +MOA+203:200.00' +PRI+AAA:100.00' +RFF+LI:1' +LIN+1++121354654:BP' +IMD+F++:::TPRG item description' +QTY+21:2' +MOA+203:200.00' +PRI+AAA:100.00' +RFF+LI:1' +UNS+S' +CNT+2:1' +UNT+32+1' +UNZ+1+0001'""") + +import itertools +import collections + + +class BiDirectionalIterator(object): + def __init__(self, collection): + self.collection = collection + self.index = 0 + + def next(self): + try: + result = self.collection[self.index] + self.index += 1 + except IndexError: + raise StopIteration + return result + + def prev(self): + self.index -= 1 + if self.index < 0: + raise StopIteration + return self.collection[self.index] + + def __iter__(self): + return self + + +class AbstractComponent: + """Abstract EDIFact Component, used as a base for Components, SegmentGroup, SegmentLoop""" + def __init__(self, **kwargs): + self.mandatory = kwargs.get("mandatory", False) + + def _from_segment_iter(self, messages): + raise NotImplementedError() + + +class Component(AbstractComponent, Segment): + """ + EDIFact Component. + A simple wrapper for Segment + """ + def __init__( + self, + tag: str, + *elements, + **kwargs + ): + AbstractComponent.__init__(self, **kwargs) + Segment.__init__(self, tag, *elements) + + def _from_segment_iter(self, iterator): + segment = iterator.next() + + if self.tag == segment.tag: + self.elements = segment.elements + return + + if self.mandatory: + raise Exception("Missing %s, found %s" % (self.tag, segment)) + + iterator.prev() + + +class SegmentGroupMetaClass(type): + """ + Metaclass to maintain an ordered list of components. + Required for compatibility with Python 3.5. In 3.6 the + properties of a class are strictly ordered. + """ + @classmethod + def __prepare__(cls, name, bases): + return collections.OrderedDict() + + def __new__(cls, name, bases, classdict): + result = type.__new__(cls, name, bases, dict(classdict)) + exclude = set(dir(type)) + + result.__components__ = [] + + for k, v in classdict.items(): + if k not in exclude and isinstance(v, AbstractComponent): + result.__components__.append(k) + return result + + +class SegmentGroup(AbstractComponent, metaclass=SegmentGroupMetaClass): + """ + Describes a static group of Components + """ + def _from_segment_iter(self, isegment): + i = 0 + + icomponent = iter(self.__components__) + + try: + while True: + component_name = next(icomponent) + component = getattr(self, component_name) + component._from_segment_iter(isegment) + except StopIteration: + pass + + def from_message(self, message): + imessage = BiDirectionalIterator(message.segments) + self._from_segment_iter(imessage) + + def to_message(self): + raise NotImplementedError() + + def __str__(self): + res = [] + for component_name in iter(self.__components__): + component = getattr(self, component_name) + res.append(str(component)) + return "\n".join(res) + + +class SegmentLoop(AbstractComponent): + """ + Describes a repeating SegmentGroup + """ + def __init__(self, component, **kwargs): + super(SegmentLoop, self).__init__(**kwargs) + self.min = kwargs.get("min", 0) + self.max = kwargs.get("max", 0) + + if self.mandatory and self.min < 1: + self.min = 1 + + if self.max < self.min: + self.max = self.min + + self.__component__ = component + self.value = [] + + def _from_segment_iter(self, isegment): + i = 0 + while i < self.max: + + try: + component = self.__component__() + component._from_segment_iter(isegment) + self.value.append(component) + except BaseException: + isegment.prev() + if self.mandatory and i < self.min: + raise Exception("Missing %s" % + (self.__component__.__name__)) + break + + i += 1 + + if i < self.min: + raise Exception("minimum required not met") + + def __str__(self): + res = [] + for v in self.value: + res.append(str(v)) + return "{} = {}".format( + self.__component__.__name__, + str(res) + ) + + +class OrderLine(SegmentGroup): + line_id = Component("LIN", mandatory=True) + description = Component("IMD", mandatory=True) + quantity = Component("QTY", mandatory=True) + moa = Component("MOA") + pri = Component("PRI") + rff = Component("RFF", mandatory=True) + + + + +class Order(SegmentGroup): + purchase_order_id = Component("BGM", mandatory=True) + date = Component("DTM", mandatory=True) + delivery_date = Component("DTM", mandatory=True) + delivery_instructions = Component("FTX", mandatory=True) + supplier_id_gln = Component("NAD", mandatory=True) + supplier_id_tprg = Component("NAD", mandatory=True) + ref = Component("RFF", mandatory=True) + ship_to = Component("NAD", mandatory=True) + + ship_to_contact = Component("CTA", mandatory=True) + ship_to_phone = Component("COM", mandatory=True) + ship_to_email = Component("COM", mandatory=True) + cux = Component("CUX", mandatory=True) + tdt = Component("TDT", mandatory=True) + + lines = SegmentLoop( + OrderLine, + max=99, + mandatory=True + ) + + uns = Component("UNS", mandatory=True) + cnt = Component("CNT", mandatory=True) + + +TYPE_TO_PARSER_DICT = { + "ORDERS": Order +} + + +for message in interchange.get_messages(): + cls = TYPE_TO_PARSER_DICT.get(message.type) + if not cls: + raise NotImplementedError("Unsupported message type '{}'".format(message.type)) + + obj = cls() + obj.from_message(message) + print(str(obj)) From 23f218d246be5af3b86cd547b9710226ad58d019 Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 13:48:33 +0000 Subject: [PATCH 02/14] Adds high level mapping functionality, initial version --- pydifact/api.py | 2 +- pydifact/mapping.py | 232 ++++++++++++++++++++++++++++++++++ pydifact/segmentcollection.py | 13 ++ pydifact/segments.py | 5 +- 4 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 pydifact/mapping.py diff --git a/pydifact/api.py b/pydifact/api.py index 5cff1e0..912b50a 100644 --- a/pydifact/api.py +++ b/pydifact/api.py @@ -11,7 +11,7 @@ def __init__(cls, name, bases, attrs): if not hasattr(cls, "plugins"): cls.plugins = [] else: - if not hasattr(cls, "__omitted__"): + if not getattr(cls, "__omitted__", False): cls.plugins.append(cls) diff --git a/pydifact/mapping.py b/pydifact/mapping.py new file mode 100644 index 0000000..3914348 --- /dev/null +++ b/pydifact/mapping.py @@ -0,0 +1,232 @@ +import collections +import itertools +from typing import Callable, List, Optional, Tuple, Union + +from pydifact.segmentcollection import Message +from pydifact.api import PluginMount, EDISyntaxError +from pydifact.segments import Segment as Seg, SegmentFactory + + +class BiDirectionalIterator(object): + """ + Bi-directional iterator. Used as a convenience when parsing messages + """ + def __init__(self, collection): + self.collection = collection + self.index = 0 + + def next(self): + try: + result = self.collection[self.index] + self.index += 1 + except IndexError: + raise StopIteration + return result + + def prev(self): + self.index -= 1 + if self.index < 0: + raise StopIteration + return self.collection[self.index] + + def __iter__(self): + return self + + +class AbstractMappingComponent: + """ + Abstract EDIFact Component, used as a base for Segments, SegmentGroups, + Loops + """ + def __init__(self, **kwargs): + self.mandatory = kwargs.get("mandatory", False) + + def _from_segments(self, messages): + raise NotImplementedError() + + def _to_segments(self): + raise NotImplementedError() + + +class Segment(AbstractMappingComponent): + """ + EDIFact Component. + A simple wrapper for Segment + """ + def __init__( + self, + tag: str, + *elements, + **kwargs + ): + super(Segment, self).__init__(**kwargs) + + self.__component__ = SegmentFactory.create_segment( + tag, [], + validate=False + ) + + @property + def tag(self): + return self.__component__.tag + + def __str__(self): + return "{} {}".format( + type(self.__component__), + str(self.__component__) + ) + + def __getitem__(self, key): + return self.__component__[key] + + def __setitem__(self, key, value): + self.__component__[key] = value + + def validate(self) -> bool: + return self.__component__.validate() + + def _from_segments(self, iterator): + segment = iterator.next() + + if self.tag == segment.tag: + self.__component__ = segment + return + + if self.mandatory: + raise EDISyntaxError("Missing %s, found %s" % (self.tag, segment)) + + iterator.prev() + + def _to_segments(self): + return self.__component__ + + +class SegmentGroupMetaClass(type): + """ + Metaclass to maintain an ordered list of components. + Required for compatibility with Python 3.5. In 3.6 the + properties of a class are strictly ordered. + """ + @classmethod + def __prepare__(cls, name, bases): + return collections.OrderedDict() + + def __new__(cls, name, bases, classdict): + result = type.__new__(cls, name, bases, dict(classdict)) + exclude = set(dir(type)) + + result.__components__ = [] + + for k, v in classdict.items(): + if k not in exclude and isinstance(v, AbstractMappingComponent): + result.__components__.append(k) + return result + + +class SegmentGroup(AbstractMappingComponent, metaclass=SegmentGroupMetaClass): + """ + Describes a group of AbstractMappingComponent + """ + def _from_segments(self, isegment): + i = 0 + + icomponent = iter(self.__components__) + + try: + while True: + component_name = next(icomponent) + component = getattr(self, component_name) + component._from_segments(isegment) + except StopIteration: + pass + + def _to_segments(self): + segments = [] + + for component_name in self.__components__: + component = getattr(self, component_name) + component_segments = component._to_segments() + + if isinstance(component_segments, list): + segments += component_segments + else: + segments.append(component_segments) + + return segments + + def from_message(self, message): + imessage = BiDirectionalIterator(message.segments) + self._from_segments(imessage) + + def to_message(self, reference_number: str, identifier: Tuple): + segments = self._to_segments() + return Message.from_segments(reference_number, identifier, segments) + + def __str__(self): + res = [] + for component_name in iter(self.__components__): + component = getattr(self, component_name) + res.append(str(component)) + return "\n".join(res) + + +class Loop(AbstractMappingComponent): + """ + Describes a repeating SegmentGroup + """ + def __init__(self, component, **kwargs): + super(Loop, self).__init__(**kwargs) + self.min = kwargs.get("min", 0) + self.max = kwargs.get("max", 0) + + if self.mandatory and self.min < 1: + self.min = 1 + + if self.max < self.min: + self.max = self.min + + self.__component__ = component + self.value = [] + + def _from_segments(self, isegment): + i = 0 + while i < self.max: + + try: + component = self.__component__() + component._from_segments(isegment) + self.value.append(component) + except EDISyntaxError: + isegment.prev() + if self.mandatory and i < self.min: + raise EDISyntaxError("Missing %s" % (self.__component__.__name__)) + break + + i += 1 + + if i < self.min: + raise EDISyntaxError("Minimum required not met") + + def _to_segments(self): + segments = [] + + for value in self.value: + segments += value._to_segments() + + return segments + + def __str__(self): + res = [] + for v in self.value: + res.append(str(v)) + return "{} = {}".format( + self.__component__.__name__, + str(res) + ) + + def __getitem__(self, key): + return self.value[key] + + def __setitem__(self, key, value): + self.value[key] = value + diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index b635f67..e73339e 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -381,6 +381,19 @@ def validate(self): """ pass + @classmethod + def from_segments( + cls, reference_number, identifier, segments: list or collections.Iterable + ) -> "AbstractSegmentsContainer": + """Create a new AbstractSegmentsContainer instance from a iterable list of segments. + + :param reference_number: Reference number + :param identifier: Identifier + :param segments: The segments of the EDI interchange + :type segments: list/iterable of Segment + """ + + return cls(reference_number, identifier).add_segments(segments) class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContainer): diff --git a/pydifact/segments.py b/pydifact/segments.py index 97987d7..cb7959c 100644 --- a/pydifact/segments.py +++ b/pydifact/segments.py @@ -141,8 +141,9 @@ def create_segment( ) for Plugin in SegmentProvider.plugins: - if Plugin().tag == name: + if getattr(Plugin, "tag", "") == name: s = Plugin(name, *elements) + break else: # we don't support this kind of EDIFACT segment (yet), so # just create a generic Segment() @@ -155,4 +156,4 @@ def create_segment( ) # FIXME: characters is not used! - return Segment(name, *elements) + return s From 65227a3b55fc7d7dc33c83db58343f0ce61ee365 Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 13:56:41 +0000 Subject: [PATCH 03/14] WIP --- wip.py | 250 ++++++++++++--------------------------------------------- 1 file changed, 51 insertions(+), 199 deletions(-) diff --git a/wip.py b/wip.py index 01970be..5f3377e 100644 --- a/wip.py +++ b/wip.py @@ -1,9 +1,18 @@ -from typing import Callable, List, Optional, Tuple, Union +from pydifact.segmentcollection import Interchange +from pydifact.segments import Segment +import pydifact.mapping as mapping -from pydifact.segmentcollection import Interchange -from pydifact.segments import Segment, SegmentProvider +class BGM(Segment): + __omitted__ = False + + tag = "BGM" + +class LIN(Segment): + __omitted__ = False + + tag = "LIN" interchange = Interchange.from_str("""UNA:+.?*' @@ -39,213 +48,46 @@ UNT+32+1' UNZ+1+0001'""") -import itertools -import collections - - -class BiDirectionalIterator(object): - def __init__(self, collection): - self.collection = collection - self.index = 0 - - def next(self): - try: - result = self.collection[self.index] - self.index += 1 - except IndexError: - raise StopIteration - return result - - def prev(self): - self.index -= 1 - if self.index < 0: - raise StopIteration - return self.collection[self.index] - - def __iter__(self): - return self - - -class AbstractComponent: - """Abstract EDIFact Component, used as a base for Components, SegmentGroup, SegmentLoop""" - def __init__(self, **kwargs): - self.mandatory = kwargs.get("mandatory", False) - - def _from_segment_iter(self, messages): - raise NotImplementedError() - - -class Component(AbstractComponent, Segment): - """ - EDIFact Component. - A simple wrapper for Segment - """ - def __init__( - self, - tag: str, - *elements, - **kwargs - ): - AbstractComponent.__init__(self, **kwargs) - Segment.__init__(self, tag, *elements) - - def _from_segment_iter(self, iterator): - segment = iterator.next() - - if self.tag == segment.tag: - self.elements = segment.elements - return - - if self.mandatory: - raise Exception("Missing %s, found %s" % (self.tag, segment)) - - iterator.prev() - - -class SegmentGroupMetaClass(type): - """ - Metaclass to maintain an ordered list of components. - Required for compatibility with Python 3.5. In 3.6 the - properties of a class are strictly ordered. - """ - @classmethod - def __prepare__(cls, name, bases): - return collections.OrderedDict() - - def __new__(cls, name, bases, classdict): - result = type.__new__(cls, name, bases, dict(classdict)) - exclude = set(dir(type)) - - result.__components__ = [] - - for k, v in classdict.items(): - if k not in exclude and isinstance(v, AbstractComponent): - result.__components__.append(k) - return result - - -class SegmentGroup(AbstractComponent, metaclass=SegmentGroupMetaClass): - """ - Describes a static group of Components - """ - def _from_segment_iter(self, isegment): - i = 0 - - icomponent = iter(self.__components__) - try: - while True: - component_name = next(icomponent) - component = getattr(self, component_name) - component._from_segment_iter(isegment) - except StopIteration: - pass - - def from_message(self, message): - imessage = BiDirectionalIterator(message.segments) - self._from_segment_iter(imessage) - - def to_message(self): - raise NotImplementedError() - - def __str__(self): - res = [] - for component_name in iter(self.__components__): - component = getattr(self, component_name) - res.append(str(component)) - return "\n".join(res) - - -class SegmentLoop(AbstractComponent): - """ - Describes a repeating SegmentGroup - """ - def __init__(self, component, **kwargs): - super(SegmentLoop, self).__init__(**kwargs) - self.min = kwargs.get("min", 0) - self.max = kwargs.get("max", 0) - - if self.mandatory and self.min < 1: - self.min = 1 - - if self.max < self.min: - self.max = self.min - - self.__component__ = component - self.value = [] - - def _from_segment_iter(self, isegment): - i = 0 - while i < self.max: - - try: - component = self.__component__() - component._from_segment_iter(isegment) - self.value.append(component) - except BaseException: - isegment.prev() - if self.mandatory and i < self.min: - raise Exception("Missing %s" % - (self.__component__.__name__)) - break - - i += 1 - - if i < self.min: - raise Exception("minimum required not met") - - def __str__(self): - res = [] - for v in self.value: - res.append(str(v)) - return "{} = {}".format( - self.__component__.__name__, - str(res) - ) - - -class OrderLine(SegmentGroup): - line_id = Component("LIN", mandatory=True) - description = Component("IMD", mandatory=True) - quantity = Component("QTY", mandatory=True) - moa = Component("MOA") - pri = Component("PRI") - rff = Component("RFF", mandatory=True) - - - - -class Order(SegmentGroup): - purchase_order_id = Component("BGM", mandatory=True) - date = Component("DTM", mandatory=True) - delivery_date = Component("DTM", mandatory=True) - delivery_instructions = Component("FTX", mandatory=True) - supplier_id_gln = Component("NAD", mandatory=True) - supplier_id_tprg = Component("NAD", mandatory=True) - ref = Component("RFF", mandatory=True) - ship_to = Component("NAD", mandatory=True) - - ship_to_contact = Component("CTA", mandatory=True) - ship_to_phone = Component("COM", mandatory=True) - ship_to_email = Component("COM", mandatory=True) - cux = Component("CUX", mandatory=True) - tdt = Component("TDT", mandatory=True) - - lines = SegmentLoop( +class OrderLine(mapping.SegmentGroup): + line_id = mapping.Segment("LIN", mandatory=True) + description = mapping.Segment("IMD", mandatory=True) + quantity = mapping.Segment("QTY", mandatory=True) + moa = mapping.Segment("MOA") + pri = mapping.Segment("PRI") + rff = mapping.Segment("RFF", mandatory=True) + + +class Order(mapping.SegmentGroup): + purchase_order_id = mapping.Segment("BGM", mandatory=True) + date = mapping.Segment("DTM", mandatory=True) + delivery_date = mapping.Segment("DTM", mandatory=True) + delivery_instructions = mapping.Segment("FTX", mandatory=True) + supplier_id_gln = mapping.Segment("NAD", mandatory=True) + supplier_id_tprg = mapping.Segment("NAD", mandatory=True) + ref = mapping.Segment("RFF", mandatory=True) + ship_to = mapping.Segment("NAD", mandatory=True) + + ship_to_contact = mapping.Segment("CTA", mandatory=True) + ship_to_phone = mapping.Segment("COM", mandatory=True) + ship_to_email = mapping.Segment("COM", mandatory=True) + cux = mapping.Segment("CUX", mandatory=True) + tdt = mapping.Segment("TDT", mandatory=True) + + lines = mapping.Loop( OrderLine, max=99, mandatory=True ) - uns = Component("UNS", mandatory=True) - cnt = Component("CNT", mandatory=True) + uns = mapping.Segment("UNS", mandatory=True) + cnt = mapping.Segment("CNT", mandatory=True) TYPE_TO_PARSER_DICT = { "ORDERS": Order } - for message in interchange.get_messages(): cls = TYPE_TO_PARSER_DICT.get(message.type) if not cls: @@ -253,4 +95,14 @@ class Order(SegmentGroup): obj = cls() obj.from_message(message) - print(str(obj)) + + reconstituted = obj.to_message(message.reference_number, message.identifier) + + #print(str(obj)) + #print(obj.purchase_order_id[0]) + + assert isinstance(obj.purchase_order_id._to_segments(), BGM) + + #print(message.segments) + + assert str(message) == str(reconstituted), "Original message should match reconstituted message" From 05d906e12d711e0c2f3bfdcc163dfb4e469397b3 Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 13:57:34 +0000 Subject: [PATCH 04/14] Formatting --- pydifact/mapping.py | 29 ++++++++++------------------- wip.py | 28 +++++++++++++--------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/pydifact/mapping.py b/pydifact/mapping.py index 3914348..392e5e8 100644 --- a/pydifact/mapping.py +++ b/pydifact/mapping.py @@ -11,6 +11,7 @@ class BiDirectionalIterator(object): """ Bi-directional iterator. Used as a convenience when parsing messages """ + def __init__(self, collection): self.collection = collection self.index = 0 @@ -38,6 +39,7 @@ class AbstractMappingComponent: Abstract EDIFact Component, used as a base for Segments, SegmentGroups, Loops """ + def __init__(self, **kwargs): self.mandatory = kwargs.get("mandatory", False) @@ -53,28 +55,18 @@ class Segment(AbstractMappingComponent): EDIFact Component. A simple wrapper for Segment """ - def __init__( - self, - tag: str, - *elements, - **kwargs - ): + + def __init__(self, tag: str, *elements, **kwargs): super(Segment, self).__init__(**kwargs) - self.__component__ = SegmentFactory.create_segment( - tag, [], - validate=False - ) + self.__component__ = SegmentFactory.create_segment(tag, [], validate=False) @property def tag(self): return self.__component__.tag def __str__(self): - return "{} {}".format( - type(self.__component__), - str(self.__component__) - ) + return "{} {}".format(type(self.__component__), str(self.__component__)) def __getitem__(self, key): return self.__component__[key] @@ -107,6 +99,7 @@ class SegmentGroupMetaClass(type): Required for compatibility with Python 3.5. In 3.6 the properties of a class are strictly ordered. """ + @classmethod def __prepare__(cls, name, bases): return collections.OrderedDict() @@ -127,6 +120,7 @@ class SegmentGroup(AbstractMappingComponent, metaclass=SegmentGroupMetaClass): """ Describes a group of AbstractMappingComponent """ + def _from_segments(self, isegment): i = 0 @@ -174,6 +168,7 @@ class Loop(AbstractMappingComponent): """ Describes a repeating SegmentGroup """ + def __init__(self, component, **kwargs): super(Loop, self).__init__(**kwargs) self.min = kwargs.get("min", 0) @@ -219,14 +214,10 @@ def __str__(self): res = [] for v in self.value: res.append(str(v)) - return "{} = {}".format( - self.__component__.__name__, - str(res) - ) + return "{} = {}".format(self.__component__.__name__, str(res)) def __getitem__(self, key): return self.value[key] def __setitem__(self, key, value): self.value[key] = value - diff --git a/wip.py b/wip.py index 5f3377e..eee89b7 100644 --- a/wip.py +++ b/wip.py @@ -1,5 +1,5 @@ from pydifact.segmentcollection import Interchange -from pydifact.segments import Segment +from pydifact.segments import Segment import pydifact.mapping as mapping @@ -15,7 +15,8 @@ class LIN(Segment): tag = "LIN" -interchange = Interchange.from_str("""UNA:+.?*' +interchange = Interchange.from_str( + """UNA:+.?*' UNB+UNOA:4+5021376940009:14+1111111111111:14+200421:1000+0001+ORDERS' UNH+1+ORDERS:D:01B:UN:EAN010' BGM+220+123456+9' @@ -46,7 +47,8 @@ class LIN(Segment): UNS+S' CNT+2:1' UNT+32+1' -UNZ+1+0001'""") +UNZ+1+0001'""" +) class OrderLine(mapping.SegmentGroup): @@ -74,19 +76,13 @@ class Order(mapping.SegmentGroup): cux = mapping.Segment("CUX", mandatory=True) tdt = mapping.Segment("TDT", mandatory=True) - lines = mapping.Loop( - OrderLine, - max=99, - mandatory=True - ) + lines = mapping.Loop(OrderLine, max=99, mandatory=True) uns = mapping.Segment("UNS", mandatory=True) cnt = mapping.Segment("CNT", mandatory=True) -TYPE_TO_PARSER_DICT = { - "ORDERS": Order -} +TYPE_TO_PARSER_DICT = {"ORDERS": Order} for message in interchange.get_messages(): cls = TYPE_TO_PARSER_DICT.get(message.type) @@ -98,11 +94,13 @@ class Order(mapping.SegmentGroup): reconstituted = obj.to_message(message.reference_number, message.identifier) - #print(str(obj)) - #print(obj.purchase_order_id[0]) + # print(str(obj)) + # print(obj.purchase_order_id[0]) assert isinstance(obj.purchase_order_id._to_segments(), BGM) - #print(message.segments) + # print(message.segments) - assert str(message) == str(reconstituted), "Original message should match reconstituted message" + assert str(message) == str( + reconstituted + ), "Original message should match reconstituted message" From 14b7343b6c79ddfa931f5103ef15cc7aab84b630 Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 16:35:44 +0000 Subject: [PATCH 05/14] More testing --- pydifact/__init__.py | 4 ++++ wip.py | 45 +++++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/pydifact/__init__.py b/pydifact/__init__.py index caaa1d3..ff0e062 100644 --- a/pydifact/__init__.py +++ b/pydifact/__init__.py @@ -40,6 +40,10 @@ # EDIFACT Syntax Rules # http://www.unece.org/fileadmin/DAM/trade/edifact/untdid/d422_s.htm#normative +from . import mapping + +# generic EDIFACT implementation tutorial +# http://www.gxs.co.uk/wp-content/uploads/tutorial_edifact.pdf # UNECE Syntax Implementation Guidelines # https://www.unece.org/fileadmin/DAM/trade/untdid/texts/d423.htm diff --git a/wip.py b/wip.py index eee89b7..64cde77 100644 --- a/wip.py +++ b/wip.py @@ -1,6 +1,5 @@ from pydifact.segmentcollection import Interchange -from pydifact.segments import Segment -import pydifact.mapping as mapping +from pydifact import Segment, mapping, serializer class BGM(Segment): @@ -46,7 +45,7 @@ class LIN(Segment): RFF+LI:1' UNS+S' CNT+2:1' -UNT+32+1' +UNT+1+27' UNZ+1+0001'""" ) @@ -84,23 +83,35 @@ class Order(mapping.SegmentGroup): TYPE_TO_PARSER_DICT = {"ORDERS": Order} -for message in interchange.get_messages(): - cls = TYPE_TO_PARSER_DICT.get(message.type) - if not cls: - raise NotImplementedError("Unsupported message type '{}'".format(message.type)) +message = next(interchange.get_messages()) - obj = cls() - obj.from_message(message) +cls = TYPE_TO_PARSER_DICT.get(message.type) +if not cls: + raise NotImplementedError("Unsupported message type '{}'".format(message.type)) - reconstituted = obj.to_message(message.reference_number, message.identifier) +obj = cls() +obj.from_message(message) - # print(str(obj)) - # print(obj.purchase_order_id[0]) +reconstituted = obj.to_message(message.reference_number, message.identifier) - assert isinstance(obj.purchase_order_id._to_segments(), BGM) +# print(str(obj)) +# print(obj.purchase_order_id[0]) - # print(message.segments) +assert isinstance(obj.purchase_order_id._to_segments(), BGM) - assert str(message) == str( - reconstituted - ), "Original message should match reconstituted message" +# print(message.segments) + +assert str(message) == str( + reconstituted +), "Original message should match reconstituted message" + +reconstituted_interchange = Interchange( + interchange.sender, interchange.recipient, + interchange.control_reference, + interchange.syntax_identifier, + interchange.delimiters, + interchange.timestamp +) +reconstituted_interchange.add_message(reconstituted) + +assert reconstituted_interchange.serialize() == interchange.serialize(), "Original interchange should match reconstituted interchange" From 857e45f3dd9f9b8560d1dd838983ef46dbbcd15d Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 17:20:48 +0000 Subject: [PATCH 06/14] Add some typing, clean up the hackery a touch --- .gitignore | 3 + .pylintrc | 597 ++++++++++++++++++++++++++++++++++++++++++++ pydifact/mapping.py | 123 ++++++--- wip.py | 2 +- 4 files changed, 691 insertions(+), 34 deletions(-) create mode 100644 .pylintrc diff --git a/.gitignore b/.gitignore index 9150d8e..b26ebf7 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ prof .kdev4 .venv* Pipfile.lock + +# vim stuff +.vim diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..240e3ca --- /dev/null +++ b/.pylintrc @@ -0,0 +1,597 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + unused-imports + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + v, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/pydifact/mapping.py b/pydifact/mapping.py index 392e5e8..1e78464 100644 --- a/pydifact/mapping.py +++ b/pydifact/mapping.py @@ -1,22 +1,44 @@ +# Pydifact - a python edifact library +# +# Copyright (c) 2021 Karl Southern +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + import collections -import itertools -from typing import Callable, List, Optional, Tuple, Union +from typing import List, Tuple, Iterator from pydifact.segmentcollection import Message -from pydifact.api import PluginMount, EDISyntaxError +from pydifact.api import EDISyntaxError from pydifact.segments import Segment as Seg, SegmentFactory -class BiDirectionalIterator(object): +class BiDirectionalIterator: """ - Bi-directional iterator. Used as a convenience when parsing messages + Bi-directional iterator. Used as a convenience when parsing Message + Segments. """ - def __init__(self, collection): + def __init__(self, collection: List): self.collection = collection self.index = 0 - def next(self): + def __next__(self): try: result = self.collection[self.index] self.index += 1 @@ -24,12 +46,27 @@ def next(self): raise StopIteration return result - def prev(self): + def __prev__(self): self.index -= 1 if self.index < 0: raise StopIteration return self.collection[self.index] + def __getitem__(self, key): + return self.collection[key] + + def next(self): + """ + Alias for __next__ method. + """ + return self.__next__() + + def prev(self): + """ + Alias to __prev__ method. + """ + return self.__prev__() + def __iter__(self): return self @@ -43,12 +80,26 @@ class AbstractMappingComponent: def __init__(self, **kwargs): self.mandatory = kwargs.get("mandatory", False) - def _from_segments(self, messages): + def from_segments(self, iterator: BiDirectionalIterator): + """ + Convert a BiDirectionalIterator of Segment instances into this mapping + component. + """ raise NotImplementedError() - def _to_segments(self): + def to_segments(self) -> List[Seg]: + """ + Converts a mapping component to a list of Segment. + """ raise NotImplementedError() + # pylint: disable=no-self-use + def validate(self) -> bool: + """ + Mapping component validation. + """ + return True + class Segment(AbstractMappingComponent): """ @@ -59,13 +110,15 @@ class Segment(AbstractMappingComponent): def __init__(self, tag: str, *elements, **kwargs): super(Segment, self).__init__(**kwargs) - self.__component__ = SegmentFactory.create_segment(tag, [], validate=False) + self.__component__ = SegmentFactory.create_segment( + tag, *elements, validate=False + ) @property - def tag(self): + def tag(self) -> str: return self.__component__.tag - def __str__(self): + def __str__(self) -> str: return "{} {}".format(type(self.__component__), str(self.__component__)) def __getitem__(self, key): @@ -77,7 +130,7 @@ def __setitem__(self, key, value): def validate(self) -> bool: return self.__component__.validate() - def _from_segments(self, iterator): + def from_segments(self, iterator: BiDirectionalIterator): segment = iterator.next() if self.tag == segment.tag: @@ -89,7 +142,7 @@ def _from_segments(self, iterator): iterator.prev() - def _to_segments(self): + def to_segments(self): return self.__component__ @@ -101,7 +154,7 @@ class SegmentGroupMetaClass(type): """ @classmethod - def __prepare__(cls, name, bases): + def __prepare__(cls, _name, _bases): return collections.OrderedDict() def __new__(cls, name, bases, classdict): @@ -121,25 +174,23 @@ class SegmentGroup(AbstractMappingComponent, metaclass=SegmentGroupMetaClass): Describes a group of AbstractMappingComponent """ - def _from_segments(self, isegment): - i = 0 - + def from_segments(self, iterator: Iterator): icomponent = iter(self.__components__) try: while True: component_name = next(icomponent) component = getattr(self, component_name) - component._from_segments(isegment) + component.from_segments(iterator) except StopIteration: pass - def _to_segments(self): + def to_segments(self): segments = [] for component_name in self.__components__: component = getattr(self, component_name) - component_segments = component._to_segments() + component_segments = component.to_segments() if isinstance(component_segments, list): segments += component_segments @@ -148,15 +199,21 @@ def _to_segments(self): return segments - def from_message(self, message): - imessage = BiDirectionalIterator(message.segments) - self._from_segments(imessage) + def from_message(self, message: Message): + """ + Create a mapping from a Message. + """ + iterator = BiDirectionalIterator(message.segments) + self.from_segments(iterator) def to_message(self, reference_number: str, identifier: Tuple): - segments = self._to_segments() + """ + Convert this mapping component into a new Message + """ + segments = self.to_segments() return Message.from_segments(reference_number, identifier, segments) - def __str__(self): + def __str__(self) -> str: res = [] for component_name in iter(self.__components__): component = getattr(self, component_name) @@ -183,16 +240,16 @@ def __init__(self, component, **kwargs): self.__component__ = component self.value = [] - def _from_segments(self, isegment): + def from_segments(self, iterator: BiDirectionalIterator): i = 0 while i < self.max: try: component = self.__component__() - component._from_segments(isegment) + component.from_segments(iterator) self.value.append(component) except EDISyntaxError: - isegment.prev() + iterator.prev() if self.mandatory and i < self.min: raise EDISyntaxError("Missing %s" % (self.__component__.__name__)) break @@ -202,15 +259,15 @@ def _from_segments(self, isegment): if i < self.min: raise EDISyntaxError("Minimum required not met") - def _to_segments(self): + def to_segments(self): segments = [] for value in self.value: - segments += value._to_segments() + segments += value.to_segments() return segments - def __str__(self): + def __str__(self) -> str: res = [] for v in self.value: res.append(str(v)) diff --git a/wip.py b/wip.py index 64cde77..a4fd863 100644 --- a/wip.py +++ b/wip.py @@ -97,7 +97,7 @@ class Order(mapping.SegmentGroup): # print(str(obj)) # print(obj.purchase_order_id[0]) -assert isinstance(obj.purchase_order_id._to_segments(), BGM) +assert isinstance(obj.purchase_order_id.to_segments(), BGM) # print(message.segments) From 061ec2d029fe71787037a34d18f7886cbc215b0d Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 3 Feb 2021 17:42:56 +0000 Subject: [PATCH 07/14] Add some tests before the end of the day --- wip.py => tests/test_mapping.py | 132 +++++++++++++++++--------------- 1 file changed, 70 insertions(+), 62 deletions(-) rename wip.py => tests/test_mapping.py (56%) diff --git a/wip.py b/tests/test_mapping.py similarity index 56% rename from wip.py rename to tests/test_mapping.py index a4fd863..19ca310 100644 --- a/wip.py +++ b/tests/test_mapping.py @@ -1,5 +1,21 @@ +# pydifact - a python edifact library +# Copyright (C) 2021 Karl Southern +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +import unittest from pydifact.segmentcollection import Interchange -from pydifact import Segment, mapping, serializer +from pydifact import Segment, mapping class BGM(Segment): @@ -14,42 +30,6 @@ class LIN(Segment): tag = "LIN" -interchange = Interchange.from_str( - """UNA:+.?*' -UNB+UNOA:4+5021376940009:14+1111111111111:14+200421:1000+0001+ORDERS' -UNH+1+ORDERS:D:01B:UN:EAN010' -BGM+220+123456+9' -DTM+137:20150410:102' -DTM+2:20150710:102' -FTX+DIN+++DELIVERY INSTRUCTIONS DESCRIPTION' -NAD+BY+5021376940009::9' -NAD+SU+1111111111111::9' -RFF+IA:123456' -NAD+ST+++Mr John Smith+The Bungalow:Hastings Road+Preston+:::Lancashire+SW1A 1AA' -CTA+LB+:Mr John Smith' -COM+01772 999999:TE' -COM+johnsmith@gmail.com:EM' -CUX+2:GBP:9' -TDT+20+++++SD' -LIN+1++121354654:BP' -IMD+F++:::TPRG item description' -QTY+21:2' -MOA+203:200.00' -PRI+AAA:100.00' -RFF+LI:1' -LIN+1++121354654:BP' -IMD+F++:::TPRG item description' -QTY+21:2' -MOA+203:200.00' -PRI+AAA:100.00' -RFF+LI:1' -UNS+S' -CNT+2:1' -UNT+1+27' -UNZ+1+0001'""" -) - - class OrderLine(mapping.SegmentGroup): line_id = mapping.Segment("LIN", mandatory=True) description = mapping.Segment("IMD", mandatory=True) @@ -81,37 +61,65 @@ class Order(mapping.SegmentGroup): cnt = mapping.Segment("CNT", mandatory=True) -TYPE_TO_PARSER_DICT = {"ORDERS": Order} - -message = next(interchange.get_messages()) +SAMPLE = """UNA:+.?*' +UNB+UNOA:4+5021376940009:14+1111111111111:14+200421:1000+0001+ORDERS' +UNH+1+ORDERS:D:01B:UN:EAN010' +BGM+220+123456+9' +DTM+137:20150410:102' +DTM+2:20150710:102' +FTX+DIN+++DELIVERY INSTRUCTIONS DESCRIPTION' +NAD+BY+5021376940009::9' +NAD+SU+1111111111111::9' +RFF+IA:123456' +NAD+ST+++Mr John Smith+The Bungalow:Hastings Road+Preston+:::Lancashire+SW1A 1AA' +CTA+LB+:Mr John Smith' +COM+01772 999999:TE' +COM+johnsmith@gmail.com:EM' +CUX+2:GBP:9' +TDT+20+++++SD' +LIN+1++121354654:BP' +IMD+F++:::TPRG item description' +QTY+21:2' +MOA+203:200.00' +PRI+AAA:100.00' +RFF+LI:1' +LIN+1++121354654:BP' +IMD+F++:::TPRG item description' +QTY+21:2' +MOA+203:200.00' +PRI+AAA:100.00' +RFF+LI:1' +UNS+S' +CNT+2:1' +UNT+1+27' +UNZ+1+0001'""" -cls = TYPE_TO_PARSER_DICT.get(message.type) -if not cls: - raise NotImplementedError("Unsupported message type '{}'".format(message.type)) -obj = cls() -obj.from_message(message) +class MappingTest(unittest.TestCase): -reconstituted = obj.to_message(message.reference_number, message.identifier) + def test_read_interchange(self): + interchange = Interchange.from_str(SAMPLE) + message = next(interchange.get_messages()) -# print(str(obj)) -# print(obj.purchase_order_id[0]) + try: + obj = Order() + obj.from_message(message) + except Exception as err: + raise AssertionError( + "Could not read Message into Order mapping! {}".format( + repr(err) + ) + ) -assert isinstance(obj.purchase_order_id.to_segments(), BGM) + def test_ensure_mapped_bgm_segment(self): + interchange = Interchange.from_str(SAMPLE) + message = next(interchange.get_messages()) -# print(message.segments) + obj = Order() + obj.from_message(message) -assert str(message) == str( - reconstituted -), "Original message should match reconstituted message" + self.assertTrue(isinstance(obj.purchase_order_id.to_segments(), BGM)) -reconstituted_interchange = Interchange( - interchange.sender, interchange.recipient, - interchange.control_reference, - interchange.syntax_identifier, - interchange.delimiters, - interchange.timestamp -) -reconstituted_interchange.add_message(reconstituted) -assert reconstituted_interchange.serialize() == interchange.serialize(), "Original interchange should match reconstituted interchange" +if __name__ == "__main__": + unittest.main() From ba426edafa43c30132e9bb4af6b99b0d025df620 Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 10 Feb 2021 11:55:21 +0000 Subject: [PATCH 08/14] Fix Message.from_str --- pydifact/segmentcollection.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index e73339e..e73fe3a 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -395,6 +395,25 @@ def from_segments( return cls(reference_number, identifier).add_segments(segments) + @classmethod + def from_str(cls, string: str) -> "AbstractSegmentsContainer": + segments = Parser().parse(string) + + unh = None + todo = [] + + for segment in segments: + if segment.tag == "UNH": + unh = segment + continue + if segment.tag == "UNT": + continue + todo.append(segment) + + if not unh: + raise EDISyntaxError("Missing header in message") + return cls.from_segments(unh[0], unh[1], todo) + class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContainer): """ From d37aaee622baa0587795ac53694b2f58c51b806c Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 17 Feb 2021 10:42:51 +0000 Subject: [PATCH 09/14] Missing append method --- pydifact/mapping.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pydifact/mapping.py b/pydifact/mapping.py index 1e78464..31fc875 100644 --- a/pydifact/mapping.py +++ b/pydifact/mapping.py @@ -278,3 +278,9 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.value[key] = value + + def append(self, value: AbstractMappingComponent): + """ + Append an item to the loop + """ + self.value.append(value) From 3ef2562d6c6e283f46af263b51908badcc647d2c Mon Sep 17 00:00:00 2001 From: Karl Southern Date: Wed, 17 Feb 2021 16:53:58 +0000 Subject: [PATCH 10/14] WIP present --- pydifact/mapping.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pydifact/mapping.py b/pydifact/mapping.py index 31fc875..73e2e95 100644 --- a/pydifact/mapping.py +++ b/pydifact/mapping.py @@ -100,6 +100,13 @@ def validate(self) -> bool: """ return True + @property + def present(self) -> bool: + """ + Is the mapping component present? + """ + raise NotImplementedError() + class Segment(AbstractMappingComponent): """ @@ -113,6 +120,7 @@ def __init__(self, tag: str, *elements, **kwargs): self.__component__ = SegmentFactory.create_segment( tag, *elements, validate=False ) + self.__present__ = True @property def tag(self) -> str: @@ -126,6 +134,8 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.__component__[key] = value + if not self.__present__: + self.__present__ = True def validate(self) -> bool: return self.__component__.validate() @@ -135,16 +145,23 @@ def from_segments(self, iterator: BiDirectionalIterator): if self.tag == segment.tag: self.__component__ = segment + self.__present__ = True return if self.mandatory: raise EDISyntaxError("Missing %s, found %s" % (self.tag, segment)) + self.__present__ = False + iterator.prev() def to_segments(self): return self.__component__ + @property + def present(self) -> bool: + return self.__present__ + class SegmentGroupMetaClass(type): """ @@ -220,6 +237,10 @@ def __str__(self) -> str: res.append(str(component)) return "\n".join(res) + @property + def present(self) -> bool: + return any(getattr(self, component_name).present for component_name in self.__components__) + class Loop(AbstractMappingComponent): """ @@ -284,3 +305,7 @@ def append(self, value: AbstractMappingComponent): Append an item to the loop """ self.value.append(value) + + @property + def present(self) -> bool: + return any(value.present for value in self.value) From 0a04bf234c61f8812bd5f804b5a8a4d643e681ae Mon Sep 17 00:00:00 2001 From: vnikolayev1 Date: Fri, 14 Apr 2023 21:40:36 +0300 Subject: [PATCH 11/14] [ADD] 5052 Edifact - EDI implementation - added delfor and deljit validation --- pydifact/msg_validation.py | 393 +++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 pydifact/msg_validation.py diff --git a/pydifact/msg_validation.py b/pydifact/msg_validation.py new file mode 100644 index 0000000..27bd711 --- /dev/null +++ b/pydifact/msg_validation.py @@ -0,0 +1,393 @@ +from mapping import SegmentGroup, Segment, Loop +from segmentcollection import Interchange + + +class DelforTransportInformation(SegmentGroup): + """SG7 TDT-DTM""" + transport_details = Segment("TDT", mandatory=True) + document_date = Segment("DTM", mandatory=False) + + +class DelforDocumentDetails(SegmentGroup): + """SG5 DOC-DTM""" + document_details = Segment("DOC", mandatory=True) + document_date = Segment("DTM", mandatory=False) + + +class DelforContactInformation(SegmentGroup): + """SG3 CTA-COM""" + contact = Segment("CTA", mandatory=True) + communication = Segment("COM", mandatory=False) + + +class DelforSellerBuyerAddressInformation(SegmentGroup): + """SG2 NAD-LOC-SG3""" + seller_details = Segment("NAD", mandatory=True) + location_details = Segment("LOC", mandatory=False) + buyer_contact_information = Loop(DelforContactInformation, max=1, mandatory=False) + + +class DelforReferenceDateTimePeriodN(SegmentGroup): + """SG10,SG1 RFF-DTM""" + account_reference = Segment("RFF", mandatory=True) + document_date = Segment("DTM", mandatory=False) + + +class DelforReferenceDateTimePeriod(SegmentGroup): + """SG13 RFF-DTM""" + account_reference = Segment("RFF", mandatory=True) + document_date = Segment("DTM", mandatory=True) + + +class DelforSchedulingConditions(SegmentGroup): + """No segment, but can be looped""" + scheduling_conditions = Segment("SCC", mandatory=True) + + +class DelforQtyToBeDelivered(SegmentGroup): + """SG12 Quantity to be delivered QTY-SCC-DTM-SG13""" + quantities = Segment("QTY", mandatory=True) + scheduling_conditions = Loop(DelforSchedulingConditions, max=200, mandatory=True) + earliest_delivery_date = Segment("DTM", mandatory=True) + latest_delivery_date = Segment("DTM", mandatory=True) + referenca_datetime_period = Loop( + DelforReferenceDateTimePeriod, max=1, mandatory=False) + + +class DelforQtyCumulativeBackorder(SegmentGroup): + """SG12 Quantity to be delivered QTY-SCC-DTM-SG13""" + quantities = Segment("QTY", mandatory=True) + scheduling_conditions = Loop(DelforSchedulingConditions, max=200, mandatory=False) + earliest_delivery_date = Segment("DTM", mandatory=False) + latest_delivery_date = Segment("DTM", mandatory=False) + referenca_datetime_period = Loop( + DelforReferenceDateTimePeriod, max=1, mandatory=False) + + +class DelforQtyDispatch(SegmentGroup): + """SG12 Quantity to be delivered QTY-SCC-DTM-SG13""" + quantities = Segment("QTY", mandatory=True) + scheduling_conditions = Loop(DelforSchedulingConditions, max=200, mandatory=False) + earliest_delivery_date = Segment("DTM", mandatory=False) + latest_delivery_date = Segment("DTM", mandatory=False) + referenca_datetime_period = Loop( + DelforReferenceDateTimePeriod, max=1, mandatory=True) + + +class DelforItemInformation(SegmentGroup): + """SG8 LIN-PIA-IMD-MEA-ALI-GIN-GIR-LOC-DTM-FTX-SG9-SG10-SG11-SG12-SG14""" + # 9,10,11,12 SG4/SG8 + line_item = Segment("LIN", mandatory=True) + additional_product_id = Segment("PIA", mandatory=False) + item_description = Segment("IMD", mandatory=True) + measure = Segment("MEA", mandatory=False) + additional_info = Segment("ALI", mandatory=False) + goods_id_number = Segment("GIN", mandatory=False) + related_id_number = Segment("GIR", mandatory=False) + location_identification = Segment("LOC", mandatory=True) + date_time_period = Segment("DTM", mandatory=True) + free_text = Segment("FTX", mandatory=False) + # 13 SG4/SG8/SG10 RFF-DTM + order_number = Loop(DelforReferenceDateTimePeriodN, max=1, mandatory=True) + # 14,15,16 SG4/SG8/SG12 QTY-SCC-DTM-SG13 with qty_6063=12 (Dispatch quantity) + quantity_dispatch = Loop(DelforQtyDispatch, max=1, mandatory=True) + # 17 SG4/SG8/SG12 QTY-SCC-DTM-SG13 with qty_6063=78 Cumulative quantity scheduled) + cumulative_quantity_scheduled = Loop( + DelforQtyCumulativeBackorder, max=1, mandatory=True) + # 18 SG4/SG8/SG12 QTY-SCC-DTM-SG13 with qty_6063=70 (Cumulative quantity received) + cumulative_quantity_received = Loop( + DelforQtyCumulativeBackorder, max=1, mandatory=True) + # 19 SG4/SG8/SG12 QTY-SCC-DTM-SG13 with qty_6063=83 (Backorder quantity) + backorder_quantity = Loop(DelforQtyCumulativeBackorder, max=1, mandatory=True) + # 20,21,22,23 SG4/SG8/SG12 QTY-SCC-DTM-SG13 with qty_6063=113 + qty_to_be_delivered = Loop(DelforQtyToBeDelivered, max=999, mandatory=True) + + +class DelforDeliveryScheduleDetail(SegmentGroup): + """SG4 NAD-LOC-FTC-SG5-SG6-SG7-SG8""" + # 8 + ship_to = Segment("NAD", mandatory=True) + location_to = Segment("LOC", mandatory=False) + ftc = Segment("FTC", mandatory=False) + document_information = Loop(DelforDocumentDetails, max=1, mandatory=False) + contact_information = Loop(DelforContactInformation, max=1, mandatory=False) + transport_details = Loop(DelforTransportInformation, max=1, mandatory=False) + # 9-23 + item_information = Loop(DelforItemInformation, max=1, mandatory=True) + + +class DelforMessage(SegmentGroup): + """Groups message due to EDI implementation guide DELFOR UN D96A + more info https://www.stylusstudio.com/edifact/D96A/DELFOR.htm#SG8 """ + # --- Heading section get checked automatically--- + + # interchange_header = Segment("UNB", mandatory=True) + # 1 + # message_header = Segment("UNH", mandatory=True) + # 2 + message_id = Segment("BGM", mandatory=True) + # 3 DTM + document_date = Segment("DTM", mandatory=True) + # 4 SG1 RFF-DTM + schedule_reference = Loop(DelforReferenceDateTimePeriodN, max=1, mandatory=True) + # 5,6 SG2 NAD-LOC-SG3 + seller_address = Loop(DelforSellerBuyerAddressInformation, max=1, mandatory=True) + buyer_address = Loop(DelforSellerBuyerAddressInformation, max=1, mandatory=True) + # 7 + section_splitters = Segment("UNS", mandatory=True) + + # --- Delivery Schedule Detail Section --- + # 8-23 + delivery_schedule_details = Loop( + DelforDeliveryScheduleDetail, max=1, mandatory=True) + + # --- Delivery Schedule Summary Section --- + # 24,24(?) + uns = Segment("UNS", mandatory=True) + cnt = Segment("CNT", mandatory=True) + + +def validate_delfor(inc_message): + """Validates delfor UND96A message, returns it's object""" + type_to_parser_dict = {"DELFOR": DelforMessage} + from_str = Interchange.from_str(inc_message) + message = next(from_str.Interchange.get_messages()) + cls = type_to_parser_dict.get(message.type) + if not cls: + raise NotImplementedError("Unsupported message type '{}'".format(message.type)) + return message + # obj = cls() + # obj.from_message(message) + + +# Occurrences, not a segment groups +class DejitCommunication(SegmentGroup): + """Not a segment group""" + communication = Segment("COM", mandatory=False) + + +class DejitArticleDescription(SegmentGroup): + """Not a segment group""" + article_description = Segment("IMD", mandatory=False) + + +class DejitDatetime(SegmentGroup): + """Not a segment group""" + delivery_datetime = Segment("DTM", mandatory=True) + + +class DejitDatetime(SegmentGroup): + """Not a segment group""" + delivery_datetime = Segment("DTM", mandatory=True) + + +class DejitRelatedIdNumber(SegmentGroup): + """Not a segment group""" + related_id_num = Segment("GIR", mandatory=True) + + +class DejitFreeText(SegmentGroup): + """Not a segment group""" + free_text = Segment("FTX", mandatory=True) + +class DejitLocation(SegmentGroup): + """Not a segment group""" + location = Segment("LOC", mandatory=True) + + +class DejitManifest(SegmentGroup): + """Not a segment group""" + manifest_number = Segment("GIN", mandatory=True) + + +class DejitAdditionalProductId(SegmentGroup): + """Not a segment group""" + additional_product_id = Segment("PIA", mandatory=True) + + +class DejitAdditionalInformation(SegmentGroup): + """Not a segment group""" + additional_info = Segment("ALI", mandatory=True) + + +class DejitPackage(SegmentGroup): + """Not a segment group""" + package = Segment("PAC", mandatory=True) + + +# Segment groups +class DejitContactInformation(SegmentGroup): + """SG3,SG11 CTA-COM""" + contact = Segment("CTA", mandatory=True) + communication = Loop(DejitCommunication, max=5, mandatory=False) + + +class DejitNamesAddressFullInformation(SegmentGroup): + """SG2 NAD-LOC-FTX-SG3 - SG3 Mandatory""" + details = Segment("NAD", mandatory=True) + location_details = Loop(DejitLocation, max=10, mandatory=False) + free_text = Loop(DejitFreeText, max=5, mandatory=False) + buyer_contact_information = Loop(DejitContactInformation, max=1, mandatory=True) + + +class DejitNamesAddressInformation(SegmentGroup): + """SG2 NAD-LOC-FTX-SG3 - SG3 not Mandatory""" + details = Segment("NAD", mandatory=True) + location_details = Segment("LOC", mandatory=False) + free_text = Segment("FTX", mandatory=False) + contact_information = Loop(DejitContactInformation, max=1, mandatory=False) + + +class DejitReferenceDateTimePeriodN(SegmentGroup): + """SG1,SG8,SG13 RFF-DTM""" + account_reference = Segment("RFF", mandatory=True) + document_date = Segment("DTM", mandatory=False) + + +class DejitOrderReference(SegmentGroup): + """SG8 RFF""" + reference = Segment("RFF", mandatory=True) + + +class DejitTransportInformation(SegmentGroup): + """SG9 TDT-TMD""" + transport_info = Segment("TDT", mandatory=True) + transport_movement_details = Segment("TMD", mandatory=False) + + +class DejitPlaceLocationIdentification(SegmentGroup): + """SG10 LOC""" + place_location = Segment("LOC", mandatory=True) + communication_info = Loop(DejitContactInformation, max=5, mandatory=False) + + +class DejitPickUpInformation(SegmentGroup): + """SG12 QTY-SCC-DTM-SG13""" + pickup_quantity = Segment("QTY", mandatory=True) + scheduling_conditions = Segment("SCC", mandatory=False) + pickup_time = Segment("DTM", mandatory=False) + reference_period = Loop(DejitReferenceDateTimePeriodN, max=99, mandatory=False) + + +class DejitPriceCurrDate(SegmentGroup): + """SG14 PRI-CUX-DTM""" + price = Segment("PRI", mandatory=True) + currency = Segment("SCC", mandatory=False) + date_time_period = Loop(DejitDatetime, max=9, mandatory=False) + + +class DejitLineInformation(SegmentGroup): + """SG7 LIN-IMD""" + # 21 + line_item = Segment("LIN", mandatory=True) + additional_prod_id = Loop(DejitAdditionalProductId, max=10, mandatory=False) + # 22 + article_descriptions = Loop(DejitArticleDescription, max=99, mandatory=False) + additional_info = Loop(DejitAdditionalInformation, max=5, mandatory=False) + related_id_num = Loop(DejitRelatedIdNumber, max=5, mandatory=False) + free_text = Loop(DejitFreeText, max=5, mandatory=False) + package = Loop(DejitPackage, max=99, mandatory=False) + date_period = Loop(DejitDatetime, max=9, mandatory=False) + # 23 SG8 + order_number = Loop(DejitReferenceDateTimePeriodN, max=9, mandatory=False) + # 24 SG8 + pus_slb_consignment_no = Loop(DejitReferenceDateTimePeriodN, max=9, mandatory=False) + transport_info = Loop(DejitTransportInformation, max=9, mandatory=False) + # 25 SG10 + place_location_id1 = Loop(DejitPlaceLocationIdentification, max=99, mandatory=False) + # 26 SG10 + place_location_id2 = Loop(DejitPlaceLocationIdentification, max=99, mandatory=False) + # 27 SG10 + point_of_use = Loop(DejitPlaceLocationIdentification, max=99, mandatory=False) + # 28,29 SG12 + menge_date = Loop(DejitPickUpInformation, max=999, mandatory=False) + price_curr_date = Loop(DejitPriceCurrDate, max=9, mandatory=False) + + +class DejitPackageInformation(SegmentGroup): + """SG6 PCI-GIN""" + # 19 + package_info = Segment("PCI", mandatory=True) + # 20 + manifest_number = Loop(DejitManifest, max=10, mandatory=False) + + +class DejitItemLines(SegmentGroup): + """SG5 PAC-SG6""" + # 18 + package = Segment("PAC", mandatory=True) + # 19,20 SG6 + package_info = Loop(DejitPackageInformation, max=1, mandatory=False) + + +class DejitDeliverySequenceDetail(SegmentGroup): + """SG4 SEQ-SG5-SG7""" + # 17 + sequence_details = Segment("SEQ", mandatory=True) + delivery_datetime = Loop(DejitDatetime, max=5, mandatory=False) + related_id_num = Loop(DejitRelatedIdNumber, max=99, mandatory=False) + location = Loop(DejitLocation, max=5, mandatory=False) + # 18,19,20 SG5 + item_lines = Loop(DejitItemLines, max=1, mandatory=True) + # 21-29 SG7 + line_info = Loop(DejitLineInformation, max=9999, mandatory=False) + + +class DeljitMessage(SegmentGroup): + """Groups message due to EDI implementation guide DELJIT UN D.04B S3 + more info https://www.stylusstudio.com/edifact/D04B/DELJIT.htm """ + + # --- Heading section gets checked automatically--- + + # 1 + # service_str_advice = Segment("UNA", mandatory=False) + # 2 + # interchange_header = Segment("UNB", mandatory=True) + # 3 + # message_header = Segment("UNH", mandatory=True) + # 4 + beginning_msg = Segment("BGM", mandatory=True) + # 5 + dispatch_call_created = Segment("DTM", mandatory=True) + # 6 + eta_truck_control = Loop(DejitDatetime, max=10, mandatory=False) + # 7 + lsp_chattanooga = Loop(DejitDatetime, max=10, mandatory=True) + free_text = Loop(DejitFreeText, max=5, mandatory=False) + # 8 SG1 + transport_id = Loop(DejitReferenceDateTimePeriodN, max=10, mandatory=True) + # 9 SG1 + relation_no = Loop(DejitReferenceDateTimePeriodN, max=10, mandatory=True) + # 10 SG1 + dispatch_calloff_number = Loop( + DejitReferenceDateTimePeriodN, max=10, mandatory=True) + # 11 SG1 + special_transport_number = Loop( + DejitReferenceDateTimePeriodN, max=10, mandatory=True) + # 12,13,14 SG2 + driver_information = Loop( + DejitNamesAddressFullInformation, max=1, mandatory=True) + # 15 SG2 + supplier_info = Loop( + DejitNamesAddressInformation, max=1, mandatory=True) + # 16 SG2 + recipient_info = Loop( + DejitNamesAddressInformation, max=1, mandatory=True) + # 17-29 SG4 + delivery_sequence = Loop( + DejitDeliverySequenceDetail, max=9999, mandatory=True) + # 30 + message_trailer = Segment("UNT", mandatory=True) + # 31 + interchange_trailer = Segment("UNZ", mandatory=True) + + +def validate_deljit(inc_message): + """Validates deljit D:04B:UND message, returns it's message""" + type_to_parser_dict = {"DELJIT": DeljitMessage} + from_str = Interchange.from_str(inc_message) + message = next(from_str.Interchange.get_messages()) + cls = type_to_parser_dict.get(message.type) + if not cls: + raise NotImplementedError("Unsupported message type '{}'".format(message.type)) + return message From aeaefa74cb81ebb64f6af8b00f984abb82c374c1 Mon Sep 17 00:00:00 2001 From: vnikolayev1 Date: Mon, 17 Apr 2023 19:58:28 +0300 Subject: [PATCH 12/14] [ADD] 5052 Edifact - EDI implementation - import/typo fix --- pydifact/msg_validation.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pydifact/msg_validation.py b/pydifact/msg_validation.py index 27bd711..9b7572e 100644 --- a/pydifact/msg_validation.py +++ b/pydifact/msg_validation.py @@ -1,5 +1,5 @@ -from mapping import SegmentGroup, Segment, Loop -from segmentcollection import Interchange +from .mapping import SegmentGroup, Segment, Loop +from .segmentcollection import Interchange class DelforTransportInformation(SegmentGroup): @@ -176,11 +176,6 @@ class DejitDatetime(SegmentGroup): delivery_datetime = Segment("DTM", mandatory=True) -class DejitDatetime(SegmentGroup): - """Not a segment group""" - delivery_datetime = Segment("DTM", mandatory=True) - - class DejitRelatedIdNumber(SegmentGroup): """Not a segment group""" related_id_num = Segment("GIR", mandatory=True) From df73bbb2c74831efe96c0abd99bb255476a8aa02 Mon Sep 17 00:00:00 2001 From: vnikolayev1 Date: Mon, 17 Apr 2023 20:15:12 +0300 Subject: [PATCH 13/14] [ADD] 5052 Edifact - EDI implementation - import/typo fix --- pydifact/msg_validation.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pydifact/msg_validation.py b/pydifact/msg_validation.py index 9b7572e..53f5cce 100644 --- a/pydifact/msg_validation.py +++ b/pydifact/msg_validation.py @@ -150,8 +150,7 @@ class DelforMessage(SegmentGroup): def validate_delfor(inc_message): """Validates delfor UND96A message, returns it's object""" type_to_parser_dict = {"DELFOR": DelforMessage} - from_str = Interchange.from_str(inc_message) - message = next(from_str.Interchange.get_messages()) + message = next(inc_message.Interchange.get_messages()) cls = type_to_parser_dict.get(message.type) if not cls: raise NotImplementedError("Unsupported message type '{}'".format(message.type)) @@ -380,8 +379,7 @@ class DeljitMessage(SegmentGroup): def validate_deljit(inc_message): """Validates deljit D:04B:UND message, returns it's message""" type_to_parser_dict = {"DELJIT": DeljitMessage} - from_str = Interchange.from_str(inc_message) - message = next(from_str.Interchange.get_messages()) + message = next(inc_message.Interchange.get_messages()) cls = type_to_parser_dict.get(message.type) if not cls: raise NotImplementedError("Unsupported message type '{}'".format(message.type)) From 1deec29e1dade68abf1b772acc7a309a43319e1f Mon Sep 17 00:00:00 2001 From: vnikolayev1 Date: Mon, 17 Apr 2023 20:18:05 +0300 Subject: [PATCH 14/14] [ADD] 5052 Edifact - EDI implementation - import/typo fix --- pydifact/msg_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydifact/msg_validation.py b/pydifact/msg_validation.py index 53f5cce..6174cb9 100644 --- a/pydifact/msg_validation.py +++ b/pydifact/msg_validation.py @@ -150,7 +150,7 @@ class DelforMessage(SegmentGroup): def validate_delfor(inc_message): """Validates delfor UND96A message, returns it's object""" type_to_parser_dict = {"DELFOR": DelforMessage} - message = next(inc_message.Interchange.get_messages()) + message = next(inc_message.get_messages()) cls = type_to_parser_dict.get(message.type) if not cls: raise NotImplementedError("Unsupported message type '{}'".format(message.type)) @@ -379,7 +379,7 @@ class DeljitMessage(SegmentGroup): def validate_deljit(inc_message): """Validates deljit D:04B:UND message, returns it's message""" type_to_parser_dict = {"DELJIT": DeljitMessage} - message = next(inc_message.Interchange.get_messages()) + message = next(inc_message.get_messages()) cls = type_to_parser_dict.get(message.type) if not cls: raise NotImplementedError("Unsupported message type '{}'".format(message.type))