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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]