nedb-engine 1.2.0__tar.gz → 1.2.2__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.
Files changed (26) hide show
  1. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/PKG-INFO +1 -1
  2. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/pyproject.toml +1 -1
  3. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/__init__.py +1 -1
  4. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/engine.py +8 -2
  5. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/server.py +5 -1
  6. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/wrap_redis.py +196 -10
  7. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/.gitignore +0 -0
  8. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/LICENSE +0 -0
  9. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/README.md +0 -0
  10. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/autoindex.py +0 -0
  11. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/backends/__init__.py +0 -0
  12. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/backends/redis_backend.py +0 -0
  13. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/cascade.py +0 -0
  14. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/concurrent.py +0 -0
  15. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/crypto.py +0 -0
  16. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/index.py +0 -0
  17. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/log.py +0 -0
  18. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/merkle.py +0 -0
  19. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/mongo.py +0 -0
  20. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/query.py +0 -0
  21. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/redis_compat.py +0 -0
  22. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/relations.py +0 -0
  23. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/resp2.py +0 -0
  24. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/snapshot.py +0 -0
  25. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/sql.py +0 -0
  26. {nedb_engine-1.2.0 → nedb_engine-1.2.2}/python/nedb/store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedb-engine
3
- Version: 1.2.0
3
+ Version: 1.2.2
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.2.0"
13
+ version = "1.2.2"
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"
@@ -47,4 +47,4 @@ __all__ = [
47
47
  "wrap_redis", "WrappedRedis",
48
48
  "_native", "__has_native__",
49
49
  ]
50
- __version__ = "1.2.0"
50
+ __version__ = "1.2.2"
@@ -29,7 +29,9 @@ def apply_op(store: MVCCStore, relations: Relations, indexes: Indexes, op: Op,
29
29
  """Deterministically fold one op into materialized state."""
30
30
  p = op.payload
31
31
  if op.op == "put":
32
- key, coll, doc = p["key"], p["coll"], p["doc"]
32
+ key, coll = p["key"], p["coll"]
33
+ doc = dict(p["doc"]) # copy so we don't mutate the op payload
34
+ doc["_seq"] = op.seq # inject sequence number into every stored doc
33
35
  old = store.get(key)
34
36
  if old is not None:
35
37
  indexes.remove(coll, key, old)
@@ -638,7 +640,10 @@ class NEDB:
638
640
  if op.op == "put" and op.payload.get("key") == key_p:
639
641
  queue.append(op.seq)
640
642
  break
641
- for seq in queue:
643
+ # BFS feed found dep_seqs back into the queue for full
644
+ # transitive traversal (mirrors the backward TRACE frontier loop).
645
+ while queue:
646
+ seq = queue.pop(0)
642
647
  for dep_seq in self.cause_map.get(seq, []):
643
648
  if dep_seq in visited_seqs_fwd:
644
649
  continue
@@ -650,6 +655,7 @@ class NEDB:
650
655
  dep_doc = self.store.get(dep_key, as_of)
651
656
  if dep_doc is not None:
652
657
  out_fwd.append((dep_key, dep_doc))
658
+ queue.append(dep_seq) # continue BFS
653
659
  rows = out_fwd
654
660
 
655
661
  if plan.get("limit") is not None:
@@ -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
- kw = {k: b[k] for k in ("client", "nonce", "idem") if b.get(k) is not None}
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
- # Build the NEDB engine and replay from Redis Stream
133
- self._db = _NEDB()
134
- self._db._backend = self._backend
135
- self._reload()
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", NEDBSurface(r, db_name))
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") -> WrappedRedis:
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