Add Python CultMesh database facade

This commit is contained in:
Meta Vens 2026-06-14 01:56:35 +01:00
commit c125b0fb20
6 changed files with 133 additions and 13 deletions

View file

@ -21,6 +21,9 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti.
Exposes C#-matching client entrypoints for Verse discovery, peer exchange,
raw CultNet client construction, local nodes, catalogs, sessions, streams,
authority leases, and simulation fact commits.
- Python CultMesh database facade:
`node.database` owns the local typed document read/write/snapshot/sync surface
so Python callers do not need to reach through the node composition wrapper.
- Python CultMesh committed simulation facts:
The live interop harness sends Python a simulation observation, receives a
consensus candidate, then requests the committed `gamecult.mesh.simulation_fact`

View file

@ -223,11 +223,11 @@ from cultmesh_py import CultMesh, CultMeshGameSessionOptions, peer_exchange_requ
note_doc = define_database_entry_type("mesh.note", [("body", 0)])
node = CultMesh.start_node("mesh.cc", runtime_id="python-runtime")
node.register_document(note_doc)
node.put(note_doc, "note:1", {"body": "hello"})
live_note = node.get_required(note_doc, "note:1")
put_message = node.put_raw_message(note_doc, "note:2", {"body": "wire me"}, shard_id="interop", shard_epoch=1)
delete_message = node.delete_raw_message(note_doc, "note:2", shard_id="interop", shard_epoch=1)
node.database.register_document(note_doc)
node.database.put(note_doc, "note:1", {"body": "hello"})
live_note = node.database.get_required(note_doc, "note:1")
put_message = node.database.put_raw_message(note_doc, "note:2", {"body": "wire me"}, shard_id="interop", shard_epoch=1)
delete_message = node.database.delete_raw_message(note_doc, "note:2", shard_id="interop", shard_epoch=1)
peers = CultMesh.create_peer_catalog()
response = peers.create_response(peer_exchange_request("pex-1", verse_id="local"))
@ -238,8 +238,8 @@ mesh_peers = client.fetch_peers(verse_id="python-interop", roles=["read-replica"
client.sync_peer_catalog(peers, verse_id="python-interop", roles=["read-replica"])
raw_client = CultNetRawClient("127.0.0.1", 3075)
node.sync_snapshot(raw_client, schema_ids=[note_doc.catalog_entry().schema_id])
node.sync_shard_log(raw_client, shard_id="interop", shard_epoch=1)
node.database.sync_snapshot(raw_client, schema_ids=[note_doc.catalog_entry().schema_id])
node.database.sync_shard_log(raw_client, shard_id="interop", shard_epoch=1)
streams = CultMesh.create_stream_catalog()

View file

@ -29,6 +29,7 @@ EXPECTED_EXPORTS: dict[str, tuple[str, ...]] = {
),
"cultmesh_py": (
"CultMesh",
"CultMeshDatabase",
"CultMeshGameSession",
"CultMeshPeerExchangeClient",
"CultMeshSimulationFact",

View file

@ -1,6 +1,6 @@
from .client import CultMeshDiscoveryClient, CultMeshPeerExchangeClient, CultMeshVerseDiscoveryClient
from .facade import CultMesh
from .node import CultMeshNode, create_node
from .node import CultMeshDatabase, CultMeshNode, create_node
from .session import (
CultMeshGameSession,
CultMeshGameSessionOptions,
@ -40,6 +40,7 @@ from .wire import (
__all__ = [
"CultMeshNode",
"CultMeshDatabase",
"CultMesh",
"CultMeshGameSession",
"CultMeshGameSessionOptions",

View file

@ -22,6 +22,118 @@ class CultMeshNode:
cache: CultCache = field(default_factory=CultCache)
runtime_id: str = "python-runtime"
documents: list[DocumentDefinition[Any]] = field(default_factory=list)
database: "CultMeshDatabase" = field(init=False)
def __post_init__(self) -> None:
self.database = CultMeshDatabase(self)
def register_document(self, document: DocumentDefinition[Any]) -> None:
self.database.register_document(document)
def put(self, document: DocumentDefinition[Any], key: str, value: Any) -> None:
self.database.put(document, key, value)
def put_raw_message(
self,
document: DocumentDefinition[Any],
key: str,
value: Any,
*,
message_id: str = "",
shard_id: str | None = None,
shard_epoch: int | None = None,
) -> CultNetMessage:
return self.database.put_raw_message(
document,
key,
value,
message_id=message_id,
shard_id=shard_id,
shard_epoch=shard_epoch,
)
def get(self, document: DocumentDefinition[Any], key: str) -> Any:
return self.database.get(document, key)
def get_required(self, document: DocumentDefinition[Any], key: str) -> Any:
return self.database.get_required(document, key)
def delete(self, document: DocumentDefinition[Any], key: str) -> None:
self.database.delete(document, key)
def delete_raw_message(
self,
document: DocumentDefinition[Any],
key: str,
*,
message_id: str = "",
shard_id: str | None = None,
shard_epoch: int | None = None,
) -> CultNetMessage:
return self.database.delete_raw_message(
document,
key,
message_id=message_id,
shard_id=shard_id,
shard_epoch=shard_epoch,
)
def snapshot(self) -> dict[str, dict[str, Any]]:
return self.database.snapshot()
def pull(self) -> None:
self.database.pull()
def sync_snapshot(
self,
client: CultNetRawClient,
*,
schema_ids: list[str] | None = None,
record_keys: list[str] | None = None,
shard_id: str | None = None,
shard_epoch: int | None = None,
) -> list[CultNetAppliedRecord]:
return self.database.sync_snapshot(
client,
schema_ids=schema_ids,
record_keys=record_keys,
shard_id=shard_id,
shard_epoch=shard_epoch,
)
def sync_shard_log(
self,
client: CultNetRawClient,
*,
shard_id: str,
shard_epoch: int | None = None,
after_sequence: int = 0,
limit: int | None = None,
) -> list[CultNetAppliedRecord]:
return self.database.sync_shard_log(
client,
shard_id=shard_id,
shard_epoch=shard_epoch,
after_sequence=after_sequence,
limit=limit,
)
class CultMeshDatabase:
def __init__(self, node: CultMeshNode) -> None:
self._node = node
@property
def cache(self) -> CultCache:
return self._node.cache
@property
def documents(self) -> list[DocumentDefinition[Any]]:
return self._node.documents
@property
def runtime_id(self) -> str:
return self._node.runtime_id
def register_document(self, document: DocumentDefinition[Any]) -> None:
self.cache.register_document_type(document)

View file

@ -47,6 +47,7 @@ from cultnet_py.interop_peer import wire_message_schema_descriptors
from cultmesh_py import create_node
from cultmesh_py import (
CultMesh,
CultMeshDatabase,
CultMeshGameSessionOptions,
CultMeshPeerCard,
CultMeshPeerCatalog,
@ -867,13 +868,15 @@ class CultCacheTests(unittest.TestCase):
document = define_database_entry_type("mesh.note", [("body", 0)])
store_path = Path(tmp) / "mesh.cc"
node = create_node(store_path, runtime_id="mesh-test")
node.register_document(document)
node.put(document, "note:1", {"body": "hello"})
self.assertIsInstance(node.database, CultMeshDatabase)
node.database.register_document(document)
node.database.put(document, "note:1", {"body": "hello"})
self.assertEqual(node.get_required(document, "note:1")["body"], "hello")
reopened = create_node(store_path, runtime_id="mesh-test")
reopened.register_document(document)
reopened.pull()
self.assertEqual(reopened.get(document, "note:1")["body"], "hello")
reopened.database.register_document(document)
reopened.database.pull()
self.assertEqual(reopened.database.get(document, "note:1")["body"], "hello")
def test_cultmesh_facade_matches_peer_runtime_entrypoints(self) -> None:
with tempfile.TemporaryDirectory() as tmp: