CultLib/packages/cultnet-py
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens a5d1be650d Mark every Cult document [MessagePackObject]
PlayerData, CultMeshSimulationFact, the Networking interop peer's
CultNetInteropNote, the cultnet-py benchmark item and the Caching test
documents lacked [MessagePackObject]. Only the generated codec could
serialize them; MessagePackSerializer refuses such types. Non-public test
documents take [MessagePackObject(AllowPrivate = true)], as consumer
internal documents already do, and ConditionalCommitTests.Slow marks its
private backing fields [IgnoreMember].

Same [Key]s, so payloads are unchanged: a pre/post probe over the
Networking assembly and both interop peers showed identical schema id,
content hash and payload bytes for all 9 documents, PlayerData equal once
its Guid.NewGuid() default id is masked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:38:08 +02:00
..
src/cultnet_py Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py 2026-09-10 13:10:47 +02:00
tests Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py 2026-09-10 13:10:47 +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.