cultcache-py 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: cultcache-py
3
+ Version: 0.2.0
4
+ Summary: Python CultCache runtime for GameCult typed state.
5
+ Author: GameCult
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: msgpack>=1.0
14
+
15
+ # cultcache-py
16
+
17
+ `cultcache-py` is the Python CultCache runtime. Callers work with registered
18
+ domain documents while the cache owns schema identity, routing, globals, name
19
+ lookups, indexes, and backing-store envelopes.
20
+
21
+ It is intentionally small. It is not an ORM, a database, or distributed consensus wearing rented authority.
22
+
23
+ It is the bottom of the Python CultLib stack and depends only on `msgpack`.
24
+ `cultnet-py` (transport and wire contracts) and `cultmesh-py` (nodes,
25
+ discovery, sessions, local server) build on it, mirroring `cultcache-rs` /
26
+ `cultnet-rs` / `cultmesh-rs` and the npm packages.
27
+
28
+ ## Current Shape
29
+
30
+ - document types are registered explicitly
31
+ - a registered cache can be used in memory without attaching a backing store
32
+ - `SingleFileMessagePackBackingStore` writes `cultcache.store.v1` snapshots:
33
+ `[format, schemaCatalog, records]`
34
+ - records store raw MessagePack payload bytes under schema-catalog identity
35
+ - payloads decode only through the registered document definition for that type
36
+ - unknown persisted types fail closed
37
+ - global documents are singleton-style per type
38
+ - type-specific backing stores beat generic backing stores
39
+ - the JSONL store uses only the Python standard library for bootstrap consumers
40
+ - `define_database_entry_type(...)` emits Rust/C#-style slot-indexed
41
+ MessagePack array payloads for cross-runtime `DatabaseEntry` contracts
42
+
43
+ ## Example
44
+
45
+ ```python
46
+ from dataclasses import dataclass, asdict
47
+ from cultcache_py import CultCache, define_document_type
48
+
49
+ @dataclass
50
+ class Settings:
51
+ theme: str
52
+ retries: int
53
+
54
+ settings_doc = define_document_type(
55
+ "settings",
56
+ encode=lambda value: asdict(value),
57
+ decode=lambda payload: Settings(**payload),
58
+ global_document=True,
59
+ )
60
+
61
+ cache = (
62
+ CultCache.builder()
63
+ .register_document_type(settings_doc)
64
+ .build()
65
+ )
66
+
67
+ cache.put_global(settings_doc, Settings(theme="ash", retries=3))
68
+ settings = cache.get_required_global(settings_doc)
69
+ ```
70
+
71
+ Add `JsonLinesBackingStore` or `SingleFileMessagePackBackingStore` when the
72
+ cache should persist state; local in-memory cache reads and writes do not need a
73
+ store.
74
+
75
+ ## Public Surface
76
+
77
+ - `define_document_type(...)`
78
+ - `define_database_entry_type(...)`
79
+ - `database_entry_field(...)`
80
+ - `define_document_registry(...)`
81
+ - `CultCache.builder()`
82
+ - `register_document_type(...)`
83
+ - `register_registry(...)`
84
+ - `register_name_lookup(...)`
85
+ - `register_index(...)`
86
+ - `add_backing_store(...)`
87
+ - `add_generic_store(...)`
88
+ - `pull_all_backing_stores()`
89
+ - `get(...)`
90
+ - `get_required(...)`
91
+ - `get_all(...)`
92
+ - `get_key_by_name(...)`
93
+ - `get_by_name(...)`
94
+ - `get_key_by_index(...)`
95
+ - `get_by_index(...)`
96
+ - `get_global(...)`
97
+ - `get_required_global(...)`
98
+ - `put(...)`
99
+ - `put_envelopes(...)`
100
+ - `put_global(...)`
101
+ - `update(...)`
102
+ - `update_global(...)`
103
+ - `delete(...)`
104
+ - `delete_global(...)`
105
+ - `snapshot()`
106
+
107
+ ## Persistence Model
108
+
109
+ Backing stores persist envelopes, not caller domain objects. The cache decodes payloads through registered document definitions and rejects unknown type discriminators. This keeps Python's dynamic runtime from turning persistence into an open polymorphic sewer with a cheerful docstring.
110
+
111
+ `JsonLinesBackingStore` is the dependency-free control-plane store. It rewrites an atomic JSONL snapshot and base64-encodes payload bytes. It is designed for compact state spines, settings, ledgers, and bootstrap surfaces.
112
+
113
+ `SingleFileMessagePackBackingStore` follows the shared CultCache v1 store shape
114
+ used by the TypeScript, Rust, and C# runtimes:
115
+
116
+ - top-level MessagePack value: `[format, schemaCatalog, records]`
117
+ - `format`: `cultcache.store.v1`
118
+ - catalog entries: `[schemaId, schemaName, schemaVersion, contentHash,
119
+ canonicalSchemaJson, compatibleSchemaIds, members]`
120
+ - records: `[key, schemaId, storedAt, payload]`
121
+
122
+ Large corpora should use a sharded store or a real database. A single snapshot file is a scalpel, not a forklift.
123
+
124
+ ## DatabaseEntry Slot Contract
125
+
126
+ For cross-runtime cache entries, use `define_database_entry_type(...)` instead
127
+ of the generic JSON formatter:
128
+
129
+ ```python
130
+ from dataclasses import dataclass
131
+ from cultcache_py import define_database_entry_type
132
+
133
+ @dataclass
134
+ class Settings:
135
+ theme: str
136
+ retries: int = 0
137
+
138
+ settings_doc = define_database_entry_type(
139
+ "settings",
140
+ [
141
+ ("theme", 0),
142
+ ("retries", 1, 0),
143
+ ],
144
+ cls=Settings,
145
+ )
146
+ ```
147
+
148
+ The payload is a MessagePack array. Field keys are durable slot indexes:
149
+
150
+ - key `0` writes array slot 0
151
+ - key `4` writes array slot 4
152
+ - unused slots are written as nil
153
+ - deleted fields should leave their slots reserved
154
+ - newly added fields should use new keys
155
+ - fields with defaults tolerate missing or nil slots when older payloads are read
156
+
157
+ That matches the Rust `#[derive(DatabaseEntry)]` formatter shape and the C#
158
+ `[Key(n)]` intent: schema evolution comes from stable field slots, not source
159
+ member order.
160
+
161
+ ## Wire Parity
162
+
163
+ See [docs/python-runtime-parity.md](../../docs/python-runtime-parity.md) for
164
+ the evidence map across the three Python packages.
165
+
166
+ The store interop peer is `cultcache_py.interop`:
167
+
168
+ ```powershell
169
+ python -m cultcache_py.interop write --file cache.cc --runtime-id python
170
+ python -m cultcache_py.interop read --file cache.cc
171
+ ```
172
+
173
+ `packages/cultcache-ts/test/cult-cache.test.ts` includes Python in the shared
174
+ CultCache v1 parity matrix with TypeScript, Rust, and C#. `cultcache_py` ships
175
+ a `py.typed` marker so downstream type checkers can inspect the package surface.
176
+
177
+ ## Tests
178
+
179
+ ```powershell
180
+ $env:PYTHONPATH="$PWD\packages\cultcache-py\src"
181
+ python -m unittest discover -s packages\cultcache-py\tests
182
+ ```
183
+
184
+ The suite runs with only this package on the path. If a test here needs
185
+ `cultnet_py` or `cultmesh_py`, it belongs in that package.
@@ -0,0 +1,171 @@
1
+ # cultcache-py
2
+
3
+ `cultcache-py` is the Python CultCache runtime. Callers work with registered
4
+ domain documents while the cache owns schema identity, routing, globals, name
5
+ lookups, indexes, and backing-store envelopes.
6
+
7
+ It is intentionally small. It is not an ORM, a database, or distributed consensus wearing rented authority.
8
+
9
+ It is the bottom of the Python CultLib stack and depends only on `msgpack`.
10
+ `cultnet-py` (transport and wire contracts) and `cultmesh-py` (nodes,
11
+ discovery, sessions, local server) build on it, mirroring `cultcache-rs` /
12
+ `cultnet-rs` / `cultmesh-rs` and the npm packages.
13
+
14
+ ## Current Shape
15
+
16
+ - document types are registered explicitly
17
+ - a registered cache can be used in memory without attaching a backing store
18
+ - `SingleFileMessagePackBackingStore` writes `cultcache.store.v1` snapshots:
19
+ `[format, schemaCatalog, records]`
20
+ - records store raw MessagePack payload bytes under schema-catalog identity
21
+ - payloads decode only through the registered document definition for that type
22
+ - unknown persisted types fail closed
23
+ - global documents are singleton-style per type
24
+ - type-specific backing stores beat generic backing stores
25
+ - the JSONL store uses only the Python standard library for bootstrap consumers
26
+ - `define_database_entry_type(...)` emits Rust/C#-style slot-indexed
27
+ MessagePack array payloads for cross-runtime `DatabaseEntry` contracts
28
+
29
+ ## Example
30
+
31
+ ```python
32
+ from dataclasses import dataclass, asdict
33
+ from cultcache_py import CultCache, define_document_type
34
+
35
+ @dataclass
36
+ class Settings:
37
+ theme: str
38
+ retries: int
39
+
40
+ settings_doc = define_document_type(
41
+ "settings",
42
+ encode=lambda value: asdict(value),
43
+ decode=lambda payload: Settings(**payload),
44
+ global_document=True,
45
+ )
46
+
47
+ cache = (
48
+ CultCache.builder()
49
+ .register_document_type(settings_doc)
50
+ .build()
51
+ )
52
+
53
+ cache.put_global(settings_doc, Settings(theme="ash", retries=3))
54
+ settings = cache.get_required_global(settings_doc)
55
+ ```
56
+
57
+ Add `JsonLinesBackingStore` or `SingleFileMessagePackBackingStore` when the
58
+ cache should persist state; local in-memory cache reads and writes do not need a
59
+ store.
60
+
61
+ ## Public Surface
62
+
63
+ - `define_document_type(...)`
64
+ - `define_database_entry_type(...)`
65
+ - `database_entry_field(...)`
66
+ - `define_document_registry(...)`
67
+ - `CultCache.builder()`
68
+ - `register_document_type(...)`
69
+ - `register_registry(...)`
70
+ - `register_name_lookup(...)`
71
+ - `register_index(...)`
72
+ - `add_backing_store(...)`
73
+ - `add_generic_store(...)`
74
+ - `pull_all_backing_stores()`
75
+ - `get(...)`
76
+ - `get_required(...)`
77
+ - `get_all(...)`
78
+ - `get_key_by_name(...)`
79
+ - `get_by_name(...)`
80
+ - `get_key_by_index(...)`
81
+ - `get_by_index(...)`
82
+ - `get_global(...)`
83
+ - `get_required_global(...)`
84
+ - `put(...)`
85
+ - `put_envelopes(...)`
86
+ - `put_global(...)`
87
+ - `update(...)`
88
+ - `update_global(...)`
89
+ - `delete(...)`
90
+ - `delete_global(...)`
91
+ - `snapshot()`
92
+
93
+ ## Persistence Model
94
+
95
+ Backing stores persist envelopes, not caller domain objects. The cache decodes payloads through registered document definitions and rejects unknown type discriminators. This keeps Python's dynamic runtime from turning persistence into an open polymorphic sewer with a cheerful docstring.
96
+
97
+ `JsonLinesBackingStore` is the dependency-free control-plane store. It rewrites an atomic JSONL snapshot and base64-encodes payload bytes. It is designed for compact state spines, settings, ledgers, and bootstrap surfaces.
98
+
99
+ `SingleFileMessagePackBackingStore` follows the shared CultCache v1 store shape
100
+ used by the TypeScript, Rust, and C# runtimes:
101
+
102
+ - top-level MessagePack value: `[format, schemaCatalog, records]`
103
+ - `format`: `cultcache.store.v1`
104
+ - catalog entries: `[schemaId, schemaName, schemaVersion, contentHash,
105
+ canonicalSchemaJson, compatibleSchemaIds, members]`
106
+ - records: `[key, schemaId, storedAt, payload]`
107
+
108
+ Large corpora should use a sharded store or a real database. A single snapshot file is a scalpel, not a forklift.
109
+
110
+ ## DatabaseEntry Slot Contract
111
+
112
+ For cross-runtime cache entries, use `define_database_entry_type(...)` instead
113
+ of the generic JSON formatter:
114
+
115
+ ```python
116
+ from dataclasses import dataclass
117
+ from cultcache_py import define_database_entry_type
118
+
119
+ @dataclass
120
+ class Settings:
121
+ theme: str
122
+ retries: int = 0
123
+
124
+ settings_doc = define_database_entry_type(
125
+ "settings",
126
+ [
127
+ ("theme", 0),
128
+ ("retries", 1, 0),
129
+ ],
130
+ cls=Settings,
131
+ )
132
+ ```
133
+
134
+ The payload is a MessagePack array. Field keys are durable slot indexes:
135
+
136
+ - key `0` writes array slot 0
137
+ - key `4` writes array slot 4
138
+ - unused slots are written as nil
139
+ - deleted fields should leave their slots reserved
140
+ - newly added fields should use new keys
141
+ - fields with defaults tolerate missing or nil slots when older payloads are read
142
+
143
+ That matches the Rust `#[derive(DatabaseEntry)]` formatter shape and the C#
144
+ `[Key(n)]` intent: schema evolution comes from stable field slots, not source
145
+ member order.
146
+
147
+ ## Wire Parity
148
+
149
+ See [docs/python-runtime-parity.md](../../docs/python-runtime-parity.md) for
150
+ the evidence map across the three Python packages.
151
+
152
+ The store interop peer is `cultcache_py.interop`:
153
+
154
+ ```powershell
155
+ python -m cultcache_py.interop write --file cache.cc --runtime-id python
156
+ python -m cultcache_py.interop read --file cache.cc
157
+ ```
158
+
159
+ `packages/cultcache-ts/test/cult-cache.test.ts` includes Python in the shared
160
+ CultCache v1 parity matrix with TypeScript, Rust, and C#. `cultcache_py` ships
161
+ a `py.typed` marker so downstream type checkers can inspect the package surface.
162
+
163
+ ## Tests
164
+
165
+ ```powershell
166
+ $env:PYTHONPATH="$PWD\packages\cultcache-py\src"
167
+ python -m unittest discover -s packages\cultcache-py\tests
168
+ ```
169
+
170
+ The suite runs with only this package on the path. If a test here needs
171
+ `cultnet_py` or `cultmesh_py`, it belongs in that package.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cultcache-py"
7
+ version = "0.2.0"
8
+ description = "Python CultCache runtime for GameCult typed state."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "GameCult" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ ]
19
+ dependencies = ["msgpack>=1.0"]
20
+ [project.scripts]
21
+ cultcache-py-interop = "cultcache_py.interop:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.setuptools.package-data]
27
+ cultcache_py = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ from .backing_store import BackingStore, CultCacheEnvelope
2
+ from .backing_store import CultCacheSchemaCatalogEntry, CultCacheSchemaCatalogMember
3
+ from .cache import CultCache, CultCacheBuilder
4
+ from .documents import (
5
+ DatabaseEntryField,
6
+ DocumentDefinition,
7
+ database_entry_field,
8
+ define_database_entry_type,
9
+ define_document_registry,
10
+ define_document_type,
11
+ )
12
+ from .stores import JsonLinesBackingStore, SingleFileMessagePackBackingStore
13
+
14
+ __all__ = [
15
+ "BackingStore",
16
+ "CultCache",
17
+ "CultCacheBuilder",
18
+ "CultCacheEnvelope",
19
+ "CultCacheSchemaCatalogEntry",
20
+ "CultCacheSchemaCatalogMember",
21
+ "DatabaseEntryField",
22
+ "DocumentDefinition",
23
+ "JsonLinesBackingStore",
24
+ "SingleFileMessagePackBackingStore",
25
+ "database_entry_field",
26
+ "define_database_entry_type",
27
+ "define_document_registry",
28
+ "define_document_type",
29
+ ]
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Protocol
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CultCacheSchemaCatalogMember:
10
+ slot: int
11
+ member_name: str
12
+ type_name: str
13
+ is_reference: bool = False
14
+ is_many: bool = False
15
+ target_schema_name: str | None = None
16
+ is_name: bool = False
17
+ index_alias: str | None = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CultCacheSchemaCatalogEntry:
22
+ schema_id: str
23
+ schema_name: str
24
+ schema_version: str
25
+ content_hash: str
26
+ canonical_schema_json: str
27
+ compatible_schema_ids: tuple[str, ...] = field(default_factory=tuple)
28
+ members: tuple[CultCacheSchemaCatalogMember, ...] = field(default_factory=tuple)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class CultCacheEnvelope:
33
+ key: str
34
+ type: str
35
+ payload: bytes
36
+ stored_at: str
37
+ schema_id: str | None = None
38
+ catalog_entry: CultCacheSchemaCatalogEntry | None = None
39
+
40
+ @classmethod
41
+ def create(
42
+ cls,
43
+ *,
44
+ key: str,
45
+ type: str,
46
+ payload: bytes,
47
+ schema_id: str | None = None,
48
+ catalog_entry: CultCacheSchemaCatalogEntry | None = None,
49
+ ) -> "CultCacheEnvelope":
50
+ return cls(
51
+ key=key,
52
+ type=type,
53
+ payload=payload,
54
+ stored_at=datetime.now(timezone.utc).isoformat(),
55
+ schema_id=schema_id,
56
+ catalog_entry=catalog_entry,
57
+ )
58
+
59
+
60
+ class BackingStore(Protocol):
61
+ def pull_all(self) -> list[CultCacheEnvelope]:
62
+ ...
63
+
64
+ def push(self, envelope: CultCacheEnvelope) -> None:
65
+ ...
66
+
67
+ def delete(self, type: str, key: str) -> None:
68
+ ...
69
+
70
+ def push_all(self, envelopes: list[CultCacheEnvelope]) -> None:
71
+ existing = {
72
+ (envelope.type, envelope.key): envelope for envelope in self.pull_all()
73
+ }
74
+ for envelope in envelopes:
75
+ existing[(envelope.type, envelope.key)] = envelope
76
+ self._replace_all(list(existing.values()))
77
+
78
+ def _replace_all(self, envelopes: list[CultCacheEnvelope]) -> None:
79
+ ...