Skip to content

Commit 23f218d

Browse files
theangryangelBlommaertsEdwin
authored andcommitted
Adds high level mapping functionality, initial version
1 parent fbb8fa3 commit 23f218d

File tree

4 files changed

+249
-3
lines changed

4 files changed

+249
-3
lines changed

pydifact/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def __init__(cls, name, bases, attrs):
1111
if not hasattr(cls, "plugins"):
1212
cls.plugins = []
1313
else:
14-
if not hasattr(cls, "__omitted__"):
14+
if not getattr(cls, "__omitted__", False):
1515
cls.plugins.append(cls)
1616

1717

pydifact/mapping.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import collections
2+
import itertools
3+
from typing import Callable, List, Optional, Tuple, Union
4+
5+
from pydifact.segmentcollection import Message
6+
from pydifact.api import PluginMount, EDISyntaxError
7+
from pydifact.segments import Segment as Seg, SegmentFactory
8+
9+
10+
class BiDirectionalIterator(object):
11+
"""
12+
Bi-directional iterator. Used as a convenience when parsing messages
13+
"""
14+
def __init__(self, collection):
15+
self.collection = collection
16+
self.index = 0
17+
18+
def next(self):
19+
try:
20+
result = self.collection[self.index]
21+
self.index += 1
22+
except IndexError:
23+
raise StopIteration
24+
return result
25+
26+
def prev(self):
27+
self.index -= 1
28+
if self.index < 0:
29+
raise StopIteration
30+
return self.collection[self.index]
31+
32+
def __iter__(self):
33+
return self
34+
35+
36+
class AbstractMappingComponent:
37+
"""
38+
Abstract EDIFact Component, used as a base for Segments, SegmentGroups,
39+
Loops
40+
"""
41+
def __init__(self, **kwargs):
42+
self.mandatory = kwargs.get("mandatory", False)
43+
44+
def _from_segments(self, messages):
45+
raise NotImplementedError()
46+
47+
def _to_segments(self):
48+
raise NotImplementedError()
49+
50+
51+
class Segment(AbstractMappingComponent):
52+
"""
53+
EDIFact Component.
54+
A simple wrapper for Segment
55+
"""
56+
def __init__(
57+
self,
58+
tag: str,
59+
*elements,
60+
**kwargs
61+
):
62+
super(Segment, self).__init__(**kwargs)
63+
64+
self.__component__ = SegmentFactory.create_segment(
65+
tag, [],
66+
validate=False
67+
)
68+
69+
@property
70+
def tag(self):
71+
return self.__component__.tag
72+
73+
def __str__(self):
74+
return "{} {}".format(
75+
type(self.__component__),
76+
str(self.__component__)
77+
)
78+
79+
def __getitem__(self, key):
80+
return self.__component__[key]
81+
82+
def __setitem__(self, key, value):
83+
self.__component__[key] = value
84+
85+
def validate(self) -> bool:
86+
return self.__component__.validate()
87+
88+
def _from_segments(self, iterator):
89+
segment = iterator.next()
90+
91+
if self.tag == segment.tag:
92+
self.__component__ = segment
93+
return
94+
95+
if self.mandatory:
96+
raise EDISyntaxError("Missing %s, found %s" % (self.tag, segment))
97+
98+
iterator.prev()
99+
100+
def _to_segments(self):
101+
return self.__component__
102+
103+
104+
class SegmentGroupMetaClass(type):
105+
"""
106+
Metaclass to maintain an ordered list of components.
107+
Required for compatibility with Python 3.5. In 3.6 the
108+
properties of a class are strictly ordered.
109+
"""
110+
@classmethod
111+
def __prepare__(cls, name, bases):
112+
return collections.OrderedDict()
113+
114+
def __new__(cls, name, bases, classdict):
115+
result = type.__new__(cls, name, bases, dict(classdict))
116+
exclude = set(dir(type))
117+
118+
result.__components__ = []
119+
120+
for k, v in classdict.items():
121+
if k not in exclude and isinstance(v, AbstractMappingComponent):
122+
result.__components__.append(k)
123+
return result
124+
125+
126+
class SegmentGroup(AbstractMappingComponent, metaclass=SegmentGroupMetaClass):
127+
"""
128+
Describes a group of AbstractMappingComponent
129+
"""
130+
def _from_segments(self, isegment):
131+
i = 0
132+
133+
icomponent = iter(self.__components__)
134+
135+
try:
136+
while True:
137+
component_name = next(icomponent)
138+
component = getattr(self, component_name)
139+
component._from_segments(isegment)
140+
except StopIteration:
141+
pass
142+
143+
def _to_segments(self):
144+
segments = []
145+
146+
for component_name in self.__components__:
147+
component = getattr(self, component_name)
148+
component_segments = component._to_segments()
149+
150+
if isinstance(component_segments, list):
151+
segments += component_segments
152+
else:
153+
segments.append(component_segments)
154+
155+
return segments
156+
157+
def from_message(self, message):
158+
imessage = BiDirectionalIterator(message.segments)
159+
self._from_segments(imessage)
160+
161+
def to_message(self, reference_number: str, identifier: Tuple):
162+
segments = self._to_segments()
163+
return Message.from_segments(reference_number, identifier, segments)
164+
165+
def __str__(self):
166+
res = []
167+
for component_name in iter(self.__components__):
168+
component = getattr(self, component_name)
169+
res.append(str(component))
170+
return "\n".join(res)
171+
172+
173+
class Loop(AbstractMappingComponent):
174+
"""
175+
Describes a repeating SegmentGroup
176+
"""
177+
def __init__(self, component, **kwargs):
178+
super(Loop, self).__init__(**kwargs)
179+
self.min = kwargs.get("min", 0)
180+
self.max = kwargs.get("max", 0)
181+
182+
if self.mandatory and self.min < 1:
183+
self.min = 1
184+
185+
if self.max < self.min:
186+
self.max = self.min
187+
188+
self.__component__ = component
189+
self.value = []
190+
191+
def _from_segments(self, isegment):
192+
i = 0
193+
while i < self.max:
194+
195+
try:
196+
component = self.__component__()
197+
component._from_segments(isegment)
198+
self.value.append(component)
199+
except EDISyntaxError:
200+
isegment.prev()
201+
if self.mandatory and i < self.min:
202+
raise EDISyntaxError("Missing %s" % (self.__component__.__name__))
203+
break
204+
205+
i += 1
206+
207+
if i < self.min:
208+
raise EDISyntaxError("Minimum required not met")
209+
210+
def _to_segments(self):
211+
segments = []
212+
213+
for value in self.value:
214+
segments += value._to_segments()
215+
216+
return segments
217+
218+
def __str__(self):
219+
res = []
220+
for v in self.value:
221+
res.append(str(v))
222+
return "{} = {}".format(
223+
self.__component__.__name__,
224+
str(res)
225+
)
226+
227+
def __getitem__(self, key):
228+
return self.value[key]
229+
230+
def __setitem__(self, key, value):
231+
self.value[key] = value
232+

pydifact/segmentcollection.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,19 @@ def validate(self):
381381
"""
382382

383383
pass
384+
@classmethod
385+
def from_segments(
386+
cls, reference_number, identifier, segments: list or collections.Iterable
387+
) -> "AbstractSegmentsContainer":
388+
"""Create a new AbstractSegmentsContainer instance from a iterable list of segments.
389+
390+
:param reference_number: Reference number
391+
:param identifier: Identifier
392+
:param segments: The segments of the EDI interchange
393+
:type segments: list/iterable of Segment
394+
"""
395+
396+
return cls(reference_number, identifier).add_segments(segments)
384397

385398

386399
class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContainer):

pydifact/segments.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@ def create_segment(
141141
)
142142

143143
for Plugin in SegmentProvider.plugins:
144-
if Plugin().tag == name:
144+
if getattr(Plugin, "tag", "") == name:
145145
s = Plugin(name, *elements)
146+
break
146147
else:
147148
# we don't support this kind of EDIFACT segment (yet), so
148149
# just create a generic Segment()
@@ -155,4 +156,4 @@ def create_segment(
155156
)
156157

157158
# FIXME: characters is not used!
158-
return Segment(name, *elements)
159+
return s

0 commit comments

Comments
 (0)