CultLib/packages/cultnet-py
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens cf16cb7c1f Merge origin/main: CultCache publication Cuts 1-3 and the RUDP hardening
One conflict, at the end of Admit in CultCache.cs. The branch had added the
IdsInMemoryOnly marking of a load's minted ids before Apply; main had turned
the thread's hold into a Hold carrying Changes and Failures and run the
journals right after Apply. Both are kept, in that order: the marking touches
only the records and runs no user code, then Apply mints the Sequences, the
hold records the changes, and the journals run under the gate.

Id planning and Project do not collide. Ids are planned and applied first,
Resolve (variants, then Project, then the name and index rules) reads the
filled documents, land runs, and only then Apply mints. A Project getter that
throws is caught by the same handler that undoes the id plans, so a refused
admission changes no document and mints nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:33:11 +02:00
..
src/cultnet_py Merge origin/main: CultCache publication Cuts 1-3 and the RUDP hardening 2026-09-29 23:33:11 +02:00
tests Merge origin/main: CultCache publication Cuts 1-3 and the RUDP hardening 2026-09-29 23:33:11 +02:00
tools/GameCult.Caching.Benchmark Mark every Cult document [MessagePackObject] 2026-09-13 23:38:08 +02:00
pyproject.toml Drop the license classifier setuptools now refuses 2026-09-10 13:55:46 +02:00
README.md Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py 2026-09-10 13:10:47 +02:00

cultnet-py

cultnet-py is the Python CultNet runtime: schema-v0 MessagePack wire helpers, 4-byte big-endian framing, the raw client, database subscriptions, shard catch-up and replication, simulation observations and consensus, witness artifact bundles, the RUDP codec/session/socket transport, and the CultMesh wire contracts that both a node and a bare peer have to agree on.

It depends on cultcache-py and msgpack; AES-GCM helpers on CultNetSecret light up with the optional crypto extra. cultmesh-py builds on it. The layering mirrors cultnet-rs and cultnet-ts.

Example

from datetime import UTC, datetime

from cultnet_py import (
    CultNetSecret,
    CultNetServerSecurityOptions,
    CultNetRawClient,
    CultNetSimulationObservation,
    CultNetWitnessArtifactBundle,
    apply_raw_snapshot,
    apply_shard_log_response,
    compute_simulation_claim_hash,
    database_subscribe,
    decode_frame,
    encode_frame,
    hello,
    parse_message,
    shard_catalog_request,
    simulation_observation,
    witness_artifact_bundle,
)

payload = hello(runtime_id="python-runtime").to_bytes()
message = parse_message(decode_frame(encode_frame(payload)))

client = CultNetRawClient("127.0.0.1", 3075)
catalog = client.fetch_schema_catalog(kinds=["wire_message"])
snapshot = client.fetch_snapshot_response(schema_ids=["cultnet.interop-note"])
shard_catalog = client.fetch_shard_catalog(schema_ids=["cultnet.interop-note"])
# With a local CultCache and the matching registered document definitions:
# applied = apply_raw_snapshot(cache, [note_doc], snapshot)

subscription = database_subscribe(subscription_id="ui", schema_ids=["cultnet.interop-note"])
shard_request = shard_catalog_request(message_id="shards", schema_ids=["cultnet.interop-note"])
shard_log = client.fetch_shard_log_response(shard_id="interop", shard_epoch=1, after_sequence=0)
# log_changes = apply_shard_log_response(cache, [note_doc], shard_log)
with client.subscribe_database(subscription_id="ui", schema_ids=["cultnet.interop-note"]) as live:
    initial_snapshot = live.read_next_snapshot_response()

claim_hash = compute_simulation_claim_hash("frame:42", "subject:player-1", "hit")
observation = simulation_observation(
    message_id="obs-1",
    witness_runtime_id="python-runtime",
    shard_id="interop",
    shard_epoch=1,
    frame=42,
    subject_id="player-1",
    claim_kind="hit",
    claim_hash=claim_hash,
)
typed_observation = CultNetSimulationObservation.from_wire(observation.to_wire())
witness = witness_artifact_bundle(
    bundle_id="bundle-1",
    witness_kind="interop-proof",
    captured_at="2026-06-13T00:00:00Z",
    subject={"documentType": "cultnet.interop-note", "subjectId": "note:python"},
    contracts=[{"role": "payload", "schemaId": "cultnet.interop-note"}],
    artifacts=[{"role": "log", "uri": "cultcache://bundle-1/log", "mediaType": "text/plain"}],
    provenance={"pipelineId": "interop", "runId": "run-1", "runtimeId": "python-runtime"},
)
typed_witness = CultNetWitnessArtifactBundle.from_wire(witness)
witness_payload = typed_witness.to_payload()

security = CultNetServerSecurityOptions.development()
token = CultNetSecret.create_session_token(
    "318fb4b6-ff5e-4c4f-b911-d81807de53a8",
    datetime(2035, 1, 1, tzinfo=UTC),
    security,
    session_version=1,
)
session = CultNetSecret.try_validate_session_token(token, security)

The raw-client request surface can ride RUDP when the peer advertises a rudp:// endpoint and shares the connection id for that lane; create_rudp_schema_transport(...) builds that transport, and cultmesh_py.CultMesh.create_client(endpoint="rudp://...") selects it from an endpoint string.

CultMesh wire contracts

cultnet_py.cultmesh_contracts carries the Verse catalog, peer exchange, authority lease, stream negotiation, and committed simulation fact contracts. They live here rather than in cultmesh-py for the same reason CultMeshPeerCard lives in cultnet-rs: the interop peer has to speak them without owning a node. cultmesh_py re-exports every one of them, so callers that already import from there keep working.

RUDP

CultNetRudpSession is the shared reliable/unreliable/sequenced/ordered session over the RUDP packet codec, with fragmentation above max_fragment_bytes, a bounded reliable window, reliable expiry, and a bounded fragment reassembly map that evicts the oldest stranded set and counts it in fragment_sets_evicted rather than refusing new payloads. The socket-backed transport, reconnect policy/controller, and reconnect loop follow the Rust and TypeScript entrypoint pattern.

Interop peer

The peer can serve, dial, and probe the same raw-state interop lane used by the TypeScript, Rust, and C# test peers, and answers Verse catalog and peer exchange requests without a CultMesh node:

python -m cultnet_py.interop_peer serve --runtime-id python-peer --runtime-kind python --display-name "Python Peer" --agent-id python-agent --advertise-host 127.0.0.1 --tcp-port 3075 --discovery-port 4075 --discovery-group 239.77.44.11 --schema-path ..\cultnet-ts\integration\contracts\cultnet.interop-note.schema.json
python -m cultnet_py.interop_peer dial --runtime-id python-client --runtime-kind python --display-name "Python Client" --agent-id python-client --target-host 127.0.0.1 --target-port 3075 --schema-path ..\cultnet-ts\integration\contracts\cultnet.interop-note.schema.json
python -m cultnet_py.interop_peer probe --runtime-id python-prober --discovery-port 4075 --discovery-group 239.77.44.11

packages/cultnet-ts/test/interop/cultnet-interop.test.ts drives it inside the live TS/Rust/C#/Python peer ring.

Performance baseline

The package ships a lightweight benchmark for Python-owned hot paths. In a CultLib source checkout, it can also run the C# reference benchmark beside it:

python -m cultnet_py.benchmark --records 1000 --json
python -m cultnet_py.compare_csharp --records 1000 --json

It reports slot-indexed DatabaseEntry encode/decode throughput, framed CultNet MessagePack parse throughput, raw snapshot application into a registered cache, and public CultCache upsert/get throughput. The C# compare command also runs packages/cultnet-py/tools/GameCult.Caching.Benchmark and reports median Python-to-C# ratios for the shared public cache operations. It uses three samples by default; pass --samples when you need a different evidence shape. Treat the numbers as a local baseline, not a claim that Python matches the C# reference in every workload.

Tests

$env:PYTHONPATH="$PWD\packages\cultcache-py\src;$PWD\packages\cultnet-py\src"
python -m unittest discover -s packages\cultnet-py\tests

The suite runs without cultmesh_py on the path. See docs/python-runtime-parity.md for the evidence map across the three Python packages.