34
34
35
35
36
36
class AbstractSegmentsContainer :
37
- """Represent a collection of EDI Segments for both reading and writing .
37
+ """Abstract base class of subclasses containing collection of segments .
38
38
39
- You should not instantiate AbstractSegmentsContainer itself, but subclass it use that.
39
+ :class:`AbstractSegmentsContainer` is the superclass of several classes such as
40
+ :class:`RawSegmentCollection` and :class:`Interchange` and contains methods common
41
+ to them.
40
42
41
- The segments list in AbstractSegmentsContainer includes header and footer segments too.
42
- Inheriting envelopes must NOT include these elements in .segments, as get_header_element() and
43
- get_footer_element() should provide these elements on-the-fly.
43
+ **Implementation detail:** Subclasses must set :attr:`HEADER_TAG` and
44
+ :attr:`FOOTER_TAG`.
44
45
45
- Inheriting classes must set HEADER_TAG and FOOTER_TAG
46
+ :param extra_header_elements: A list of elements to be appended at the end
47
+ of the header segment (same format as :class:`~pydifact.segments.Segment`
48
+ constructor elements).
49
+
50
+ :param characters: The set of control characters
51
+
52
+ .. attribute:: segments
53
+
54
+ The segments that comprise the container. This does not include the envelope
55
+ (that is, the header and footer) segments. To get the envolope segments, use
56
+ as :meth:`get_header_segment` and :meth:`get_footer_segment`.
57
+
58
+ .. attribute:: characters
59
+
60
+ The control characters (a :class:`~pydifact.control.Characters` object).
46
61
"""
47
62
48
63
HEADER_TAG : str = None
@@ -53,13 +68,6 @@ def __init__(
53
68
extra_header_elements : List [Union [str , List [str ]]] = None ,
54
69
characters : Optional [Characters ] = None ,
55
70
):
56
- """
57
- :param extra_header_elements: a list of elements to be appended at the end
58
- of the header segment (same format as Segment() constructor *elements).
59
- :param characters: the set of control characters
60
- """
61
-
62
- # The segments that make up this message
63
71
self .segments = []
64
72
65
73
# set of control characters
@@ -81,10 +89,9 @@ def from_str(
81
89
) -> "AbstractSegmentsContainer" :
82
90
"""Create an instance from a string.
83
91
84
- This method is intended for usage in inheriting classes, not it AbstractSegmentsContainer itself.
85
- :param string: The EDI content
86
- :param parser: A parser to convert the tokens to segments, defaults to `Parser`
87
- :param characters: the set of control characters
92
+ :param string: The EDI content.
93
+ :param parser: A parser to convert the tokens to segments; defaults to `Parser`.
94
+ :param characters: The set of control characters.
88
95
"""
89
96
if parser is None :
90
97
parser = Parser (characters = characters )
@@ -99,11 +106,12 @@ def from_segments(
99
106
segments : Union [List , Iterable ],
100
107
characters : Optional [Characters ] = None ,
101
108
) -> "AbstractSegmentsContainer" :
102
- """Create a new AbstractSegmentsContainer instance from a iterable list of segments.
109
+ """Create an instance from a list of segments.
103
110
104
- :param segments: The segments of the EDI interchange
105
- :param characters: the set of control characters
111
+ :param segments: The segments of the EDI interchange.
106
112
:type segments: list/iterable of Segment
113
+
114
+ :param characters: The set of control characters.
107
115
"""
108
116
109
117
# create a new instance of AbstractSegmentsContainer and return it
@@ -115,11 +123,13 @@ def get_segments(
115
123
name : str ,
116
124
predicate : Callable = None , # Python3.9+ Callable[[Segment], bool]
117
125
) -> list :
118
- """Get all the segments that match the requested name.
126
+ """Get all segments that match the requested name.
119
127
120
- :param name: The name of the segments to return
121
- :param predicate: Optional predicate callable that returns True if the given segment matches a condition
122
- :rtype: list of Segment
128
+ :param name: The name of the segments to return.
129
+ :param predicate: Optional callable that accepts a segment as argument.
130
+ Only segments for which the returned value is ``True'' are returned.
131
+
132
+ :rtype: list of :class:`Segment` objects.
123
133
"""
124
134
for segment in self .segments :
125
135
if segment .tag == name and (predicate is None or predicate (segment )):
@@ -132,10 +142,11 @@ def get_segment(
132
142
) -> Optional [Segment ]:
133
143
"""Get the first segment that matches the requested name.
134
144
135
- :return: The requested segment, or None if not found
136
- :param name: The name of the segment to return
145
+ :param name: The name of the segment to return.
137
146
:param predicate: Optional predicate that must match on the segments
138
- to return
147
+ to return.
148
+
149
+ :return: The requested segment, or None if not found.
139
150
"""
140
151
for segment in self .get_segments (name , predicate ):
141
152
return segment
@@ -146,17 +157,18 @@ def split_by(
146
157
self ,
147
158
start_segment_tag : str ,
148
159
) -> Iterable : # Python3.9+ Iterable["RawSegmentCollection"]
149
- """Split a segment collection by tag.
150
-
151
- Everything before the first start segment is ignored, so if no matching
152
- start segment is found at all, returned result is empty.
160
+ """Split the segment collection by tag.
153
161
162
+ Assuming the collection contains tags ``["A", "B", "A", "A", "B", "D"]``,
163
+ ``split_by("A")`` would return ``[["A", "B"], ["A"], ["A", "B", "D"]]``.
164
+ Everything before the first start segment is ignored, so if no matching start
165
+ segment is found at all, the returned result is empty.
154
166
155
167
:param start_segment_tag:
156
168
the segment tag we want to use as separator
157
169
158
- :return: generator of segment collections. The start tag is included in
159
- each yielded collection
170
+ :return: Generator of segment collections. The start tag is included in
171
+ each yielded collection.
160
172
"""
161
173
current_list = None
162
174
@@ -176,13 +188,16 @@ def split_by(
176
188
def add_segments (
177
189
self , segments : Union [List [Segment ], Iterable ]
178
190
) -> "AbstractSegmentsContainer" :
179
- """Add multiple segments to the collection. Passing a UNA segment means setting/overriding the control
180
- characters and setting the serializer to output the Service String Advice. If you wish to change the control
181
- characters from the default and not output the Service String Advice, change self.characters instead,
182
- without passing a UNA Segment.
191
+ """Append a list of segments to the collection.
183
192
184
- :param segments: The segments to add
185
- :type segments: list or iterable of Segments
193
+ For the :class:`Interchange` subclass, passing a ``UNA`` segment means
194
+ setting/overriding the control characters and setting the serializer to output
195
+ the Service String Advice. If you wish to change the control characters from the
196
+ default and not output the Service String Advice, change :attr:`characters`
197
+ instead, without passing a ``UNA`` Segment.
198
+
199
+ :param segments: The segments to add.
200
+ :type segments: List or iterable of :class:`~pydifact.segments.Segment` objects.
186
201
"""
187
202
for segment in segments :
188
203
self .add_segment (segment )
@@ -192,31 +207,41 @@ def add_segments(
192
207
def add_segment (self , segment : Segment ) -> "AbstractSegmentsContainer" :
193
208
"""Append a segment to the collection.
194
209
195
- Note: skips segments that are header or footer tags of this segment container type.
210
+ Note: skips segments that are header or footer tags of this segment container
211
+ type.
212
+
196
213
:param segment: The segment to add
197
214
"""
198
215
if not segment .tag in (self .HEADER_TAG , self .FOOTER_TAG ):
199
216
self .segments .append (segment )
200
217
return self
201
218
202
219
def get_header_segment (self ) -> Optional [Segment ]:
203
- """Craft and return this container header segment (if any)
220
+ """Craft and return a header segment.
221
+
222
+ :meth:`get_header_segment` creates and returns an appropriate
223
+ :class:`~pydifact.segments.Segment` object that can serve as a header of the
224
+ current object. This is useful, for example, when serializing the current object.
204
225
205
- :returns: None if there is no header for that container
226
+ Although the current object may have been created by reading a string (e.g.
227
+ with :meth:`from_str`), :meth:`get_header_segment` does not return the header
228
+ segment that was read by the string; that segment would have been useful only
229
+ during reading and it is the job of :meth:`from_str` to check it.
206
230
"""
207
231
return None
208
232
209
233
def get_footer_segment (self ) -> Optional [Segment ]:
210
- """Craft and return this container footer segment (if any)
234
+ """Craft and return a footer segment.
211
235
212
- :returns: None if there is no footer for that container
236
+ This is similar to :meth:`get_header_segment`, but for the footer segment.
213
237
"""
214
238
return None
215
239
216
240
def serialize (self , break_lines : bool = False ) -> str :
217
- """Serialize all the segments added to this object.
241
+ """Return the string representation of the object.
218
242
219
- :param break_lines: if True, insert line break after each segment terminator.
243
+ :param break_lines: If ``True``, inserts line break after each segment
244
+ terminator.
220
245
"""
221
246
header = self .get_header_segment ()
222
247
footer = self .get_footer_segment ()
@@ -235,18 +260,13 @@ def serialize(self, break_lines: bool = False) -> str:
235
260
)
236
261
237
262
def validate (self ):
238
- """Validates this container .
263
+ """Validate the object .
239
264
240
- This method must be overridden in implementing subclasses, and should make sure that
241
- the container is implemented correctly.
242
-
243
- It does not return anything and should raise an Exception in case of errors.
265
+ Raises an exception if the object is invalid.
244
266
"""
245
267
raise NotImplementedError
246
268
247
269
def __str__ (self ) -> str :
248
- """Allow the object to be serialized by casting to a string."""
249
-
250
270
return self .serialize ()
251
271
252
272
@@ -352,12 +372,12 @@ def validate(self):
352
372
353
373
class Message (AbstractSegmentsContainer ):
354
374
"""
355
- A message (started by UNH segment, ended by UNT segment)
375
+ A message (started by UNH_ segment, ended by UNT_ segment)
356
376
357
377
Optional features of UNH are not yet supported.
358
378
359
- https://www.stylusstudio.com/edifact/40100/UNH_.htm
360
- https://www.stylusstudio.com/edifact/40100/UNT_.htm
379
+ .. _UNH: https://www.stylusstudio.com/edifact/40100/UNH_.htm
380
+ .. _UNT: https://www.stylusstudio.com/edifact/40100/UNT_.htm
361
381
"""
362
382
363
383
HEADER_TAG = "UNH"
@@ -406,19 +426,20 @@ def validate(self):
406
426
407
427
408
428
class Interchange (FileSourcableMixin , UNAHandlingMixin , AbstractSegmentsContainer ):
409
- """
410
- An interchange (started by UNB segment, ended by UNZ segment)
429
+ """An EDIFACT interchange.
411
430
412
- Optional features of UNB are not yet supported.
431
+ In EDIFACT, the **interchange** is the entire document at the highest level. Except
432
+ for its header (a UNB_ segment) and footer (a UNZ_ segment), it consists of one or
433
+ more **messages**.
413
434
414
- Functional groups are not yet supported
435
+ :class:`Interchange` currently does not support functional groups and optional
436
+ features of UNB.
415
437
416
- Messages are supported, see get_message() and get_message(), but are
417
- optional: interchange segments can be accessed without going through
418
- messages.
438
+ :class:`Interchange` supports all methods of :class:`AbstractSegmentsContainer` plus
439
+ some additional methods.
419
440
420
- https://www.stylusstudio.com/edifact/40100/UNB_.htm
421
- https://www.stylusstudio.com/edifact/40100/UNZ_.htm
441
+ .. _UNB: https://www.stylusstudio.com/edifact/40100/UNB_.htm
442
+ .. _UNZ: https://www.stylusstudio.com/edifact/40100/UNZ_.htm
422
443
"""
423
444
424
445
HEADER_TAG = "UNB"
@@ -455,11 +476,6 @@ def get_header_segment(self) -> Segment:
455
476
)
456
477
457
478
def get_footer_segment (self ) -> Segment :
458
- """:returns a (UNZ) footer segment with correct segment count and control reference.
459
-
460
- It counts either of the number of messages or, if used, of the number of functional groups
461
- in an interchange (TODO)."""
462
-
463
479
# FIXME: count functional groups (UNG/UNE) correctly
464
480
cnt = 0
465
481
for segment in self .segments :
@@ -475,11 +491,12 @@ def get_footer_segment(self) -> Segment:
475
491
)
476
492
477
493
def get_messages (self ) -> List [Message ]:
478
- """parses a list of messages out of the internal segments .
494
+ """Get list of messages in the interchange .
479
495
480
- :raises EDISyntaxError if constraints are not met (e.g. UNH/UNT both correct)
496
+ Using :meth:`get_messages` is optional; interchange segments can be accessed
497
+ directly without going through messages.
481
498
482
- TODO: parts of this here are better done in the validate() method
499
+ :raises: :class:`EDISyntaxError` if the interchange contents are not correct.
483
500
"""
484
501
485
502
message = None
@@ -511,6 +528,7 @@ def get_messages(self) -> List[Message]:
511
528
raise EDISyntaxError ("UNH segment was not closed with a UNT segment." )
512
529
513
530
def add_message (self , message : Message ) -> "Interchange" :
531
+ """Append a message to the interchange."""
514
532
segments = (
515
533
[message .get_header_segment ()]
516
534
+ message .segments
0 commit comments