One distribution named cultcache-py carried three libraries. Rust and TypeScript layer them as cultcache < cultnet < cultmesh; Python now does the same, one package per layer, module names unchanged so every `from cultmesh_py import ...` keeps working. Two imports ran against that layering and are resolved by moving code to where the dependency direction says it lives, not by adding a shim: - cultnet_py.interop_peer imported the Verse/peer catalog, authority lease, stream, and simulation-fact contracts from cultmesh_py. They are wire contracts a bare peer has to speak without owning a node, which is why cultnet-rs and cultnet-ts keep them in the net layer. They now live in cultnet_py.cultmesh_contracts; cultmesh_py re-exports them. - cultcache_py.benchmark imported cultnet_py. It measures framed CultNet parse beside cache paths, so it and compare_csharp move to cultnet_py. verify checks all three packages and moves to cultmesh_py. The single 6,693-line test module is split by what each test touches: a test using any cultmesh_py name lands in cultmesh-py, else cultnet_py in cultnet-py, else cultcache-py. Each suite runs with only its layer and the layers below on the path, so a test that reaches up fails to import rather than quietly binding the packages back together. 11 / 53 / 78 = the same 142. Workflows install all three; the publish job is one matrix over the three packages keyed by tag prefix. The TS test harnesses put all three src directories on PYTHONPATH. cultcache-py bumps to 0.2.0 because it no longer contains cultnet_py or cultmesh_py; the new packages start at 0.1.0. Nothing was ever published, so no PyPI consumer moves. cultcache-py 11 tests, path: cultcache-py/src cultnet-py 53 tests, path: + cultnet-py/src cultmesh-py 78 tests, path: + cultmesh-py/src python -m cultmesh_py.verify -> ok, all five checks cultnet_py.interop_peer imports with cultmesh_py absent Not run here: the cultnet-ts interop ring and cultcache-ts store matrix need dotnet/kotlin peers; their PYTHONPATH change is four lines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
156 lines
6.7 KiB
Markdown
156 lines
6.7 KiB
Markdown
# 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
|
|
|
|
```python
|
|
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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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
|
|
|
|
```powershell
|
|
$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](../../docs/python-runtime-parity.md) for the
|
|
evidence map across the three Python packages.
|