From 53c15a8db56f537cd8ee766a8da141b481d79875 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 14 Jun 2026 04:44:51 +0100 Subject: [PATCH] Type Python subscription document records --- packages/cultcache-py/PARITY.md | 11 +++++++---- .../cultcache-py/src/cultnet_py/subscription.py | 15 +++++++++++---- packages/cultcache-py/tests/test_cultcache.py | 8 ++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/cultcache-py/PARITY.md b/packages/cultcache-py/PARITY.md index ab5cfbeb..2255bcae 100644 --- a/packages/cultcache-py/PARITY.md +++ b/packages/cultcache-py/PARITY.md @@ -47,10 +47,13 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti. - Python CultNet database subscription changes: Provides typed `CultNetDatabaseChange` parsing plus `CultNetDatabaseSubscription.read_next_change(...)` / `iter_changes(...)` for - live raw database subscriptions. Initial snapshot frames can be read as typed - `CultNetRawSnapshotResponse` values through `read_next_snapshot_response(...)` - / `iter_snapshot_responses(...)`, while raw message reads remain available - for callers that own their own dispatch loop. + live raw database subscriptions. Put/update changes expose their contained + raw document as a typed `CultNetRawDocumentRecord` while preserving the legacy + dict-shaped `document` projection. Initial snapshot frames can be read as + typed `CultNetRawSnapshotResponse` values through + `read_next_snapshot_response(...)` / `iter_snapshot_responses(...)`, while + raw message reads remain available for callers that own their own dispatch + loop. - Python CultMesh discovery client: Fetches Verse catalogs and peer exchange responses from the live Python endpoint over CultNet frames. diff --git a/packages/cultcache-py/src/cultnet_py/subscription.py b/packages/cultcache-py/src/cultnet_py/subscription.py index 3d5b9b46..6756376b 100644 --- a/packages/cultcache-py/src/cultnet_py/subscription.py +++ b/packages/cultcache-py/src/cultnet_py/subscription.py @@ -3,6 +3,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any +from .snapshot import CultNetRawDocumentRecord + @dataclass(frozen=True) class CultNetDatabaseChange: @@ -10,6 +12,7 @@ class CultNetDatabaseChange: subscription_id: str change_kind: str document: dict[str, Any] | None = None + raw_document: CultNetRawDocumentRecord | None = None schema_id: str | None = None record_key: str | None = None @@ -22,16 +25,18 @@ class CultNetDatabaseChange: raise ValueError(f"unsupported database changeKind {change_kind!r}") document = message.get("document") document_record = dict(document) if isinstance(document, dict) else None + raw_document = CultNetRawDocumentRecord.from_wire(document_record) if document_record is not None else None schema_id = _optional_string(message.get("schemaId")) record_key = _optional_string(message.get("recordKey")) - if document_record is not None: - schema_id = schema_id or _optional_string(document_record.get("schemaId")) - record_key = record_key or _optional_string(document_record.get("recordKey")) + if raw_document is not None: + schema_id = schema_id or raw_document.schema_id + record_key = record_key or raw_document.record_key return cls( message_id=str(message.get("messageId") or ""), subscription_id=str(message.get("subscriptionId") or ""), change_kind=change_kind, document=document_record, + raw_document=raw_document, schema_id=schema_id, record_key=record_key, ) @@ -43,7 +48,9 @@ class CultNetDatabaseChange: "subscriptionId": self.subscription_id, "changeKind": self.change_kind, } - if self.document is not None: + if self.raw_document is not None: + wire["document"] = self.raw_document.to_wire() + elif self.document is not None: wire["document"] = self.document if self.schema_id is not None: wire["schemaId"] = self.schema_id diff --git a/packages/cultcache-py/tests/test_cultcache.py b/packages/cultcache-py/tests/test_cultcache.py index 7dfea16a..3e7a0ea0 100644 --- a/packages/cultcache-py/tests/test_cultcache.py +++ b/packages/cultcache-py/tests/test_cultcache.py @@ -372,6 +372,8 @@ class CultCacheTests(unittest.TestCase): }) self.assertEqual(put_change.schema_id, "schema-note") self.assertEqual(put_change.record_key, "note:1") + self.assertIsInstance(put_change.raw_document, CultNetRawDocumentRecord) + self.assertEqual(put_change.raw_document.payload, b"payload") self.assertEqual(put_change.to_wire()["document"]["recordKey"], "note:1") delete_change = CultNetDatabaseChange.from_wire({ @@ -383,6 +385,7 @@ class CultCacheTests(unittest.TestCase): "recordKey": "note:1", }) self.assertIsNone(delete_change.document) + self.assertIsNone(delete_change.raw_document) self.assertEqual(delete_change.schema_id, "schema-note") self.assertEqual(delete_change.to_wire()["recordKey"], "note:1") @@ -635,6 +638,8 @@ class CultCacheTests(unittest.TestCase): self.assertEqual(snapshot.to_wire()["schemaVersion"], "cultnet.snapshot_response_raw.v0") self.assertEqual(change.change_kind, "added") self.assertEqual(change.record_key, "item:sub") + self.assertIsInstance(change.raw_document, CultNetRawDocumentRecord) + self.assertEqual(change.raw_document.record_key, "item:sub") self.assertEqual(received_versions, [ "cultnet.database_subscribe.v0", "cultnet.document_put_raw.v0", @@ -1972,8 +1977,11 @@ class CultCacheTests(unittest.TestCase): self.assertEqual(subscription_change.change_kind, "added") self.assertEqual(subscription_change.record_key, "note:2") self.assertEqual(subscription_change.document["recordKey"], "note:2") + self.assertIsInstance(subscription_change.raw_document, CultNetRawDocumentRecord) + self.assertEqual(subscription_change.raw_document.schema_id, "mesh.server_note.v1") self.assertEqual(subscription_delete.change_kind, "removed") self.assertEqual(subscription_delete.record_key, "note:2") + self.assertIsNone(subscription_delete.raw_document) self.assertIsNone(node.database.get(document, "note:2")) def test_cultmesh_authority_lease_requires_live_matching_lease(self) -> None: