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/queues.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import (
|
|
6
|
+
IO,
|
|
7
|
+
Iterator,
|
|
8
|
+
Literal,
|
|
9
|
+
NamedTuple,
|
|
10
|
+
overload,
|
|
11
|
+
Protocol,
|
|
12
|
+
runtime_checkable,
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from .manager import AsyncBeaverBase, atomic, emits
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .core import AsyncBeaverDB
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QueueItem[T](NamedTuple):
|
|
25
|
+
"""A data class representing a single item retrieved from the queue."""
|
|
26
|
+
|
|
27
|
+
priority: float
|
|
28
|
+
timestamp: float
|
|
29
|
+
data: T
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class IBeaverQueue[T: BaseModel](Protocol):
|
|
34
|
+
"""
|
|
35
|
+
The Synchronous Protocol exposed to the user via BeaverBridge.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def put(self, data: T, priority: float) -> None: ...
|
|
39
|
+
|
|
40
|
+
def peek(self) -> QueueItem[T] | None: ...
|
|
41
|
+
|
|
42
|
+
# Overloads for get
|
|
43
|
+
@overload
|
|
44
|
+
def get(
|
|
45
|
+
self, block: Literal[True] = True, timeout: float | None = None
|
|
46
|
+
) -> QueueItem[T]: ...
|
|
47
|
+
@overload
|
|
48
|
+
def get(self, block: Literal[False]) -> QueueItem[T]: ...
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None: ...
|
|
51
|
+
def count(self) -> int: ...
|
|
52
|
+
def dump(self, fp: IO[str] | None = None) -> dict | None: ...
|
|
53
|
+
|
|
54
|
+
def __len__(self) -> int: ...
|
|
55
|
+
def __iter__(self) -> Iterator[QueueItem[T]]: ...
|
|
56
|
+
def __bool__(self) -> bool: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AsyncBeaverQueue[T: BaseModel](AsyncBeaverBase[T]):
|
|
60
|
+
"""
|
|
61
|
+
A wrapper providing a Pythonic interface to a persistent, multi-process
|
|
62
|
+
producer-consumer priority queue.
|
|
63
|
+
Refactored for Async-First architecture (v2.0).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@emits("put", payload=lambda *args, **kwargs: dict())
|
|
67
|
+
@atomic
|
|
68
|
+
async def put(self, data: T, priority: float):
|
|
69
|
+
"""
|
|
70
|
+
Adds an item to the queue with a specific priority.
|
|
71
|
+
"""
|
|
72
|
+
await self.connection.execute(
|
|
73
|
+
"INSERT INTO __beaver_priority_queues__ (queue_name, priority, timestamp, data) VALUES (?, ?, ?, ?)",
|
|
74
|
+
(self._name, priority, time.time(), self._serialize(data)),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def _get_item_atomically(self, pop: bool = True) -> QueueItem[T] | None:
|
|
78
|
+
"""
|
|
79
|
+
Performs a single, atomic attempt to retrieve and remove the
|
|
80
|
+
highest-priority item from the queue.
|
|
81
|
+
"""
|
|
82
|
+
# We need a transaction to ensure we don't peek/get an item that someone else steals
|
|
83
|
+
# Note: If called from peek/get, they might handle locking via @atomic or logic.
|
|
84
|
+
# Since this helper is used inside @atomic methods, we just need execution.
|
|
85
|
+
|
|
86
|
+
cursor = await self.connection.execute(
|
|
87
|
+
"""
|
|
88
|
+
SELECT rowid, priority, timestamp, data
|
|
89
|
+
FROM __beaver_priority_queues__
|
|
90
|
+
WHERE queue_name = ?
|
|
91
|
+
ORDER BY priority ASC, timestamp ASC
|
|
92
|
+
LIMIT 1
|
|
93
|
+
""",
|
|
94
|
+
(self._name,),
|
|
95
|
+
)
|
|
96
|
+
result = await cursor.fetchone()
|
|
97
|
+
|
|
98
|
+
if result is None:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
rowid, priority, timestamp, data = result
|
|
102
|
+
|
|
103
|
+
if pop:
|
|
104
|
+
await self.connection.execute(
|
|
105
|
+
"DELETE FROM __beaver_priority_queues__ WHERE rowid = ?", (rowid,)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return QueueItem(
|
|
109
|
+
priority=priority, timestamp=timestamp, data=self._deserialize(data)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@atomic
|
|
113
|
+
async def peek(self) -> QueueItem[T] | None:
|
|
114
|
+
"""
|
|
115
|
+
Retrieves the first item of the queue without removing it.
|
|
116
|
+
"""
|
|
117
|
+
return await self._get_item_atomically(pop=False)
|
|
118
|
+
|
|
119
|
+
@atomic
|
|
120
|
+
async def _try_pop_atomic(self) -> QueueItem[T] | None:
|
|
121
|
+
"""Helper to check and pop one item under lock."""
|
|
122
|
+
return await self._get_item_atomically(pop=True)
|
|
123
|
+
|
|
124
|
+
async def _get_loop_impl(self, block: bool, timeout: float | None) -> QueueItem[T]:
|
|
125
|
+
"""
|
|
126
|
+
The polling loop. It acquires the lock only briefly during the check,
|
|
127
|
+
allowing producers to interleave 'put' operations while we wait.
|
|
128
|
+
"""
|
|
129
|
+
# 1. Non-blocking fast path
|
|
130
|
+
if not block:
|
|
131
|
+
item = await self._try_pop_atomic()
|
|
132
|
+
if item is None:
|
|
133
|
+
raise IndexError("get from an empty queue.")
|
|
134
|
+
return item
|
|
135
|
+
|
|
136
|
+
# 2. Blocking loop
|
|
137
|
+
start_time = time.time()
|
|
138
|
+
while True:
|
|
139
|
+
item = await self._try_pop_atomic()
|
|
140
|
+
|
|
141
|
+
if item is not None:
|
|
142
|
+
return item
|
|
143
|
+
|
|
144
|
+
if timeout is not None and (time.time() - start_time) > timeout:
|
|
145
|
+
raise TimeoutError("Timeout expired while waiting for an item.")
|
|
146
|
+
|
|
147
|
+
# Yield control to allow producers to acquire the lock and put items
|
|
148
|
+
await asyncio.sleep(0.1)
|
|
149
|
+
|
|
150
|
+
# We override the public get to use the loop implementation
|
|
151
|
+
# NOTE: We do NOT decorate this with @atomic, as it manages its own locking scope.
|
|
152
|
+
@emits("get", payload=lambda *args, **kwargs: dict())
|
|
153
|
+
async def get(
|
|
154
|
+
self, block: bool = True, timeout: float | None = None
|
|
155
|
+
) -> QueueItem[T]:
|
|
156
|
+
return await self._get_loop_impl(block, timeout)
|
|
157
|
+
|
|
158
|
+
async def count(self) -> int:
|
|
159
|
+
cursor = await self.connection.execute(
|
|
160
|
+
"SELECT COUNT(*) FROM __beaver_priority_queues__ WHERE queue_name = ?",
|
|
161
|
+
(self._name,),
|
|
162
|
+
)
|
|
163
|
+
count = await cursor.fetchone()
|
|
164
|
+
return count[0] if count else 0
|
|
165
|
+
|
|
166
|
+
async def clear(self):
|
|
167
|
+
await self.connection.execute(
|
|
168
|
+
"DELETE FROM __beaver_priority_queues__ WHERE queue_name = ?",
|
|
169
|
+
(self._name,),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# --- Iterators ---
|
|
173
|
+
|
|
174
|
+
async def __aiter__(self):
|
|
175
|
+
cursor = await self.connection.execute(
|
|
176
|
+
"""
|
|
177
|
+
SELECT priority, timestamp, data
|
|
178
|
+
FROM __beaver_priority_queues__
|
|
179
|
+
WHERE queue_name = ?
|
|
180
|
+
ORDER BY priority ASC, timestamp ASC
|
|
181
|
+
""",
|
|
182
|
+
(self._name,),
|
|
183
|
+
)
|
|
184
|
+
async for row in cursor:
|
|
185
|
+
yield QueueItem(
|
|
186
|
+
priority=row["priority"],
|
|
187
|
+
timestamp=row["timestamp"],
|
|
188
|
+
data=self._deserialize(row["data"]),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def dump(self, fp: IO[str] | None = None) -> dict | None:
|
|
192
|
+
items_list = []
|
|
193
|
+
async for item in self:
|
|
194
|
+
data = item.data
|
|
195
|
+
if self._model and isinstance(data, BaseModel):
|
|
196
|
+
data = json.loads(data.model_dump_json())
|
|
197
|
+
|
|
198
|
+
items_list.append(
|
|
199
|
+
{"priority": item.priority, "timestamp": item.timestamp, "data": data}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
metadata = {
|
|
203
|
+
"type": "Queue",
|
|
204
|
+
"name": self._name,
|
|
205
|
+
"count": len(items_list),
|
|
206
|
+
"dump_date": datetime.now(timezone.utc).isoformat(),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
dump_obj = {"metadata": metadata, "items": items_list}
|
|
210
|
+
|
|
211
|
+
if fp:
|
|
212
|
+
json.dump(dump_obj, fp, indent=2)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
return dump_obj
|
beaver/security.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
9
|
+
from cryptography.hazmat.primitives import hashes
|
|
10
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
11
|
+
from cryptography.hazmat.backends import default_backend
|
|
12
|
+
except ImportError:
|
|
13
|
+
# We define dummy classes or raise errors if accessed without dependencies
|
|
14
|
+
Fernet = None # type: ignore
|
|
15
|
+
|
|
16
|
+
def require_security():
|
|
17
|
+
raise ImportError(
|
|
18
|
+
'The "security" extra is required to use this feature. '
|
|
19
|
+
'Please install it with: pip install "beaver-db[security]"'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
else:
|
|
23
|
+
|
|
24
|
+
def require_security():
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Cipher:
|
|
29
|
+
"""
|
|
30
|
+
Handles symmetric encryption using Fernet (AES-128 CBC + HMAC-SHA256).
|
|
31
|
+
Used by Encrypted Dictionaries to secure values at rest.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, secret: str, salt: bytes | None = None):
|
|
35
|
+
require_security()
|
|
36
|
+
if not salt:
|
|
37
|
+
salt = os.urandom(16)
|
|
38
|
+
self.salt = salt
|
|
39
|
+
self.key = self._derive_key(secret, salt)
|
|
40
|
+
self.fernet = Fernet(self.key)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _derive_key(secret: str, salt: bytes) -> bytes:
|
|
44
|
+
"""Derives a 32-byte, url-safe base64 key from the secret using PBKDF2."""
|
|
45
|
+
kdf = PBKDF2HMAC(
|
|
46
|
+
algorithm=hashes.SHA256(),
|
|
47
|
+
length=32,
|
|
48
|
+
salt=salt,
|
|
49
|
+
iterations=100_000,
|
|
50
|
+
backend=default_backend(),
|
|
51
|
+
)
|
|
52
|
+
return base64.urlsafe_b64encode(kdf.derive(secret.encode()))
|
|
53
|
+
|
|
54
|
+
def encrypt(self, data: bytes) -> bytes:
|
|
55
|
+
"""Encrypts bytes."""
|
|
56
|
+
return self.fernet.encrypt(data)
|
|
57
|
+
|
|
58
|
+
def decrypt(self, token: bytes) -> bytes:
|
|
59
|
+
"""Decrypts bytes. Raises ValueError if the key is incorrect."""
|
|
60
|
+
try:
|
|
61
|
+
return self.fernet.decrypt(token)
|
|
62
|
+
except InvalidToken:
|
|
63
|
+
raise ValueError("Invalid secret key or corrupted data.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Secret(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
A value type for secure, one-way password hashing.
|
|
69
|
+
Stores only the hash and salt as base64 strings, ensuring JSON compatibility.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
hash: str
|
|
73
|
+
salt: str
|
|
74
|
+
|
|
75
|
+
def __init__(self, value: str | None = None, **kwargs):
|
|
76
|
+
"""
|
|
77
|
+
Can be initialized in two ways:
|
|
78
|
+
1. New Secret: Secret("my-password") -> Hashes and stores it as b64 strings.
|
|
79
|
+
2. Loading: Secret(hash="...", salt="...") -> Reconstructs it.
|
|
80
|
+
"""
|
|
81
|
+
# Ensure security deps are present
|
|
82
|
+
if value is not None or (not kwargs.get("hash")):
|
|
83
|
+
require_security()
|
|
84
|
+
|
|
85
|
+
if value is not None:
|
|
86
|
+
# 1. User provided a plain-text password to hash
|
|
87
|
+
salt_bytes = os.urandom(16)
|
|
88
|
+
kdf = PBKDF2HMAC(
|
|
89
|
+
algorithm=hashes.SHA256(),
|
|
90
|
+
length=32,
|
|
91
|
+
salt=salt_bytes,
|
|
92
|
+
iterations=100_000,
|
|
93
|
+
backend=default_backend(),
|
|
94
|
+
)
|
|
95
|
+
hashed_bytes = kdf.derive(value.encode())
|
|
96
|
+
|
|
97
|
+
# Store as base64 strings
|
|
98
|
+
super().__init__(
|
|
99
|
+
hash=base64.b64encode(hashed_bytes).decode("utf-8"),
|
|
100
|
+
salt=base64.b64encode(salt_bytes).decode("utf-8"),
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
# 2. Pydantic is loading from arguments (hash/salt)
|
|
104
|
+
super().__init__(**kwargs)
|
|
105
|
+
|
|
106
|
+
def __eq__(self, other: Any) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Checks equality.
|
|
109
|
+
- If comparing to another Secret, checks hash equality.
|
|
110
|
+
- If comparing to a string, checks if the string hashes to the stored value.
|
|
111
|
+
"""
|
|
112
|
+
if isinstance(other, Secret):
|
|
113
|
+
return self.hash == other.hash and self.salt == other.salt
|
|
114
|
+
|
|
115
|
+
if isinstance(other, str):
|
|
116
|
+
require_security()
|
|
117
|
+
|
|
118
|
+
# Decode stored strings back to bytes
|
|
119
|
+
try:
|
|
120
|
+
salt_bytes = base64.b64decode(self.salt)
|
|
121
|
+
hash_bytes = base64.b64decode(self.hash)
|
|
122
|
+
except (ValueError, TypeError):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
kdf = PBKDF2HMAC(
|
|
126
|
+
algorithm=hashes.SHA256(),
|
|
127
|
+
length=32,
|
|
128
|
+
salt=salt_bytes,
|
|
129
|
+
iterations=100_000,
|
|
130
|
+
backend=default_backend(),
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
kdf.verify(other.encode(), hash_bytes)
|
|
134
|
+
return True
|
|
135
|
+
except Exception:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def __repr__(self) -> str:
|
|
141
|
+
return f"Secret(hash={self.hash[:8]}...)"
|
|
142
|
+
|
|
143
|
+
def __str__(self) -> str:
|
|
144
|
+
return "********"
|