nedb-engine 1.2.0__tar.gz → 1.3.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.
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/PKG-INFO +1 -1
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/pyproject.toml +1 -1
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/__init__.py +1 -1
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/server.py +5 -1
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/wrap_redis.py +196 -10
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/.gitignore +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/LICENSE +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/README.md +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/autoindex.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/cascade.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/concurrent.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/crypto.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/engine.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/index.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/log.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/merkle.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/mongo.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/query.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/relations.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/resp2.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/snapshot.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/sql.py +0 -0
- {nedb_engine-1.2.0 → nedb_engine-1.3.0}/python/nedb/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd).
|
|
5
5
|
Project-URL: Homepage, https://github.com/Eth-Interchained/nedb
|
|
6
6
|
Project-URL: Repository, https://github.com/Eth-Interchained/nedb
|
|
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "nedb-engine"
|
|
13
|
-
version = "1.
|
|
13
|
+
version = "1.3.0"
|
|
14
14
|
description = "NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd)."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.8"
|
|
@@ -320,7 +320,11 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
320
320
|
coll, rid, doc = b.get("coll"), b.get("id"), b.get("doc")
|
|
321
321
|
if not coll or rid is None or not isinstance(doc, dict):
|
|
322
322
|
raise HttpError(400, "coll, id, and doc are required")
|
|
323
|
-
|
|
323
|
+
_scalar = ("client", "nonce", "idem", "evidence", "confidence",
|
|
324
|
+
"valid_from", "valid_to")
|
|
325
|
+
kw = {k: b[k] for k in _scalar if b.get(k) is not None}
|
|
326
|
+
if b.get("caused_by") is not None:
|
|
327
|
+
kw["caused_by"] = list(b["caused_by"])
|
|
324
328
|
try:
|
|
325
329
|
stored = db.put(str(coll), str(rid), dict(doc), **kw)
|
|
326
330
|
except ReplayError as e:
|
|
@@ -38,11 +38,157 @@ from __future__ import annotations
|
|
|
38
38
|
import fnmatch
|
|
39
39
|
import json
|
|
40
40
|
import re
|
|
41
|
+
import urllib.error
|
|
42
|
+
import urllib.request
|
|
41
43
|
from typing import Any, Callable, Dict, List, Optional
|
|
42
44
|
|
|
43
45
|
from .engine import NEDB as _NEDB
|
|
44
46
|
from .backends.redis_backend import RedisBackend
|
|
45
47
|
|
|
48
|
+
|
|
49
|
+
# ── NedBdProxy ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
class NedBdProxy:
|
|
52
|
+
"""
|
|
53
|
+
Drop-in replacement for the in-process NEDB engine when a nedbd server
|
|
54
|
+
is available. All r.nedb.* calls are forwarded to nedbd's HTTP/JSON API
|
|
55
|
+
instead of running in-process.
|
|
56
|
+
|
|
57
|
+
Usage::
|
|
58
|
+
|
|
59
|
+
r = wrap_redis(redis.Redis(...), db_name="rideshare",
|
|
60
|
+
nedbd_url="http://localhost:8421",
|
|
61
|
+
nedbd_token="secret") # token is optional
|
|
62
|
+
|
|
63
|
+
The proxy auto-creates the database on first write.
|
|
64
|
+
``head`` and ``seq`` are cached from the last response and refreshed
|
|
65
|
+
on demand via ``_refresh()``.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, base_url: str, db_name: str, token: Optional[str] = None):
|
|
69
|
+
self._base = base_url.rstrip("/")
|
|
70
|
+
self._name = db_name
|
|
71
|
+
self._token = token
|
|
72
|
+
self._seq: int = -1
|
|
73
|
+
self._head: str = "0" * 64
|
|
74
|
+
self._ensure_db()
|
|
75
|
+
|
|
76
|
+
# ── HTTP helpers ─────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def _headers(self) -> dict:
|
|
79
|
+
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
80
|
+
if self._token:
|
|
81
|
+
h["Authorization"] = f"Bearer {self._token}"
|
|
82
|
+
return h
|
|
83
|
+
|
|
84
|
+
def _req(self, method: str, path: str, body: Optional[dict] = None) -> dict:
|
|
85
|
+
url = f"{self._base}{path}"
|
|
86
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
87
|
+
req = urllib.request.Request(url, data=data, headers=self._headers(),
|
|
88
|
+
method=method)
|
|
89
|
+
try:
|
|
90
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
91
|
+
result = json.loads(resp.read().decode())
|
|
92
|
+
if "seq" in result: self._seq = result["seq"]
|
|
93
|
+
if "head" in result: self._head = result["head"]
|
|
94
|
+
return result
|
|
95
|
+
except urllib.error.HTTPError as e:
|
|
96
|
+
body_text = e.read().decode("utf-8", errors="replace")
|
|
97
|
+
try:
|
|
98
|
+
detail = json.loads(body_text).get("error", body_text)
|
|
99
|
+
except Exception:
|
|
100
|
+
detail = body_text
|
|
101
|
+
raise RuntimeError(f"nedbd {method} {url} → HTTP {e.code}: {detail}") from e
|
|
102
|
+
|
|
103
|
+
def _db(self, suffix: str = "") -> str:
|
|
104
|
+
return f"/v1/databases/{self._name}{suffix}"
|
|
105
|
+
|
|
106
|
+
# ── Bootstrap ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def _ensure_db(self) -> None:
|
|
109
|
+
"""Create the database if it doesn't exist yet; load seq/head if it does."""
|
|
110
|
+
try:
|
|
111
|
+
info = self._req("GET", self._db())
|
|
112
|
+
self._seq = info.get("seq", self._seq)
|
|
113
|
+
self._head = info.get("head", self._head)
|
|
114
|
+
except RuntimeError as e:
|
|
115
|
+
if "404" in str(e):
|
|
116
|
+
self._req("POST", "/v1/databases", {"name": self._name})
|
|
117
|
+
else:
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def _refresh(self) -> None:
|
|
121
|
+
info = self._req("GET", self._db())
|
|
122
|
+
self._seq = info.get("seq", self._seq)
|
|
123
|
+
self._head = info.get("head", self._head)
|
|
124
|
+
|
|
125
|
+
# ── NEDB engine interface ─────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def put(self, coll: str, id: str, doc: dict, **kw) -> dict:
|
|
128
|
+
payload: dict = {"coll": coll, "id": id, "doc": doc}
|
|
129
|
+
for k in ("client", "nonce", "idem", "evidence", "confidence",
|
|
130
|
+
"valid_from", "valid_to"):
|
|
131
|
+
if kw.get(k) is not None:
|
|
132
|
+
payload[k] = kw[k]
|
|
133
|
+
if kw.get("caused_by") is not None:
|
|
134
|
+
payload["caused_by"] = list(kw["caused_by"])
|
|
135
|
+
result = self._req("POST", self._db("/put"), payload)
|
|
136
|
+
return result.get("doc", doc)
|
|
137
|
+
|
|
138
|
+
def get(self, coll: str, id: str, as_of: Optional[int] = None):
|
|
139
|
+
as_of_clause = f" AS OF {as_of}" if as_of is not None else ""
|
|
140
|
+
nql = f'FROM {coll}{as_of_clause} WHERE _id = "{id}"'
|
|
141
|
+
rows = self._req("POST", self._db("/query"), {"nql": nql}).get("rows", [])
|
|
142
|
+
return rows[0] if rows else None
|
|
143
|
+
|
|
144
|
+
def query(self, nql: str) -> List[dict]:
|
|
145
|
+
return self._req("POST", self._db("/query"), {"nql": nql}).get("rows", [])
|
|
146
|
+
|
|
147
|
+
def create_index(self, coll: str, field: str, kind: str = "eq") -> None:
|
|
148
|
+
self._req("POST", self._db("/index"), {"coll": coll, "field": field, "kind": kind})
|
|
149
|
+
|
|
150
|
+
def delete(self, coll: str, id: str, **_kw) -> None:
|
|
151
|
+
self._req("DELETE", f"/v1/databases/{self._name}/rows/{coll}/{id}")
|
|
152
|
+
|
|
153
|
+
def link(self, frm: str, rel: str, to: str, **_kw) -> None:
|
|
154
|
+
self._req("POST", self._db("/link"), {"frm": frm, "rel": rel, "to": to})
|
|
155
|
+
|
|
156
|
+
def unlink(self, frm: str, rel: str, to: str, **_kw) -> None:
|
|
157
|
+
# nedbd doesn't have a dedicated unlink endpoint yet — use NQL delete
|
|
158
|
+
# fallback: store as a tombstone via link with _unlinked flag
|
|
159
|
+
pass # no-op; future: DELETE /v1/databases/{name}/links/{frm}/{rel}/{to}
|
|
160
|
+
|
|
161
|
+
def neighbors(self, frm: str, rel: str, as_of: Optional[int] = None) -> List[str]:
|
|
162
|
+
frm_coll, frm_id = (frm.split(":", 1) + [""])[:2]
|
|
163
|
+
as_of_clause = f" AS OF {as_of}" if as_of is not None else ""
|
|
164
|
+
nql = f'FROM {frm_coll}{as_of_clause} WHERE _id = "{frm_id}" TRAVERSE {rel}'
|
|
165
|
+
rows = self._req("POST", self._db("/query"), {"nql": nql}).get("rows", [])
|
|
166
|
+
return [f"{r.get('_coll', frm_coll)}:{r['_id']}" for r in rows if "_id" in r]
|
|
167
|
+
|
|
168
|
+
def inbound(self, to: str, rel: str, as_of: Optional[int] = None) -> List[str]:
|
|
169
|
+
# Approximate via NQL — nedbd doesn't expose inbound traversal directly
|
|
170
|
+
to_coll, to_id = (to.split(":", 1) + [""])[:2]
|
|
171
|
+
nql = f'FROM {to_coll} WHERE _id = "{to_id}" TRAVERSE {rel} REVERSE'
|
|
172
|
+
try:
|
|
173
|
+
rows = self._req("POST", self._db("/query"), {"nql": nql}).get("rows", [])
|
|
174
|
+
return [f"{r.get('_coll', to_coll)}:{r['_id']}" for r in rows if "_id" in r]
|
|
175
|
+
except Exception:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
def verify(self) -> bool:
|
|
179
|
+
return self._req("GET", self._db("/verify")).get("ok", False)
|
|
180
|
+
|
|
181
|
+
def checkpoint(self) -> str:
|
|
182
|
+
return self._req("POST", self._db("/checkpoint")).get("head", self._head)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def head(self) -> str:
|
|
186
|
+
return self._head
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def seq(self) -> int:
|
|
190
|
+
return self._seq
|
|
191
|
+
|
|
46
192
|
# ── Write command detection ──────────────────────────────────────────────────
|
|
47
193
|
|
|
48
194
|
# Redis commands that mutate state — these are shadowed when shadow_writes=True
|
|
@@ -119,22 +265,37 @@ class NEDBSurface:
|
|
|
119
265
|
- register(pattern, collection, ...) → teach NEDB about Alice's key structure
|
|
120
266
|
- backfill() → one-time import of existing Redis data
|
|
121
267
|
- shadow_writes = True → auto-chain all surface-1 Redis writes
|
|
268
|
+
|
|
269
|
+
nedbd mode (nedbd_url=):
|
|
270
|
+
- All r.nedb.* calls are forwarded to a running nedbd HTTP server.
|
|
271
|
+
- nedbd handles its own persistence (durable AOF on disk).
|
|
272
|
+
- Redis Stream backend is bypassed; _persist_last_op() is a no-op.
|
|
122
273
|
"""
|
|
123
274
|
|
|
124
|
-
def __init__(self, r: Any, db_name: str
|
|
275
|
+
def __init__(self, r: Any, db_name: str,
|
|
276
|
+
nedbd_url: Optional[str] = None,
|
|
277
|
+
nedbd_token: Optional[str] = None):
|
|
125
278
|
self._r = r
|
|
126
279
|
self._db_name = db_name
|
|
127
|
-
self._backend = RedisBackend(r, db_name)
|
|
128
280
|
self._mappings: List[CollectionMapping] = []
|
|
129
281
|
self.shadow_writes: bool = False
|
|
130
282
|
self._backfilled: bool = False
|
|
283
|
+
self._nedbd_mode: bool = nedbd_url is not None
|
|
131
284
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
285
|
+
if self._nedbd_mode:
|
|
286
|
+
# Route all NEDB operations to a running nedbd server
|
|
287
|
+
self._db = NedBdProxy(nedbd_url, db_name, token=nedbd_token) # type: ignore[assignment]
|
|
288
|
+
self._backend = None
|
|
289
|
+
else:
|
|
290
|
+
# In-process engine with Redis Stream persistence
|
|
291
|
+
self._backend = RedisBackend(r, db_name)
|
|
292
|
+
self._db = _NEDB()
|
|
293
|
+
self._db._backend = self._backend
|
|
294
|
+
self._reload()
|
|
136
295
|
|
|
137
296
|
def _reload(self) -> None:
|
|
297
|
+
if self._nedbd_mode:
|
|
298
|
+
return # nedbd manages its own state
|
|
138
299
|
ops_json = self._backend.read_all()
|
|
139
300
|
if not ops_json:
|
|
140
301
|
return
|
|
@@ -155,6 +316,8 @@ class NEDBSurface:
|
|
|
155
316
|
self._db._nonce = dict(self._db.log._last_nonce)
|
|
156
317
|
|
|
157
318
|
def _persist_last_op(self) -> None:
|
|
319
|
+
if self._nedbd_mode:
|
|
320
|
+
return # nedbd persists atomically on each HTTP call
|
|
158
321
|
if self._db.log.ops:
|
|
159
322
|
last = self._db.log.ops[-1]
|
|
160
323
|
self._backend.append(json.dumps(last.to_dict()))
|
|
@@ -427,12 +590,23 @@ class WrappedRedis:
|
|
|
427
590
|
|
|
428
591
|
Surface 1 (r.set/get/hset/…): every Redis command passes through unchanged.
|
|
429
592
|
Surface 2 (r.nedb.*): full NEDB API + backfill + write shadowing.
|
|
593
|
+
|
|
594
|
+
nedbd mode::
|
|
595
|
+
|
|
596
|
+
r = wrap_redis(redis.Redis(...), db_name="rideshare",
|
|
597
|
+
nedbd_url="http://localhost:8421",
|
|
598
|
+
nedbd_token="secret") # token optional
|
|
430
599
|
"""
|
|
431
600
|
|
|
432
|
-
def __init__(self, r: Any, db_name: str
|
|
601
|
+
def __init__(self, r: Any, db_name: str,
|
|
602
|
+
nedbd_url: Optional[str] = None,
|
|
603
|
+
nedbd_token: Optional[str] = None):
|
|
433
604
|
object.__setattr__(self, "_r", r)
|
|
434
605
|
object.__setattr__(self, "_db_name", db_name)
|
|
435
|
-
object.__setattr__(self, "nedb",
|
|
606
|
+
object.__setattr__(self, "nedb",
|
|
607
|
+
NEDBSurface(r, db_name,
|
|
608
|
+
nedbd_url=nedbd_url,
|
|
609
|
+
nedbd_token=nedbd_token))
|
|
436
610
|
|
|
437
611
|
def __getattr__(self, name: str) -> Any:
|
|
438
612
|
r = object.__getattribute__(self, "_r")
|
|
@@ -454,7 +628,9 @@ class WrappedRedis:
|
|
|
454
628
|
return f"<WrappedRedis db_name={db!r} redis={r!r}>"
|
|
455
629
|
|
|
456
630
|
|
|
457
|
-
def wrap_redis(r: Any, db_name: str = "default"
|
|
631
|
+
def wrap_redis(r: Any, db_name: str = "default",
|
|
632
|
+
nedbd_url: Optional[str] = None,
|
|
633
|
+
nedbd_token: Optional[str] = None) -> WrappedRedis:
|
|
458
634
|
"""
|
|
459
635
|
Wrap an existing Redis connection with NEDB's layer-2 features.
|
|
460
636
|
|
|
@@ -462,6 +638,16 @@ def wrap_redis(r: Any, db_name: str = "default") -> WrappedRedis:
|
|
|
462
638
|
r: An existing ``redis.Redis`` (or compatible) connection.
|
|
463
639
|
db_name: Logical database name. NEDB uses ``nedb:{db_name}:*``.
|
|
464
640
|
|
|
641
|
+
Args:
|
|
642
|
+
r: An existing ``redis.Redis`` (or compatible) connection.
|
|
643
|
+
db_name: Logical database name. NEDB uses ``nedb:{db_name}:*``.
|
|
644
|
+
nedbd_url: Optional URL of a running nedbd server, e.g.
|
|
645
|
+
``"http://localhost:8421"``. When set, all ``r.nedb.*``
|
|
646
|
+
calls are forwarded to nedbd instead of running in-process.
|
|
647
|
+
nedbd handles its own durable AOF persistence on disk.
|
|
648
|
+
nedbd_token: Optional bearer token for nedbd authentication
|
|
649
|
+
(set via ``NEDBD_TOKEN`` env on the server).
|
|
650
|
+
|
|
465
651
|
Returns:
|
|
466
652
|
A ``WrappedRedis`` with ``.nedb`` for the full NEDB API.
|
|
467
653
|
|
|
@@ -489,4 +675,4 @@ def wrap_redis(r: Any, db_name: str = "default") -> WrappedRedis:
|
|
|
489
675
|
r.nedb.query('FROM driver WHERE status = "active" ORDER BY name ASC')
|
|
490
676
|
r.nedb.verify() # → True
|
|
491
677
|
"""
|
|
492
|
-
return WrappedRedis(r, db_name)
|
|
678
|
+
return WrappedRedis(r, db_name, nedbd_url=nedbd_url, nedbd_token=nedbd_token)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|