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.
Files changed (45) hide show
  1. mock_edi-0.1.0/CHANGELOG.md +71 -0
  2. mock_edi-0.1.0/CONTRIBUTING.md +142 -0
  3. mock_edi-0.1.0/Dockerfile +7 -0
  4. mock_edi-0.1.0/LICENSE +21 -0
  5. mock_edi-0.1.0/MANIFEST.in +3 -0
  6. mock_edi-0.1.0/PKG-INFO +349 -0
  7. mock_edi-0.1.0/README.md +318 -0
  8. mock_edi-0.1.0/examples/client.py +117 -0
  9. mock_edi-0.1.0/examples/demo.sh +133 -0
  10. mock_edi-0.1.0/mock_edi.egg-info/PKG-INFO +349 -0
  11. mock_edi-0.1.0/mock_edi.egg-info/SOURCES.txt +43 -0
  12. mock_edi-0.1.0/mock_edi.egg-info/dependency_links.txt +1 -0
  13. mock_edi-0.1.0/mock_edi.egg-info/entry_points.txt +2 -0
  14. mock_edi-0.1.0/mock_edi.egg-info/top_level.txt +1 -0
  15. mock_edi-0.1.0/mockedi/__init__.py +46 -0
  16. mock_edi-0.1.0/mockedi/__main__.py +106 -0
  17. mock_edi-0.1.0/mockedi/ack.py +204 -0
  18. mock_edi-0.1.0/mockedi/as2.py +252 -0
  19. mock_edi-0.1.0/mockedi/db.py +436 -0
  20. mock_edi-0.1.0/mockedi/delivery.py +228 -0
  21. mock_edi-0.1.0/mockedi/documents.py +282 -0
  22. mock_edi-0.1.0/mockedi/edifact.py +196 -0
  23. mock_edi-0.1.0/mockedi/envelope.py +378 -0
  24. mock_edi-0.1.0/mockedi/partners.py +104 -0
  25. mock_edi-0.1.0/mockedi/pipeline.py +458 -0
  26. mock_edi-0.1.0/mockedi/schema.py +1432 -0
  27. mock_edi-0.1.0/mockedi/server.py +867 -0
  28. mock_edi-0.1.0/mockedi/transactions.py +649 -0
  29. mock_edi-0.1.0/mockedi/validate.py +462 -0
  30. mock_edi-0.1.0/mockedi/x12.py +240 -0
  31. mock_edi-0.1.0/pyproject.toml +44 -0
  32. mock_edi-0.1.0/setup.cfg +4 -0
  33. mock_edi-0.1.0/tests/support.py +221 -0
  34. mock_edi-0.1.0/tests/test_ack.py +159 -0
  35. mock_edi-0.1.0/tests/test_as2.py +158 -0
  36. mock_edi-0.1.0/tests/test_behaviours.py +198 -0
  37. mock_edi-0.1.0/tests/test_choreography.py +168 -0
  38. mock_edi-0.1.0/tests/test_control.py +249 -0
  39. mock_edi-0.1.0/tests/test_delivery.py +175 -0
  40. mock_edi-0.1.0/tests/test_dictionary.py +166 -0
  41. mock_edi-0.1.0/tests/test_edifact.py +103 -0
  42. mock_edi-0.1.0/tests/test_envelope.py +122 -0
  43. mock_edi-0.1.0/tests/test_orders.py +166 -0
  44. mock_edi-0.1.0/tests/test_validate.py +194 -0
  45. 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.
@@ -0,0 +1,7 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY mockedi ./mockedi
4
+ COPY pyproject.toml README.md LICENSE ./
5
+ RUN pip install --no-cache-dir .
6
+ EXPOSE 8080
7
+ ENTRYPOINT ["mock-edi", "--host", "0.0.0.0", "--port", "8080"]
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.
@@ -0,0 +1,3 @@
1
+ include LICENSE README.md CHANGELOG.md CONTRIBUTING.md Dockerfile
2
+ recursive-include examples *.py *.sh *.edi
3
+ recursive-include tests *.py
@@ -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
+ [![CI](https://github.com/rseufert/mock-edi/actions/workflows/ci.yml/badge.svg)](https://github.com/rseufert/mock-edi/actions/workflows/ci.yml)
35
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
37
+ [![PyPI](https://img.shields.io/pypi/v/mock-edi)](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.