recordstore 0.11.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.
- recordstore-0.11.0/LICENSE +28 -0
- recordstore-0.11.0/PKG-INFO +12 -0
- recordstore-0.11.0/README.md +151 -0
- recordstore-0.11.0/pyproject.toml +17 -0
- recordstore-0.11.0/setup.cfg +4 -0
- recordstore-0.11.0/src/recordstore/__init__.py +25 -0
- recordstore-0.11.0/src/recordstore/recordstore.py +1201 -0
- recordstore-0.11.0/src/recordstore.egg-info/PKG-INFO +12 -0
- recordstore-0.11.0/src/recordstore.egg-info/SOURCES.txt +16 -0
- recordstore-0.11.0/src/recordstore.egg-info/dependency_links.txt +1 -0
- recordstore-0.11.0/src/recordstore.egg-info/requires.txt +6 -0
- recordstore-0.11.0/src/recordstore.egg-info/top_level.txt +1 -0
- recordstore-0.11.0/tests/test_boundaries.py +47 -0
- recordstore-0.11.0/tests/test_recordstore.py +653 -0
- recordstore-0.11.0/tests/test_recordstore_bee.py +124 -0
- recordstore-0.11.0/tests/test_recordstore_feed.py +172 -0
- recordstore-0.11.0/tests/test_recordstore_fuzz.py +85 -0
- recordstore-0.11.0/tests/test_stamp_auto.py +81 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Peter Foldiak
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: recordstore
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: Versioned key-record store over a content-addressed bytes store (memory or Ethereum Swarm/Bee backends)
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Provides-Extra: bee
|
|
9
|
+
Requires-Dist: requests; extra == "bee"
|
|
10
|
+
Provides-Extra: feeds
|
|
11
|
+
Requires-Dist: swarm-bee; extra == "feeds"
|
|
12
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# recordstore
|
|
2
|
+
|
|
3
|
+
A versioned key→record store over a content-addressed bytes store — a thin
|
|
4
|
+
database kernel between an immutable blob store (such as [Ethereum
|
|
5
|
+
Swarm](https://www.ethswarm.org/)) and an application that wants to think in
|
|
6
|
+
records and versions rather than blobs and references.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from recordstore import RecordStore, MemoryBytesStore
|
|
10
|
+
|
|
11
|
+
blobs = MemoryBytesStore()
|
|
12
|
+
store = RecordStore(blobs)
|
|
13
|
+
store.put("users/alice", {"name": "Alice", "role": "admin"})
|
|
14
|
+
store.put("users/bob", {"name": "Bob"})
|
|
15
|
+
root = store.commit() # one reference identifies this entire version
|
|
16
|
+
|
|
17
|
+
store.get("users/alice") # {'name': 'Alice', 'role': 'admin'}
|
|
18
|
+
list(store.keys("users/")) # ['users/alice', 'users/bob']
|
|
19
|
+
|
|
20
|
+
snapshot = RecordStore.at(root, blobs) # frozen view of that version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Concurrent writers converge without a lock server — if the shared pointer
|
|
24
|
+
moved under a commit, it three-way merges and retries:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from recordstore import MemoryPointer
|
|
28
|
+
|
|
29
|
+
pointer = MemoryPointer(root) # a shared "latest version" name
|
|
30
|
+
a = RecordStore(blobs, pointer=pointer) # two writers open the same version
|
|
31
|
+
b = RecordStore(blobs, pointer=pointer)
|
|
32
|
+
a.put("users/carol", {"name": "Carol"}); a.commit(reconcile=True)
|
|
33
|
+
b.put("users/dave", {"name": "Dave"}); b.commit(reconcile=True) # folds in a's change
|
|
34
|
+
# the pointer now names a version containing both carol and dave
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
Content-addressed stores give you immutable `put(bytes) → ref` /
|
|
40
|
+
`get(ref) → bytes` and nothing else: no keys, no typed records, no
|
|
41
|
+
transactions, no snapshots. `recordstore` adds exactly that missing layer
|
|
42
|
+
and nothing more:
|
|
43
|
+
|
|
44
|
+
- **Records instead of raw bytes** — values are any JSON-compatible object,
|
|
45
|
+
stored under string keys.
|
|
46
|
+
- **Atomic, versioned commits** — mutations are staged in memory;
|
|
47
|
+
`commit()` lands all of them as one new **root reference**. A reader
|
|
48
|
+
either sees all of a commit or none of it.
|
|
49
|
+
- **Snapshot isolation** — `RecordStore.at(root, blobs)` pins one root and
|
|
50
|
+
sees a frozen, self-consistent dataset for arbitrarily long reads, with
|
|
51
|
+
no locking: the whole dataset-at-a-version *is* one reference.
|
|
52
|
+
- **Canonical roots** — encodings are deterministic, so **equal content
|
|
53
|
+
produces an equal root reference**, regardless of the insertion/deletion
|
|
54
|
+
history that produced it. Versions are content-addressable, comparable
|
|
55
|
+
with a string equality check, and cheap to diff.
|
|
56
|
+
- **Three-way merge** — `RecordStore.merge(base, ours, theirs)` reconciles
|
|
57
|
+
two divergent versions; canonicity makes unchanged subtrees merge for free
|
|
58
|
+
and equal edits conflict-free, with conflicts raised or settled by a
|
|
59
|
+
`resolver(key, base, ours, theirs)` you supply. The diff is O(divergence),
|
|
60
|
+
not O(dataset).
|
|
61
|
+
- **Multi-writer, no lock server** — `commit(reconcile=True)` makes concurrent
|
|
62
|
+
writers converge: if the pointer moved under you it three-way merges and
|
|
63
|
+
retries instead of overwriting. Race-free in-process; best-effort across
|
|
64
|
+
processes over a Swarm feed. A commutative resolver keeps 3+ writers
|
|
65
|
+
order-independent.
|
|
66
|
+
- **Structural sharing** — versions are stored as a persistent
|
|
67
|
+
(copy-on-write) compacted radix trie; a commit writes only the blobs
|
|
68
|
+
along the changed paths, and unchanged subtrees are shared between
|
|
69
|
+
versions.
|
|
70
|
+
- **Concurrent I/O** — `items()` (bulk read), `get_many`/`put_many`, and a
|
|
71
|
+
pooled keep-alive HTTP session let `BeeBytesStore` parallelise round trips
|
|
72
|
+
instead of paying one per record — the difference between usable and not on
|
|
73
|
+
a high-latency link.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install "recordstore @ git+https://github.com/petfold/recordstore.git@v0.10.0"
|
|
79
|
+
|
|
80
|
+
# with the Bee (Swarm) bytes backend's HTTP dependency:
|
|
81
|
+
pip install "recordstore[bee] @ git+https://github.com/petfold/recordstore.git@v0.10.0"
|
|
82
|
+
|
|
83
|
+
# with the Swarm feed pointer (adds swarm-bee for SOC/secp256k1 signing):
|
|
84
|
+
pip install "recordstore[feeds] @ git+https://github.com/petfold/recordstore.git@v0.10.0"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Python ≥ 3.9. The core imports only the standard library; both extra
|
|
88
|
+
dependencies are imported lazily — `requests` only by `BeeBytesStore`
|
|
89
|
+
(`[bee]`), `swarm-bee` only by `SwarmFeedPointer` (`[feeds]`).
|
|
90
|
+
|
|
91
|
+
## The pieces
|
|
92
|
+
|
|
93
|
+
| Layer | What it does | Implementations |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `BytesStore` | `put(bytes) → ref`, `get(ref) → bytes` | `MemoryBytesStore` (in-memory, testing), `BeeBytesStore` (Swarm Bee node over `/bytes` — the blob endpoint, not the raw `/chunks/{address}` primitive) |
|
|
96
|
+
| trie (internal) | canonical persistent radix trie mapping keys to value blobs | — |
|
|
97
|
+
| `RecordStore` | staging, `commit()` / `commit(reconcile=True)`, snapshots, sorted `keys()`/`items()`, three-way `merge()` | — |
|
|
98
|
+
| `Pointer` | mutable name for the latest root | `MemoryPointer`, `FilePointer` (atomic local file), `SwarmFeedPointer` (owner-signed Swarm feed, over `swarm-bee`) |
|
|
99
|
+
|
|
100
|
+
Nothing above `RecordStore` ever sees a stored blob or a trie node.
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
- **[User guide](docs/USER_GUIDE.md)** — concepts, full API, the canonicity
|
|
105
|
+
contract, running against a real Bee node, versioning patterns, error
|
|
106
|
+
handling, and current limitations.
|
|
107
|
+
|
|
108
|
+
## Testing
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 -m pytest tests/ # unit + fuzz + boundary tests
|
|
112
|
+
|
|
113
|
+
BEE_API=http://<node>:1633 BEE_BATCH=<batchID> \
|
|
114
|
+
python3 -m pytest tests/test_recordstore_bee.py -v # bytes backend, live node
|
|
115
|
+
|
|
116
|
+
pip install "recordstore[feeds]" # needs swarm-bee
|
|
117
|
+
BEE_API=http://<node>:1633 BEE_BATCH=<batchID> \
|
|
118
|
+
python3 -m pytest tests/test_recordstore_feed.py -v # feed pointer, live node
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The fuzz suite runs randomized put/delete histories against a plain-dict
|
|
122
|
+
oracle and asserts the canonical-root property throughout. The Bee
|
|
123
|
+
integration tests skip automatically unless `BEE_API` is set (the feed test
|
|
124
|
+
also needs `swarm-bee` installed); against a real (non-dev) node always
|
|
125
|
+
provide `BEE_BATCH` with a purchased postage batch id.
|
|
126
|
+
|
|
127
|
+
## Background
|
|
128
|
+
|
|
129
|
+
This is a Python re-implementation of an old idea — content-addressed,
|
|
130
|
+
canonical-root, versioned key-value storage — best known from Ethereum's
|
|
131
|
+
Merkle Patricia Trie and from Noms/Dolt's "prolly trees." The value here is
|
|
132
|
+
fit, not novelty: a much simpler canonical encoding than MPT (avoiding the
|
|
133
|
+
exact bug class that once caused a chain split), a far smaller scope than
|
|
134
|
+
Dolt/Irmin (no query language; a single three-way merge primitive, not a
|
|
135
|
+
merge engine), and — as far as we could
|
|
136
|
+
find — the first implementation of this pattern for Python with a Swarm/Bee
|
|
137
|
+
backend. See the [user guide's background section](docs/USER_GUIDE.md#0-background-is-this-reinventing-the-wheel)
|
|
138
|
+
for the full comparison.
|
|
139
|
+
|
|
140
|
+
## Status
|
|
141
|
+
|
|
142
|
+
Extracted from [petfold/ontodag](https://github.com/petfold/ontodag)
|
|
143
|
+
(July 2026) with history preserved; validated against a live Bee 2.8.1
|
|
144
|
+
light node on Gnosis mainnet (roundtrips, canonical roots on real BMT
|
|
145
|
+
references, network retrievability). `SwarmFeedPointer` (owner-signed Swarm
|
|
146
|
+
feed, over `swarm-bee`) landed in v0.4.0; three-way `merge` in v0.8.0;
|
|
147
|
+
auto-reconciling `commit(reconcile=True)` in v0.9.0; a best-effort feed
|
|
148
|
+
`compare_and_set` (cross-process reconcile) in v0.10.0. Known gaps — a Swarm
|
|
149
|
+
feed has no atomic index claim, so exactly-simultaneous same-index writes can
|
|
150
|
+
still race (in-process reconcile is race-free); one blob per record — are
|
|
151
|
+
detailed in the [user guide](docs/USER_GUIDE.md#limitations-and-roadmap).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "recordstore"
|
|
7
|
+
version = "0.11.0"
|
|
8
|
+
description = "Versioned key-record store over a content-addressed bytes store (memory or Ethereum Swarm/Bee backends)"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "BSD-3-Clause" }
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
bee = ["requests"]
|
|
14
|
+
feeds = ["swarm-bee"]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
where = ["src"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .recordstore import (
|
|
2
|
+
RecordStore,
|
|
3
|
+
MemoryBytesStore,
|
|
4
|
+
BeeBytesStore,
|
|
5
|
+
MemoryPointer,
|
|
6
|
+
FilePointer,
|
|
7
|
+
SwarmFeedPointer,
|
|
8
|
+
MergeConflict,
|
|
9
|
+
ABSENT,
|
|
10
|
+
DELETE,
|
|
11
|
+
canonical_bytes,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"RecordStore",
|
|
16
|
+
"MemoryBytesStore",
|
|
17
|
+
"BeeBytesStore",
|
|
18
|
+
"MemoryPointer",
|
|
19
|
+
"FilePointer",
|
|
20
|
+
"SwarmFeedPointer",
|
|
21
|
+
"MergeConflict",
|
|
22
|
+
"ABSENT",
|
|
23
|
+
"DELETE",
|
|
24
|
+
"canonical_bytes",
|
|
25
|
+
]
|