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.
- nedb_engine_client-1.0.2/PKG-INFO +80 -0
- nedb_engine_client-1.0.2/README.md +55 -0
- nedb_engine_client-1.0.2/nedb_client/__init__.py +16 -0
- nedb_engine_client-1.0.2/nedb_client/client.py +367 -0
- nedb_engine_client-1.0.2/nedb_engine_client.egg-info/PKG-INFO +80 -0
- nedb_engine_client-1.0.2/nedb_engine_client.egg-info/SOURCES.txt +9 -0
- nedb_engine_client-1.0.2/nedb_engine_client.egg-info/dependency_links.txt +1 -0
- nedb_engine_client-1.0.2/nedb_engine_client.egg-info/requires.txt +1 -0
- nedb_engine_client-1.0.2/nedb_engine_client.egg-info/top_level.txt +1 -0
- nedb_engine_client-1.0.2/pyproject.toml +36 -0
- nedb_engine_client-1.0.2/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.24
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nedb_client
|
|
@@ -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*"]
|