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. GPKE states a clock time on the n-th Werktag after the ÜT, or on the ÜT itself; GeLi Gas, WiM and MaBiS run to 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 65 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.

Metadata

Release files for makotest 0.22.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.22.0
File Size Uploaded
makotest-0.22.0.tar.gz 2.2 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for makotest 0.22.0
File
makotest-0.22.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
makotest-0.22.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.22.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.11 abi3 Linux glibc 2.17+ ARM64 Details
makotest-0.22.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
makotest-0.22.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 13.5 MB

Release files / makotest-0.22.0.tar.gz

Download URL makotest-0.22.0.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
118552bc9f53e6b2a79a05a4ec33343162a227488fe00be8444f6c5caace8067
BLAKE2b-256 checksum
How to use checksums
bd48ec36e3abdb6b730a1e177dc85f5a5627378ef388e650ee88322a207ca1be
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 21, 2026.

Transparency log

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

Download URL makotest-0.22.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
87b9af12b397edb68332ab7ee3d2a1054fa16686147a21c76e50390b2fc51f52
BLAKE2b-256 checksum
How to use checksums
9197b7d4b5d804b3c5b0fe5cf10df6b0a67dd9f05e80ca1df352a76524776af5
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 21, 2026.

Transparency log

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

Download URL makotest-0.22.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.3 MB
Tags CPython 3.11 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
d4edd1b8af2f799716b1e83e708bd45c7f46896d36f5616613afc4e0e3e9cf8a
BLAKE2b-256 checksum
How to use checksums
a87493e2f0eab271587ef471f89cc31fff4a46e8aa48dbaab1399b2ce06c70de
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 21, 2026.

Transparency log

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

Download URL makotest-0.22.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.3 MB
Tags CPython 3.11 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
e4a83183b0b3e01ad19d4d2a4f6f67430c71795ee6316a91a5a0414b283f7814
BLAKE2b-256 checksum
How to use checksums
aa48e0c6f613796806114b8fcf8b1975d9e73df8dc72995e2859fb3ae0fb3724
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 21, 2026.

Transparency log

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

Download URL makotest-0.22.0-cp311-abi3-macosx_11_0_arm64.whl
Size 2.4 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
eaee279dc1492fa238e99641a75ee58d90dd69106eed06ca8379f307f22c6c27
BLAKE2b-256 checksum
How to use checksums
4da87e63a69d2209ea325a676c50a87a03b21e92a968134712f2147689dc0cfb
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 21, 2026.

Transparency log

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

Download URL makotest-0.22.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
89457926d1259b56a26a1c54604c083df58fee8c204450dfd68611117c5fcd34
BLAKE2b-256 checksum
How to use checksums
1ef1adc588efe4655c2145a78bf90e9b469121c799eb13404f7fb42240dcaf35
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 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.22.0 This release

6 release files

0.21.0

6 release files

0.20.0

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