edison-parser 0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Amadeus s.a.s
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ exclude CHANGELOG.md
2
+ exclude CONTRIBUTING.md
3
+ exclude .gitignore
4
+ exclude .pre-commit-config.yaml
5
+ recursive-exclude .github *
6
+ recursive-exclude docs *
7
+ recursive-exclude tests *
@@ -0,0 +1,426 @@
1
+ Metadata-Version: 2.4
2
+ Name: edison-parser
3
+ Version: 0.1
4
+ Summary: An EDIFACT parser with a real syntax tree.
5
+ Author-email: Alex Prengère <alex.prengere@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AmadeusITGroup/edison
8
+ Project-URL: Changelog, https://github.com/AmadeusITGroup/edison/blob/main/CHANGELOG.md
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Programming Language :: Python :: Implementation :: CPython
17
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pydot>=3.0.0
23
+ Requires-Dist: typing-extensions>=4.10
24
+ Dynamic: license-file
25
+
26
+ <div align="center">
27
+ <img src="docs/edison.png" width="360" height="360" alt="Edison">
28
+ <h1>Edison</h1>
29
+ <p><strong>An EDIFACT parser with a real syntax tree.</strong></p>
30
+ </div>
31
+
32
+ Edison is a Python library for parsing EDIFACT messages. Instead of treating a
33
+ message as a flat stream of segments, it models valid messages as a tree of
34
+ transitions. You define the schema once, then parse, validate, or pretty-print
35
+ messages against that structure.
36
+
37
+ ## Highlights
38
+
39
+ - Define EDIFACT syntax trees directly in Python.
40
+ - Attach parsing callbacks to the segments you care about.
41
+ - Select schemas by message version.
42
+ - Model conditional, mandatory, and repeatable segments.
43
+ - Handle escaped EDIFACT separators and EBCDIC control characters.
44
+ - Properly tokenize segments with specified lengths like `BLB`
45
+ - Export transition graphs with `pydot`.
46
+
47
+ ## Installation
48
+
49
+ Edison requires Python 3.12 or newer.
50
+
51
+ ```bash
52
+ pip install edison-parser
53
+ ```
54
+
55
+ ## Quick start
56
+
57
+ Subclass `BaseParser`, define one or more schemas, and attach callback method
58
+ names to the schema nodes you want to process.
59
+
60
+ ```python
61
+ from edison import BaseParser
62
+
63
+
64
+ class PIZZAQParser(BaseParser):
65
+ schemas = {
66
+ (0, 0): [
67
+ ["UNH"],
68
+ ["CVR"],
69
+ ["REF"],
70
+ [
71
+ "PIZ",
72
+ [
73
+ ["TYP", [], "parse_typ"],
74
+ ["REF", [], "parse_ref"],
75
+ ],
76
+ ],
77
+ ["UNT"],
78
+ ],
79
+ }
80
+
81
+ def __init__(self):
82
+ self.summary = {}
83
+ self.pizza = None
84
+
85
+ def parse_typ(self, segment: str):
86
+ self.pizza = segment[4:]
87
+
88
+ def parse_ref(self, segment: str):
89
+ refs = segment[4:].split("*")
90
+ self.summary[self.pizza] = len(refs)
91
+
92
+
93
+ edi = """
94
+ UNH+1+PIZZAQ:1:1:1A+00012480825035'
95
+ CVR+USD'
96
+ REF+LUIS'
97
+ PIZ+1'
98
+ TYP+MARGARITA'
99
+ REF+1*2'
100
+ PIZ+2'
101
+ TYP+PESTO'
102
+ REF+3'
103
+ UNT+19+1'
104
+ """
105
+
106
+ parser = PIZZAQParser()
107
+ parser.parse(edi)
108
+
109
+ assert parser.summary == {"MARGARITA": 2, "PESTO": 1}
110
+ assert PIZZAQParser.validate(edi)
111
+ print(PIZZAQParser.show(edi))
112
+ ```
113
+
114
+ <div align="center">
115
+ <img src="docs/Image.png" width="360" height="360" alt="Edison">
116
+ <p><strong>Tree-based structure.</strong></p>
117
+ </div>
118
+
119
+ ## Schema format
120
+
121
+ A schema is a dictionary keyed by the minimum supported version:
122
+
123
+ ```python
124
+ schemas = {
125
+ (0, 0): [
126
+ ["UNH"],
127
+ ["BGM"],
128
+ ["UNT"],
129
+ ],
130
+ }
131
+ ```
132
+
133
+ Each segment node accepts these positional fields:
134
+
135
+ ```python
136
+ ["TAG", children, action, repetition, flag]
137
+ ```
138
+
139
+ | Field | Default | Meaning |
140
+ | --- | --- | --- |
141
+ | `TAG` | required | Three-character EDIFACT segment tag. |
142
+ | `children` | `[]` | Nested segment group. |
143
+ | `action` | `None` | Parser method name called with the raw segment. |
144
+ | `repetition` | `99999` | `1` means single-use; any higher value means repeatable. |
145
+ | `flag` | `"C"` | `"C"` for conditional, `"M"` for mandatory. |
146
+
147
+ ## Main API
148
+
149
+ | API | Use |
150
+ | --- | --- |
151
+ | `parser.parse(edi, version=None)` | Walk an EDI message and call matching segment actions. |
152
+ | `Parser.validate(edi, version=None)` | Return `True` when all segments follow the schema transitions. |
153
+ | `Parser.show(edi, version=None, colorize=True)` | Return a formatted, indented representation of an EDI message. |
154
+ | `Parser.build_dot_object(version=None, include_up_links=True)` | Build a `pydot.Dot` transition graph. |
155
+ | `extract_envelope_headers(edi)` | Quickly return the `UNB` and `UNH` envelope segments. |
156
+ | `extract_envelope_footers(edi)` | Quickly return the `UNT` and `UNZ` envelope segments. |
157
+ | `escape(data)` / `unescape(data)` | Escape or unescape EDIFACT separators: `+`, `:`, `*`, `'`. |
158
+ | `normalize_ebcdic_separators(edi)` | Convert EBCDIC EDIFACT separators to ASCII safely. |
159
+
160
+ ## Advanced usage
161
+
162
+ <details>
163
+ <summary>Versioned syntax trees</summary>
164
+
165
+ Define multiple syntax trees when the same message evolves across versions. The
166
+ parser selects the highest schema version less than or equal to the `version`
167
+ argument, while parsing callbacks can stay the same.
168
+
169
+ ```python
170
+ from edison import BaseParser
171
+
172
+
173
+ class RouteParser(BaseParser):
174
+ schemas = {
175
+ (0, 0): [
176
+ ["UNH"],
177
+ ["ORG", [], "parse_origin"],
178
+ ["DST", [], "parse_destination"],
179
+ ["UNT"],
180
+ ],
181
+ (2, 0): [
182
+ ["UNH"],
183
+ [
184
+ "LEG",
185
+ [
186
+ ["ORG", [], "parse_origin"],
187
+ ["DST", [], "parse_destination"],
188
+ ],
189
+ ],
190
+ ["UNT"],
191
+ ],
192
+ }
193
+
194
+ def __init__(self):
195
+ self.route = {}
196
+
197
+ def parse_origin(self, segment: str):
198
+ self.route["origin"] = segment[4:]
199
+
200
+ def parse_destination(self, segment: str):
201
+ self.route["destination"] = segment[4:]
202
+
203
+
204
+ legacy = RouteParser()
205
+ legacy.parse("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))
206
+
207
+ modern = RouteParser()
208
+ modern.parse("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))
209
+
210
+ assert legacy.route == modern.route == {
211
+ "origin": "PAR",
212
+ "destination": "NCE",
213
+ }
214
+ assert RouteParser.validate("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))
215
+ assert RouteParser.validate("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))
216
+ ```
217
+
218
+ When `version` is omitted, incomplete, or newer than all known schemas, Edison
219
+ uses the latest schema. If a requested version is older than the first schema,
220
+ `find_transitions()` raises `ValueError`.
221
+
222
+ </details>
223
+
224
+ <details>
225
+ <summary>Mandatory and repeatable segments</summary>
226
+
227
+ `repetition` controls whether the transition graph can come back to the same
228
+ node:
229
+
230
+ - `1` means the node can be used once in a given sequence.
231
+ - Any value greater than `1` means the node is repeatable. The transition graph
232
+ only distinguishes `1` from "repeatable"; it does not count exact occurrence
233
+ limits such as `2` versus `99999`.
234
+
235
+ Mandatory nodes constrain valid paths through the tree. Once a mandatory node is
236
+ next in a sequence, the parser cannot skip over it to reach a later sibling, a
237
+ parent, or a child that would bypass it. A mandatory child does not make its
238
+ parent mandatory by itself: if the whole group is required, mark the group node
239
+ as `"M"` too.
240
+
241
+ ```python
242
+ from edison import BaseParser
243
+
244
+
245
+ class OrderParser(BaseParser):
246
+ schemas = {
247
+ (0, 0): [
248
+ ["UNH"],
249
+ [
250
+ "ITM",
251
+ [
252
+ ["DSC"],
253
+ ["QTY", [], "parse_quantity", 1, "M"],
254
+ ["PRI"],
255
+ ],
256
+ None,
257
+ 99999,
258
+ "M",
259
+ ],
260
+ ["UNT"],
261
+ ],
262
+ }
263
+
264
+ def __init__(self):
265
+ self.quantities = []
266
+
267
+ def parse_quantity(self, segment: str):
268
+ self.quantities.append(segment[4:])
269
+
270
+
271
+ assert OrderParser.validate("UNH'ITM'DSC'QTY+2'PRI'ITM'QTY+1'UNT'")
272
+ assert not OrderParser.validate("UNH'UNT'") # ITM is mandatory.
273
+ assert not OrderParser.validate("UNH'ITM'PRI'UNT'") # QTY is mandatory.
274
+ assert not OrderParser.validate("UNH'ITM'QTY+2'QTY+3'UNT'") # QTY repeats once.
275
+ ```
276
+
277
+ </details>
278
+
279
+ <details>
280
+ <summary>Shared headers, footers, and parser hooks</summary>
281
+
282
+ Use `header` and `footer` when several schemas share envelope segments. They are
283
+ automatically prepended and appended to every schema at class compilation time.
284
+
285
+ ```python
286
+ from edison import BaseParser
287
+
288
+
289
+ class EnvelopeParser(BaseParser):
290
+ header = [["UNH", [], "on_segment"]]
291
+ footer = [["UNT", [], "on_segment"]]
292
+ schemas = {(0, 0): [["BGM", [], "on_segment"]]}
293
+
294
+ def __init__(self):
295
+ self.segments = []
296
+
297
+ def set_up(self):
298
+ self.segments.clear()
299
+
300
+ def on_segment(self, segment: str):
301
+ self.segments.append(segment)
302
+
303
+ def tear_down(self):
304
+ self.count = len(self.segments)
305
+ ```
306
+
307
+ `set_up()` runs before parsing starts, and `tear_down()` runs after parsing
308
+ finishes.
309
+
310
+ </details>
311
+
312
+ <details>
313
+ <summary>Transition graphs</summary>
314
+
315
+ `pydot` is installed with Edison. To write PNG files, make sure Graphviz is
316
+ available on your system, then call `build_dot_object()` on a `BaseParser`
317
+ subclass.
318
+
319
+ ```python
320
+ from edison import BaseParser
321
+
322
+
323
+ class Parser(BaseParser):
324
+ schemas = {
325
+ (0, 0): [
326
+ ["UNH"],
327
+ [
328
+ "BGM",
329
+ [
330
+ ["DTM"],
331
+ ],
332
+ ],
333
+ ["UNT"],
334
+ ],
335
+ }
336
+
337
+
338
+ dot = Parser.build_dot_object()
339
+ dot.write_png("transitions.png")
340
+ ```
341
+
342
+ You can pass the same version selector used by parsing, and hide upward links
343
+ for a simpler tree-like graph:
344
+
345
+ ```python
346
+ dot = Parser.build_dot_object(version=(13, 2), include_up_links=False)
347
+ dot.write_png("transitions-v13.png")
348
+ ```
349
+
350
+ Because `build_dot_object()` returns the underlying `pydot.Dot` object, you can
351
+ customize it before writing.
352
+
353
+ </details>
354
+
355
+ <details>
356
+ <summary>Escaping and EBCDIC EDIFACT separators</summary>
357
+
358
+ Edison understands escaped EDIFACT separators and can normalize EBCDIC separator
359
+ characters often found in binary payloads.
360
+
361
+ ```python
362
+ from edison import normalize_ebcdic_separators, escape, unescape
363
+
364
+
365
+ assert escape("REC+") == r"REC\+"
366
+ assert unescape(r"REC\+") == "REC+"
367
+ assert normalize_ebcdic_separators("UNB\x1dSENDER\x1fREC") == "UNB+SENDER:REC"
368
+ ```
369
+
370
+ The segment tokenizer also handles escaped quotes, so content such as
371
+ `IFT+1+AIRLINE\'SERVER'` stays in a single EDIFACT segment.
372
+
373
+ </details>
374
+
375
+ <details>
376
+ <summary>EDIFACT envelope extraction</summary>
377
+
378
+ For the common case of reading EDIFACT envelope boundaries, use
379
+ `extract_envelope_headers()` to lazily return the first `UNB`/`UNH` segments or
380
+ `extract_envelope_footers()` to lazily return the last `UNT`/`UNZ` segments:
381
+
382
+ ```python
383
+ from edison import extract_envelope_footers, extract_envelope_headers
384
+
385
+ edi = "UNB+SENDER+RECEIVER'UNH+1+PIZZAQ:1:1:1A'BGM+1'UNT+3+1'UNZ+1+1'"
386
+ headers = extract_envelope_headers(edi)
387
+ footers = extract_envelope_footers(edi)
388
+
389
+ assert headers.unb == "UNB+SENDER+RECEIVER"
390
+ assert headers.unh == "UNH+1+PIZZAQ:1:1:1A"
391
+ assert footers.unt == "UNT+3+1"
392
+ assert footers.unz == "UNZ+1+1"
393
+ ```
394
+
395
+ </details>
396
+
397
+ ## Development
398
+
399
+ <details>
400
+ <summary>Local commands</summary>
401
+
402
+ This repository uses `uv`, `tox`, `pytest`, `ruff`, `black`, `ty`, and
403
+ pre-commit hooks.
404
+
405
+ ```bash
406
+ uv tool install tox --with tox-uv
407
+ tox -e py
408
+ ```
409
+
410
+ Build distributions with:
411
+
412
+ ```bash
413
+ uv build --sdist --wheel -o dist/
414
+ ```
415
+
416
+ Run the full pre-commit suite with:
417
+
418
+ ```bash
419
+ uv tool install prek==0.4.10
420
+ prek run --all-files
421
+ ```
422
+
423
+ CI runs tests through tox on CPython 3.12 and 3.14 across Linux, Windows, and
424
+ macOS, then builds source and wheel distributions.
425
+
426
+ </details>