diff --git a/packages/cultcache-py/PARITY.md b/packages/cultcache-py/PARITY.md index 4d2ec5ca..cf123248 100644 --- a/packages/cultcache-py/PARITY.md +++ b/packages/cultcache-py/PARITY.md @@ -17,6 +17,10 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti. - Python CultMesh discovery client: Fetches Verse catalogs and peer exchange responses from the live Python endpoint over CultNet frames. +- Python CultMesh facade: + 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 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 4bdd911c..c2909c38 100644 --- a/packages/cultcache-py/README.md +++ b/packages/cultcache-py/README.md @@ -219,7 +219,7 @@ local prediction reconciliation: ```python from cultcache_py import define_database_entry_type from cultnet_py import CultNetClientAuthorityScope, CultNetRawClient -from cultmesh_py import CultMesh, CultMeshDiscoveryClient, CultMeshGameSessionOptions, peer_exchange_request +from cultmesh_py import CultMesh, CultMeshGameSessionOptions, peer_exchange_request note_doc = define_database_entry_type("mesh.note", [("body", 0)]) node = CultMesh.start_node("mesh.cc", runtime_id="python-runtime") @@ -232,7 +232,7 @@ delete_message = node.delete_raw_message(note_doc, "note:2", shard_id="interop", peers = CultMesh.create_peer_catalog() response = peers.create_response(peer_exchange_request("pex-1", verse_id="local")) -client = CultMeshDiscoveryClient("127.0.0.1", 3075) +client = CultMesh.create_verse_discovery_client("127.0.0.1", 3075) verses = client.fetch_verses(transport_version="cultmesh.v0") mesh_peers = client.fetch_peers(verse_id="python-interop", roles=["read-replica"]) client.sync_peer_catalog(peers, verse_id="python-interop", roles=["read-replica"]) diff --git a/packages/cultcache-py/src/cultcache_py/verify.py b/packages/cultcache-py/src/cultcache_py/verify.py index c2d78cf3..246d6559 100644 --- a/packages/cultcache-py/src/cultcache_py/verify.py +++ b/packages/cultcache-py/src/cultcache_py/verify.py @@ -30,8 +30,10 @@ EXPECTED_EXPORTS: dict[str, tuple[str, ...]] = { "cultmesh_py": ( "CultMesh", "CultMeshGameSession", + "CultMeshPeerExchangeClient", "CultMeshSimulationFact", "CultMeshStreamCatalog", + "CultMeshVerseDiscoveryClient", "simulation_fact_document", ), } diff --git a/packages/cultcache-py/src/cultmesh_py/__init__.py b/packages/cultcache-py/src/cultmesh_py/__init__.py index 24e7238c..e48cdc9c 100644 --- a/packages/cultcache-py/src/cultmesh_py/__init__.py +++ b/packages/cultcache-py/src/cultmesh_py/__init__.py @@ -1,4 +1,4 @@ -from .client import CultMeshDiscoveryClient +from .client import CultMeshDiscoveryClient, CultMeshPeerExchangeClient, CultMeshVerseDiscoveryClient from .facade import CultMesh from .node import CultMeshNode, create_node from .session import ( @@ -53,6 +53,8 @@ __all__ = [ "CultMeshAuthorityLease", "CultMeshAuthorityLeaseCatalog", "CultMeshDiscoveryClient", + "CultMeshPeerExchangeClient", + "CultMeshVerseDiscoveryClient", "CultMeshStreamCatalog", "CultMeshStreamConsumerProfile", "CultMeshStreamDescriptor", diff --git a/packages/cultcache-py/src/cultmesh_py/client.py b/packages/cultcache-py/src/cultmesh_py/client.py index 527c187a..91fa9b82 100644 --- a/packages/cultcache-py/src/cultmesh_py/client.py +++ b/packages/cultcache-py/src/cultmesh_py/client.py @@ -114,3 +114,11 @@ class CultMeshDiscoveryClient: limit=limit, ) return catalog.apply_response(response) + + +class CultMeshVerseDiscoveryClient(CultMeshDiscoveryClient): + pass + + +class CultMeshPeerExchangeClient(CultMeshDiscoveryClient): + pass diff --git a/packages/cultcache-py/src/cultmesh_py/facade.py b/packages/cultcache-py/src/cultmesh_py/facade.py index 889eea13..18eb7d00 100644 --- a/packages/cultcache-py/src/cultmesh_py/facade.py +++ b/packages/cultcache-py/src/cultmesh_py/facade.py @@ -2,6 +2,9 @@ from __future__ import annotations from pathlib import Path +from cultnet_py import CultNetRawClient + +from .client import CultMeshPeerExchangeClient, CultMeshVerseDiscoveryClient from .node import CultMeshNode, create_node from .session import CultMeshGameSession, CultMeshGameSessionOptions from .simulation import CultMeshSimulationFactCommitter @@ -38,6 +41,24 @@ class CultMesh: def create_stream_catalog() -> CultMeshStreamCatalog: return CultMeshStreamCatalog() + @staticmethod + def create_verse_discovery_client( + host: str = "localhost", + port: int = 3075, + *, + timeout_seconds: float = 4.0, + ) -> CultMeshVerseDiscoveryClient: + return CultMeshVerseDiscoveryClient(host, port, timeout_seconds) + + @staticmethod + def create_peer_exchange_client( + host: str = "localhost", + port: int = 3075, + *, + timeout_seconds: float = 4.0, + ) -> CultMeshPeerExchangeClient: + return CultMeshPeerExchangeClient(host, port, timeout_seconds) + @staticmethod def create_simulation_fact_committer(node: CultMeshNode) -> CultMeshSimulationFactCommitter: return CultMeshSimulationFactCommitter(node) @@ -48,3 +69,21 @@ class CultMesh: options: CultMeshGameSessionOptions | None = None, ) -> CultMeshGameSession: return CultMeshGameSession(node, options) + + @staticmethod + def create_client( + host: str = "localhost", + port: int = 3075, + *, + timeout_seconds: float = 4.0, + ) -> CultNetRawClient: + return CultNetRawClient(host, port, timeout_seconds) + + @staticmethod + def connect_client( + host: str = "localhost", + port: int = 3075, + *, + timeout_seconds: float = 4.0, + ) -> CultNetRawClient: + return CultNetRawClient(host, port, timeout_seconds) diff --git a/packages/cultcache-py/tests/test_cultcache.py b/packages/cultcache-py/tests/test_cultcache.py index 15c2ab66..0fb7b8e0 100644 --- a/packages/cultcache-py/tests/test_cultcache.py +++ b/packages/cultcache-py/tests/test_cultcache.py @@ -54,6 +54,7 @@ from cultmesh_py import ( CultMeshAuthorityLease, CultMeshAuthorityLeaseCatalog, CultMeshDiscoveryClient, + CultMeshPeerExchangeClient, CultMeshStreamCatalog, CultMeshStreamConsumerProfile, CultMeshStreamDescriptor, @@ -61,6 +62,7 @@ from cultmesh_py import ( CultMeshVerseCatalog, CultMeshVerseCompatibility, CultMeshVerseDescriptor, + CultMeshVerseDiscoveryClient, peer_exchange_request, simulation_fact_document, verse_catalog_request, @@ -884,6 +886,20 @@ class CultCacheTests(unittest.TestCase): self.assertIsInstance(CultMesh.create_peer_catalog(), CultMeshPeerCatalog) self.assertIsInstance(CultMesh.create_authority_lease_catalog(), CultMeshAuthorityLeaseCatalog) self.assertIsInstance(CultMesh.create_stream_catalog(), CultMeshStreamCatalog) + verse_client = CultMesh.create_verse_discovery_client("127.0.0.1", 4010, timeout_seconds=1.5) + peer_client = CultMesh.create_peer_exchange_client("127.0.0.1", 4011, timeout_seconds=1.5) + raw_client = CultMesh.create_client("127.0.0.1", 4012, timeout_seconds=1.5) + connected_client = CultMesh.connect_client("127.0.0.1", 4013, timeout_seconds=1.5) + self.assertIsInstance(verse_client, CultMeshVerseDiscoveryClient) + self.assertIsInstance(peer_client, CultMeshPeerExchangeClient) + self.assertIsInstance(verse_client, CultMeshDiscoveryClient) + self.assertIsInstance(peer_client, CultMeshDiscoveryClient) + self.assertIsInstance(raw_client, CultNetRawClient) + self.assertIsInstance(connected_client, CultNetRawClient) + self.assertEqual((verse_client.host, verse_client.port, verse_client.timeout_seconds), ("127.0.0.1", 4010, 1.5)) + self.assertEqual((peer_client.host, peer_client.port, peer_client.timeout_seconds), ("127.0.0.1", 4011, 1.5)) + self.assertEqual((raw_client.host, raw_client.port, raw_client.timeout_seconds), ("127.0.0.1", 4012, 1.5)) + self.assertEqual((connected_client.host, connected_client.port, connected_client.timeout_seconds), ("127.0.0.1", 4013, 1.5)) def test_cultmesh_simulation_fact_uses_csharp_slot_contract(self) -> None: import msgpack # type: ignore