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.
- cultcache_py-0.2.0/PKG-INFO +185 -0
- cultcache_py-0.2.0/README.md +171 -0
- cultcache_py-0.2.0/pyproject.toml +27 -0
- cultcache_py-0.2.0/setup.cfg +4 -0
- cultcache_py-0.2.0/src/cultcache_py/__init__.py +29 -0
- cultcache_py-0.2.0/src/cultcache_py/backing_store.py +79 -0
- cultcache_py-0.2.0/src/cultcache_py/cache.py +383 -0
- cultcache_py-0.2.0/src/cultcache_py/documents.py +303 -0
- cultcache_py-0.2.0/src/cultcache_py/interop.py +102 -0
- cultcache_py-0.2.0/src/cultcache_py/py.typed +1 -0
- cultcache_py-0.2.0/src/cultcache_py/stores.py +355 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/PKG-INFO +185 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/SOURCES.txt +16 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/dependency_links.txt +1 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/entry_points.txt +2 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/requires.txt +1 -0
- cultcache_py-0.2.0/src/cultcache_py.egg-info/top_level.txt +1 -0
- cultcache_py-0.2.0/tests/test_cultcache.py +259 -0
|
@@ -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,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
|
+
...
|