mock-edi 0.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.
- mock_edi-0.1.0/CHANGELOG.md +71 -0
- mock_edi-0.1.0/CONTRIBUTING.md +142 -0
- mock_edi-0.1.0/Dockerfile +7 -0
- mock_edi-0.1.0/LICENSE +21 -0
- mock_edi-0.1.0/MANIFEST.in +3 -0
- mock_edi-0.1.0/PKG-INFO +349 -0
- mock_edi-0.1.0/README.md +318 -0
- mock_edi-0.1.0/examples/client.py +117 -0
- mock_edi-0.1.0/examples/demo.sh +133 -0
- mock_edi-0.1.0/mock_edi.egg-info/PKG-INFO +349 -0
- mock_edi-0.1.0/mock_edi.egg-info/SOURCES.txt +43 -0
- mock_edi-0.1.0/mock_edi.egg-info/dependency_links.txt +1 -0
- mock_edi-0.1.0/mock_edi.egg-info/entry_points.txt +2 -0
- mock_edi-0.1.0/mock_edi.egg-info/top_level.txt +1 -0
- mock_edi-0.1.0/mockedi/__init__.py +46 -0
- mock_edi-0.1.0/mockedi/__main__.py +106 -0
- mock_edi-0.1.0/mockedi/ack.py +204 -0
- mock_edi-0.1.0/mockedi/as2.py +252 -0
- mock_edi-0.1.0/mockedi/db.py +436 -0
- mock_edi-0.1.0/mockedi/delivery.py +228 -0
- mock_edi-0.1.0/mockedi/documents.py +282 -0
- mock_edi-0.1.0/mockedi/edifact.py +196 -0
- mock_edi-0.1.0/mockedi/envelope.py +378 -0
- mock_edi-0.1.0/mockedi/partners.py +104 -0
- mock_edi-0.1.0/mockedi/pipeline.py +458 -0
- mock_edi-0.1.0/mockedi/schema.py +1432 -0
- mock_edi-0.1.0/mockedi/server.py +867 -0
- mock_edi-0.1.0/mockedi/transactions.py +649 -0
- mock_edi-0.1.0/mockedi/validate.py +462 -0
- mock_edi-0.1.0/mockedi/x12.py +240 -0
- mock_edi-0.1.0/pyproject.toml +44 -0
- mock_edi-0.1.0/setup.cfg +4 -0
- mock_edi-0.1.0/tests/support.py +221 -0
- mock_edi-0.1.0/tests/test_ack.py +159 -0
- mock_edi-0.1.0/tests/test_as2.py +158 -0
- mock_edi-0.1.0/tests/test_behaviours.py +198 -0
- mock_edi-0.1.0/tests/test_choreography.py +168 -0
- mock_edi-0.1.0/tests/test_control.py +249 -0
- mock_edi-0.1.0/tests/test_delivery.py +175 -0
- mock_edi-0.1.0/tests/test_dictionary.py +166 -0
- mock_edi-0.1.0/tests/test_edifact.py +103 -0
- mock_edi-0.1.0/tests/test_envelope.py +122 -0
- mock_edi-0.1.0/tests/test_orders.py +166 -0
- mock_edi-0.1.0/tests/test_validate.py +194 -0
- mock_edi-0.1.0/tests/test_x12.py +124 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Every release of [mock-edi](https://pypi.org/project/mock-edi/). The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
|
+
versions follow [semantic versioning](https://semver.org/spec/v2.0.0.html) -
|
|
6
|
+
while the major version is 0, a minor bump may change behaviour, and each entry
|
|
7
|
+
says so where it does.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
Nothing yet.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-09-24
|
|
14
|
+
|
|
15
|
+
The first release: a mock EDI trading partner that answers an order with the
|
|
16
|
+
documents a real one sends.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **The dictionary** (`schema.py`). Elements, segments, loops and transaction
|
|
21
|
+
sets for ten transaction sets across both dialects, with the code lists that
|
|
22
|
+
make validation mean something. Everything else is derived from it -
|
|
23
|
+
parsing, validation, generation, and the dictionary published at
|
|
24
|
+
`/_mock/dictionary`, which is therefore the rules themselves rather than a
|
|
25
|
+
description of them that can drift.
|
|
26
|
+
- **ASC X12** (`x12.py`): the fixed-width ISA, with the delimiters read out of
|
|
27
|
+
it rather than assumed; GS/GE functional groups; ST/SE transaction sets with
|
|
28
|
+
the counts their trailers carry; 00401 and 00501, which differ in what ISA11
|
|
29
|
+
means.
|
|
30
|
+
- **UN/EDIFACT** (`edifact.py`): the UNA service string advice, the `?`
|
|
31
|
+
release character, composite elements, and an implicit functional group so
|
|
32
|
+
that everything above the wire can treat both dialects alike.
|
|
33
|
+
- **850/ORDERS in, 997/855/856/810 and CONTRL/ORDRSP/DESADV/INVOIC out.** One
|
|
34
|
+
pipeline drives both dialects, so what holds for an X12 flow holds for the
|
|
35
|
+
EDIFACT one. Documents reference each other the way real ones do: the
|
|
36
|
+
invoice names the shipment, the shipment names the order.
|
|
37
|
+
- **Validation** (`validate.py`, `ack.py`) against the dictionary, rendered as
|
|
38
|
+
a real 997 (`AK3`/`AK4` with X12 error codes) or CONTRL (`UCS`/`UCD` with
|
|
39
|
+
EDIFACT ones), and as prose for humans. The fatal-versus-noted policy is the
|
|
40
|
+
mock's own, and is written down rather than left to be inferred.
|
|
41
|
+
- **Partner behaviours**: `accept`, `short-ship`, `reject-line`, `reject-all`,
|
|
42
|
+
`no-ack`, `duplicate-invoice` and `strict`, changed at runtime through
|
|
43
|
+
`/_mock/partners/<id>`. Two rules outrank them, because real sellers apply
|
|
44
|
+
them too: an item not in the catalogue is rejected, and a price the seller
|
|
45
|
+
disagrees with is billed at the seller's price and flagged `IP`.
|
|
46
|
+
- **AS2** (`as2.py`): `AS2-From`/`AS2-To`, synchronous and asynchronous MDNs,
|
|
47
|
+
and the `Received-Content-MIC` under sha1, sha256 or sha512. S/MIME is
|
|
48
|
+
deliberately absent - it needs certificates and a cryptography library, and
|
|
49
|
+
this package has no dependencies - so a signed or encrypted payload is
|
|
50
|
+
refused with an MDN that says so rather than mangled.
|
|
51
|
+
- **Delivery** (`delivery.py`): a partner with an `as2_url` has its documents
|
|
52
|
+
POSTed to it, in the order they were queued, on one background thread, with
|
|
53
|
+
the header names spelled the way AS2 spells them. `urllib` re-cases them,
|
|
54
|
+
which is legal HTTP and an unusual spelling for AS2, so `http.client` is
|
|
55
|
+
used instead.
|
|
56
|
+
- **A queue with due times** (`pipeline.py`). Every delay defaults to zero, so
|
|
57
|
+
a test reads four documents back on the next line; configure real delays and
|
|
58
|
+
release them with `POST /_mock/advance?all`, because a test that sleeps is
|
|
59
|
+
slow and flaky and one that advances a clock is neither.
|
|
60
|
+
- **A control plane** under `/_mock`: health, state, partners, catalogue,
|
|
61
|
+
orders, the document archive with raw payloads, the mailbox and outbox,
|
|
62
|
+
`advance`, `send`, MDNs, the request log, `validate` and `reset`.
|
|
63
|
+
- **Deterministic demo data**: four partners covering the interesting
|
|
64
|
+
behaviours, a twelve-item catalogue with valid UPC check digits, and two
|
|
65
|
+
finished orders so the endpoints are not empty on a cold start.
|
|
66
|
+
- **228 tests**, every one of them over real HTTP. The one that matters most
|
|
67
|
+
validates every document the mock *writes* against the dictionary it uses to
|
|
68
|
+
check what it reads; it found six real bugs the first time it ran.
|
|
69
|
+
|
|
70
|
+
[Unreleased]: https://github.com/rseufert/mock-edi/compare/v0.1.0...HEAD
|
|
71
|
+
[0.1.0]: https://github.com/rseufert/mock-edi/releases/tag/v0.1.0
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking. mock-edi is a mock trading partner: it speaks the wire
|
|
4
|
+
shapes of ASC X12 and UN/EDIFACT over AS2 so that EDI integrations can be built
|
|
5
|
+
and tested without a counterparty. Everything below is about keeping it useful
|
|
6
|
+
for that.
|
|
7
|
+
|
|
8
|
+
## What the project values
|
|
9
|
+
|
|
10
|
+
These are not style preferences; they decide what gets merged.
|
|
11
|
+
|
|
12
|
+
**Fidelity over convenience.** If a real partner behaves a certain way, the
|
|
13
|
+
mock behaves that way - even when the real behaviour is inconvenient. ISA is
|
|
14
|
+
padded to its fixed widths and its delimiters are read back out of it rather
|
|
15
|
+
than assumed. `TDS` carries an integer with two implied decimals. A rejected
|
|
16
|
+
line commits to no delivery date. A mock that accepts what a real translator
|
|
17
|
+
rejects teaches a client a lie it will discover in production.
|
|
18
|
+
|
|
19
|
+
**Say when you are guessing.** Where trading partners genuinely differ, the
|
|
20
|
+
mock picks one profile and says so out loud rather than implying authority.
|
|
21
|
+
Line-level status in an `ORDRSP` is the clearest case: the confirmed quantity
|
|
22
|
+
goes in `QTY+113` with the shortfall in `QTY+83` and the reason in `FTX+AAO`,
|
|
23
|
+
and both the code and the README state that this is a choice. Inventing a
|
|
24
|
+
segment or a code that the standards do not have costs more than leaving a gap.
|
|
25
|
+
|
|
26
|
+
**No dependencies.** The Python standard library and SQLite, nothing else. A
|
|
27
|
+
mock you cannot install in a locked-down CI image is a mock nobody runs. This
|
|
28
|
+
is not negotiable, and it is why S/MIME is refused rather than half-built.
|
|
29
|
+
|
|
30
|
+
**Declare, do not hand-write.** Segments, elements, loops, code lists and
|
|
31
|
+
transaction sets are declarations in `mockedi/schema.py`; parsing, validation,
|
|
32
|
+
generation and the published dictionary are derived from them. If you find
|
|
33
|
+
yourself writing the same shape in two places, the declaration is missing.
|
|
34
|
+
|
|
35
|
+
**Refuse rather than half-implement.** An encrypted AS2 payload, an unknown
|
|
36
|
+
transaction set and an interchange from an unregistered sender all produce an
|
|
37
|
+
answer that names what *is* supported. Silently ignoring something is the one
|
|
38
|
+
thing worse than not having it.
|
|
39
|
+
|
|
40
|
+
**The wire is the product.** Behaviour a client cannot observe does not need to
|
|
41
|
+
exist; behaviour it can observe needs to be right. Control numbers come from
|
|
42
|
+
number ranges and shipment ids are derived so they are reproducible, because a
|
|
43
|
+
client sees those. No warehouse is simulated, because no client can tell.
|
|
44
|
+
|
|
45
|
+
## Getting set up
|
|
46
|
+
|
|
47
|
+
Nothing to install:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/rseufert/mock-edi
|
|
51
|
+
cd mock-edi
|
|
52
|
+
python3 -m mockedi --port 8080 # it is already runnable
|
|
53
|
+
python3 -m unittest discover -s tests -v
|
|
54
|
+
python3 tools/check_docs.py
|
|
55
|
+
python3 tools/check_changelog.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Python 3.8 or newer. There is no build step, no virtualenv to create and
|
|
59
|
+
nothing to compile.
|
|
60
|
+
|
|
61
|
+
## Where things live
|
|
62
|
+
|
|
63
|
+
| Adding this | Goes here | Notes |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| A segment or element | `mockedi/schema.py` | Define it once and reference it; `N1`, `DTM` and `RFF` are shared across sets |
|
|
66
|
+
| A code value | `mockedi/schema.py` | The code lists are what make validation mean something - a list that accepts everything acknowledges everything |
|
|
67
|
+
| A transaction set | `mockedi/schema.py`, then `transactions.py` | The definition first, then a reader or a writer, then a row in `SET_FOR_KIND` |
|
|
68
|
+
| A partner behaviour | `mockedi/documents.py` (`decide`) and `db.BEHAVIOURS` | Keep the precedence rules in the docstring true |
|
|
69
|
+
| A validation check | `mockedi/validate.py` | Produce a finding, not a sentence: it has to render as both a 997 and a CONTRL |
|
|
70
|
+
| An endpoint | `mockedi/server.py` | Add it to the index page and the README table too |
|
|
71
|
+
| A CLI flag | `mockedi/__main__.py` and `server.Config` | |
|
|
72
|
+
|
|
73
|
+
If a change touches more than one of these, it is usually two changes.
|
|
74
|
+
|
|
75
|
+
## What a good pull request looks like
|
|
76
|
+
|
|
77
|
+
- **A test that goes over HTTP.** Every test in `tests/` drives a real mock on
|
|
78
|
+
a real socket; nothing is stubbed. Put it in the module for the surface you
|
|
79
|
+
touched, or add one and give it a row in `docs/FILES.md`.
|
|
80
|
+
- **Assertions that could fail.** Check a total against the lines it
|
|
81
|
+
summarises, not against itself. The nastiest bug in this project's short
|
|
82
|
+
history - a loop walker that treated a repeated `N1` as a second use -
|
|
83
|
+
produced plausible-looking 997s and was caught only by validating the mock's
|
|
84
|
+
own output against its own dictionary.
|
|
85
|
+
- **Documentation that keeps up.** `tools/check_docs.py` fails the build if a
|
|
86
|
+
tracked file has no row in `docs/FILES.md`, if a row names a file that is
|
|
87
|
+
gone, or if a module is missing from the README's layout block. It checks
|
|
88
|
+
coverage, not prose - keeping the prose true is on you.
|
|
89
|
+
- **A line in the changelog.** `tools/check_changelog.py` fails a pull request
|
|
90
|
+
that touches `mockedi/` without adding an entry under `## [Unreleased]` - an
|
|
91
|
+
entry, not merely a changed file. It is what a user of the published package
|
|
92
|
+
reads. The same check holds released sections to being history and refuses to
|
|
93
|
+
let an entry waiting for a release disappear. A change that genuinely needs
|
|
94
|
+
no entry - a comment, a rename, a pure refactor - can carry the
|
|
95
|
+
`no changelog` label, which lifts that one rule and leaves the others
|
|
96
|
+
standing.
|
|
97
|
+
- **No new dependencies.** See above.
|
|
98
|
+
- **A commit message that says what changed and why.** The why is the part a
|
|
99
|
+
reader cannot reconstruct. Wrap at 72 characters.
|
|
100
|
+
|
|
101
|
+
Small, focused pull requests are easier to take than large ones. If you are
|
|
102
|
+
unsure whether something fits, open an issue first and say what you are trying
|
|
103
|
+
to test against the mock - that is usually the fastest way to the right shape.
|
|
104
|
+
|
|
105
|
+
## Reporting a missing or wrong shape
|
|
106
|
+
|
|
107
|
+
The most useful bug report contains the interchange a real trading partner
|
|
108
|
+
sent, with anything sensitive removed, beside what the mock produced. Segment
|
|
109
|
+
tags, element positions, the exact envelope and the version all matter. If you
|
|
110
|
+
cannot share a document, the transaction set and a description of the
|
|
111
|
+
difference is still plenty to work with.
|
|
112
|
+
|
|
113
|
+
`POST /_mock/validate` is often the fastest way to show one: it returns the
|
|
114
|
+
mock's reading of a document as prose, without changing anything.
|
|
115
|
+
|
|
116
|
+
## Releasing (maintainers)
|
|
117
|
+
|
|
118
|
+
`pyproject.toml` is the only place the version is written; `mockedi.__version__`
|
|
119
|
+
reads it back from the installed package metadata.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# bump `version` in pyproject.toml, commit, then:
|
|
123
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
124
|
+
gh release create v0.2.0 --generate-notes # or write the notes by hand
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Publishing the GitHub Release runs the tests, builds the distributions, checks
|
|
128
|
+
that the tag, `pyproject.toml` and the built wheel agree, and uploads to PyPI
|
|
129
|
+
through [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) - there
|
|
130
|
+
is no API token anywhere. Running the `Publish` workflow by hand publishes to
|
|
131
|
+
TestPyPI instead. Add the release to [`CHANGELOG.md`](CHANGELOG.md) in the same
|
|
132
|
+
commit as the version bump.
|
|
133
|
+
|
|
134
|
+
One practical note: PyPI's index propagates per edge node, so an install
|
|
135
|
+
immediately after a release can still fetch the previous version. Pin the exact
|
|
136
|
+
version when verifying (`pip install mock-edi==0.2.0`) rather than trusting a
|
|
137
|
+
plain upgrade.
|
|
138
|
+
|
|
139
|
+
## Licence
|
|
140
|
+
|
|
141
|
+
By contributing you agree that your work is licensed under the
|
|
142
|
+
[MIT Licence](LICENSE), the same terms as the rest of the project.
|
mock_edi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mock-edi contributors
|
|
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.
|
mock_edi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mock-edi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A mock EDI trading partner: X12 and EDIFACT over AS2, with the whole order-to-cash choreography, over SQLite, with zero dependencies.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/rseufert/mock-edi
|
|
7
|
+
Project-URL: Repository, https://github.com/rseufert/mock-edi
|
|
8
|
+
Project-URL: Issues, https://github.com/rseufert/mock-edi/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/rseufert/mock-edi/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: edi,x12,edifact,as2,mock,850,856,810,997,orders,desadv,invoic,edi-integration,testing
|
|
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.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
+
Classifier: Topic :: Office/Business
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# mock-edi
|
|
33
|
+
|
|
34
|
+
[](https://github.com/rseufert/mock-edi/actions/workflows/ci.yml)
|
|
35
|
+
[](https://www.python.org/downloads/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](https://pypi.org/project/mock-edi/)
|
|
38
|
+
|
|
39
|
+
**A mock EDI trading partner.** Not an EDI library and not an AS2 server — the
|
|
40
|
+
thing on the *other end*. Send it an 850 and it sends back a 997, then an 855
|
|
41
|
+
that answers line by line, then an 856 with a shipment tree, then an 810 that
|
|
42
|
+
bills what shipped. Send it an EDIFACT `ORDERS` and the same thing happens in
|
|
43
|
+
`CONTRL` / `ORDRSP` / `DESADV` / `INVOIC`.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
you ──850──▶ mock-edi
|
|
47
|
+
◀──997── the syntax parsed
|
|
48
|
+
◀──855── 2 lines: one confirmed, one short
|
|
49
|
+
◀──856── shipment / order / item, with a tracking number
|
|
50
|
+
◀──810── 1132.80, terms 2% 10 net 30
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
There is plenty of open source for *speaking* EDI — OpenAS2 and mendelson will
|
|
54
|
+
terminate an AS2 connection, and a dozen libraries will parse an X12 segment.
|
|
55
|
+
What none of them is, is a counterparty. To test the code that runs when an
|
|
56
|
+
856 arrives unannounced, or when the invoice comes twice, or when the
|
|
57
|
+
acknowledgment never comes at all, you need a partner that does those things
|
|
58
|
+
on demand. Real ones do them on their own schedule, and getting one to do it
|
|
59
|
+
deliberately is a support ticket and a fortnight.
|
|
60
|
+
|
|
61
|
+
- **Zero dependencies.** Python 3.8+ standard library and SQLite, nothing else.
|
|
62
|
+
It installs in a locked-down CI image.
|
|
63
|
+
- **Real wire shapes.** A 106-character fixed-width ISA that declares its own
|
|
64
|
+
delimiters, EDIFACT's `?` release character, composite elements, `TDS` with
|
|
65
|
+
two implied decimals, `HL` parent pointers, `AK3`/`AK4` error codes, MDNs
|
|
66
|
+
with a `Received-Content-MIC`.
|
|
67
|
+
- **Failure on demand.** Short shipments, rejected lines, refused orders,
|
|
68
|
+
duplicate invoices, a strict partner, and a partner that never answers —
|
|
69
|
+
each one PATCH away.
|
|
70
|
+
- **It validates its own output.** Every document the mock writes is checked
|
|
71
|
+
against the same dictionary it checks yours against. There is a test for it.
|
|
72
|
+
|
|
73
|
+
MIT licensed. ASC X12 and UN/EDIFACT are standards published by their
|
|
74
|
+
respective bodies; AS2 is RFC 4130. This project implements publicly
|
|
75
|
+
documented wire formats for testing purposes and is not affiliated with or
|
|
76
|
+
endorsed by any standards body or vendor.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install mock-edi
|
|
84
|
+
mock-edi --port 8080
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
curl -X POST --data-binary @order.edi http://127.0.0.1:8080/edi
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"accepted": true,
|
|
94
|
+
"partner": "ACME",
|
|
95
|
+
"dialect": "X12",
|
|
96
|
+
"orders": ["4500000042"],
|
|
97
|
+
"transactionSets": [
|
|
98
|
+
{"code": "850", "control": "0001", "kind": "order", "accepted": true, "findings": []}
|
|
99
|
+
],
|
|
100
|
+
"queued": [
|
|
101
|
+
{"kind": "acknowledgment", "code": "997", "reference": "000000077", "dueAt": "..."},
|
|
102
|
+
{"kind": "response", "code": "855", "reference": "4500000042", "dueAt": "..."},
|
|
103
|
+
{"kind": "despatch", "code": "856", "reference": "4500000042", "dueAt": "..."},
|
|
104
|
+
{"kind": "invoice", "code": "810", "reference": "4500000042", "dueAt": "..."}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then collect what it sent you:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
curl "http://127.0.0.1:8080/_mock/mailbox?raw"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
ISA*00* *00* *ZZ*MOCKEDI *ZZ*ACME *260924*1030*U*00401*000000001*0*T*>~
|
|
117
|
+
GS*PR*MOCKEDI*ACME*20260924*1030*2*X*004010~
|
|
118
|
+
ST*855*0002~
|
|
119
|
+
BAK*00*AD*4500000042*20260924***20260924*5100002~
|
|
120
|
+
...
|
|
121
|
+
PO1*1*100*EA*12.50**VP*WIDGET-001*UP*076123400003~
|
|
122
|
+
ACK*IA*100*EA*068*20260926~
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Run it from a checkout with no install at all, or in a container:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python3 -m mockedi --port 8080
|
|
129
|
+
docker build -t mock-edi . && docker run -p 8080:8080 mock-edi
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A guided tour of every endpoint, in curl:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bash examples/demo.sh
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## What it serves
|
|
139
|
+
|
|
140
|
+
| Surface | Endpoint |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| AS2 inbound | `POST /as2` — answers with an MDN, synchronous or asynchronous |
|
|
143
|
+
| Asynchronous MDN inbound | `POST /as2/mdn` — a partner's receipt for something the mock sent |
|
|
144
|
+
| Plain EDI inbound | `POST /edi` — the same pipeline, answering with a JSON summary |
|
|
145
|
+
| Validate only | `POST /_mock/validate` — findings, and nothing changed |
|
|
146
|
+
| Mailbox | `GET /_mock/mailbox` — collect what is waiting; `?leave` to peek, `?raw` for payloads |
|
|
147
|
+
| Outbox | `GET /_mock/outbox` — the queue, including what is not due yet |
|
|
148
|
+
| Release the queue | `POST /_mock/advance` — `?seconds=N` or `?all` |
|
|
149
|
+
| Send out of band | `POST /_mock/send` — replay an invoice, or send one unprompted |
|
|
150
|
+
| Partners | `GET/POST /_mock/partners`, `GET/PATCH/DELETE /_mock/partners/<id>` |
|
|
151
|
+
| Orders | `GET /_mock/orders`, `GET /_mock/orders/<po>` |
|
|
152
|
+
| Archive | `GET /_mock/documents`, `GET /_mock/interchanges`, `GET /_mock/interchanges/<id>?raw` |
|
|
153
|
+
| Receipts | `GET /_mock/mdns` |
|
|
154
|
+
| The dictionary | `GET /_mock/dictionary`, `/_mock/dictionary/X12/850` |
|
|
155
|
+
| Health and state | `GET /_mock/health`, `GET /_mock/state`, `GET /_mock/requests` |
|
|
156
|
+
| Reset | `POST /_mock/reset` |
|
|
157
|
+
| Index page | `GET /` |
|
|
158
|
+
|
|
159
|
+
## The documents
|
|
160
|
+
|
|
161
|
+
| Business document | X12 | EDIFACT |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| Purchase order | **850** | **ORDERS** |
|
|
164
|
+
| Purchase order response | **855** | **ORDRSP** |
|
|
165
|
+
| Despatch advice / ship notice | **856** | **DESADV** |
|
|
166
|
+
| Invoice | **810** | **INVOIC** |
|
|
167
|
+
| Syntax acknowledgment | **997** | **CONTRL** |
|
|
168
|
+
|
|
169
|
+
Both dialects are read and written from one dictionary
|
|
170
|
+
([`mockedi/schema.py`](mockedi/schema.py)), and one pipeline drives both, so
|
|
171
|
+
what you assert about an X12 flow holds for the EDIFACT one. `GET
|
|
172
|
+
/_mock/dictionary/X12/850` serves that dictionary as JSON — the actual rules,
|
|
173
|
+
not a description of them that can go stale.
|
|
174
|
+
|
|
175
|
+
Coverage is the commonly traded core of each set, not the full standard. A
|
|
176
|
+
real 850 admits some fifty segment types and almost nobody sends more than a
|
|
177
|
+
dozen; the mock implements the dozen, validates them properly, and reports an
|
|
178
|
+
unrecognised segment rather than pretending to understand it.
|
|
179
|
+
|
|
180
|
+
## Partner behaviours
|
|
181
|
+
|
|
182
|
+
Four partners are seeded. Change any of them at runtime:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
curl -X PATCH -H 'Content-Type: application/json' \
|
|
186
|
+
-d '{"behaviour":"short-ship"}' \
|
|
187
|
+
http://127.0.0.1:8080/_mock/partners/ACME
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Behaviour | What the partner does |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `accept` | Confirms everything in full and ships what was ordered. |
|
|
193
|
+
| `short-ship` | Confirms less than was ordered (`855` `IQ`, `ORDRSP` `QTY+83`), and ships and invoices the confirmed quantity. |
|
|
194
|
+
| `reject-line` | Refuses one line outright (`IR`) and leaves it out of the shipment and the invoice. |
|
|
195
|
+
| `reject-all` | Acknowledges the syntax, then refuses the order (`BAK` `RJ`). |
|
|
196
|
+
| `no-ack` | Says nothing at all. No 997, no 855. For testing your chase-up timer — the failure that actually costs money. |
|
|
197
|
+
| `duplicate-invoice` | Sends the invoice twice with the same invoice number, as a partner with a retry bug does. |
|
|
198
|
+
| `strict` | Rejects a transaction set for any finding, not only a fatal one. |
|
|
199
|
+
|
|
200
|
+
Two rules apply whatever the behaviour says, because they are what real
|
|
201
|
+
sellers actually do:
|
|
202
|
+
|
|
203
|
+
- an item that is not in the catalogue is rejected (`IR`), and
|
|
204
|
+
- a price the seller disagrees with is billed at the seller's price and
|
|
205
|
+
flagged `IP`. Price discrepancies are the commonest EDI dispute there is.
|
|
206
|
+
|
|
207
|
+
## Timing
|
|
208
|
+
|
|
209
|
+
By default every document is released the moment it is produced, so a test can
|
|
210
|
+
POST an order and read four documents back on the next line. Give them delays
|
|
211
|
+
when what you are testing is the waiting:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
mock-edi --ack-delay 2000 --response-delay 30000 --invoice-delay 86400000
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Nothing is released on a timer of its own. `POST /_mock/advance?all` releases
|
|
218
|
+
whatever is queued, whenever it was due — a test that has to sleep is slow and
|
|
219
|
+
flaky, and one that advances the clock is neither.
|
|
220
|
+
|
|
221
|
+
## AS2
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
curl -X POST --data-binary @order.edi \
|
|
225
|
+
-H 'Content-Type: application/edi-x12' \
|
|
226
|
+
-H 'AS2-From: ACME' -H 'AS2-To: MOCKEDI' \
|
|
227
|
+
-H 'Message-ID: <po-1@acme.example>' \
|
|
228
|
+
-H 'Disposition-Notification-To: edi@acme.example' \
|
|
229
|
+
-H 'Disposition-Notification-Options: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256' \
|
|
230
|
+
http://127.0.0.1:8080/as2
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
comes back as a `multipart/report` MDN with the MIC of what arrived:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
Disposition: automatic-action/MDN-sent-automatically; processed
|
|
237
|
+
Received-Content-MIC: +H1EWvEMSJH/IHGsjy7c/dviFRwLgRoGBmxnTEbMkGA=, sha256
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Name a `Receipt-Delivery-Option` and the response is `202` with the MDN posted
|
|
241
|
+
back to that URL instead.
|
|
242
|
+
|
|
243
|
+
**S/MIME is deliberately not implemented.** Signing and encrypting AS2
|
|
244
|
+
payloads needs certificates and a cryptography library, and this project has no
|
|
245
|
+
dependencies on purpose. A message that arrives encrypted or signed is refused
|
|
246
|
+
with an MDN saying exactly that, rather than being mangled. If your integration
|
|
247
|
+
must be tested against signed AS2, this mock is the wrong tool and will tell
|
|
248
|
+
you so on the first message.
|
|
249
|
+
|
|
250
|
+
## Making the mock come to you
|
|
251
|
+
|
|
252
|
+
A partner with no `as2_url` is a mailbox. Give one a URL and the mock stops
|
|
253
|
+
being something you poll and becomes something that *arrives*:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
curl -X PATCH -H 'Content-Type: application/json' \
|
|
257
|
+
-d '{"as2_url":"http://localhost:9000/as2"}' \
|
|
258
|
+
http://127.0.0.1:8080/_mock/partners/ACME
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Documents are then POSTed to your listener with AS2 headers, in the order they
|
|
262
|
+
were queued, and whatever MDN you return is recorded against them in
|
|
263
|
+
`/_mock/outbox`.
|
|
264
|
+
|
|
265
|
+
## Validation
|
|
266
|
+
|
|
267
|
+
Every inbound document is checked against the dictionary, and the findings
|
|
268
|
+
become a real 997 or CONTRL — `AK3`/`AK4` with X12 error codes, `UCS`/`UCD`
|
|
269
|
+
with EDIFACT ones. Ask for the findings as prose instead:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
curl -X POST --data-binary @broken.edi http://127.0.0.1:8080/_mock/validate
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"clean": false,
|
|
278
|
+
"groupCode": "R",
|
|
279
|
+
"explain": [
|
|
280
|
+
"850/0001: rejected",
|
|
281
|
+
" BEG at segment 2: ZZ is not a code BEG01 accepts (00, 01, 04, 05, 06, 07, ...)",
|
|
282
|
+
" BEG at segment 2: BEG05 is not a valid date: '2026-09-24'",
|
|
283
|
+
" PO1 at segment 3 in the PO1 loop: PO102 must be a number, got 'ten'",
|
|
284
|
+
" SE01 counts 99 segments, the message holds 6 (4)"
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Severity is the mock's own policy, and it is stated rather than implied. A
|
|
290
|
+
*fatal* finding rejects the transaction set — an unknown set, a missing
|
|
291
|
+
mandatory segment or element, a control number that does not match its
|
|
292
|
+
trailer, a segment count that does not add up. Everything else is accepted
|
|
293
|
+
with errors noted: an invalid code, a length violation, a malformed date, a
|
|
294
|
+
segment the set does not define. A partner set to `strict` rejects on either.
|
|
295
|
+
|
|
296
|
+
Two limits, stated plainly: loop *membership* and repetition counts are
|
|
297
|
+
checked but loop *sequence* is not, and conditional requirements ("if PO104 is
|
|
298
|
+
present then PO103 must be") are not modelled. Both would need a rule language
|
|
299
|
+
to express, and the mock would rather leave them out than pretend.
|
|
300
|
+
|
|
301
|
+
## Layout
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
mockedi/schema.py elements, segments, loops, transaction sets (add shapes here)
|
|
305
|
+
mockedi/envelope.py the shape both dialects share, and delimiter handling
|
|
306
|
+
mockedi/x12.py reading and writing ASC X12 interchanges
|
|
307
|
+
mockedi/edifact.py reading and writing UN/EDIFACT interchanges
|
|
308
|
+
mockedi/validate.py checking a document against the dictionary
|
|
309
|
+
mockedi/ack.py turning findings into a 997 or a CONTRL
|
|
310
|
+
mockedi/transactions.py business documents in, business documents out
|
|
311
|
+
mockedi/documents.py what the seller decides, and the shipment and invoice
|
|
312
|
+
mockedi/partners.py who we trade with, and how each one misbehaves
|
|
313
|
+
mockedi/pipeline.py the choreography: an order in, four documents back
|
|
314
|
+
mockedi/delivery.py posting to a partner that has somewhere to receive
|
|
315
|
+
mockedi/as2.py AS2 headers, the MIC, and the MDN
|
|
316
|
+
mockedi/db.py SQLite: schema, number ranges, demo data
|
|
317
|
+
mockedi/server.py HTTP: AS2, /edi, and the control plane
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) explains how they fit together;
|
|
321
|
+
[`docs/FILES.md`](docs/FILES.md) is an index of every file in the repository.
|
|
322
|
+
|
|
323
|
+
## Tests
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
python3 -m unittest discover -s tests -v
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
228 tests, every one of them talking to a real mock over real HTTP. Nothing is
|
|
330
|
+
stubbed. The most valuable one is in `tests/test_dictionary.py`: every document
|
|
331
|
+
the mock generates is validated against the same dictionary it validates yours
|
|
332
|
+
with, so the day someone adds a segment to a writer and forgets the
|
|
333
|
+
definition, the suite says so.
|
|
334
|
+
|
|
335
|
+
## Extending it
|
|
336
|
+
|
|
337
|
+
Add a segment or a transaction set in `schema.py` and it is parsed, validated
|
|
338
|
+
and published in `/_mock/dictionary` without touching anything else. Add a
|
|
339
|
+
*behaviour* in `documents.decide()`. Add an endpoint in `server.py`.
|
|
340
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) says where each kind of change goes and what
|
|
341
|
+
a good pull request carries.
|
|
342
|
+
|
|
343
|
+
## See also
|
|
344
|
+
|
|
345
|
+
[mock-sap](https://github.com/rseufert/mock-sap) — the same idea for SAP:
|
|
346
|
+
OData V2 and V4, BAPI/RFC and IDoc shapes over SQLite, also with zero
|
|
347
|
+
dependencies. An IDoc `ORDERS05` and an X12 850 are the same business
|
|
348
|
+
document, so the two mocks make a reasonable pair of ends for testing a
|
|
349
|
+
middleware layer.
|