draftedi 1.1.0__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.
- draftedi-1.1.0/LICENSE +21 -0
- draftedi-1.1.0/PKG-INFO +207 -0
- draftedi-1.1.0/README.md +174 -0
- draftedi-1.1.0/pyproject.toml +64 -0
- draftedi-1.1.0/setup.cfg +4 -0
- draftedi-1.1.0/src/draftedi/__init__.py +70 -0
- draftedi-1.1.0/src/draftedi/parser.py +400 -0
- draftedi-1.1.0/src/draftedi/validate.py +661 -0
- draftedi-1.1.0/src/draftedi.egg-info/PKG-INFO +207 -0
- draftedi-1.1.0/src/draftedi.egg-info/SOURCES.txt +14 -0
- draftedi-1.1.0/src/draftedi.egg-info/dependency_links.txt +1 -0
- draftedi-1.1.0/src/draftedi.egg-info/requires.txt +7 -0
- draftedi-1.1.0/src/draftedi.egg-info/top_level.txt +1 -0
- draftedi-1.1.0/tests/test_infra.py +55 -0
- draftedi-1.1.0/tests/test_parser.py +626 -0
- draftedi-1.1.0/tests/test_validate.py +845 -0
draftedi-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Jackson Yaw
|
|
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.
|
draftedi-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: draftedi
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Production-grade X12 EDI parser for Python
|
|
5
|
+
Author: Yaw Way
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yawway365/draftedi-core
|
|
8
|
+
Project-URL: Repository, https://github.com/yawway365/draftedi-core
|
|
9
|
+
Project-URL: Issues, https://github.com/yawway365/draftedi-core/issues
|
|
10
|
+
Keywords: edi,x12,parser,healthcare,b2b,integration,ansi
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-timeout>=2.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.13; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# draftedi
|
|
35
|
+
|
|
36
|
+
Production-grade X12 EDI parser and validator for Python. Zero dependencies.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install draftedi
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart — Parse
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from draftedi.parser import parse_edi_file
|
|
53
|
+
|
|
54
|
+
raw = open("sample.edi", "rb").read()
|
|
55
|
+
# or use bytes directly:
|
|
56
|
+
# raw = b"ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER *260222*1200*^*00501*000000001*0*P*:~..."
|
|
57
|
+
|
|
58
|
+
result = parse_edi_file(raw, source="sample.edi")
|
|
59
|
+
|
|
60
|
+
for interchange in result["interchanges"]:
|
|
61
|
+
print(interchange["isa_sender_id"]) # e.g. "SENDER "
|
|
62
|
+
print(interchange["element_sep"]) # e.g. "*"
|
|
63
|
+
for group in interchange["groups"]:
|
|
64
|
+
for transaction in group["transactions"]:
|
|
65
|
+
for segment in transaction["segments"]:
|
|
66
|
+
print(segment["segment_id"]) # e.g. "BEG", "PO1", "SE"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`parse_edi_file` returns plain `TypedDict` / `dict` instances — no custom objects, no ORM. Index directly with string keys.
|
|
70
|
+
|
|
71
|
+
## Quickstart — Validate (no spec data needed)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from draftedi.validate import check_envelope, check_element_data_type
|
|
75
|
+
|
|
76
|
+
# Envelope check: SE01 reported count vs actual parsed count
|
|
77
|
+
issues = check_envelope(transaction, transaction["segments"])
|
|
78
|
+
# issues is a list[ValidationIssue] — plain dicts
|
|
79
|
+
|
|
80
|
+
# Element data type check
|
|
81
|
+
error = check_element_data_type("20260222", "DT", "BEG03")
|
|
82
|
+
# error is None if valid, or an error string if invalid
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both functions operate on the output of `parse_edi_file` with no spec data required.
|
|
86
|
+
|
|
87
|
+
## Spec-aware validation (BYOS)
|
|
88
|
+
|
|
89
|
+
`draftedi` ships without X12 spec databases — ANSI/X12 spec data is copyrighted and cannot be bundled. Spec-aware validation follows a **Bring Your Own Spec (BYOS)** model: you supply spec data as Python dicts, and the library applies loop-aware rules against your parsed output.
|
|
90
|
+
|
|
91
|
+
Minimal example:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from draftedi.validate import build_nav_tree, assign_loop_paths, check_mandatory_segments
|
|
95
|
+
|
|
96
|
+
# spec_segments: a list of dicts you provide, following the expected schema.
|
|
97
|
+
# Each dict describes a segment's position, requirement, max use, and loop membership.
|
|
98
|
+
spec_segments = [
|
|
99
|
+
{
|
|
100
|
+
"segment_id": "BEG",
|
|
101
|
+
"segment_requirement": "M",
|
|
102
|
+
"segment_maximum_use": 1,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"type": "loop",
|
|
106
|
+
"loop_id": "PO1",
|
|
107
|
+
"segments": [
|
|
108
|
+
{
|
|
109
|
+
"segment_id": "PO1",
|
|
110
|
+
"segment_requirement": "M",
|
|
111
|
+
"segment_maximum_use": 999999,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "loop",
|
|
115
|
+
"loop_id": "N1",
|
|
116
|
+
"segments": [
|
|
117
|
+
{
|
|
118
|
+
"segment_id": "N1",
|
|
119
|
+
"segment_requirement": "O",
|
|
120
|
+
"segment_maximum_use": 200,
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
root = build_nav_tree(spec_segments)
|
|
129
|
+
loop_assignments = assign_loop_paths(transaction["segments"], root)
|
|
130
|
+
issues = check_mandatory_segments(loop_assignments, spec_segments)
|
|
131
|
+
# issues: list[ValidationIssue]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The same `loop_assignments` output can be passed to `check_segment_max_use` and `check_relational_conditions`.
|
|
135
|
+
|
|
136
|
+
## API reference
|
|
137
|
+
|
|
138
|
+
### Parser functions
|
|
139
|
+
|
|
140
|
+
| Function | Returns | Raises |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `parse_edi_file(raw_bytes: bytes, source: str = "manual upload") -> ParsedFile` | Full document hierarchy as a `ParsedFile` TypedDict | `ValueError` on malformed input |
|
|
143
|
+
| `parse_interchange(x12_text: str) -> SeparatorInfo` | Separator characters extracted from the ISA header | `ValueError` if text does not start with `ISA` or is shorter than 106 characters |
|
|
144
|
+
|
|
145
|
+
### Parser TypedDicts
|
|
146
|
+
|
|
147
|
+
The parsed hierarchy maps directly onto the X12 envelope structure.
|
|
148
|
+
|
|
149
|
+
- **`ParsedFile`** — top-level container. Key field: `interchanges` (list of `Interchange`).
|
|
150
|
+
- **`Interchange`** — one ISA/IEA envelope. Key fields: `isa_sender_id`, `isa_receiver_id`, `element_sep`, `segment_term`, `groups`.
|
|
151
|
+
- **`FunctionalGroup`** — one GS/GE envelope. Key fields: `gs_sender_id`, `gs_receiver_id`, `transactions`.
|
|
152
|
+
- **`Transaction`** — one ST/SE envelope. Key fields: `transaction_set_id`, `control_number`, `segments`.
|
|
153
|
+
- **`Segment`** — one X12 segment. Key fields: `segment_id`, `position`, `loop_path`, `elements`.
|
|
154
|
+
- **`Element`** — one element within a segment. Key fields: `element_pos`, `value_text`, `is_composite`, `components`.
|
|
155
|
+
- **`Component`** — one component within a composite element. Key fields: `component_pos`, `value_text`.
|
|
156
|
+
- **`SeparatorInfo`** — separator characters detected from the ISA header. Key fields: `element_sep`, `component_sep`, `repetition_sep`, `segment_term`.
|
|
157
|
+
|
|
158
|
+
### Validator — spec-free functions
|
|
159
|
+
|
|
160
|
+
- `check_element_data_type(value, element_type, element_id)` — validates AN, N/N0–N9, DT, TM, R, and ID element types. Returns an error string or `None`.
|
|
161
|
+
- `check_element_length(value, min_length, max_length)` — validates character length bounds. Returns an error string or `None`.
|
|
162
|
+
- `check_envelope(transaction, parsed_segments)` — checks SE01 reported segment count against the actual count of parsed segments. Returns a list of `ValidationIssue`.
|
|
163
|
+
- `check_relational_conditions(seg_id, rc_list, val_by_pos)` — checks P (Paired), R (Required), E (Exclusion), C (Conditional), and L (List Conditional) relational conditions. Returns a list of `ValidationIssue`.
|
|
164
|
+
- `leaf_loop(loop_path)` — extracts the innermost loop ID from a path string such as `"PO1/N1"` → `"N1"`. Returns `None` if input is `None`.
|
|
165
|
+
- `make_issue(severity, category, segment_id, segment_position, element_pos, message)` — constructs a `ValidationIssue` dict.
|
|
166
|
+
|
|
167
|
+
### Validator — spec-aware functions
|
|
168
|
+
|
|
169
|
+
- `build_nav_tree(spec_segments)` — converts a hierarchical spec structure into a `LoopEntry` navigation tree used by `assign_loop_paths`.
|
|
170
|
+
- `build_spec_lookup(spec_segments, parent_loop_id)` — flattens a spec into a `(segment_id, loop_id) -> spec_dict` lookup dict.
|
|
171
|
+
- `assign_loop_paths(parsed_segments, root)` — walks parsed segments and assigns a loop path to each using a stack-based state machine. Returns a list of `(segment_dict, loop_path_or_None)` tuples.
|
|
172
|
+
- `check_mandatory_segments(loop_assignments, spec_segments)` — loop-aware check for segments with M (Mandatory) requirement. Returns a list of `ValidationIssue`.
|
|
173
|
+
- `check_segment_max_use(loop_assignments, spec_segments)` — loop-aware check for segments that exceed their specified maximum use count. Returns a list of `ValidationIssue`.
|
|
174
|
+
|
|
175
|
+
### Validator types
|
|
176
|
+
|
|
177
|
+
**`ValidationIssue`** — a `TypedDict` with the following fields:
|
|
178
|
+
|
|
179
|
+
| Field | Type | Description |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `severity` | `str` | `"error"` or `"warning"` |
|
|
182
|
+
| `category` | `str` | Broad classification of the issue (e.g. `"envelope"`, `"data_type"`) |
|
|
183
|
+
| `segment_id` | `str` | ID of the segment where the issue occurred |
|
|
184
|
+
| `segment_position` | `int` | Position of the segment within the transaction |
|
|
185
|
+
| `element_pos` | `Optional[int]` | Position of the element within the segment, if applicable |
|
|
186
|
+
| `message` | `str` | Human-readable description of the issue |
|
|
187
|
+
|
|
188
|
+
**`SegEntry`** — wraps a segment ID and its spec dict. Used internally by spec-aware functions.
|
|
189
|
+
|
|
190
|
+
**`LoopEntry`** — represents a node in the navigation tree. Fields: `loop_id`, `trigger_id` (the segment ID that starts this loop), `children` (list of `LoopEntry`).
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
pip install -e ".[dev]"
|
|
196
|
+
pytest
|
|
197
|
+
ruff check src tests
|
|
198
|
+
mypy src
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Contributing
|
|
202
|
+
|
|
203
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](LICENSE).
|
draftedi-1.1.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# draftedi
|
|
2
|
+
|
|
3
|
+
Production-grade X12 EDI parser and validator for Python. Zero dependencies.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install draftedi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quickstart — Parse
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from draftedi.parser import parse_edi_file
|
|
20
|
+
|
|
21
|
+
raw = open("sample.edi", "rb").read()
|
|
22
|
+
# or use bytes directly:
|
|
23
|
+
# raw = b"ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER *260222*1200*^*00501*000000001*0*P*:~..."
|
|
24
|
+
|
|
25
|
+
result = parse_edi_file(raw, source="sample.edi")
|
|
26
|
+
|
|
27
|
+
for interchange in result["interchanges"]:
|
|
28
|
+
print(interchange["isa_sender_id"]) # e.g. "SENDER "
|
|
29
|
+
print(interchange["element_sep"]) # e.g. "*"
|
|
30
|
+
for group in interchange["groups"]:
|
|
31
|
+
for transaction in group["transactions"]:
|
|
32
|
+
for segment in transaction["segments"]:
|
|
33
|
+
print(segment["segment_id"]) # e.g. "BEG", "PO1", "SE"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`parse_edi_file` returns plain `TypedDict` / `dict` instances — no custom objects, no ORM. Index directly with string keys.
|
|
37
|
+
|
|
38
|
+
## Quickstart — Validate (no spec data needed)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from draftedi.validate import check_envelope, check_element_data_type
|
|
42
|
+
|
|
43
|
+
# Envelope check: SE01 reported count vs actual parsed count
|
|
44
|
+
issues = check_envelope(transaction, transaction["segments"])
|
|
45
|
+
# issues is a list[ValidationIssue] — plain dicts
|
|
46
|
+
|
|
47
|
+
# Element data type check
|
|
48
|
+
error = check_element_data_type("20260222", "DT", "BEG03")
|
|
49
|
+
# error is None if valid, or an error string if invalid
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Both functions operate on the output of `parse_edi_file` with no spec data required.
|
|
53
|
+
|
|
54
|
+
## Spec-aware validation (BYOS)
|
|
55
|
+
|
|
56
|
+
`draftedi` ships without X12 spec databases — ANSI/X12 spec data is copyrighted and cannot be bundled. Spec-aware validation follows a **Bring Your Own Spec (BYOS)** model: you supply spec data as Python dicts, and the library applies loop-aware rules against your parsed output.
|
|
57
|
+
|
|
58
|
+
Minimal example:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from draftedi.validate import build_nav_tree, assign_loop_paths, check_mandatory_segments
|
|
62
|
+
|
|
63
|
+
# spec_segments: a list of dicts you provide, following the expected schema.
|
|
64
|
+
# Each dict describes a segment's position, requirement, max use, and loop membership.
|
|
65
|
+
spec_segments = [
|
|
66
|
+
{
|
|
67
|
+
"segment_id": "BEG",
|
|
68
|
+
"segment_requirement": "M",
|
|
69
|
+
"segment_maximum_use": 1,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "loop",
|
|
73
|
+
"loop_id": "PO1",
|
|
74
|
+
"segments": [
|
|
75
|
+
{
|
|
76
|
+
"segment_id": "PO1",
|
|
77
|
+
"segment_requirement": "M",
|
|
78
|
+
"segment_maximum_use": 999999,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"type": "loop",
|
|
82
|
+
"loop_id": "N1",
|
|
83
|
+
"segments": [
|
|
84
|
+
{
|
|
85
|
+
"segment_id": "N1",
|
|
86
|
+
"segment_requirement": "O",
|
|
87
|
+
"segment_maximum_use": 200,
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
root = build_nav_tree(spec_segments)
|
|
96
|
+
loop_assignments = assign_loop_paths(transaction["segments"], root)
|
|
97
|
+
issues = check_mandatory_segments(loop_assignments, spec_segments)
|
|
98
|
+
# issues: list[ValidationIssue]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The same `loop_assignments` output can be passed to `check_segment_max_use` and `check_relational_conditions`.
|
|
102
|
+
|
|
103
|
+
## API reference
|
|
104
|
+
|
|
105
|
+
### Parser functions
|
|
106
|
+
|
|
107
|
+
| Function | Returns | Raises |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `parse_edi_file(raw_bytes: bytes, source: str = "manual upload") -> ParsedFile` | Full document hierarchy as a `ParsedFile` TypedDict | `ValueError` on malformed input |
|
|
110
|
+
| `parse_interchange(x12_text: str) -> SeparatorInfo` | Separator characters extracted from the ISA header | `ValueError` if text does not start with `ISA` or is shorter than 106 characters |
|
|
111
|
+
|
|
112
|
+
### Parser TypedDicts
|
|
113
|
+
|
|
114
|
+
The parsed hierarchy maps directly onto the X12 envelope structure.
|
|
115
|
+
|
|
116
|
+
- **`ParsedFile`** — top-level container. Key field: `interchanges` (list of `Interchange`).
|
|
117
|
+
- **`Interchange`** — one ISA/IEA envelope. Key fields: `isa_sender_id`, `isa_receiver_id`, `element_sep`, `segment_term`, `groups`.
|
|
118
|
+
- **`FunctionalGroup`** — one GS/GE envelope. Key fields: `gs_sender_id`, `gs_receiver_id`, `transactions`.
|
|
119
|
+
- **`Transaction`** — one ST/SE envelope. Key fields: `transaction_set_id`, `control_number`, `segments`.
|
|
120
|
+
- **`Segment`** — one X12 segment. Key fields: `segment_id`, `position`, `loop_path`, `elements`.
|
|
121
|
+
- **`Element`** — one element within a segment. Key fields: `element_pos`, `value_text`, `is_composite`, `components`.
|
|
122
|
+
- **`Component`** — one component within a composite element. Key fields: `component_pos`, `value_text`.
|
|
123
|
+
- **`SeparatorInfo`** — separator characters detected from the ISA header. Key fields: `element_sep`, `component_sep`, `repetition_sep`, `segment_term`.
|
|
124
|
+
|
|
125
|
+
### Validator — spec-free functions
|
|
126
|
+
|
|
127
|
+
- `check_element_data_type(value, element_type, element_id)` — validates AN, N/N0–N9, DT, TM, R, and ID element types. Returns an error string or `None`.
|
|
128
|
+
- `check_element_length(value, min_length, max_length)` — validates character length bounds. Returns an error string or `None`.
|
|
129
|
+
- `check_envelope(transaction, parsed_segments)` — checks SE01 reported segment count against the actual count of parsed segments. Returns a list of `ValidationIssue`.
|
|
130
|
+
- `check_relational_conditions(seg_id, rc_list, val_by_pos)` — checks P (Paired), R (Required), E (Exclusion), C (Conditional), and L (List Conditional) relational conditions. Returns a list of `ValidationIssue`.
|
|
131
|
+
- `leaf_loop(loop_path)` — extracts the innermost loop ID from a path string such as `"PO1/N1"` → `"N1"`. Returns `None` if input is `None`.
|
|
132
|
+
- `make_issue(severity, category, segment_id, segment_position, element_pos, message)` — constructs a `ValidationIssue` dict.
|
|
133
|
+
|
|
134
|
+
### Validator — spec-aware functions
|
|
135
|
+
|
|
136
|
+
- `build_nav_tree(spec_segments)` — converts a hierarchical spec structure into a `LoopEntry` navigation tree used by `assign_loop_paths`.
|
|
137
|
+
- `build_spec_lookup(spec_segments, parent_loop_id)` — flattens a spec into a `(segment_id, loop_id) -> spec_dict` lookup dict.
|
|
138
|
+
- `assign_loop_paths(parsed_segments, root)` — walks parsed segments and assigns a loop path to each using a stack-based state machine. Returns a list of `(segment_dict, loop_path_or_None)` tuples.
|
|
139
|
+
- `check_mandatory_segments(loop_assignments, spec_segments)` — loop-aware check for segments with M (Mandatory) requirement. Returns a list of `ValidationIssue`.
|
|
140
|
+
- `check_segment_max_use(loop_assignments, spec_segments)` — loop-aware check for segments that exceed their specified maximum use count. Returns a list of `ValidationIssue`.
|
|
141
|
+
|
|
142
|
+
### Validator types
|
|
143
|
+
|
|
144
|
+
**`ValidationIssue`** — a `TypedDict` with the following fields:
|
|
145
|
+
|
|
146
|
+
| Field | Type | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `severity` | `str` | `"error"` or `"warning"` |
|
|
149
|
+
| `category` | `str` | Broad classification of the issue (e.g. `"envelope"`, `"data_type"`) |
|
|
150
|
+
| `segment_id` | `str` | ID of the segment where the issue occurred |
|
|
151
|
+
| `segment_position` | `int` | Position of the segment within the transaction |
|
|
152
|
+
| `element_pos` | `Optional[int]` | Position of the element within the segment, if applicable |
|
|
153
|
+
| `message` | `str` | Human-readable description of the issue |
|
|
154
|
+
|
|
155
|
+
**`SegEntry`** — wraps a segment ID and its spec dict. Used internally by spec-aware functions.
|
|
156
|
+
|
|
157
|
+
**`LoopEntry`** — represents a node in the navigation tree. Fields: `loop_id`, `trigger_id` (the segment ID that starts this loop), `children` (list of `LoopEntry`).
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
pytest
|
|
164
|
+
ruff check src tests
|
|
165
|
+
mypy src
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "draftedi"
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "Production-grade X12 EDI parser for Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Yaw Way"},
|
|
10
|
+
]
|
|
11
|
+
keywords = ["edi", "x12", "parser", "healthcare", "b2b", "integration", "ansi"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Topic :: Office/Business",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"pytest-cov>=5.0",
|
|
32
|
+
"pytest-timeout>=2.0",
|
|
33
|
+
"ruff>=0.8",
|
|
34
|
+
"mypy>=1.13",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/yawway365/draftedi-core"
|
|
39
|
+
Repository = "https://github.com/yawway365/draftedi-core"
|
|
40
|
+
Issues = "https://github.com/yawway365/draftedi-core/issues"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
addopts = ["--tb=short", "-q", "--timeout=30"]
|
|
52
|
+
markers = [
|
|
53
|
+
"known_bug: marks tests for known parser bugs",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py39"
|
|
58
|
+
line-length = 100
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.9"
|
|
62
|
+
strict = true
|
|
63
|
+
warn_return_any = true
|
|
64
|
+
warn_unused_configs = true
|
draftedi-1.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""draftedi — X12 EDI parser and validator.
|
|
2
|
+
|
|
3
|
+
Provides pure-Python X12 EDI parsing (``parse_edi_file``,
|
|
4
|
+
``parse_interchange``) and database-free validation functions
|
|
5
|
+
(``check_element_data_type``, ``check_envelope``, etc.).
|
|
6
|
+
|
|
7
|
+
All public names are listed in ``__all__``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__version__ = "1.1.0"
|
|
13
|
+
|
|
14
|
+
from draftedi.parser import (
|
|
15
|
+
parse_edi_file,
|
|
16
|
+
parse_interchange,
|
|
17
|
+
ParsedFile,
|
|
18
|
+
Interchange,
|
|
19
|
+
FunctionalGroup,
|
|
20
|
+
Transaction,
|
|
21
|
+
Segment,
|
|
22
|
+
Element,
|
|
23
|
+
Component,
|
|
24
|
+
SeparatorInfo,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from draftedi.validate import (
|
|
28
|
+
check_element_data_type,
|
|
29
|
+
check_element_length,
|
|
30
|
+
check_relational_conditions,
|
|
31
|
+
leaf_loop,
|
|
32
|
+
check_envelope,
|
|
33
|
+
build_nav_tree,
|
|
34
|
+
assign_loop_paths,
|
|
35
|
+
build_spec_lookup,
|
|
36
|
+
check_mandatory_segments,
|
|
37
|
+
check_segment_max_use,
|
|
38
|
+
make_issue,
|
|
39
|
+
SegEntry,
|
|
40
|
+
LoopEntry,
|
|
41
|
+
ValidationIssue,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"__version__",
|
|
46
|
+
"parse_edi_file",
|
|
47
|
+
"parse_interchange",
|
|
48
|
+
"ParsedFile",
|
|
49
|
+
"Interchange",
|
|
50
|
+
"FunctionalGroup",
|
|
51
|
+
"Transaction",
|
|
52
|
+
"Segment",
|
|
53
|
+
"Element",
|
|
54
|
+
"Component",
|
|
55
|
+
"SeparatorInfo",
|
|
56
|
+
"check_element_data_type",
|
|
57
|
+
"check_element_length",
|
|
58
|
+
"check_relational_conditions",
|
|
59
|
+
"leaf_loop",
|
|
60
|
+
"check_envelope",
|
|
61
|
+
"build_nav_tree",
|
|
62
|
+
"assign_loop_paths",
|
|
63
|
+
"build_spec_lookup",
|
|
64
|
+
"check_mandatory_segments",
|
|
65
|
+
"check_segment_max_use",
|
|
66
|
+
"make_issue",
|
|
67
|
+
"SegEntry",
|
|
68
|
+
"LoopEntry",
|
|
69
|
+
"ValidationIssue",
|
|
70
|
+
]
|