makotest (Python)

Python test & simulation toolkit for MaKo platforms: BDEW identifier check digits, Werktag/Fristen arithmetic, AHB-validated EDIFACT, seeded EPEX curves, and a pytest plugin — over the same Rust core the platform runs.

makotest — Python test & simulation toolkit

makotest generates regulator-conformant inputs, simulates the counterparties a MaKo platform talks to, and asserts on the result — from Python.

It is not mako-specific. Everything it drives is a public wire contract (EDIFACT over AS4, REST, CloudEvents), so it can exercise any MaKo implementation.

from makotest import malo_from_base, add_werktage, validate_edifact

malo_from_base("5123869678")    # '51238696780' — BDEW check digit applied
add_werktage("2026-12-24", 2)   # '2026-12-29' — skips holidays and the weekend

report = validate_edifact(utilmd_bytes, "2026-10-01")
report.is_valid                 # MIG + AHB + semantic rules, on that format version

The binding boundary

The toolkit is a PyO3 extension over the same Rust crates the platform runs. Wire-format concerns are never reimplemented in Python: a second implementation would drift from the BDEW profiles at the first Formatumstellung, and a harness that disagrees with production about validity is worse than none.

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

ConcernHome
EDIFACT MIG/AHB/semantic validationRust — edi-energy
MaLo/MeLo check digits and formatsRust — rubo4e::identifiers
Werktag / Feiertag calendarRust — mako-engine::fristen
EPEX curves, load profilesPython
Counterparty behaviour, fixturesPython

Because validation runs the platform's own AHB engine, makotest proves process and integration behaviour. It is not an independent check of format conformance — the BDEW reference examples remain the authority there.


Install

pip install makotest                  # core: identifiers, Fristen, EDIFACT
pip install 'makotest[data]'          # + EPEX / Lastgang generators
pip install 'makotest[bo4e]'          # + BO4E business objects
pip install 'makotest[hypothesis]'    # + property-based strategies

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


Identifiers

A random 11-digit string is almost never a valid Marktlokations-ID, so a test that invents one silently exercises the rejection path instead of the happy path. Generate them instead:

from makotest import malo_from_base, malo_check_digit, malo_is_valid, melo_is_valid

malo_from_base("5123869678")     # '51238696780'
malo_check_digit("5123869678")   # 0
malo_is_valid("51238696780")     # True
malo_is_valid("51238696781")     # False — wrong check digit

melo_is_valid("DE00014559929E00856996N5139699L01")   # True (33 chars)
melo_is_valid("51238696780")                         # False — that is a MaLo

Fristen

Regulated processes are deadline-driven, so time is an input, never ambient. The calendar is BDEW's conservative-inclusive one: a day observed as a holiday in any German state is a non-Werktag, so no Frist is ever computed shorter than the AHB requires for some participant.

from makotest import add_werktage, is_werktag, next_werktag

is_werktag("2026-01-06")       # False — Heilige Drei Könige (BY, BW, ST only)
add_werktage("2026-12-24", 2)  # '2026-12-29' — 25/26 holidays, 27/28 weekend
next_werktag("2026-11-07")     # '2026-11-09' — Saturday rolls to Monday

EDIFACT validation

from makotest.assertions import assert_edifact_valid, assert_rule_fires

assert_edifact_valid(mscons_bytes, on="2025-10-01")
assert_rule_fires(bad_bytes, "SEM-MSCONS-LOCATION-FORMAT", on="2025-10-01")

Pass the date the message would really be sent. A message valid under FV2025-10-01 can be invalid under FV2026-10-01, and defaulting to "today" silently hides that.

assert_rule_fires is the negative-case counterpart: it proves the rule you think catches a defect is the one that actually caught it, rather than some unrelated error further up the stack.


Building EDIFACT

makotest builds messages through the same Rust builders the platform uses, so a message it constructs is one the platform would accept.

from makotest import UtilmdTransaction, build_utilmd, build_interchange

msg = build_utilmd(
    pruefidentifikator=55001,
    sender="4012345000023",
    receiver="9900357000004",
    release="S2.1",
    document_date="20251101",
    transactions=[
        UtilmdTransaction(
            object_type="melo",
            object_id="DE00014559929E00856996N5139699L01",
            process_dates=[("163", "20251101")],   # delivery start
            references=[("Z13", "55001")],
        )
    ],
)

build_utilmd and build_mscons return a message (UNHUNT). That is not sendable on its own — the wire unit a market partner receives is an interchange:

wire = build_interchange(
    sender="4012345000023",
    receiver="9900357000004",
    dar="REF001",
    messages=[msg],
    date="260802", time="0915",
)
# UNB+UNOC:3+4012345000023:14+9900357000004:500+260802:0915+REF001'...UNZ+1+REF001'

Building and validation are deliberately separate steps: a test must be able to construct a knowingly-invalid message and assert that the right rule rejects it. Pass the result to assert_edifact_valid when you want the happy path.


pytest plugin

The plugin registers through the standard pytest11 entry point — no conftest.py wiring.

def test_frist_is_met(frozen_clock, epex_sim):
    frozen_clock.advance_werktage(4)   # BDEW calendar, not naive +4 days
    assert frozen_clock.date == "2026-11-09"
Fixturesepex_sim, frozen_clock, makotest_seed, mako_endpoint
Markers@pytest.mark.regulatory("GPKE Teil 2"), @pytest.mark.requires_docker
Options--mako-endpoint URL, --makotest-seed N

Only makotest.plugin imports pytest. The generators and simulators are plain objects usable from a script or notebook, so a demo and a CI test drive the same code path.


EPEX curves

Deterministic day-ahead curves in the MTU-keyed shape a platform ingests, with negative prices supported — load-bearing for §51 EEG Vergütungsausfall and §41a EnWG dynamic-tariff caps.

from makotest.generators import EpexSim

sim = EpexSim(seed=42)
list(sim.day("2026-11-01", profile="winter_peak"))
list(sim.day("2026-06-21", profile="solar_glut", negative_hours=6))

Profiles: flat, winter_peak, solar_glut, volatile. Same seed reproduces the curve exactly, and day order does not affect output.

Curves are synthetic test fixtures — never present them as market data.


BO4E generation

makotest builds business objects from BO4E 202607, the release mako's rubo4e generates from. Assert it once per session; testing v202607 objects against a platform on another generation produces passes that mean nothing.

from makotest.assertions import assert_bo4e_generation_matches
assert_bo4e_generation_matches(platform.bo4e_version)

Development

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

just test-makotest      # maturin develop + pytest
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.


Status

Pre-1.0. Shipping: the Rust core (identifiers, Fristen, EDIFACT build + interchange + validation), the EPEX generator, domain assertions and the pytest plugin. The counterparty simulators — an AS4 market partner that answers per EBD, then BIKO and iMSys/SMGW — are specified but not yet built.

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

Edit this page ↗