nedb-engine-client 3.2.1__tar.gz → 3.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_client-3.2.1 → nedb_engine_client-3.3.0}/PKG-INFO +1 -1
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_client/__init__.py +1 -1
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_client/client.py +99 -8
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/PKG-INFO +1 -1
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/pyproject.toml +1 -1
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/README.md +0 -0
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/SOURCES.txt +0 -0
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/dependency_links.txt +0 -0
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/requires.txt +0 -0
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/top_level.txt +0 -0
- {nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/setup.cfg +0 -0
|
@@ -14,6 +14,7 @@ All /v1/databases/* routes are covered. The client handles:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import urllib.parse
|
|
17
18
|
from typing import Any, Dict, List, Optional, Union
|
|
18
19
|
|
|
19
20
|
try:
|
|
@@ -120,6 +121,33 @@ class NedbClient:
|
|
|
120
121
|
raise RuntimeError("NedbClient not open — use 'async with NedbClient(...) as c'")
|
|
121
122
|
return self._write_client
|
|
122
123
|
|
|
124
|
+
# ── Id handling ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _seg(value: str) -> str:
|
|
128
|
+
"""Percent-encode one URL path segment.
|
|
129
|
+
|
|
130
|
+
`delete("t", "a/slash")` used to interpolate the id straight into the
|
|
131
|
+
path, so the `/` split it and the route matched a different id — the
|
|
132
|
+
call returned False ("no such document") for a document that existed.
|
|
133
|
+
`safe=""` is deliberate: `/` must be encoded, not passed through.
|
|
134
|
+
"""
|
|
135
|
+
return urllib.parse.quote(str(value), safe="")
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _nql_str(value: str) -> str:
|
|
139
|
+
"""Escape a value for use inside a double-quoted NQL string literal.
|
|
140
|
+
|
|
141
|
+
The engine's lexer collapses `\\"` to a literal quote and leaves every
|
|
142
|
+
OTHER backslash alone, so only the quote needs escaping. One case is
|
|
143
|
+
genuinely unrepresentable: a value ENDING in a backslash would produce
|
|
144
|
+
`...\\"`, which the lexer reads as an escaped quote and the string
|
|
145
|
+
never terminates. Callers that might see such an id should use the
|
|
146
|
+
`rows/:coll/:id` route, which takes the id from the URL path and has no
|
|
147
|
+
quoting to get wrong.
|
|
148
|
+
"""
|
|
149
|
+
return str(value).replace('"', '\\"')
|
|
150
|
+
|
|
123
151
|
# ── Internal HTTP helpers ─────────────────────────────────────────────────
|
|
124
152
|
|
|
125
153
|
async def _raise(self, resp: httpx.Response) -> None:
|
|
@@ -196,11 +224,45 @@ class NedbClient:
|
|
|
196
224
|
if client_id is not None: payload["client"] = client_id
|
|
197
225
|
return await self._put_raw(payload)
|
|
198
226
|
|
|
199
|
-
async def get(self, coll: str, id: str
|
|
227
|
+
async def get(self, coll: str, id: str,
|
|
228
|
+
as_of: Optional[int] = None) -> Optional[Dict[str, Any]]:
|
|
200
229
|
"""
|
|
201
230
|
Fetch the current version of a document. Returns the doc dict or None.
|
|
231
|
+
|
|
232
|
+
With ``as_of``, returns the version at or before that sequence number —
|
|
233
|
+
the single-document form of time travel.
|
|
234
|
+
|
|
235
|
+
Uses ``GET /v1/databases/<db>/rows/<coll>/<id>``, which takes the id
|
|
236
|
+
from the URL path. This used to build ``FROM coll WHERE _id = "..."``
|
|
237
|
+
and interpolate the id into it, which made every id containing a double
|
|
238
|
+
quote unreachable: the call returned None — meaning "no such document"
|
|
239
|
+
— for a document ``put()`` had stored and ``FROM coll`` returned. An id
|
|
240
|
+
ending in a backslash could not be escaped at all.
|
|
241
|
+
|
|
242
|
+
A missing document is ``200 {"row": null}`` on this route, not 404 —
|
|
243
|
+
deliberately, so that a 404/405 unambiguously means "the server does
|
|
244
|
+
not have this route" and the client can fall back to the query path.
|
|
245
|
+
Requires nedb-engine >= 3.3.0; older servers take the fallback.
|
|
202
246
|
"""
|
|
203
|
-
|
|
247
|
+
path = f"/v1/databases/{self._db}/rows/{self._seg(coll)}/{self._seg(id)}"
|
|
248
|
+
params = {"as_of": as_of} if as_of is not None else None
|
|
249
|
+
resp = await self._rc().get(path, params=params)
|
|
250
|
+
|
|
251
|
+
# The route answers 200 with `row: null` for a missing document,
|
|
252
|
+
# precisely so this cannot be confused with "no such route". A
|
|
253
|
+
# 404/405 therefore means the server predates the route.
|
|
254
|
+
if resp.status_code in (404, 405):
|
|
255
|
+
return await self._get_via_query(coll, id, as_of)
|
|
256
|
+
if not resp.is_success:
|
|
257
|
+
await self._raise(resp)
|
|
258
|
+
return resp.json().get("row")
|
|
259
|
+
|
|
260
|
+
async def _get_via_query(self, coll: str, id: str,
|
|
261
|
+
as_of: Optional[int] = None) -> Optional[Dict[str, Any]]:
|
|
262
|
+
"""Pre-3.3.0 fallback for :meth:`get` — a point lookup built as NQL."""
|
|
263
|
+
as_of_clause = f" AS OF {int(as_of)}" if as_of is not None else ""
|
|
264
|
+
nql = f'FROM {coll}{as_of_clause} WHERE _id = "{self._nql_str(id)}" LIMIT 1'
|
|
265
|
+
result = await self._query_raw(nql)
|
|
204
266
|
rows = result.get("rows", [])
|
|
205
267
|
return rows[0] if rows else None
|
|
206
268
|
|
|
@@ -210,7 +272,8 @@ class NedbClient:
|
|
|
210
272
|
The object history is preserved in the DAG; the live id pointer is removed.
|
|
211
273
|
Returns True if the document existed.
|
|
212
274
|
"""
|
|
213
|
-
resp = await self._wc().delete(
|
|
275
|
+
resp = await self._wc().delete(
|
|
276
|
+
f"/v1/databases/{self._db}/rows/{self._seg(coll)}/{self._seg(id)}")
|
|
214
277
|
if resp.status_code == 404:
|
|
215
278
|
return False
|
|
216
279
|
if not resp.is_success:
|
|
@@ -226,12 +289,40 @@ class NedbClient:
|
|
|
226
289
|
FROM <coll>
|
|
227
290
|
[AS OF <seq>]
|
|
228
291
|
[VALID AS OF "<date>"]
|
|
229
|
-
[WHERE
|
|
230
|
-
[ORDER BY field [DESC]]
|
|
231
|
-
[LIMIT n]
|
|
232
|
-
[GROUP BY field COUNT|SUM|AVG|MIN|MAX]
|
|
233
|
-
[TRACE caused_by [REVERSE]]
|
|
292
|
+
[WHERE <predicate>]
|
|
234
293
|
[SEARCH "text"]
|
|
294
|
+
[TRAVERSE <relation>]
|
|
295
|
+
[TRACE caused_by [REVERSE]]
|
|
296
|
+
[GROUP BY field [COUNT|SUM f|AVG f|MIN f|MAX f]]
|
|
297
|
+
[COUNT | SUM f | AVG f | MIN f | MAX f]
|
|
298
|
+
[HAVING <predicate>]
|
|
299
|
+
[ORDER BY field [ASC|DESC] (, field [ASC|DESC])*]
|
|
300
|
+
[LIMIT n] [OFFSET n]
|
|
301
|
+
|
|
302
|
+
Clauses are evaluated in SQL's order regardless of how they are
|
|
303
|
+
written: FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY -> OFFSET ->
|
|
304
|
+
LIMIT.
|
|
305
|
+
|
|
306
|
+
``<predicate>`` is a full boolean expression (AND binds tighter than
|
|
307
|
+
OR; parentheses nest to any depth)::
|
|
308
|
+
|
|
309
|
+
field = != < <= > >= value
|
|
310
|
+
field [NOT] IN (v1, v2, ...)
|
|
311
|
+
field [NOT] BETWEEN low AND high -- inclusive, as in SQL
|
|
312
|
+
field [NOT] LIKE|ILIKE "pat" -- % any run, _ any one char
|
|
313
|
+
field IS [NOT] NULL -- absent OR explicitly null
|
|
314
|
+
NOT (...) / (... OR ...) / ...
|
|
315
|
+
|
|
316
|
+
An ordering comparison against a missing or null field is never true,
|
|
317
|
+
so ``WHERE fee < 5`` will not return a row that has no ``fee``. Use
|
|
318
|
+
``IS NULL`` to select those rows.
|
|
319
|
+
|
|
320
|
+
A clause the server does not implement is REJECTED rather than
|
|
321
|
+
silently ignored, so a typo raises instead of quietly answering a
|
|
322
|
+
different question.
|
|
323
|
+
|
|
324
|
+
Requires nedb-engine >= 3.3.0 for IN / BETWEEN / LIKE / IS NULL / OR /
|
|
325
|
+
NOT / OFFSET / HAVING / multi-key ORDER BY and bare aggregates.
|
|
235
326
|
"""
|
|
236
327
|
result = await self._query_raw(nql)
|
|
237
328
|
return result.get("rows", [])
|
|
File without changes
|
{nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/requires.txt
RENAMED
|
File without changes
|
{nedb_engine_client-3.2.1 → nedb_engine_client-3.3.0}/nedb_engine_client.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|