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>
259 lines
10 KiB
Python
259 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from cultcache_py import (
|
|
CultCache,
|
|
JsonLinesBackingStore,
|
|
SingleFileMessagePackBackingStore,
|
|
define_database_entry_type,
|
|
define_document_type,
|
|
)
|
|
from cultcache_py.interop import read_note, write_note
|
|
|
|
|
|
@dataclass
|
|
class Item:
|
|
name: str
|
|
category: str
|
|
value: int
|
|
|
|
|
|
def item_doc():
|
|
return define_document_type(
|
|
"item",
|
|
encode=lambda item: asdict(item),
|
|
decode=lambda raw: Item(**raw),
|
|
name="name",
|
|
indexes={"category": "category"},
|
|
)
|
|
|
|
|
|
class CultCacheTests(unittest.TestCase):
|
|
def test_round_trips_registered_documents(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
document = item_doc()
|
|
cache = (
|
|
CultCache.builder()
|
|
.register_document_type(document)
|
|
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
|
|
.build()
|
|
)
|
|
cache.pull_all_backing_stores()
|
|
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
|
|
|
|
loaded = (
|
|
CultCache.builder()
|
|
.register_document_type(document)
|
|
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
|
|
.build()
|
|
)
|
|
loaded.pull_all_backing_stores()
|
|
|
|
self.assertEqual(loaded.get_required(document, "item:potion").value, 50)
|
|
self.assertEqual(loaded.get_key_by_name(document, "Potion"), "item:potion")
|
|
self.assertEqual(loaded.get_by_index(document, "category", "Consumable").name, "Potion")
|
|
|
|
def test_global_document_uses_singleton_key(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
settings = define_document_type("settings", global_document=True)
|
|
cache = (
|
|
CultCache.builder()
|
|
.register_document_type(settings)
|
|
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
|
|
.build()
|
|
)
|
|
cache.pull_all_backing_stores()
|
|
cache.put_global(settings, {"theme": "ash"})
|
|
self.assertEqual(cache.get_required_global(settings)["theme"], "ash")
|
|
|
|
def test_update_and_delete(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
document = item_doc()
|
|
cache = (
|
|
CultCache.builder()
|
|
.register_document_type(document)
|
|
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
|
|
.build()
|
|
)
|
|
cache.pull_all_backing_stores()
|
|
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
|
|
cache.update(document, "item:potion", lambda item: Item(item.name, item.category, item.value + 5))
|
|
self.assertEqual(cache.get_required(document, "item:potion").value, 55)
|
|
cache.delete(document, "item:potion")
|
|
self.assertIsNone(cache.get(document, "item:potion"))
|
|
|
|
def test_incremental_lookup_updates_replace_stale_name_and_index_entries(self) -> None:
|
|
document = item_doc()
|
|
cache = CultCache()
|
|
cache.register_document_type(document)
|
|
|
|
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
|
|
cache.put(document, "item:potion", Item(name="Elixir", category="Rare", value=80))
|
|
|
|
self.assertIsNone(cache.get_key_by_name(document, "Potion"))
|
|
self.assertIsNone(cache.get_key_by_index(document, "category", "Consumable"))
|
|
self.assertEqual(cache.get_key_by_name(document, "Elixir"), "item:potion")
|
|
self.assertEqual(cache.get_by_index(document, "category", "Rare").name, "Elixir")
|
|
|
|
def test_incremental_lookup_delete_restores_duplicate_owner(self) -> None:
|
|
document = item_doc()
|
|
cache = CultCache()
|
|
cache.register_document_type(document)
|
|
|
|
cache.put(document, "item:first", Item(name="Potion", category="Consumable", value=50))
|
|
cache.put(document, "item:second", Item(name="Potion", category="Consumable", value=60))
|
|
self.assertEqual(cache.get_key_by_name(document, "Potion"), "item:second")
|
|
self.assertEqual(cache.get_key_by_index(document, "category", "Consumable"), "item:second")
|
|
|
|
cache.delete(document, "item:second")
|
|
|
|
self.assertEqual(cache.get_key_by_name(document, "Potion"), "item:first")
|
|
self.assertEqual(cache.get_key_by_index(document, "category", "Consumable"), "item:first")
|
|
|
|
def test_database_entry_formatter_uses_slot_indexed_messagepack_array(self) -> None:
|
|
try:
|
|
import msgpack # type: ignore
|
|
except ModuleNotFoundError:
|
|
self.skipTest("msgpack optional dependency is not installed")
|
|
|
|
@dataclass
|
|
class Settings:
|
|
theme: str
|
|
retries: int = 0
|
|
|
|
document = define_database_entry_type(
|
|
"settings",
|
|
[
|
|
("theme", 0),
|
|
("retries", 2, 0),
|
|
],
|
|
cls=Settings,
|
|
)
|
|
|
|
payload = document.encode_payload(Settings(theme="ash", retries=3))
|
|
self.assertEqual(msgpack.unpackb(payload, raw=False), ["ash", None, 3])
|
|
self.assertEqual(document.decode_payload(msgpack.packb(["ash"], use_bin_type=True)).retries, 0)
|
|
|
|
def test_document_catalog_entry_is_cached_after_first_derivation(self) -> None:
|
|
document = define_database_entry_type(
|
|
"settings",
|
|
[
|
|
("theme", 0),
|
|
("retries", 2, 0),
|
|
],
|
|
)
|
|
|
|
first = document.catalog_entry()
|
|
second = document.catalog_entry()
|
|
|
|
self.assertIs(first, second)
|
|
self.assertEqual([member.slot for member in first.members], [0, 2])
|
|
|
|
def test_messagepack_store_writes_v1_snapshot(self) -> None:
|
|
try:
|
|
import msgpack # type: ignore
|
|
except ModuleNotFoundError:
|
|
self.skipTest("msgpack optional dependency is not installed")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
document = define_database_entry_type(
|
|
"settings",
|
|
[("theme", 0)],
|
|
schema_id="settings",
|
|
schema_name="settings",
|
|
schema_version="settings.v1",
|
|
)
|
|
store_path = Path(tmp) / "cache.msgpack"
|
|
cache = (
|
|
CultCache.builder()
|
|
.register_document_type(document)
|
|
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
|
|
.build()
|
|
)
|
|
cache.pull_all_backing_stores()
|
|
cache.put(document, "app", {"theme": "ash"})
|
|
|
|
raw = msgpack.unpackb(store_path.read_bytes(), raw=False)
|
|
self.assertEqual(raw[0], "cultcache.store.v1")
|
|
self.assertEqual(raw[1][0][0], "settings")
|
|
self.assertEqual(raw[2][0][0], "app")
|
|
self.assertEqual(raw[2][0][1], "settings")
|
|
|
|
def test_messagepack_store_recovers_schema_stamped_record_missing_catalog_entry(self) -> None:
|
|
try:
|
|
import msgpack # type: ignore
|
|
except ModuleNotFoundError:
|
|
self.skipTest("msgpack optional dependency is not installed")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
document = define_database_entry_type(
|
|
"runtime-policy",
|
|
[
|
|
("schema_version", 0),
|
|
("name", 1),
|
|
("value", 2),
|
|
],
|
|
schema_id="schema-current",
|
|
schema_name="tests.schema_stamped_entry",
|
|
schema_version="tests.schema_stamped_entry.v1",
|
|
)
|
|
store_path = Path(tmp) / "missing-catalog.msgpack"
|
|
store_path.write_bytes(msgpack.packb([
|
|
"cultcache.store.v1",
|
|
[],
|
|
[[
|
|
"record-1",
|
|
"sha256:stale-schema-id-from-cold-record",
|
|
"2026-06-25T12:00:00Z",
|
|
msgpack.packb([
|
|
"tests.schema_stamped_entry.v1",
|
|
"schema-stamped",
|
|
"still readable",
|
|
], use_bin_type=True),
|
|
]],
|
|
], use_bin_type=True))
|
|
|
|
cache = (
|
|
CultCache.builder()
|
|
.register_document_type(document)
|
|
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
|
|
.build()
|
|
)
|
|
|
|
cache.pull_all_backing_stores()
|
|
self.assertEqual(cache.get_required(document, "record-1")["value"], "still readable")
|
|
envelope = cache.get_required_envelope(document, "record-1")
|
|
self.assertEqual(envelope.type, "runtime-policy")
|
|
self.assertEqual(envelope.schema_id, "sha256:stale-schema-id-from-cold-record")
|
|
|
|
def test_interop_cli_helpers_round_trip_v1_store(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
store_path = str(Path(tmp) / "cache.msgpack")
|
|
written = write_note(store_path, "python-test")
|
|
loaded = read_note(store_path)
|
|
self.assertEqual(loaded["documentId"], written["documentId"])
|
|
self.assertEqual(loaded["authorRuntimeId"], "python-test")
|
|
self.assertIn("interop", loaded["tags"])
|
|
|
|
def test_cache_put_without_backing_store_keeps_in_memory_value(self) -> None:
|
|
document = define_database_entry_type(
|
|
"bench.memory_item",
|
|
[
|
|
("name", 0),
|
|
("value", 1, 0),
|
|
],
|
|
)
|
|
cache = CultCache()
|
|
cache.register_document_type(document)
|
|
|
|
cache.put(document, "item:one", {"name": "one", "value": 1})
|
|
|
|
self.assertEqual(cache.get(document, "item:one"), {"name": "one", "value": 1})
|
|
self.assertEqual(cache.get_required_envelope(document, "item:one").key, "item:one")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|