Skip to main content

makotest

Test & simulation toolkit for German market-communication (MaKo) platforms.

Builds regulator-conformant EDIFACT, simulates the counterparties a MaKo platform talks to — in EDIFACT, so a test can feed the answer back — and asserts on both wire contracts it exposes: the messages and the event stream.

makotest targets mako first but is not mako-specific. Everything it drives is public (EDIFACT over AS4, REST, CloudEvents), so it can exercise any MaKo implementation.

from makotest import antwort_obligation, malo_from_base, validate_edifact

malo_from_base("5123869601")  # '51238696012' — BDEW check digit applied

o = antwort_obligation(55001)  # what a Netzbetreiber owes on an Anmeldung
o.clock_time  # '11:00' — a clock time, not n × 24 h
o.due_at("2026-03-02T09:00:00Z")  # '2026-03-03T11:00:00+01:00'

validate_edifact(utilmd_bytes, "2026-10-01").is_valid  # MIG + AHB + semantic
pip install makotest                  # no runtime dependencies
pip install 'makotest[hypothesis]'    # + property-based strategies

Nothing is pinned because nothing needs to be: everything regulated is compiled into the extension, and the Europe/Berlin day comes from its tz database — so there is no tzdata requirement on Windows, where zoneinfo ships no data.

Wheels are abi3 (abi3-py311) — one wheel serves Python 3.11 and later.

The same answers are reachable from a shell, for whoever is holding a real message rather than writing a test:

$ makotest validate inbound.edi --on 2026-04-01
$ makotest frist 55001 --received 2026-03-02T09:00:00Z
$ makotest id 9900357000004      # → satisfies NEITHER check-digit procedure
$ makotest codes --pid 55001     # what a counterparty may answer with

Why

Deadlines are legal obligations, message content is defined by AHB rule tables, and a wrong Prüfidentifikator is a compliance defect rather than a bug. A test has to express "this is what the counterparty is entitled to send, and this is what we owe them by when" — which a curl script cannot.

Two failure modes shape the whole design, because both produce a green suite that proves nothing:

  • a message whose Prüfidentifikator has no AHB rules validates — having checked nothing;
  • an assertion naming an event type the platform does not declare finds no such event — forever.

Every validation report therefore carries rules_applied, and every event assertion resolves the type against the platform's own catalog first.

Design

One source of truth. EDIFACT construction and validation, identifier check digits, the Werktag calendar, the published answer Fristen and the CloudEvents catalog come from the same Rust crates the platform runs, through PyO3 bindings. None is reimplemented in Python: a second implementation drifts from the BDEW documents at the first Formatumstellung, and a harness that disagrees with the system under test about what is valid — or about when a Frist expires — is worse than none.

The rule: anything a regulator defines in a table is Rust; anything shaped by test ergonomics is Python.

Concern Home
EDIFACT build + MIG/AHB/semantic validation, release per format version Rust — edi-energy
Identifier check digits (MaLo, MP-ID, EIC, §8.2 resources) Rust — rubo4e
Werktag calendar, acknowledgement clocks, answer Fristen Rust — mako-fristen
Antwortcodes per Entscheidungsbaum Rust — mako-pruefung
CloudEvents type catalog and subscription matcher Rust — mako-events
Counterparty behaviour, EPEX curves, fixtures Python

Because validation runs the platform's own AHB engine, makotest proves process and integration behaviour — not format conformance. The BDEW reference examples remain the authority for that.

Deterministic by construction. Every generator is seeded, every clock is injected, and the format version is an argument rather than a read of today's date. Two runs of the same scenario produce byte-identical EDIFACT.

Framework-agnostic core, pytest on top. Only makotest.plugin imports pytest, so a demo and a CI test drive the same code path.

Two failures, two exceptions. AssertionError means the system under test is wrong; ValueError means the test is.


A tour

Five families, four Frist shapes, and Gas differs on the wire too — a Gas UTILMD names its Marktlokation as the Meldepunkt (LOC+172, never Z16), its answers name a G_… Codeliste where a GPKE answer names its EBD, and UTILMD runs a parallel G… release track on the same date as S…. The builders follow the track: a ("malo", …) location lands where the column of the Sparte puts it.

Fristen have four shapes, so ask the table. "A Werktage Frist expires at 17:00 Berlin" is true of the WiM MSB-Wechsel windows and of nothing else — GPKE states a clock time on the n-th Werktag after the ÜT, or on the ÜT itself, and GeLi Gas the end of the n-th Werktag. The two GPKE shapes share a clock time and land a day apart.

assert_deadline_is(response["deadline"], received=received, pid=55001)
assert_frist_met(55001, received=received, answered_at=answer["sent_at"])

The send date picks the format version, and the column fills the rest. A builder states what the test knows — the Vorgang, the Marktlokation, the Beginn — and the message is completed to the Prüfschablone of its Prüfidentifikator: the Kunde, the Stammdaten and the Transaktionsgrund a 55001 demands are there when the simulator validates it. What the test states stays, even where the column forbids it, so a knowingly invalid message is still buildable. Pinning a release by hand and validating on a date where another is in force reports the mismatch rather than the message.

msg = build_utilmd(
    55001,
    sender=LF,
    receiver=NB,
    on="2026-04-01",
    transactions=[
        UtilmdTransaction("VORGANG-1", locations=[("malo", malo)], dates=[("92", start)])
    ],
)
wire = build_interchange(
    sender=LF, receiver=NB, dar="REF1", messages=[msg], on="2026-04-01"
)
assert_edifact_valid(wire, on="2026-04-01")

Counterparties answer in EDIFACT — or badly, or not at all. The unhappy modes are the ones worth having: a platform that only ever sees a punctual, conformant partner has never had its Fristüberwachung exercised.

def test_nb_bestaetigt(nb_sim, anmeldung):
    nb_sim.on(55001).bestaetigung(antwort_code="A51", ebd="E_0623")
    reply = nb_sim.receive(anmeldung, received_at="2026-03-02T09:00:00Z")

    assert reply.pid == 55002  # the AHB answer PID — never Anfrage + 1
    platform.ingest(reply.business)  # a rendered interchange, not a dict


def test_frist_faellt(nb_sim, anmeldung):
    nb_sim.on(55001).timeout()  # no answer, not even a CONTRL
    assert not nb_sim.receive(anmeldung)


def test_verspaetete_antwort(nb_sim, anmeldung):
    nb_sim.on(55001).bestaetigung(delay_werktage=3)  # right message, wrong day
    reply = nb_sim.receive(anmeldung, received_at="2026-03-02T09:00:00Z")
    assert reply.answered_at > reply.due_at

An interchange carries several messages, each its own Vorgang — the reply answers all of them, and reply.pid raises rather than speaking for one.

A counterparty remembers what it accepted. A Netzbetreiber holding an open Vorgang for a Marktlokation does not answer a second Anmeldung the way it answered the first — E_0622 publishes A06 „Andere Anmeldung in Bearbeitung" for exactly that, and a platform re-sending a confirmed request is otherwise never contradicted.

nb_sim.on(55001).bestaetigung(antwort_code="A51", ebd="E_0623")
nb_sim.on(55001).bei_offenem_vorgang().ablehnung(antwort_code="A06", ...)

Only a Bestätigung opens a Vorgang, and the register is keyed on the Lokation — the Vorgangsnummer is the sender's reference, and a duplicate carries a new one.

Answers are read back structurally, not by matching bytes: LOC+Z16 and LOC+Z17 differ by one character, and a substring check passes on the wrong one.

v = validate_edifact(reply.business, on).messages[0].vorgaenge[0]
v.location("melo"), v.iso_date("92"), v.antwort_code

An answer states a code its Entscheidungsbaum publishes. SG4 STS+E01 is AHB-Muss on every Antwortnachricht, and a code means nothing outside its tree — A02 is „Vorlauffrist nicht eingehalten" in E_0607 and something else in E_0622. The catalogue decides: a code the tree has no leaf for, or one whose Cluster contradicts the answer PID it would ride, is refused at binding time.

antwort_code("E_0622", "A06").bedeutung  # what the BDEW says it means
antwort_codes_for_pid(55001)  # the tree's whole outcome space
assert_antwort_code(reply.antwort_code, ebd="E_0622", accepted=False)

The catalogue serves three wires — SG4 STS+E01 on a UTILMD, AJT on a REMADV and an ORDRSP — with one lookup for all of them. That is what makes every one of the 61 published answer obligations answerable: a Frist and an Antwortcode are of no use if the answer's message type cannot be built. tests/test_wim_esa.py::TestCoverage asserts the unanswerable set is empty, so binding a further message type or losing one shows up as a failing assertion rather than as silence.

DE 1131 carries the Codeliste, which for every WiM tree is an S_xxxx and not the EBD number — E_0200 names S_0090 on a Zustimmung and S_0054 on an Ablehnung.

A Bilanzkreis is a segment group, named once. bilanzkreis="11XBK-EEG-----1" on a transaction renders the whole SG8 SEQ+Z79 Produktpaket — the Produkt-Code and CAV qualifier are AHB constants a test should not be transcribing.

Events are the other wire contract.

assert_event_emitted(webhook_bodies, "de.mako.process.*", subject=malo)
assert_no_event_emitted(webhook_bodies, "de.mako.aperak.timeout")

Strategies draw values the platform accepts. A random 11-digit string is a valid MaLo one time in ten and a random 16-character string is essentially never a valid EIC, so a hand-rolled strategy spends its budget on the rejection path.

@given(malo=malo_ids(), pid=pruefidentifikatoren(message_type="UTILMD"))
def test_every_utilmd_roundtrips(malo, pid): ...

Lastgang curves are synthetic, and say so. Four shapes — household, Gewerbe, Wärmepumpe, PV feed-in — scaled to an annual quantity with a seasonal weight. They are deliberately not Standardlastprofile: the BDEW coefficient tables are not in this build, and a generator calling itself H0 while inventing them would make every settlement asserted against it look authoritative and be wrong.

gang = smgw.deliver("2026-10-25", werte=LastgangGenerator(seed=42).day("2026-10-25"))
gang.as_mscons(pruefidentifikator=13025, ...)   # one QTY per interval, with its period

Interval data carries its measurement period. A bare QTY states a magnitude with no time reference — a receiver cannot settle against it, and the AHB does not reject it, so the flat form validates while being unusable.

EPEX days are Europe/Berlin days, so the DST days carry 92 and 100 quarter-hourly MTUs rather than 96 — and negative prices are supported, which §51 EEG and §41a EnWG dynamic tariffs both need.

EpexGenerator(seed=42).day("2026-06-21", profile="solar_glut", negative_hours=6)

The full reference — every builder, assertion, strategy and simulator, with the Fundstelle behind each rule — is in the mako documentation.


pytest plugin

Registered through the pytest11 entry point — no conftest.py wiring.

Fixtures nb_sim, biko_sim, imsys_sim, epex, lastgang, frozen_clock, makotest_seed, makotest_on, mako_endpoint
Markers @pytest.mark.regulatory("GPKE Teil 2"), @pytest.mark.requires_docker
Options --makotest-on ISO_DATE, --makotest-seed N, --mako-endpoint URL

--makotest-on pins the format version for the whole suite, so re-running on a future date shows what the next Formatumstellung breaks. Every dated fixture takes it, so one test is never about two days. --mako-endpoint names a running deployment; bring your own HTTP client.

--hypothesis-profile=makotest selects a registered profile with deadline=None and derandomize=True — a strategy here draws through the Rust core, and Hypothesis' 200 ms per-example deadline is written for pure functions.


Development

makotest is a member of the mako Cargo workspace and builds with maturin:

just test-makotest      # maturin develop + pytest
just lint-makotest      # ruff check + format check
just build-makotest     # release wheel

pyo3/extension-module is deliberately not a Cargo feature — maturin enables it at build time. Declaring it would make cargo test --workspace --all-features link the Rust test harness against it and fail on undefined Python symbols. CI exercises both paths.

abi3-py311 sets a floor of Python 3.11, so every workspace-wide cargo command builds this crate against an interpreter. The workspace pins PYO3_PYTHON to .venv/bin/python in .cargo/config.toml; create that venv once and the rest of the workspace builds:

python3.11 -m venv .venv    # or any ≥ 3.11

Without it PyO3 falls back to the first python3 on PATH — still 3.9 on macOS — and the whole workspace fails to build on a message about a crate nobody was working on.

py.typed ships with the wheel, so _native.pyi is the only thing a consumer's type checker sees; a test pins it against the compiled module in both directions.

Scope

The Marktpartner simulator carries no AS4 transport of its own: it is a plain object with receive(), so a transport layers on top instead of being a dependency of the many tests that do not need one.

A counterparty is modelled once it has a consumer. One written ahead of its consumer encodes guesses about an interface nobody has implemented, and to the next reader those guesses are indistinguishable from requirements.

The package version tracks workspace.package.version through Cargo.toml, so the wheel and the crates it binds can never report different versions.

Licence

MIT OR Apache-2.0, matching mako.

Release files for makotest 0.20.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for makotest 0.20.0
File Size Uploaded
makotest-0.20.0.tar.gz 2.2 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for makotest 0.20.0
File
makotest-0.20.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
makotest-0.20.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.11 abi3 Linux glibc 2.17+ x86-64 Details
makotest-0.20.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.11 abi3 Linux glibc 2.17+ ARM64 Details
makotest-0.20.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
makotest-0.20.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size:13.3 MB

Release files / makotest-0.20.0.tar.gz

Download URL makotest-0.20.0.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
f5e3447cc1bbbcf1571b37c881d4465d97057e9fad850659731671066fa1da09
BLAKE2b-256 checksum
How to use checksums
761506a2bec6a746320ea4a57b4cfe05ed6a3a02ac3b469a94749b81658bb71c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / makotest-0.20.0-cp311-abi3-win_amd64.whl

Download URL makotest-0.20.0-cp311-abi3-win_amd64.whl
Size 2.1 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
ebd84d260381548b737a83da58ba4a7281a598ede0d857ac5a8136b66d9b5d26
BLAKE2b-256 checksum
How to use checksums
89d85793217c64c38e19c241c83e45759e76cb6d2475129c22b7026cc4e1b274
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / makotest-0.20.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL makotest-0.20.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.2 MB
Tags CPython 3.11 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
e4c539c69211a2a79273b49bf3f788691f53d4ef9e361d8f784c8dbb07c8f7f6
BLAKE2b-256 checksum
How to use checksums
a88bad8e88ae0abb2be573c3995e8258f7558c0c2a8c2cc55e6433bc63e72c03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / makotest-0.20.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL makotest-0.20.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.2 MB
Tags CPython 3.11 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
45bada997ed6c7b28102ab4a00500a071eb8423a02695d232c4263a5a6c3d93c
BLAKE2b-256 checksum
How to use checksums
e5636346b5c98d8e8ee50e80c62a88116827008c2848b15aab6fc397664efcec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / makotest-0.20.0-cp311-abi3-macosx_11_0_arm64.whl

Download URL makotest-0.20.0-cp311-abi3-macosx_11_0_arm64.whl
Size 2.3 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e328e52095a81d27a5a57539fe060ff7a587ad68752ec26f43d8dcc9027f2bc7
BLAKE2b-256 checksum
How to use checksums
813d9bb59f0b6f834b80061ab3dac846e1e8de766b05a7bf550349f35768a60c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / makotest-0.20.0-cp311-abi3-macosx_10_12_x86_64.whl

Download URL makotest-0.20.0-cp311-abi3-macosx_10_12_x86_64.whl
Size 2.2 MB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
d7f876035f550ddc5c40eb138c1fda5c6739e8dfadb4e1e4cce2fb3c41e8a910
BLAKE2b-256 checksum
How to use checksums
1cba535b76e47fc06f1b7ffee1ce6e5eea2e4c5e60392b534db429bf00293428
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.20.0 This release

6 release files

0.18.0

6 release files

0.17.0

6 release files

0.16.0

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page