backpack-store 0.1.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.
- backpack_store-0.1.0/LICENSE +21 -0
- backpack_store-0.1.0/PKG-INFO +252 -0
- backpack_store-0.1.0/README.md +224 -0
- backpack_store-0.1.0/pyproject.toml +53 -0
- backpack_store-0.1.0/pyproject.toml.orig +40 -0
- backpack_store-0.1.0/src/backpack_store/__init__.py +49 -0
- backpack_store-0.1.0/src/backpack_store/codecs.py +318 -0
- backpack_store-0.1.0/src/backpack_store/errors.py +53 -0
- backpack_store-0.1.0/src/backpack_store/host.py +45 -0
- backpack_store-0.1.0/src/backpack_store/integration.py +122 -0
- backpack_store-0.1.0/src/backpack_store/pydantic_codec.py +87 -0
- backpack_store-0.1.0/src/backpack_store/records.py +189 -0
- backpack_store-0.1.0/src/backpack_store/store.py +519 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ella Inng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backpack-store
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Store and retrieve typed local records with identity and provenance.
|
|
5
|
+
Keywords: typed-storage,sqlite,records,provenance,local-first
|
|
6
|
+
Author: Ella Inng
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Dist: hidden-moves>=0.1,<0.2 ; extra == 'host'
|
|
15
|
+
Requires-Dist: hidden-moves-mcp>=0.1,<0.2 ; extra == 'host'
|
|
16
|
+
Requires-Dist: hidden-moves>=0.1,<0.2 ; extra == 'moves'
|
|
17
|
+
Requires-Dist: pydantic>=2.13,<3 ; extra == 'pydantic'
|
|
18
|
+
Maintainer: Outside Labs
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Homepage, https://github.com/outside-labs/backpack
|
|
21
|
+
Project-URL: Repository, https://github.com/outside-labs/backpack
|
|
22
|
+
Project-URL: Issues, https://github.com/outside-labs/backpack/issues
|
|
23
|
+
Project-URL: Documentation, https://github.com/outside-labs/backpack#readme
|
|
24
|
+
Provides-Extra: host
|
|
25
|
+
Provides-Extra: moves
|
|
26
|
+
Provides-Extra: pydantic
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Backpack
|
|
30
|
+
|
|
31
|
+
Backpack is an experimental programmable local store for typed records. The
|
|
32
|
+
SQLite API stores finite JSON with explicit codecs, local identity and provenance.
|
|
33
|
+
Records recover their Python types after closing and reopening the database.
|
|
34
|
+
|
|
35
|
+
- Product: Backpack
|
|
36
|
+
- Distribution: `backpack-store`
|
|
37
|
+
- Import: `backpack_store`
|
|
38
|
+
- Python: 3.11 or newer
|
|
39
|
+
- Runtime dependencies: none
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
python -m pip install backpack-store==0.1.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional extras are `backpack-store[pydantic]`, `backpack-store[moves]` and
|
|
48
|
+
`backpack-store[host]`. The `backpack_store` import stays the same for every extra.
|
|
49
|
+
|
|
50
|
+
## Explicit typed records
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from dataclasses import dataclass
|
|
54
|
+
from backpack_store import CodecRegistry, DataclassCodec
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Note:
|
|
58
|
+
title: str
|
|
59
|
+
body: str
|
|
60
|
+
|
|
61
|
+
registry = CodecRegistry()
|
|
62
|
+
registry.register(DataclassCodec(Note, type_key="notes.note", schema_version=1))
|
|
63
|
+
encoded = registry.encode(Note("Example", "Local text"))
|
|
64
|
+
restored = registry.decode(encoded.type_key, encoded.schema_version, encoded.payload)
|
|
65
|
+
assert isinstance(restored, Note)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A stable dotted `type_key` and positive `schema_version` identify each registered
|
|
69
|
+
codec. Registrations are explicit and reject duplicate keys or Python types.
|
|
70
|
+
Dataclass codecs support concrete non-recursive dataclasses, nested dataclasses,
|
|
71
|
+
JSON scalar fields, literals, optional/unions, typed lists, tuples and string-keyed
|
|
72
|
+
dictionaries. They validate every field; unknown or missing fields fail. A custom
|
|
73
|
+
codec can support other trusted model types. Stored names never cause imports.
|
|
74
|
+
|
|
75
|
+
Record envelopes contain a separate local UUID, payload schema version, revision,
|
|
76
|
+
UTC creation/update timestamps, normalized exact tags and optional `Provenance`.
|
|
77
|
+
Timestamps use `YYYY-MM-DDTHH:MM:SS.ffffffZ`. Provenance names the source URL,
|
|
78
|
+
system, object ID and capture time; a source ID is not the local record UUID.
|
|
79
|
+
JSON payloads must be finite objects, at most 1 MiB and 64 nesting levels. Tags
|
|
80
|
+
are case-sensitive NFC strings, trimmed, deduplicated and sorted (up to 64 supplied
|
|
81
|
+
values of 128 characters). Unknown types/versions and invalid payloads raise typed
|
|
82
|
+
errors. Importing the package opens no files or database and performs no network I/O.
|
|
83
|
+
|
|
84
|
+
## Durable local storage
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from backpack_store import Backpack
|
|
88
|
+
|
|
89
|
+
with Backpack.open("notes.sqlite") as store:
|
|
90
|
+
store.register(DataclassCodec(Note, type_key="notes.note"))
|
|
91
|
+
written = store.put(Note("Example", "Local text"), tags=["work"])
|
|
92
|
+
|
|
93
|
+
with Backpack.open("notes.sqlite") as store:
|
|
94
|
+
store.register(DataclassCodec(Note, type_key="notes.note"))
|
|
95
|
+
note = store.get(written.id)
|
|
96
|
+
record = store.get_record(written.id) # envelope plus .value
|
|
97
|
+
page = store.find(Note, tags=["work"], limit=20)
|
|
98
|
+
updated = store.update(written.id, Note("Revised", "Local text"),
|
|
99
|
+
expected_revision=record.revision, tags=["work"])
|
|
100
|
+
assert updated.revision == 2
|
|
101
|
+
assert store.delete(written.id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Each `put` creates a new UUID and returns `WriteResult(id, revision)`. `update`
|
|
105
|
+
replaces payload, tags and provenance together, preserves the local ID/type key
|
|
106
|
+
and creation time, and requires the expected revision. A stale revision raises
|
|
107
|
+
`RevisionConflictError` without changing the record. Missing reads/updates raise
|
|
108
|
+
`NotFoundError`; deletion returns whether a row existed. Payload, metadata and
|
|
109
|
+
tag writes share one transaction. Invalid input and database failures leave no
|
|
110
|
+
partial record.
|
|
111
|
+
|
|
112
|
+
`find` requires an explicitly registered Python type and matches **all** requested
|
|
113
|
+
tags. It returns at most 1,000 records (default 50) in ascending local UUID order.
|
|
114
|
+
Pass `.next_cursor` with the same type/tags to continue. Cursors are query-scoped;
|
|
115
|
+
concurrent inserts, updates or deletions can change later pages, so pagination
|
|
116
|
+
does not promise a frozen snapshot. `get_raw` returns recoverable JSON/metadata
|
|
117
|
+
without decoding an unknown registered type or payload version.
|
|
118
|
+
|
|
119
|
+
The database schema has its own version, separate from payload versions. Unknown
|
|
120
|
+
or nonempty unversioned databases fail safely. Use the connection on its opening
|
|
121
|
+
thread; a context manager closes it even after caller failure. Lock waiting is
|
|
122
|
+
bounded by `timeout` (default 5 seconds, allowed 0–60). No path is chosen by
|
|
123
|
+
default. Parent directories must already exist. Importing never opens a database.
|
|
124
|
+
|
|
125
|
+
Run `python examples/persistence.py` for a complete offline file-backed Note and
|
|
126
|
+
synthetic ProjectItem round trip, including filtering and deletion.
|
|
127
|
+
|
|
128
|
+
## Version evolution and export
|
|
129
|
+
|
|
130
|
+
Register the current codec, then explicit trusted functions for every one-version
|
|
131
|
+
step from an older payload to the current version:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
store.register(DataclassCodec(NoteV2, type_key="notes.note", schema_version=2))
|
|
135
|
+
store.register_migration("notes.note", from_version=1, to_version=2,
|
|
136
|
+
migrate=lambda old: {"title": old["title"], "text": old["body"]})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`NoteV2` is an application-defined dataclass with `title` and `text` fields. Old
|
|
140
|
+
reads upgrade in memory; `.payload` and `.schema_version` on the returned envelope
|
|
141
|
+
still describe the original stored representation. Only an explicit
|
|
142
|
+
revision-checked `update` writes the current representation. Missing migration
|
|
143
|
+
steps and newer unknown versions raise `UnknownVersionError`; failed/invalid
|
|
144
|
+
migration output raises `MigrationError`. Migration functions never come from a
|
|
145
|
+
stored module name. Database schema version 1 is independent; no destructive
|
|
146
|
+
migration or automatic database replacement is provided.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
page = store.export_jsonl("records-1.jsonl", limit=100, max_bytes=4_194_304)
|
|
150
|
+
# If page.next_cursor is set, export to another new explicit destination:
|
|
151
|
+
# store.export_jsonl("records-2.jsonl", cursor=page.next_cursor, limit=100)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Export writes UTF-8 JSONL containing raw record envelopes with `format_version: 1`,
|
|
155
|
+
type/payload versions and provenance. No codec or migration runs during export.
|
|
156
|
+
It stops at the record or byte budget (default 1,000 records / 4 MiB; maximum
|
|
157
|
+
1,000 / 64 MiB). If a single record exceeds the byte budget, it fails before
|
|
158
|
+
creating a destination. Pages use ascending UUIDs and the same concurrent-write
|
|
159
|
+
limitations as `find`. A complete temporary file is published without overwriting
|
|
160
|
+
an existing path; the destination directory must exist and support hard links.
|
|
161
|
+
Export never rewrites the database.
|
|
162
|
+
|
|
163
|
+
## Optional selected tools
|
|
164
|
+
|
|
165
|
+
The direct store remains independent of Hidden Moves and MCP. Optional `moves`
|
|
166
|
+
and `host` extras declare the capability-family dependencies. CI uses a reviewed
|
|
167
|
+
pinned family revision, runs standalone storage first, then installs the optional
|
|
168
|
+
wheels.
|
|
169
|
+
|
|
170
|
+
Installed entry-point discovery advertises `backpack`; load it explicitly and bind
|
|
171
|
+
an already-configured Backpack instance. The factory chooses no path and opens no
|
|
172
|
+
database. The application registers trusted models before selecting tools:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from backpack_store.host import build_catalog
|
|
176
|
+
from backpack_store.integration import READ_CAPABILITIES, WRITE_CAPABILITIES
|
|
177
|
+
|
|
178
|
+
read = build_catalog(store, READ_CAPABILITIES)
|
|
179
|
+
result = read.invoke("backpack.records.find", {"type_key": "notes.note", "limit": 10})
|
|
180
|
+
write = build_catalog(store, WRITE_CAPABILITIES, allow_writes=True)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Read tools are `backpack.records.get` and `.find`; local write tools are `.put` and
|
|
184
|
+
`.delete`. A write profile requires explicit host intent and explicit selection.
|
|
185
|
+
Inputs contain local IDs, registered stable type keys and finite JSON payloads,
|
|
186
|
+
never Python classes, codec imports, SQL or database paths. `put` validates the
|
|
187
|
+
current registered model; `find` enforces the storage query limits. Concrete JSON
|
|
188
|
+
views preserve record identity/version/provenance without serializing arbitrary
|
|
189
|
+
Python objects. Effect annotations describe local reads/insertion/deletion;
|
|
190
|
+
application permissions and caller approval policy remain responsible for access.
|
|
191
|
+
|
|
192
|
+
`serve_store(store, names, allow_writes=False)` serves the same catalog over local
|
|
193
|
+
MCP stdio. The application owns the connection and cleanup. For a complete notes
|
|
194
|
+
application with a fixed trusted model and explicit path/profile:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
python examples/notes_stdio.py --database notes.sqlite --profile read
|
|
198
|
+
# Select a write-capable host only when local writes are intended:
|
|
199
|
+
python examples/notes_stdio.py --database notes.sqlite --profile write
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
These are local tools. No public endpoint or remote access to the SQLite file is
|
|
203
|
+
provided. Run `python -m unittest discover -s integration_tests -v` in the optional
|
|
204
|
+
integration environment to check real stdio, selection and consumer equivalence.
|
|
205
|
+
|
|
206
|
+
## Optional Pydantic model codec
|
|
207
|
+
|
|
208
|
+
Install the `pydantic` extra to use the separate Pydantic 2 extension (supported
|
|
209
|
+
range `>=2.13,<3`; tested with 2.13.5). Importing the core never imports Pydantic.
|
|
210
|
+
The extension uses the same store, explicit identity and migration registry:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from pydantic import BaseModel
|
|
214
|
+
from backpack_store.pydantic_codec import PydanticCodec
|
|
215
|
+
|
|
216
|
+
class ValidatedNote(BaseModel):
|
|
217
|
+
title: str
|
|
218
|
+
priority: int
|
|
219
|
+
|
|
220
|
+
store.register(PydanticCodec(ValidatedNote, type_key="notes.validated", schema_version=1))
|
|
221
|
+
written = store.put(ValidatedNote(title="Example", priority=2))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Register the same trusted model after reopening. The codec accepts the exact
|
|
225
|
+
registered class, serializes aliases and JSON representations, then revalidates
|
|
226
|
+
strictly before writing. Decode also uses strict JSON validation with extra fields
|
|
227
|
+
forbidden. This catches mutated instances and `model_construct` bypasses instead
|
|
228
|
+
of trusting an already-created instance. Payloads still obey the core finite JSON
|
|
229
|
+
object limits. Object-shaped nested models and datetime JSON round trips are
|
|
230
|
+
covered; scalar/list `RootModel` classes are rejected. Private/computed fields are
|
|
231
|
+
not part of the durable representation.
|
|
232
|
+
|
|
233
|
+
This codec does not add native Pydantic type support to tool signatures. The
|
|
234
|
+
optional provider continues to use its concrete JSON views. Run
|
|
235
|
+
`python -m unittest discover -s pydantic_tests -v` in the extension environment.
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
uv build
|
|
241
|
+
python -m pip install dist/backpack_store-0.1.0-py3-none-any.whl
|
|
242
|
+
python -m unittest discover -s tests -v
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
CI installs the wheel built from its source distribution and runs the behavior
|
|
246
|
+
suite on Python 3.11/3.13 across Linux, macOS and Windows. The original generated
|
|
247
|
+
scaffold is preserved in the repository's initial commit.
|
|
248
|
+
|
|
249
|
+
This 0.1.x project is experimental. Published GitHub releases with matching `v`
|
|
250
|
+
tags run `.github/workflows/release.yaml`, verify the installed wheel and source
|
|
251
|
+
distribution, and upload through PyPI trusted publishing in environment `pypi`.
|
|
252
|
+
The initial release tag is `v0.1.0`.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Backpack
|
|
2
|
+
|
|
3
|
+
Backpack is an experimental programmable local store for typed records. The
|
|
4
|
+
SQLite API stores finite JSON with explicit codecs, local identity and provenance.
|
|
5
|
+
Records recover their Python types after closing and reopening the database.
|
|
6
|
+
|
|
7
|
+
- Product: Backpack
|
|
8
|
+
- Distribution: `backpack-store`
|
|
9
|
+
- Import: `backpack_store`
|
|
10
|
+
- Python: 3.11 or newer
|
|
11
|
+
- Runtime dependencies: none
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
python -m pip install backpack-store==0.1.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional extras are `backpack-store[pydantic]`, `backpack-store[moves]` and
|
|
20
|
+
`backpack-store[host]`. The `backpack_store` import stays the same for every extra.
|
|
21
|
+
|
|
22
|
+
## Explicit typed records
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from backpack_store import CodecRegistry, DataclassCodec
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Note:
|
|
30
|
+
title: str
|
|
31
|
+
body: str
|
|
32
|
+
|
|
33
|
+
registry = CodecRegistry()
|
|
34
|
+
registry.register(DataclassCodec(Note, type_key="notes.note", schema_version=1))
|
|
35
|
+
encoded = registry.encode(Note("Example", "Local text"))
|
|
36
|
+
restored = registry.decode(encoded.type_key, encoded.schema_version, encoded.payload)
|
|
37
|
+
assert isinstance(restored, Note)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A stable dotted `type_key` and positive `schema_version` identify each registered
|
|
41
|
+
codec. Registrations are explicit and reject duplicate keys or Python types.
|
|
42
|
+
Dataclass codecs support concrete non-recursive dataclasses, nested dataclasses,
|
|
43
|
+
JSON scalar fields, literals, optional/unions, typed lists, tuples and string-keyed
|
|
44
|
+
dictionaries. They validate every field; unknown or missing fields fail. A custom
|
|
45
|
+
codec can support other trusted model types. Stored names never cause imports.
|
|
46
|
+
|
|
47
|
+
Record envelopes contain a separate local UUID, payload schema version, revision,
|
|
48
|
+
UTC creation/update timestamps, normalized exact tags and optional `Provenance`.
|
|
49
|
+
Timestamps use `YYYY-MM-DDTHH:MM:SS.ffffffZ`. Provenance names the source URL,
|
|
50
|
+
system, object ID and capture time; a source ID is not the local record UUID.
|
|
51
|
+
JSON payloads must be finite objects, at most 1 MiB and 64 nesting levels. Tags
|
|
52
|
+
are case-sensitive NFC strings, trimmed, deduplicated and sorted (up to 64 supplied
|
|
53
|
+
values of 128 characters). Unknown types/versions and invalid payloads raise typed
|
|
54
|
+
errors. Importing the package opens no files or database and performs no network I/O.
|
|
55
|
+
|
|
56
|
+
## Durable local storage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from backpack_store import Backpack
|
|
60
|
+
|
|
61
|
+
with Backpack.open("notes.sqlite") as store:
|
|
62
|
+
store.register(DataclassCodec(Note, type_key="notes.note"))
|
|
63
|
+
written = store.put(Note("Example", "Local text"), tags=["work"])
|
|
64
|
+
|
|
65
|
+
with Backpack.open("notes.sqlite") as store:
|
|
66
|
+
store.register(DataclassCodec(Note, type_key="notes.note"))
|
|
67
|
+
note = store.get(written.id)
|
|
68
|
+
record = store.get_record(written.id) # envelope plus .value
|
|
69
|
+
page = store.find(Note, tags=["work"], limit=20)
|
|
70
|
+
updated = store.update(written.id, Note("Revised", "Local text"),
|
|
71
|
+
expected_revision=record.revision, tags=["work"])
|
|
72
|
+
assert updated.revision == 2
|
|
73
|
+
assert store.delete(written.id)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Each `put` creates a new UUID and returns `WriteResult(id, revision)`. `update`
|
|
77
|
+
replaces payload, tags and provenance together, preserves the local ID/type key
|
|
78
|
+
and creation time, and requires the expected revision. A stale revision raises
|
|
79
|
+
`RevisionConflictError` without changing the record. Missing reads/updates raise
|
|
80
|
+
`NotFoundError`; deletion returns whether a row existed. Payload, metadata and
|
|
81
|
+
tag writes share one transaction. Invalid input and database failures leave no
|
|
82
|
+
partial record.
|
|
83
|
+
|
|
84
|
+
`find` requires an explicitly registered Python type and matches **all** requested
|
|
85
|
+
tags. It returns at most 1,000 records (default 50) in ascending local UUID order.
|
|
86
|
+
Pass `.next_cursor` with the same type/tags to continue. Cursors are query-scoped;
|
|
87
|
+
concurrent inserts, updates or deletions can change later pages, so pagination
|
|
88
|
+
does not promise a frozen snapshot. `get_raw` returns recoverable JSON/metadata
|
|
89
|
+
without decoding an unknown registered type or payload version.
|
|
90
|
+
|
|
91
|
+
The database schema has its own version, separate from payload versions. Unknown
|
|
92
|
+
or nonempty unversioned databases fail safely. Use the connection on its opening
|
|
93
|
+
thread; a context manager closes it even after caller failure. Lock waiting is
|
|
94
|
+
bounded by `timeout` (default 5 seconds, allowed 0–60). No path is chosen by
|
|
95
|
+
default. Parent directories must already exist. Importing never opens a database.
|
|
96
|
+
|
|
97
|
+
Run `python examples/persistence.py` for a complete offline file-backed Note and
|
|
98
|
+
synthetic ProjectItem round trip, including filtering and deletion.
|
|
99
|
+
|
|
100
|
+
## Version evolution and export
|
|
101
|
+
|
|
102
|
+
Register the current codec, then explicit trusted functions for every one-version
|
|
103
|
+
step from an older payload to the current version:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
store.register(DataclassCodec(NoteV2, type_key="notes.note", schema_version=2))
|
|
107
|
+
store.register_migration("notes.note", from_version=1, to_version=2,
|
|
108
|
+
migrate=lambda old: {"title": old["title"], "text": old["body"]})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`NoteV2` is an application-defined dataclass with `title` and `text` fields. Old
|
|
112
|
+
reads upgrade in memory; `.payload` and `.schema_version` on the returned envelope
|
|
113
|
+
still describe the original stored representation. Only an explicit
|
|
114
|
+
revision-checked `update` writes the current representation. Missing migration
|
|
115
|
+
steps and newer unknown versions raise `UnknownVersionError`; failed/invalid
|
|
116
|
+
migration output raises `MigrationError`. Migration functions never come from a
|
|
117
|
+
stored module name. Database schema version 1 is independent; no destructive
|
|
118
|
+
migration or automatic database replacement is provided.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
page = store.export_jsonl("records-1.jsonl", limit=100, max_bytes=4_194_304)
|
|
122
|
+
# If page.next_cursor is set, export to another new explicit destination:
|
|
123
|
+
# store.export_jsonl("records-2.jsonl", cursor=page.next_cursor, limit=100)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Export writes UTF-8 JSONL containing raw record envelopes with `format_version: 1`,
|
|
127
|
+
type/payload versions and provenance. No codec or migration runs during export.
|
|
128
|
+
It stops at the record or byte budget (default 1,000 records / 4 MiB; maximum
|
|
129
|
+
1,000 / 64 MiB). If a single record exceeds the byte budget, it fails before
|
|
130
|
+
creating a destination. Pages use ascending UUIDs and the same concurrent-write
|
|
131
|
+
limitations as `find`. A complete temporary file is published without overwriting
|
|
132
|
+
an existing path; the destination directory must exist and support hard links.
|
|
133
|
+
Export never rewrites the database.
|
|
134
|
+
|
|
135
|
+
## Optional selected tools
|
|
136
|
+
|
|
137
|
+
The direct store remains independent of Hidden Moves and MCP. Optional `moves`
|
|
138
|
+
and `host` extras declare the capability-family dependencies. CI uses a reviewed
|
|
139
|
+
pinned family revision, runs standalone storage first, then installs the optional
|
|
140
|
+
wheels.
|
|
141
|
+
|
|
142
|
+
Installed entry-point discovery advertises `backpack`; load it explicitly and bind
|
|
143
|
+
an already-configured Backpack instance. The factory chooses no path and opens no
|
|
144
|
+
database. The application registers trusted models before selecting tools:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from backpack_store.host import build_catalog
|
|
148
|
+
from backpack_store.integration import READ_CAPABILITIES, WRITE_CAPABILITIES
|
|
149
|
+
|
|
150
|
+
read = build_catalog(store, READ_CAPABILITIES)
|
|
151
|
+
result = read.invoke("backpack.records.find", {"type_key": "notes.note", "limit": 10})
|
|
152
|
+
write = build_catalog(store, WRITE_CAPABILITIES, allow_writes=True)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Read tools are `backpack.records.get` and `.find`; local write tools are `.put` and
|
|
156
|
+
`.delete`. A write profile requires explicit host intent and explicit selection.
|
|
157
|
+
Inputs contain local IDs, registered stable type keys and finite JSON payloads,
|
|
158
|
+
never Python classes, codec imports, SQL or database paths. `put` validates the
|
|
159
|
+
current registered model; `find` enforces the storage query limits. Concrete JSON
|
|
160
|
+
views preserve record identity/version/provenance without serializing arbitrary
|
|
161
|
+
Python objects. Effect annotations describe local reads/insertion/deletion;
|
|
162
|
+
application permissions and caller approval policy remain responsible for access.
|
|
163
|
+
|
|
164
|
+
`serve_store(store, names, allow_writes=False)` serves the same catalog over local
|
|
165
|
+
MCP stdio. The application owns the connection and cleanup. For a complete notes
|
|
166
|
+
application with a fixed trusted model and explicit path/profile:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
python examples/notes_stdio.py --database notes.sqlite --profile read
|
|
170
|
+
# Select a write-capable host only when local writes are intended:
|
|
171
|
+
python examples/notes_stdio.py --database notes.sqlite --profile write
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
These are local tools. No public endpoint or remote access to the SQLite file is
|
|
175
|
+
provided. Run `python -m unittest discover -s integration_tests -v` in the optional
|
|
176
|
+
integration environment to check real stdio, selection and consumer equivalence.
|
|
177
|
+
|
|
178
|
+
## Optional Pydantic model codec
|
|
179
|
+
|
|
180
|
+
Install the `pydantic` extra to use the separate Pydantic 2 extension (supported
|
|
181
|
+
range `>=2.13,<3`; tested with 2.13.5). Importing the core never imports Pydantic.
|
|
182
|
+
The extension uses the same store, explicit identity and migration registry:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from pydantic import BaseModel
|
|
186
|
+
from backpack_store.pydantic_codec import PydanticCodec
|
|
187
|
+
|
|
188
|
+
class ValidatedNote(BaseModel):
|
|
189
|
+
title: str
|
|
190
|
+
priority: int
|
|
191
|
+
|
|
192
|
+
store.register(PydanticCodec(ValidatedNote, type_key="notes.validated", schema_version=1))
|
|
193
|
+
written = store.put(ValidatedNote(title="Example", priority=2))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Register the same trusted model after reopening. The codec accepts the exact
|
|
197
|
+
registered class, serializes aliases and JSON representations, then revalidates
|
|
198
|
+
strictly before writing. Decode also uses strict JSON validation with extra fields
|
|
199
|
+
forbidden. This catches mutated instances and `model_construct` bypasses instead
|
|
200
|
+
of trusting an already-created instance. Payloads still obey the core finite JSON
|
|
201
|
+
object limits. Object-shaped nested models and datetime JSON round trips are
|
|
202
|
+
covered; scalar/list `RootModel` classes are rejected. Private/computed fields are
|
|
203
|
+
not part of the durable representation.
|
|
204
|
+
|
|
205
|
+
This codec does not add native Pydantic type support to tool signatures. The
|
|
206
|
+
optional provider continues to use its concrete JSON views. Run
|
|
207
|
+
`python -m unittest discover -s pydantic_tests -v` in the extension environment.
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
uv build
|
|
213
|
+
python -m pip install dist/backpack_store-0.1.0-py3-none-any.whl
|
|
214
|
+
python -m unittest discover -s tests -v
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
CI installs the wheel built from its source distribution and runs the behavior
|
|
218
|
+
suite on Python 3.11/3.13 across Linux, macOS and Windows. The original generated
|
|
219
|
+
scaffold is preserved in the repository's initial commit.
|
|
220
|
+
|
|
221
|
+
This 0.1.x project is experimental. Published GitHub releases with matching `v`
|
|
222
|
+
tags run `.github/workflows/release.yaml`, verify the installed wheel and source
|
|
223
|
+
distribution, and upload through PyPI trusted publishing in environment `pypi`.
|
|
224
|
+
The initial release tag is `v0.1.0`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "backpack-store"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Store and retrieve typed local records with identity and provenance."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"typed-storage",
|
|
11
|
+
"sqlite",
|
|
12
|
+
"records",
|
|
13
|
+
"provenance",
|
|
14
|
+
"local-first",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[[project.authors]]
|
|
26
|
+
name = "Ella Inng"
|
|
27
|
+
|
|
28
|
+
[[project.maintainers]]
|
|
29
|
+
name = "Outside Labs"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
pydantic = ["pydantic>=2.13,<3"]
|
|
33
|
+
moves = ["hidden-moves>=0.1,<0.2"]
|
|
34
|
+
host = [
|
|
35
|
+
"hidden-moves>=0.1,<0.2",
|
|
36
|
+
"hidden-moves-mcp>=0.1,<0.2",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.entry-points."hidden_moves.moves"]
|
|
40
|
+
backpack = "backpack_store.integration:provide_moves"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/outside-labs/backpack"
|
|
44
|
+
Repository = "https://github.com/outside-labs/backpack"
|
|
45
|
+
Issues = "https://github.com/outside-labs/backpack/issues"
|
|
46
|
+
Documentation = "https://github.com/outside-labs/backpack#readme"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.uv.build-backend]
|
|
53
|
+
module-name = "backpack_store"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "backpack-store"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Store and retrieve typed local records with identity and provenance."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Ella Inng" }]
|
|
10
|
+
maintainers = [{ name = "Outside Labs" }]
|
|
11
|
+
keywords = ["typed-storage", "sqlite", "records", "provenance", "local-first"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
pydantic = ["pydantic>=2.13,<3"]
|
|
23
|
+
moves = ["hidden-moves>=0.1,<0.2"]
|
|
24
|
+
host = ["hidden-moves>=0.1,<0.2", "hidden-moves-mcp>=0.1,<0.2"]
|
|
25
|
+
|
|
26
|
+
[project.entry-points."hidden_moves.moves"]
|
|
27
|
+
backpack = "backpack_store.integration:provide_moves"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/outside-labs/backpack"
|
|
31
|
+
Repository = "https://github.com/outside-labs/backpack"
|
|
32
|
+
Issues = "https://github.com/outside-labs/backpack/issues"
|
|
33
|
+
Documentation = "https://github.com/outside-labs/backpack#readme"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.uv.build-backend]
|
|
40
|
+
module-name = "backpack_store"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Experimental typed local records with explicitly registered codecs."""
|
|
2
|
+
|
|
3
|
+
from .codecs import Codec, CodecRegistry, DataclassCodec, EncodedValue
|
|
4
|
+
from .errors import (
|
|
5
|
+
BackpackError,
|
|
6
|
+
CodecError,
|
|
7
|
+
DatabaseVersionError,
|
|
8
|
+
InvalidCursorError,
|
|
9
|
+
MigrationError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RegistrationError,
|
|
12
|
+
RevisionConflictError,
|
|
13
|
+
StorageError,
|
|
14
|
+
StoreClosedError,
|
|
15
|
+
UnknownTypeError,
|
|
16
|
+
UnknownVersionError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from .records import JSONObject, JSONValue, Provenance, RawRecord, Record
|
|
20
|
+
from .store import Backpack, ExportResult, FindPage, WriteResult
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Backpack",
|
|
24
|
+
"ExportResult",
|
|
25
|
+
"MigrationError",
|
|
26
|
+
"FindPage",
|
|
27
|
+
"WriteResult",
|
|
28
|
+
"BackpackError",
|
|
29
|
+
"Codec",
|
|
30
|
+
"CodecError",
|
|
31
|
+
"CodecRegistry",
|
|
32
|
+
"DataclassCodec",
|
|
33
|
+
"DatabaseVersionError",
|
|
34
|
+
"EncodedValue",
|
|
35
|
+
"InvalidCursorError",
|
|
36
|
+
"JSONObject",
|
|
37
|
+
"JSONValue",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
"Provenance",
|
|
40
|
+
"RawRecord",
|
|
41
|
+
"Record",
|
|
42
|
+
"RegistrationError",
|
|
43
|
+
"RevisionConflictError",
|
|
44
|
+
"StorageError",
|
|
45
|
+
"StoreClosedError",
|
|
46
|
+
"UnknownTypeError",
|
|
47
|
+
"UnknownVersionError",
|
|
48
|
+
"ValidationError",
|
|
49
|
+
]
|