diff --git a/packages/cultcache-py/PARITY.md b/packages/cultcache-py/PARITY.md index cf123248..af9d3dfb 100644 --- a/packages/cultcache-py/PARITY.md +++ b/packages/cultcache-py/PARITY.md @@ -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` diff --git a/packages/cultcache-py/README.md b/packages/cultcache-py/README.md index c2909c38..99d0baa4 100644 --- a/packages/cultcache-py/README.md +++ b/packages/cultcache-py/README.md @@ -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() diff --git a/packages/cultcache-py/src/cultcache_py/verify.py b/packages/cultcache-py/src/cultcache_py/verify.py index 246d6559..e7e6b1f0 100644 --- a/packages/cultcache-py/src/cultcache_py/verify.py +++ b/packages/cultcache-py/src/cultcache_py/verify.py @@ -29,6 +29,7 @@ EXPECTED_EXPORTS: dict[str, tuple[str, ...]] = { ), "cultmesh_py": ( "CultMesh", + "CultMeshDatabase", "CultMeshGameSession", "CultMeshPeerExchangeClient", "CultMeshSimulationFact", diff --git a/packages/cultcache-py/src/cultmesh_py/__init__.py b/packages/cultcache-py/src/cultmesh_py/__init__.py index e48cdc9c..25b9ca4d 100644 --- a/packages/cultcache-py/src/cultmesh_py/__init__.py +++ b/packages/cultcache-py/src/cultmesh_py/__init__.py @@ -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", diff --git a/packages/cultcache-py/src/cultmesh_py/node.py b/packages/cultcache-py/src/cultmesh_py/node.py index fa76cecd..811ad35f 100644 --- a/packages/cultcache-py/src/cultmesh_py/node.py +++ b/packages/cultcache-py/src/cultmesh_py/node.py @@ -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) diff --git a/packages/cultcache-py/tests/test_cultcache.py b/packages/cultcache-py/tests/test_cultcache.py index 0fb7b8e0..4dd61d19 100644 --- a/packages/cultcache-py/tests/test_cultcache.py +++ b/packages/cultcache-py/tests/test_cultcache.py @@ -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: