nitrodb 2.4.3__py3-none-any.whl
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/__init__.py +92 -0
- nedb/autoindex.py +142 -0
- nedb/backends/__init__.py +0 -0
- nedb/backends/redis_backend.py +115 -0
- nedb/cascade.py +130 -0
- nedb/concurrent.py +218 -0
- nedb/crypto.py +294 -0
- nedb/engine.py +783 -0
- nedb/index.py +98 -0
- nedb/log.py +216 -0
- nedb/merkle.py +62 -0
- nedb/mongo.py +824 -0
- nedb/proof.py +126 -0
- nedb/query.py +305 -0
- nedb/redis_compat.py +516 -0
- nedb/relations.py +51 -0
- nedb/resp2.py +250 -0
- nedb/server.py +1011 -0
- nedb/snapshot.py +216 -0
- nedb/sql.py +430 -0
- nedb/store.py +68 -0
- nedb/wrap_redis.py +725 -0
- nitrodb-2.4.3.dist-info/METADATA +64 -0
- nitrodb-2.4.3.dist-info/RECORD +27 -0
- nitrodb-2.4.3.dist-info/WHEEL +4 -0
- nitrodb-2.4.3.dist-info/entry_points.txt +2 -0
- nitrodb-2.4.3.dist-info/licenses/LICENSE +65 -0
nedb/resp2.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nedb.resp2 — RESP2 wire protocol server for nedbd.
|
|
3
|
+
|
|
4
|
+
Clients that speak Redis (redis-cli, redis-benchmark, every Redis client
|
|
5
|
+
library in every language) connect to this server and NEDB handles the
|
|
6
|
+
commands natively. No Redis installation required — nedbd IS the server.
|
|
7
|
+
|
|
8
|
+
Protocol: RESP2 (Redis Serialization Protocol v2)
|
|
9
|
+
+OK\r\n — Simple string
|
|
10
|
+
-ERR msg\r\n — Error
|
|
11
|
+
:42\r\n — Integer
|
|
12
|
+
$6\r\nhello\r\n — Bulk string ($-1 = null)
|
|
13
|
+
*3\r\n... — Array of N elements
|
|
14
|
+
|
|
15
|
+
Commands mapped to NEDB (database = selected via SELECT or default "db0"):
|
|
16
|
+
PING [msg]
|
|
17
|
+
SELECT <db_name> — switch active database (creates if needed)
|
|
18
|
+
SET key value [EX secs]
|
|
19
|
+
GET key
|
|
20
|
+
DEL key [key …]
|
|
21
|
+
EXISTS key [key …]
|
|
22
|
+
INCR key / INCRBY key n / DECR key / DECRBY key n
|
|
23
|
+
MSET k v [k v …] / MGET k [k …]
|
|
24
|
+
SETNX key value
|
|
25
|
+
HSET hash field value [field value …]
|
|
26
|
+
HGET hash field
|
|
27
|
+
HMGET hash field [field …]
|
|
28
|
+
HGETALL hash
|
|
29
|
+
HDEL hash field [field …]
|
|
30
|
+
HEXISTS hash field / HKEYS hash / HVALS hash / HLEN hash
|
|
31
|
+
SADD key member [member …] / SMEMBERS key / SISMEMBER key m / SREM key m / SCARD key
|
|
32
|
+
LPUSH key val [val …] / RPUSH key val [val …]
|
|
33
|
+
LRANGE key start stop / LLEN key / LPOP key / RPOP key
|
|
34
|
+
KEYS pattern / TYPE key / DBSIZE / FLUSHDB
|
|
35
|
+
COMMAND (stub — returns OK so redis-cli connects cleanly)
|
|
36
|
+
QUIT
|
|
37
|
+
|
|
38
|
+
NQL pass-through:
|
|
39
|
+
EVAL "FROM users WHERE status = \\"active\\"" 0 → runs NQL directly
|
|
40
|
+
|
|
41
|
+
Unsupported (on roadmap): EXPIRE TTL SUBSCRIBE PUBLISH MULTI EXEC
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import os
|
|
46
|
+
import socketserver
|
|
47
|
+
import threading
|
|
48
|
+
from typing import Any, Dict, List, Optional
|
|
49
|
+
|
|
50
|
+
from .redis_compat import RedisCompat, RedisUnsupportedError
|
|
51
|
+
|
|
52
|
+
DEFAULT_DB = "db0"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── RESP2 encoding ────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def _bulk(s: Optional[str]) -> bytes:
|
|
58
|
+
if s is None:
|
|
59
|
+
return b"$-1\r\n"
|
|
60
|
+
enc = str(s).encode()
|
|
61
|
+
return b"$" + str(len(enc)).encode() + b"\r\n" + enc + b"\r\n"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _int(n: int) -> bytes:
|
|
65
|
+
return b":" + str(int(n)).encode() + b"\r\n"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ok() -> bytes:
|
|
69
|
+
return b"+OK\r\n"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _err(msg: str) -> bytes:
|
|
73
|
+
return b"-ERR " + msg.replace("\r\n", " ").encode() + b"\r\n"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _arr(items: List[Any]) -> bytes:
|
|
77
|
+
out = b"*" + str(len(items)).encode() + b"\r\n"
|
|
78
|
+
for item in items:
|
|
79
|
+
# If the item is already RESP2-encoded bytes (e.g. from _bulk()), use it
|
|
80
|
+
# directly — calling _encode() on bytes would stringify them ("b'...'").
|
|
81
|
+
out += item if isinstance(item, bytes) else _encode(item)
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _encode(value: Any) -> bytes:
|
|
86
|
+
if value is None:
|
|
87
|
+
return b"$-1\r\n"
|
|
88
|
+
if isinstance(value, bool):
|
|
89
|
+
return _int(1 if value else 0)
|
|
90
|
+
if isinstance(value, int):
|
|
91
|
+
return _int(value)
|
|
92
|
+
if isinstance(value, (set, frozenset)):
|
|
93
|
+
return _arr([_bulk(str(x)) for x in sorted(value)])
|
|
94
|
+
if isinstance(value, list):
|
|
95
|
+
return _arr([_encode(x) for x in value])
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
parts = []
|
|
98
|
+
for k, v in value.items():
|
|
99
|
+
parts.append(_bulk(str(k)))
|
|
100
|
+
parts.append(_bulk(str(v) if v is not None else None))
|
|
101
|
+
return _arr(parts)
|
|
102
|
+
return _bulk(str(value))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── RESP2 parser ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
class RespReader:
|
|
108
|
+
def __init__(self, rfile):
|
|
109
|
+
self._f = rfile
|
|
110
|
+
|
|
111
|
+
def read_command(self) -> Optional[List[str]]:
|
|
112
|
+
line = self._f.readline()
|
|
113
|
+
if not line:
|
|
114
|
+
return None
|
|
115
|
+
line = line.rstrip(b"\r\n")
|
|
116
|
+
if not line:
|
|
117
|
+
return None
|
|
118
|
+
if line.startswith(b"*"):
|
|
119
|
+
n = int(line[1:])
|
|
120
|
+
args = []
|
|
121
|
+
for _ in range(n):
|
|
122
|
+
bulk_hdr = self._f.readline().rstrip(b"\r\n")
|
|
123
|
+
if not bulk_hdr.startswith(b"$"):
|
|
124
|
+
raise ValueError(f"Expected bulk string, got {bulk_hdr!r}")
|
|
125
|
+
length = int(bulk_hdr[1:])
|
|
126
|
+
data = self._f.read(length + 2) # +2 for \r\n
|
|
127
|
+
args.append(data[:-2].decode(errors="replace"))
|
|
128
|
+
return args
|
|
129
|
+
# Inline command (e.g. redis-cli in inline mode, PING from telnet)
|
|
130
|
+
return line.decode(errors="replace").split()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Connection handler ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
class _Handler(socketserver.StreamRequestHandler):
|
|
136
|
+
def setup(self):
|
|
137
|
+
super().setup()
|
|
138
|
+
self._db_name: str = DEFAULT_DB
|
|
139
|
+
self._rc: Optional[RedisCompat] = None
|
|
140
|
+
|
|
141
|
+
def _get_rc(self) -> RedisCompat:
|
|
142
|
+
if self._rc is None or getattr(self._rc, "_db_name", None) != self._db_name:
|
|
143
|
+
db = self.server.manager.open(self._db_name)
|
|
144
|
+
self._rc = RedisCompat(db)
|
|
145
|
+
self._rc._db_name = self._db_name # type: ignore[attr-defined]
|
|
146
|
+
return self._rc
|
|
147
|
+
|
|
148
|
+
def handle(self):
|
|
149
|
+
reader = RespReader(self.rfile)
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
args = reader.read_command()
|
|
153
|
+
except Exception:
|
|
154
|
+
break
|
|
155
|
+
if args is None:
|
|
156
|
+
break
|
|
157
|
+
if not args:
|
|
158
|
+
continue
|
|
159
|
+
cmd = args[0].upper()
|
|
160
|
+
rest = args[1:]
|
|
161
|
+
try:
|
|
162
|
+
reply = self._dispatch(cmd, rest)
|
|
163
|
+
except RedisUnsupportedError as e:
|
|
164
|
+
reply = _err(str(e))
|
|
165
|
+
except Exception as e:
|
|
166
|
+
reply = _err(str(e))
|
|
167
|
+
try:
|
|
168
|
+
self.wfile.write(reply)
|
|
169
|
+
self.wfile.flush()
|
|
170
|
+
except Exception:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
def _dispatch(self, cmd: str, args: List[str]) -> bytes:
|
|
174
|
+
rc = self._get_rc
|
|
175
|
+
|
|
176
|
+
# ── server / connection ──────────────────────────────────────────────
|
|
177
|
+
if cmd == "PING":
|
|
178
|
+
return b"+PONG\r\n" if not args else _bulk(args[0])
|
|
179
|
+
if cmd == "QUIT":
|
|
180
|
+
return _ok()
|
|
181
|
+
if cmd == "COMMAND":
|
|
182
|
+
return _ok() # stub — enough for redis-cli to connect
|
|
183
|
+
if cmd == "SELECT":
|
|
184
|
+
if not args:
|
|
185
|
+
return _err("SELECT requires a database name")
|
|
186
|
+
self._db_name = args[0]
|
|
187
|
+
self._rc = None
|
|
188
|
+
return _ok()
|
|
189
|
+
if cmd == "DBSIZE":
|
|
190
|
+
return _int(rc().execute("DBSIZE"))
|
|
191
|
+
|
|
192
|
+
# ── NQL pass-through via EVAL ────────────────────────────────────────
|
|
193
|
+
if cmd == "EVAL":
|
|
194
|
+
if not args:
|
|
195
|
+
return _err("EVAL requires a NQL string")
|
|
196
|
+
nql = args[0]
|
|
197
|
+
import json as _json
|
|
198
|
+
db = self.server.manager.open(self._db_name)
|
|
199
|
+
rows = db.query(nql)
|
|
200
|
+
# Each row is returned as a compact JSON string — clients can parse it
|
|
201
|
+
return _arr([_bulk(_json.dumps(r, separators=(",", ":"))) for r in rows])
|
|
202
|
+
|
|
203
|
+
# ── strings ──────────────────────────────────────────────────────────
|
|
204
|
+
if cmd in ("SET", "GET", "GETDEL", "SETNX", "MSET", "MGET",
|
|
205
|
+
"DEL", "UNLINK", "EXISTS", "INCR", "INCRBY",
|
|
206
|
+
"DECR", "DECRBY", "APPEND", "STRLEN", "TYPE",
|
|
207
|
+
"RENAME", "KEYS", "FLUSHDB"):
|
|
208
|
+
result = rc().execute(cmd, *args)
|
|
209
|
+
return _encode(result)
|
|
210
|
+
|
|
211
|
+
# ── hashes ───────────────────────────────────────────────────────────
|
|
212
|
+
if cmd in ("HSET", "HMSET", "HSETNX", "HGET", "HMGET",
|
|
213
|
+
"HGETALL", "HDEL", "HEXISTS", "HKEYS", "HVALS",
|
|
214
|
+
"HLEN", "HINCRBY"):
|
|
215
|
+
result = rc().execute(cmd, *args)
|
|
216
|
+
return _encode(result)
|
|
217
|
+
|
|
218
|
+
# ── sets ─────────────────────────────────────────────────────────────
|
|
219
|
+
if cmd in ("SADD", "SMEMBERS", "SISMEMBER", "SREM",
|
|
220
|
+
"SCARD", "SUNION", "SINTER", "SDIFF"):
|
|
221
|
+
result = rc().execute(cmd, *args)
|
|
222
|
+
return _encode(result)
|
|
223
|
+
|
|
224
|
+
# ── lists ────────────────────────────────────────────────────────────
|
|
225
|
+
if cmd in ("LPUSH", "RPUSH", "LRANGE", "LLEN",
|
|
226
|
+
"LINDEX", "LSET", "LPOP", "RPOP"):
|
|
227
|
+
result = rc().execute(cmd, *args)
|
|
228
|
+
return _encode(result)
|
|
229
|
+
|
|
230
|
+
# ── unsupported with clear message ───────────────────────────────────
|
|
231
|
+
if cmd in ("EXPIRE", "EXPIREAT", "TTL", "PTTL", "PEXPIRE", "PERSIST"):
|
|
232
|
+
return _err(f"{cmd} is on the NEDB roadmap — use db.expire() via the Python API.")
|
|
233
|
+
if cmd in ("SUBSCRIBE", "PUBLISH", "UNSUBSCRIBE", "PSUBSCRIBE"):
|
|
234
|
+
return _err(f"{cmd} (pub-sub) is on the NEDB roadmap.")
|
|
235
|
+
if cmd in ("MULTI", "EXEC", "DISCARD", "WATCH"):
|
|
236
|
+
return _err(f"{cmd} (transactions) is on the NEDB roadmap.")
|
|
237
|
+
|
|
238
|
+
return _err(f"unknown command '{cmd}'")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
242
|
+
allow_reuse_address = True
|
|
243
|
+
daemon_threads = True
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def make_resp2_server(manager, host: str, port: int):
|
|
247
|
+
"""Create and return a threaded TCP server that speaks RESP2."""
|
|
248
|
+
srv = _ThreadedTCPServer((host, port), _Handler)
|
|
249
|
+
srv.manager = manager
|
|
250
|
+
return srv
|