beaver-db 2.0rc2__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.
- beaver/__init__.py +16 -0
- beaver/blobs.py +223 -0
- beaver/bridge.py +167 -0
- beaver/cache.py +274 -0
- beaver/channels.py +249 -0
- beaver/cli/__init__.py +133 -0
- beaver/cli/blobs.py +225 -0
- beaver/cli/channels.py +166 -0
- beaver/cli/collections.py +500 -0
- beaver/cli/dicts.py +171 -0
- beaver/cli/lists.py +244 -0
- beaver/cli/locks.py +202 -0
- beaver/cli/logs.py +248 -0
- beaver/cli/queues.py +215 -0
- beaver/client.py +392 -0
- beaver/core.py +646 -0
- beaver/dicts.py +314 -0
- beaver/docs.py +459 -0
- beaver/events.py +155 -0
- beaver/graphs.py +212 -0
- beaver/lists.py +337 -0
- beaver/locks.py +186 -0
- beaver/logs.py +187 -0
- beaver/manager.py +203 -0
- beaver/queries.py +66 -0
- beaver/queues.py +215 -0
- beaver/security.py +144 -0
- beaver/server.py +452 -0
- beaver/sketches.py +307 -0
- beaver/types.py +32 -0
- beaver/vectors.py +198 -0
- beaver_db-2.0rc2.dist-info/METADATA +149 -0
- beaver_db-2.0rc2.dist-info/RECORD +36 -0
- beaver_db-2.0rc2.dist-info/WHEEL +4 -0
- beaver_db-2.0rc2.dist-info/entry_points.txt +2 -0
- beaver_db-2.0rc2.dist-info/licenses/LICENSE +21 -0
beaver/dicts.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from typing import (
|
|
5
|
+
IO,
|
|
6
|
+
Any,
|
|
7
|
+
Iterator,
|
|
8
|
+
Tuple,
|
|
9
|
+
overload,
|
|
10
|
+
Protocol,
|
|
11
|
+
runtime_checkable,
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from .manager import AsyncBeaverBase, atomic, emits
|
|
18
|
+
from .security import Cipher
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .core import AsyncBeaverDB
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class IBeaverDict[T: BaseModel](Protocol):
|
|
26
|
+
"""
|
|
27
|
+
The Synchronous Protocol exposed to the user via BeaverBridge.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, key: str) -> T: ...
|
|
31
|
+
def __setitem__(self, key: str, value: T) -> None: ...
|
|
32
|
+
def __delitem__(self, key: str) -> None: ...
|
|
33
|
+
def __len__(self) -> int: ...
|
|
34
|
+
def __contains__(self, key: str) -> bool: ...
|
|
35
|
+
def __iter__(self) -> Iterator[str]: ...
|
|
36
|
+
|
|
37
|
+
def get(self, key: str) -> T: ...
|
|
38
|
+
def set(self, key: str, value: T, ttl_seconds: float | None = None) -> None: ...
|
|
39
|
+
def delete(self, key: str) -> None: ...
|
|
40
|
+
|
|
41
|
+
def fetch(self, key: str, default: Any = None) -> T | Any: ...
|
|
42
|
+
def pop(self, key: str, default: Any = None) -> T | Any: ...
|
|
43
|
+
def keys(self) -> Iterator[str]: ...
|
|
44
|
+
def values(self) -> Iterator[T]: ...
|
|
45
|
+
def items(self) -> Iterator[Tuple[str, T]]: ...
|
|
46
|
+
def clear(self) -> None: ...
|
|
47
|
+
def count(self) -> int: ...
|
|
48
|
+
def dump(self, fp: IO[str] | None = None) -> dict | None: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AsyncBeaverDict[T: BaseModel](AsyncBeaverBase[T]):
|
|
52
|
+
"""
|
|
53
|
+
A wrapper providing a Pythonic interface to a dictionary in the database.
|
|
54
|
+
Refactored for Async-First architecture (v2.0).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
name: str,
|
|
60
|
+
db: "AsyncBeaverDB",
|
|
61
|
+
model: type[T] | None = None,
|
|
62
|
+
secret: str | None = None,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(name, db, model)
|
|
65
|
+
self._cipher: Cipher | None = None
|
|
66
|
+
self._secret_arg = secret
|
|
67
|
+
|
|
68
|
+
async def _init(self):
|
|
69
|
+
"""Async initialization hook."""
|
|
70
|
+
if self._secret_arg or not self.is_system():
|
|
71
|
+
await self._setup_security(self._secret_arg)
|
|
72
|
+
|
|
73
|
+
def is_system(self):
|
|
74
|
+
return self._name in [
|
|
75
|
+
"__metadata__",
|
|
76
|
+
"__security__",
|
|
77
|
+
"__beaver_event_registry__",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
async def _setup_security(self, secret: str | None):
|
|
81
|
+
"""
|
|
82
|
+
Initializes the encryption cipher.
|
|
83
|
+
Reads directly from the internal __beaver_dicts__ table to avoid recursion.
|
|
84
|
+
"""
|
|
85
|
+
if self._name == "__security__":
|
|
86
|
+
if secret:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"The internal '__security__' dictionary cannot be encrypted."
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
cursor = await self.connection.execute(
|
|
93
|
+
"SELECT value FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
|
|
94
|
+
("__security__", self._name),
|
|
95
|
+
)
|
|
96
|
+
row = await cursor.fetchone()
|
|
97
|
+
metadata = json.loads(row["value"]) if row else None
|
|
98
|
+
|
|
99
|
+
if secret is None:
|
|
100
|
+
if metadata:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Dictionary '{self._name}' is encrypted. You must provide a secret to open it."
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if metadata:
|
|
107
|
+
try:
|
|
108
|
+
salt = base64.b64decode(metadata["salt"])
|
|
109
|
+
verifier_encrypted = base64.b64decode(metadata["verifier"])
|
|
110
|
+
except (KeyError, TypeError, ValueError):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Corrupted security metadata for dictionary '{self._name}'."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
cipher = Cipher(secret, salt=salt)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
decrypted = cipher.decrypt(verifier_encrypted)
|
|
119
|
+
if decrypted != b"beaver-secure":
|
|
120
|
+
raise ValueError("Invalid secret.")
|
|
121
|
+
except Exception:
|
|
122
|
+
raise ValueError(f"Invalid secret for dictionary '{self._name}'.")
|
|
123
|
+
|
|
124
|
+
self._cipher = cipher
|
|
125
|
+
else:
|
|
126
|
+
cipher = Cipher(secret)
|
|
127
|
+
salt = cipher.salt
|
|
128
|
+
verifier_encrypted = cipher.encrypt(b"beaver-secure")
|
|
129
|
+
|
|
130
|
+
new_metadata = {
|
|
131
|
+
"salt": base64.b64encode(salt).decode("utf-8"),
|
|
132
|
+
"verifier": base64.b64encode(verifier_encrypted).decode("utf-8"),
|
|
133
|
+
"created_at": time.time(),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await self.connection.execute(
|
|
137
|
+
"INSERT OR REPLACE INTO __beaver_dicts__ (dict_name, key, value, expires_at) VALUES (?, ?, ?, ?)",
|
|
138
|
+
("__security__", self._name, json.dumps(new_metadata), None),
|
|
139
|
+
)
|
|
140
|
+
await self.connection.commit()
|
|
141
|
+
self._cipher = cipher
|
|
142
|
+
|
|
143
|
+
def _serialize(self, value: T) -> str:
|
|
144
|
+
json_str = super()._serialize(value)
|
|
145
|
+
if self._cipher:
|
|
146
|
+
encrypted_bytes = self._cipher.encrypt(json_str.encode("utf-8"))
|
|
147
|
+
return base64.urlsafe_b64encode(encrypted_bytes).decode("utf-8")
|
|
148
|
+
return json_str
|
|
149
|
+
|
|
150
|
+
def _deserialize(self, value: str) -> T:
|
|
151
|
+
json_str = value
|
|
152
|
+
if self._cipher:
|
|
153
|
+
try:
|
|
154
|
+
encrypted_bytes = base64.urlsafe_b64decode(value)
|
|
155
|
+
json_str = self._cipher.decrypt(encrypted_bytes).decode("utf-8")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"Failed to decrypt value in dictionary '{self._name}'."
|
|
159
|
+
) from e
|
|
160
|
+
return super()._deserialize(json_str)
|
|
161
|
+
|
|
162
|
+
# --- Core Async API ---
|
|
163
|
+
|
|
164
|
+
@emits("set", payload=lambda key, *args, **kwargs: dict(key=key))
|
|
165
|
+
@atomic
|
|
166
|
+
async def set(self, key: str, value: T, ttl_seconds: float | None = None):
|
|
167
|
+
"""Sets a value for a key."""
|
|
168
|
+
if self._secret_arg and not self._cipher:
|
|
169
|
+
await self._setup_security(self._secret_arg)
|
|
170
|
+
|
|
171
|
+
expires_at = None
|
|
172
|
+
if ttl_seconds is not None:
|
|
173
|
+
if not isinstance(ttl_seconds, (int, float)) or ttl_seconds <= 0:
|
|
174
|
+
raise ValueError("ttl_seconds must be a positive number.")
|
|
175
|
+
expires_at = time.time() + ttl_seconds
|
|
176
|
+
|
|
177
|
+
serialized_value = self._serialize(value)
|
|
178
|
+
|
|
179
|
+
await self.connection.execute(
|
|
180
|
+
"""
|
|
181
|
+
INSERT OR REPLACE INTO __beaver_dicts__
|
|
182
|
+
(dict_name, key, value, expires_at)
|
|
183
|
+
VALUES (?, ?, ?, ?)
|
|
184
|
+
""",
|
|
185
|
+
(self._name, key, serialized_value, expires_at),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@atomic
|
|
189
|
+
async def get(self, key: str) -> T:
|
|
190
|
+
"""Retrieves a value for a key. Raises KeyError if missing or expired."""
|
|
191
|
+
if self._secret_arg and not self._cipher:
|
|
192
|
+
await self._setup_security(self._secret_arg)
|
|
193
|
+
|
|
194
|
+
cursor = await self.connection.execute(
|
|
195
|
+
"SELECT value, expires_at FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
|
|
196
|
+
(self._name, key),
|
|
197
|
+
)
|
|
198
|
+
result = await cursor.fetchone()
|
|
199
|
+
|
|
200
|
+
if result is None:
|
|
201
|
+
raise KeyError(f"Key '{key}' not found in dictionary '{self._name}'")
|
|
202
|
+
|
|
203
|
+
raw_value, expires_at = result["value"], result["expires_at"]
|
|
204
|
+
|
|
205
|
+
if expires_at is not None and time.time() > expires_at:
|
|
206
|
+
await self.connection.execute(
|
|
207
|
+
"DELETE FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
|
|
208
|
+
(self._name, key),
|
|
209
|
+
)
|
|
210
|
+
raise KeyError(
|
|
211
|
+
f"Key '{key}' not found in dictionary '{self._name}' (expired)"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return self._deserialize(raw_value)
|
|
215
|
+
|
|
216
|
+
@emits("del", payload=lambda key, *args, **kwargs: dict(key=key))
|
|
217
|
+
@atomic
|
|
218
|
+
async def delete(self, key: str):
|
|
219
|
+
"""Deletes a key. Raises KeyError if missing."""
|
|
220
|
+
cursor = await self.connection.execute(
|
|
221
|
+
"DELETE FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
|
|
222
|
+
(self._name, key),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if cursor.rowcount == 0:
|
|
226
|
+
raise KeyError(f"Key '{key}' not found in dictionary '{self._name}'")
|
|
227
|
+
|
|
228
|
+
async def fetch(self, key: str, default: Any = None) -> T | Any:
|
|
229
|
+
try:
|
|
230
|
+
return await self.get(key)
|
|
231
|
+
except KeyError:
|
|
232
|
+
return default
|
|
233
|
+
|
|
234
|
+
@atomic
|
|
235
|
+
async def pop(self, key: str, default: Any = None) -> T | Any:
|
|
236
|
+
try:
|
|
237
|
+
value = await self.get(key)
|
|
238
|
+
await self.delete(key)
|
|
239
|
+
return value
|
|
240
|
+
except KeyError:
|
|
241
|
+
return default
|
|
242
|
+
|
|
243
|
+
async def count(self) -> int:
|
|
244
|
+
cursor = await self.connection.execute(
|
|
245
|
+
"SELECT COUNT(*) FROM __beaver_dicts__ WHERE dict_name = ?", (self._name,)
|
|
246
|
+
)
|
|
247
|
+
row = await cursor.fetchone()
|
|
248
|
+
return row[0] if row else 0
|
|
249
|
+
|
|
250
|
+
async def contains(self, key: str) -> bool:
|
|
251
|
+
cursor = await self.connection.execute(
|
|
252
|
+
"SELECT 1 FROM __beaver_dicts__ WHERE dict_name = ? AND key = ? LIMIT 1",
|
|
253
|
+
(self._name, key),
|
|
254
|
+
)
|
|
255
|
+
return await cursor.fetchone() is not None
|
|
256
|
+
|
|
257
|
+
@emits("clear", payload=lambda *args, **kwargs: dict())
|
|
258
|
+
@atomic
|
|
259
|
+
async def clear(self):
|
|
260
|
+
await self.connection.execute(
|
|
261
|
+
"DELETE FROM __beaver_dicts__ WHERE dict_name = ?",
|
|
262
|
+
(self._name,),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# --- Iterators (Async Generators) ---
|
|
266
|
+
|
|
267
|
+
async def __aiter__(self):
|
|
268
|
+
async for key in self.keys():
|
|
269
|
+
yield key
|
|
270
|
+
|
|
271
|
+
async def keys(self):
|
|
272
|
+
cursor = await self.connection.execute(
|
|
273
|
+
"SELECT key FROM __beaver_dicts__ WHERE dict_name = ?", (self._name,)
|
|
274
|
+
)
|
|
275
|
+
async for row in cursor:
|
|
276
|
+
yield row["key"]
|
|
277
|
+
|
|
278
|
+
async def values(self):
|
|
279
|
+
cursor = await self.connection.execute(
|
|
280
|
+
"SELECT value FROM __beaver_dicts__ WHERE dict_name = ?", (self._name,)
|
|
281
|
+
)
|
|
282
|
+
async for row in cursor:
|
|
283
|
+
yield self._deserialize(row["value"])
|
|
284
|
+
|
|
285
|
+
async def items(self):
|
|
286
|
+
cursor = await self.connection.execute(
|
|
287
|
+
"SELECT key, value FROM __beaver_dicts__ WHERE dict_name = ?", (self._name,)
|
|
288
|
+
)
|
|
289
|
+
async for row in cursor:
|
|
290
|
+
yield (row["key"], self._deserialize(row["value"]))
|
|
291
|
+
|
|
292
|
+
async def dump(self, fp: IO[str] | None = None) -> dict | None:
|
|
293
|
+
items = []
|
|
294
|
+
async for k, v in self.items():
|
|
295
|
+
val = v
|
|
296
|
+
if self._model and isinstance(v, BaseModel):
|
|
297
|
+
val = json.loads(v.model_dump_json())
|
|
298
|
+
items.append({"key": k, "value": val})
|
|
299
|
+
|
|
300
|
+
dump_obj = {
|
|
301
|
+
"metadata": {
|
|
302
|
+
"type": "Dict",
|
|
303
|
+
"name": self._name,
|
|
304
|
+
"count": len(items),
|
|
305
|
+
"encrypted": self._cipher is not None,
|
|
306
|
+
},
|
|
307
|
+
"items": items,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if fp:
|
|
311
|
+
json.dump(dump_obj, fp, indent=2)
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
return dump_obj
|