nedb-engine-client 1.0.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.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: nedb-engine-client
3
+ Version: 1.0.2
4
+ Summary: Async Python client for nedbd — the nedb-engine server daemon
5
+ Author: Eth-Interchained
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Eth-Interchained/nedb
8
+ Project-URL: Repository, https://github.com/Eth-Interchained/nedb
9
+ Project-URL: Documentation, https://github.com/Eth-Interchained/nedb/blob/master/client/python/README.md
10
+ Keywords: database,nedb,client,async,dag,bi-temporal,causal
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Database
21
+ Classifier: Framework :: AsyncIO
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: httpx>=0.24
25
+
26
+ # nedb-client (Python)
27
+
28
+ Async Python client for the [nedbd](https://github.com/Eth-Interchained/nedb) HTTP API.
29
+
30
+ ```bash
31
+ pip install nedb-client
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from nedb_client import NedbClient
38
+
39
+ async with NedbClient("http://127.0.0.1:7070", db="mydb") as db:
40
+ # Write
41
+ await db.put("blocks", "618000", {"height": 618000, "hash": "000abc"})
42
+
43
+ # Query (full NQL)
44
+ rows = await db.query("FROM blocks ORDER BY height DESC LIMIT 10")
45
+
46
+ # Causal provenance + bi-temporal
47
+ result = await db.put("claims", "c1", {"fact": "..."},
48
+ caused_by=["abc123..."],
49
+ valid_from="2024-01-01",
50
+ evidence="sensor-42")
51
+
52
+ # Merkle head (tamper-evident root)
53
+ head = await db.head()
54
+
55
+ # Full tamper-evidence check
56
+ report = await db.verify()
57
+ assert report["ok"]
58
+ ```
59
+
60
+ ## API
61
+
62
+ | Method | Description |
63
+ |--------|-------------|
64
+ | `put(coll, id, doc, **meta)` | Write a document |
65
+ | `get(coll, id)` | Fetch current version |
66
+ | `delete(coll, id)` | Tombstone delete |
67
+ | `query(nql)` | NQL query → list of dicts |
68
+ | `query_full(nql)` | NQL query → full response with seq + head |
69
+ | `batch(ops)` | Batch put/del in one round-trip |
70
+ | `create_index(coll, field)` | Create sorted index |
71
+ | `verify()` | BLAKE2b tamper-evidence check |
72
+ | `head()` | Current Merkle head |
73
+ | `seq()` | Current sequence number |
74
+ | `checkpoint()` | Explicit checkpoint |
75
+ | `log(limit)` | Recent write log |
76
+ | `health()` | Server health |
77
+ | `ping()` | Boolean reachability check |
78
+ | `list_databases()` | All databases on server |
79
+ | `create_database()` | Create this database |
80
+ | `drop_database()` | Drop this database |
@@ -0,0 +1,55 @@
1
+ # nedb-client (Python)
2
+
3
+ Async Python client for the [nedbd](https://github.com/Eth-Interchained/nedb) HTTP API.
4
+
5
+ ```bash
6
+ pip install nedb-client
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from nedb_client import NedbClient
13
+
14
+ async with NedbClient("http://127.0.0.1:7070", db="mydb") as db:
15
+ # Write
16
+ await db.put("blocks", "618000", {"height": 618000, "hash": "000abc"})
17
+
18
+ # Query (full NQL)
19
+ rows = await db.query("FROM blocks ORDER BY height DESC LIMIT 10")
20
+
21
+ # Causal provenance + bi-temporal
22
+ result = await db.put("claims", "c1", {"fact": "..."},
23
+ caused_by=["abc123..."],
24
+ valid_from="2024-01-01",
25
+ evidence="sensor-42")
26
+
27
+ # Merkle head (tamper-evident root)
28
+ head = await db.head()
29
+
30
+ # Full tamper-evidence check
31
+ report = await db.verify()
32
+ assert report["ok"]
33
+ ```
34
+
35
+ ## API
36
+
37
+ | Method | Description |
38
+ |--------|-------------|
39
+ | `put(coll, id, doc, **meta)` | Write a document |
40
+ | `get(coll, id)` | Fetch current version |
41
+ | `delete(coll, id)` | Tombstone delete |
42
+ | `query(nql)` | NQL query → list of dicts |
43
+ | `query_full(nql)` | NQL query → full response with seq + head |
44
+ | `batch(ops)` | Batch put/del in one round-trip |
45
+ | `create_index(coll, field)` | Create sorted index |
46
+ | `verify()` | BLAKE2b tamper-evidence check |
47
+ | `head()` | Current Merkle head |
48
+ | `seq()` | Current sequence number |
49
+ | `checkpoint()` | Explicit checkpoint |
50
+ | `log(limit)` | Recent write log |
51
+ | `health()` | Server health |
52
+ | `ping()` | Boolean reachability check |
53
+ | `list_databases()` | All databases on server |
54
+ | `create_database()` | Create this database |
55
+ | `drop_database()` | Drop this database |
@@ -0,0 +1,16 @@
1
+ """
2
+ nedb-client — async Python client for the nedbd HTTP API.
3
+
4
+ Usage:
5
+ from nedb_client import NedbClient
6
+
7
+ async with NedbClient("http://127.0.0.1:7070", db="mydb") as db:
8
+ await db.put("blocks", "618000", {"height": 618000, "hash": "000abc"})
9
+ rows = await db.query("FROM blocks ORDER BY height DESC LIMIT 10")
10
+ head = await db.head() # BLAKE2b Merkle root
11
+ """
12
+
13
+ from .client import NedbClient, NedbError
14
+
15
+ __version__ = "1.0.0"
16
+ __all__ = ["NedbClient", "NedbError"]
@@ -0,0 +1,367 @@
1
+ """
2
+ NedbClient — async HTTP client for the nedbd server.
3
+
4
+ Compatible with both v1 AOF (nedb-engine <= 2.0.x) and
5
+ v2 DAG (nedb-engine >= 2.0.4 with --dag / NEDBD_DAG=1).
6
+
7
+ All /v1/databases/* routes are covered. The client handles:
8
+ - Bearer token auth
9
+ - Separate read (3s) and write (30s) timeout clients
10
+ - Auto-create database on first write (404 → create → retry)
11
+ - Resilient queries: 400/404 returns [] instead of raising
12
+ - Async context manager for clean lifecycle management
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from typing import Any, Dict, List, Optional, Union
18
+
19
+ try:
20
+ import httpx
21
+ except ImportError as exc: # pragma: no cover
22
+ raise ImportError("nedb-client requires httpx: pip install httpx") from exc
23
+
24
+
25
+ class NedbError(Exception):
26
+ """Raised when nedbd returns a non-2xx response (except auto-handled cases)."""
27
+ def __init__(self, status: int, message: str) -> None:
28
+ self.status = status
29
+ self.message = message
30
+ super().__init__(f"NedbError {status}: {message}")
31
+
32
+
33
+ # ── Timeouts ──────────────────────────────────────────────────────────────────
34
+
35
+ _READ_TIMEOUT = httpx.Timeout(connect=2.0, read=3.0, write=10.0, pool=2.0)
36
+ _WRITE_TIMEOUT = httpx.Timeout(connect=2.0, read=30.0, write=30.0, pool=2.0)
37
+
38
+
39
+ class NedbClient:
40
+ """
41
+ Async HTTP client for nedbd.
42
+
43
+ Parameters
44
+ ----------
45
+ url : str
46
+ Base URL of the nedbd server, e.g. "http://127.0.0.1:7070".
47
+ db : str
48
+ Database name. All operations target this database.
49
+ token : str, optional
50
+ Bearer token (set NEDBD_TOKEN on the server to require it).
51
+ auto_create : bool
52
+ Automatically create the database on first write if it doesn't exist.
53
+ Default True.
54
+
55
+ Examples
56
+ --------
57
+ Async context manager (recommended):
58
+
59
+ async with NedbClient("http://127.0.0.1:7070", db="vision") as client:
60
+ await client.put("blocks", "618000", {"height": 618000})
61
+ rows = await client.query("FROM blocks LIMIT 5")
62
+
63
+ Manual lifecycle:
64
+
65
+ client = NedbClient("http://127.0.0.1:7070", db="vision")
66
+ await client.open()
67
+ try:
68
+ await client.put("blocks", "1", {"height": 1})
69
+ finally:
70
+ await client.close()
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ url: str = "http://127.0.0.1:7070",
76
+ db: str = "default",
77
+ token: str = "",
78
+ auto_create: bool = True,
79
+ ) -> None:
80
+ self._base = url.rstrip("/")
81
+ self._db = db
82
+ self._token = token
83
+ self._auto_create = auto_create
84
+ self._read_client: Optional[httpx.AsyncClient] = None
85
+ self._write_client: Optional[httpx.AsyncClient] = None
86
+ self._lock = asyncio.Lock()
87
+
88
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
89
+
90
+ async def open(self) -> "NedbClient":
91
+ """Open the underlying HTTP clients. Called automatically by __aenter__."""
92
+ headers = {"Content-Type": "application/json"}
93
+ if self._token:
94
+ headers["Authorization"] = f"Bearer {self._token}"
95
+ self._read_client = httpx.AsyncClient(base_url=self._base, headers=headers, timeout=_READ_TIMEOUT)
96
+ self._write_client = httpx.AsyncClient(base_url=self._base, headers=headers, timeout=_WRITE_TIMEOUT)
97
+ return self
98
+
99
+ async def close(self) -> None:
100
+ """Close the underlying HTTP clients. Called automatically by __aexit__."""
101
+ if self._read_client:
102
+ await self._read_client.aclose()
103
+ if self._write_client:
104
+ await self._write_client.aclose()
105
+
106
+ async def __aenter__(self) -> "NedbClient":
107
+ await self.open()
108
+ return self
109
+
110
+ async def __aexit__(self, *_: Any) -> None:
111
+ await self.close()
112
+
113
+ def _rc(self) -> httpx.AsyncClient:
114
+ if self._read_client is None:
115
+ raise RuntimeError("NedbClient not open — use 'async with NedbClient(...) as c'")
116
+ return self._read_client
117
+
118
+ def _wc(self) -> httpx.AsyncClient:
119
+ if self._write_client is None:
120
+ raise RuntimeError("NedbClient not open — use 'async with NedbClient(...) as c'")
121
+ return self._write_client
122
+
123
+ # ── Internal HTTP helpers ─────────────────────────────────────────────────
124
+
125
+ async def _raise(self, resp: httpx.Response) -> None:
126
+ try:
127
+ body = resp.json()
128
+ msg = body.get("error", resp.text)
129
+ except Exception:
130
+ msg = resp.text
131
+ raise NedbError(resp.status_code, msg)
132
+
133
+ async def _query_raw(self, nql: str) -> Dict[str, Any]:
134
+ resp = await self._rc().post(f"/v1/databases/{self._db}/query", json={"nql": nql})
135
+ if resp.status_code in (400, 404):
136
+ return {"rows": [], "count": 0, "seq": 0, "head": ""}
137
+ if not resp.is_success:
138
+ await self._raise(resp)
139
+ return resp.json()
140
+
141
+ async def _put_raw(self, payload: Dict[str, Any]) -> Dict[str, Any]:
142
+ wc = self._wc()
143
+ resp = await wc.post(f"/v1/databases/{self._db}/put", json=payload)
144
+ if resp.status_code == 404 and self._auto_create:
145
+ # DB doesn't exist yet — create it and retry once
146
+ cr = await wc.post("/v1/databases", json={"name": self._db})
147
+ if not cr.is_success and cr.status_code != 409:
148
+ await self._raise(cr)
149
+ resp = await wc.post(f"/v1/databases/{self._db}/put", json=payload)
150
+ if not resp.is_success:
151
+ await self._raise(resp)
152
+ return resp.json()
153
+
154
+ # ── Core CRUD ─────────────────────────────────────────────────────────────
155
+
156
+ async def put(
157
+ self,
158
+ coll: str,
159
+ id: str,
160
+ doc: Dict[str, Any],
161
+ *,
162
+ caused_by: Optional[List[str]] = None,
163
+ valid_from: Optional[str] = None,
164
+ valid_to: Optional[str] = None,
165
+ evidence: Optional[str] = None,
166
+ confidence: Optional[float] = None,
167
+ idem: Optional[str] = None,
168
+ nonce: Optional[int] = None,
169
+ client_id: Optional[str] = None,
170
+ ) -> Dict[str, Any]:
171
+ """
172
+ Write a document. Returns ``{"ok": True, "doc": {...}, "seq": N, "head": "..."}``.
173
+
174
+ Parameters
175
+ ----------
176
+ coll : Collection name (e.g. "blocks", "itsl_ops").
177
+ id : Document ID (must be unique within the collection).
178
+ doc : Arbitrary JSON-serialisable dict.
179
+ caused_by : List of object hashes that causally led to this write (DAG provenance).
180
+ valid_from : Bi-temporal valid-from date (ISO 8601).
181
+ valid_to : Bi-temporal valid-to date (ISO 8601).
182
+ evidence : Human-readable provenance note.
183
+ confidence : Confidence score 0–1.
184
+ idem : Idempotency key — duplicate puts with the same key are no-ops.
185
+ nonce : Replay-protection nonce (monotonically increasing per client_id).
186
+ client_id : Client identifier for replay protection.
187
+ """
188
+ payload: Dict[str, Any] = {"coll": coll, "id": id, "doc": doc}
189
+ if caused_by is not None: payload["caused_by"] = caused_by
190
+ if valid_from is not None: payload["valid_from"] = valid_from
191
+ if valid_to is not None: payload["valid_to"] = valid_to
192
+ if evidence is not None: payload["evidence"] = evidence
193
+ if confidence is not None: payload["confidence"] = confidence
194
+ if idem is not None: payload["idem"] = idem
195
+ if nonce is not None: payload["nonce"] = nonce
196
+ if client_id is not None: payload["client"] = client_id
197
+ return await self._put_raw(payload)
198
+
199
+ async def get(self, coll: str, id: str) -> Optional[Dict[str, Any]]:
200
+ """
201
+ Fetch the current version of a document. Returns the doc dict or None.
202
+ """
203
+ result = await self._query_raw(f'FROM {coll} WHERE _id = "{id}" LIMIT 1')
204
+ rows = result.get("rows", [])
205
+ return rows[0] if rows else None
206
+
207
+ async def delete(self, coll: str, id: str) -> bool:
208
+ """
209
+ Tombstone-delete a document.
210
+ The object history is preserved in the DAG; the live id pointer is removed.
211
+ Returns True if the document existed.
212
+ """
213
+ resp = await self._wc().delete(f"/v1/databases/{self._db}/rows/{coll}/{id}")
214
+ if resp.status_code == 404:
215
+ return False
216
+ if not resp.is_success:
217
+ await self._raise(resp)
218
+ return resp.json().get("ok", False)
219
+
220
+ async def query(self, nql: str) -> List[Dict[str, Any]]:
221
+ """
222
+ Run a NQL query. Returns a list of document dicts.
223
+
224
+ NQL syntax::
225
+
226
+ FROM <coll>
227
+ [AS OF <seq>]
228
+ [VALID AS OF "<date>"]
229
+ [WHERE field = value [AND ...]]
230
+ [ORDER BY field [DESC]]
231
+ [LIMIT n]
232
+ [GROUP BY field COUNT|SUM|AVG|MIN|MAX]
233
+ [TRACE caused_by [REVERSE]]
234
+ [SEARCH "text"]
235
+ """
236
+ result = await self._query_raw(nql)
237
+ return result.get("rows", [])
238
+
239
+ async def query_full(self, nql: str) -> Dict[str, Any]:
240
+ """Like :meth:`query` but returns the full response including ``seq`` and ``head``."""
241
+ return await self._query_raw(nql)
242
+
243
+ # ── Batch ─────────────────────────────────────────────────────────────────
244
+
245
+ async def batch(self, ops: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
246
+ """
247
+ Run a batch of put/del operations atomically in a single HTTP round-trip.
248
+
249
+ Each op is a dict with ``op`` ("put" or "del") and relevant fields::
250
+
251
+ await client.batch([
252
+ {"op": "put", "coll": "blocks", "id": "1", "doc": {"height": 1}},
253
+ {"op": "del", "coll": "blocks", "id": "0"},
254
+ ])
255
+ """
256
+ resp = await self._wc().post(f"/v1/databases/{self._db}/batch", json={"ops": ops})
257
+ if resp.status_code == 404 and self._auto_create:
258
+ cr = await self._wc().post("/v1/databases", json={"name": self._db})
259
+ if not cr.is_success and cr.status_code != 409:
260
+ await self._raise(cr)
261
+ resp = await self._wc().post(f"/v1/databases/{self._db}/batch", json={"ops": ops})
262
+ if not resp.is_success:
263
+ await self._raise(resp)
264
+ return resp.json().get("results", [])
265
+
266
+ # ── Indexes ───────────────────────────────────────────────────────────────
267
+
268
+ async def create_index(self, coll: str, field: str, kind: str = "sorted") -> Dict[str, Any]:
269
+ """
270
+ Create a sorted index on ``(coll, field)`` for fast ORDER BY queries.
271
+
272
+ Parameters
273
+ ----------
274
+ kind : "sorted" (default) or "eq"
275
+ """
276
+ resp = await self._wc().post(
277
+ f"/v1/databases/{self._db}/index",
278
+ json={"coll": coll, "field": field, "kind": kind},
279
+ )
280
+ if not resp.is_success:
281
+ await self._raise(resp)
282
+ return resp.json()
283
+
284
+ # ── Integrity ─────────────────────────────────────────────────────────────
285
+
286
+ async def verify(self) -> Dict[str, Any]:
287
+ """
288
+ Run a full BLAKE2b tamper-evidence check over all objects.
289
+ Returns ``{"ok": True, "objects_checked": N, "tampered": [], "head": "..."}``.
290
+ """
291
+ resp = await self._rc().get(f"/v1/databases/{self._db}/verify")
292
+ if not resp.is_success:
293
+ await self._raise(resp)
294
+ return resp.json()
295
+
296
+ async def head(self) -> str:
297
+ """Return the current BLAKE2b Merkle head of the database."""
298
+ resp = await self._rc().get(f"/v1/databases/{self._db}")
299
+ if not resp.is_success:
300
+ await self._raise(resp)
301
+ return resp.json().get("head", "")
302
+
303
+ async def seq(self) -> int:
304
+ """Return the current global sequence number."""
305
+ resp = await self._rc().get(f"/v1/databases/{self._db}")
306
+ if not resp.is_success:
307
+ await self._raise(resp)
308
+ return resp.json().get("seq", 0)
309
+
310
+ async def checkpoint(self) -> Dict[str, Any]:
311
+ """Trigger an explicit checkpoint (no-op on v2 DAG — always snapshotted)."""
312
+ resp = await self._wc().post(f"/v1/databases/{self._db}/checkpoint")
313
+ if not resp.is_success:
314
+ await self._raise(resp)
315
+ return resp.json()
316
+
317
+ async def log(self, limit: int = 50) -> List[Dict[str, Any]]:
318
+ """Return the last ``limit`` write operations."""
319
+ resp = await self._rc().get(f"/v1/databases/{self._db}/log", params={"limit": limit})
320
+ if not resp.is_success:
321
+ await self._raise(resp)
322
+ return resp.json().get("log", [])
323
+
324
+ # ── Server ────────────────────────────────────────────────────────────────
325
+
326
+ async def health(self) -> Dict[str, Any]:
327
+ """Ping the server. Returns ``{"ok": True, "version": "...", ...}``."""
328
+ resp = await self._rc().get("/health")
329
+ if not resp.is_success:
330
+ await self._raise(resp)
331
+ return resp.json()
332
+
333
+ async def ping(self) -> bool:
334
+ """Returns True if the server is reachable and healthy."""
335
+ try:
336
+ result = await self.health()
337
+ return bool(result.get("ok"))
338
+ except Exception:
339
+ return False
340
+
341
+ async def list_databases(self) -> List[str]:
342
+ """Return a list of all database names on this server."""
343
+ resp = await self._rc().get("/v1/databases")
344
+ if not resp.is_success:
345
+ await self._raise(resp)
346
+ return [d["name"] for d in resp.json().get("databases", [])]
347
+
348
+ async def create_database(self) -> Dict[str, Any]:
349
+ """Explicitly create the database. Idempotent."""
350
+ resp = await self._wc().post("/v1/databases", json={"name": self._db})
351
+ if resp.status_code == 409:
352
+ return {"database": {"name": self._db}} # already exists
353
+ if not resp.is_success:
354
+ await self._raise(resp)
355
+ return resp.json()
356
+
357
+ async def drop_database(self) -> bool:
358
+ """Drop the database and all its data. Irreversible."""
359
+ resp = await self._wc().delete(f"/v1/databases/{self._db}")
360
+ if not resp.is_success:
361
+ await self._raise(resp)
362
+ return resp.json().get("dropped", False)
363
+
364
+ # ── Repr ──────────────────────────────────────────────────────────────────
365
+
366
+ def __repr__(self) -> str:
367
+ return f"NedbClient(url={self._base!r}, db={self._db!r})"
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: nedb-engine-client
3
+ Version: 1.0.2
4
+ Summary: Async Python client for nedbd — the nedb-engine server daemon
5
+ Author: Eth-Interchained
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Eth-Interchained/nedb
8
+ Project-URL: Repository, https://github.com/Eth-Interchained/nedb
9
+ Project-URL: Documentation, https://github.com/Eth-Interchained/nedb/blob/master/client/python/README.md
10
+ Keywords: database,nedb,client,async,dag,bi-temporal,causal
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Database
21
+ Classifier: Framework :: AsyncIO
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: httpx>=0.24
25
+
26
+ # nedb-client (Python)
27
+
28
+ Async Python client for the [nedbd](https://github.com/Eth-Interchained/nedb) HTTP API.
29
+
30
+ ```bash
31
+ pip install nedb-client
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from nedb_client import NedbClient
38
+
39
+ async with NedbClient("http://127.0.0.1:7070", db="mydb") as db:
40
+ # Write
41
+ await db.put("blocks", "618000", {"height": 618000, "hash": "000abc"})
42
+
43
+ # Query (full NQL)
44
+ rows = await db.query("FROM blocks ORDER BY height DESC LIMIT 10")
45
+
46
+ # Causal provenance + bi-temporal
47
+ result = await db.put("claims", "c1", {"fact": "..."},
48
+ caused_by=["abc123..."],
49
+ valid_from="2024-01-01",
50
+ evidence="sensor-42")
51
+
52
+ # Merkle head (tamper-evident root)
53
+ head = await db.head()
54
+
55
+ # Full tamper-evidence check
56
+ report = await db.verify()
57
+ assert report["ok"]
58
+ ```
59
+
60
+ ## API
61
+
62
+ | Method | Description |
63
+ |--------|-------------|
64
+ | `put(coll, id, doc, **meta)` | Write a document |
65
+ | `get(coll, id)` | Fetch current version |
66
+ | `delete(coll, id)` | Tombstone delete |
67
+ | `query(nql)` | NQL query → list of dicts |
68
+ | `query_full(nql)` | NQL query → full response with seq + head |
69
+ | `batch(ops)` | Batch put/del in one round-trip |
70
+ | `create_index(coll, field)` | Create sorted index |
71
+ | `verify()` | BLAKE2b tamper-evidence check |
72
+ | `head()` | Current Merkle head |
73
+ | `seq()` | Current sequence number |
74
+ | `checkpoint()` | Explicit checkpoint |
75
+ | `log(limit)` | Recent write log |
76
+ | `health()` | Server health |
77
+ | `ping()` | Boolean reachability check |
78
+ | `list_databases()` | All databases on server |
79
+ | `create_database()` | Create this database |
80
+ | `drop_database()` | Drop this database |
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ nedb_client/__init__.py
4
+ nedb_client/client.py
5
+ nedb_engine_client.egg-info/PKG-INFO
6
+ nedb_engine_client.egg-info/SOURCES.txt
7
+ nedb_engine_client.egg-info/dependency_links.txt
8
+ nedb_engine_client.egg-info/requires.txt
9
+ nedb_engine_client.egg-info/top_level.txt
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nedb-engine-client"
7
+ version = "1.0.2"
8
+ description = "Async Python client for nedbd — the nedb-engine server daemon"
9
+ readme = "README.md"
10
+ license = { text = "GPL-3.0-or-later" }
11
+ authors = [{ name = "Eth-Interchained" }]
12
+ requires-python = ">=3.8"
13
+ dependencies = ["httpx>=0.24"]
14
+ keywords = ["database", "nedb", "client", "async", "dag", "bi-temporal", "causal"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Database",
26
+ "Framework :: AsyncIO",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Eth-Interchained/nedb"
31
+ Repository = "https://github.com/Eth-Interchained/nedb"
32
+ Documentation = "https://github.com/Eth-Interchained/nedb/blob/master/client/python/README.md"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["."]
36
+ include = ["nedb_client*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+