beaver-db 0.12.2__tar.gz → 0.13.1__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.
Potentially problematic release.
This version of beaver-db might be problematic. Click here for more details.
- {beaver_db-0.12.2 → beaver_db-0.13.1}/PKG-INFO +40 -3
- {beaver_db-0.12.2 → beaver_db-0.13.1}/README.md +39 -2
- beaver_db-0.13.1/beaver/__init__.py +3 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/core.py +28 -10
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/dicts.py +38 -21
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/lists.py +39 -23
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/queues.py +37 -16
- beaver_db-0.13.1/beaver/types.py +39 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver_db.egg-info/PKG-INFO +40 -3
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver_db.egg-info/SOURCES.txt +1 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/pyproject.toml +1 -1
- beaver_db-0.12.2/beaver/__init__.py +0 -2
- {beaver_db-0.12.2 → beaver_db-0.13.1}/LICENSE +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/blobs.py +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/channels.py +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/collections.py +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver/vectors.py +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver_db.egg-info/dependency_links.txt +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver_db.egg-info/requires.txt +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/beaver_db.egg-info/top_level.txt +0 -0
- {beaver_db-0.12.2 → beaver_db-0.13.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beaver-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Fast, embedded, and multi-modal DB based on SQLite for AI-powered applications.
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -219,6 +219,39 @@ attachments.put(
|
|
|
219
219
|
avatar = attachments.get("user_123_avatar.png")
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
|
|
223
|
+
## Type-Safe Data Models
|
|
224
|
+
|
|
225
|
+
For enhanced data integrity and a better developer experience, BeaverDB supports type-safe operations for dictionaries, lists, and queues. By associating a model with these data structures, you get automatic serialization and deserialization, complete with autocompletion in your editor.
|
|
226
|
+
|
|
227
|
+
This feature is designed to be flexible and works seamlessly with two kinds of models:
|
|
228
|
+
|
|
229
|
+
* **Pydantic Models**: If you're already using Pydantic, your `BaseModel` classes will work out of the box.
|
|
230
|
+
* **Lightweight `beaver.Model`**: For a zero-dependency solution, you can inherit from the built-in `beaver.Model` class, which is a standard Python class with serialization methods automatically included.
|
|
231
|
+
|
|
232
|
+
Here’s a quick example of how to use it:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from beaver import BeaverDB, Model
|
|
236
|
+
|
|
237
|
+
# Inherit from beaver.Model for a lightweight, dependency-free model
|
|
238
|
+
class User(Model):
|
|
239
|
+
name: str
|
|
240
|
+
email: str
|
|
241
|
+
|
|
242
|
+
db = BeaverDB("user_data.db")
|
|
243
|
+
|
|
244
|
+
# Associate the User model with a dictionary
|
|
245
|
+
users = db.dict("user_profiles", model=User)
|
|
246
|
+
|
|
247
|
+
# BeaverDB now handles serialization automatically
|
|
248
|
+
users["alice"] = User(name="Alice", email="alice@example.com")
|
|
249
|
+
|
|
250
|
+
# The retrieved object is a proper instance of the User class
|
|
251
|
+
retrieved_user = users["alice"]
|
|
252
|
+
print(f"Retrieved: {retrieved_user.name}") # Your editor will provide autocompletion here
|
|
253
|
+
```
|
|
254
|
+
|
|
222
255
|
## More Examples
|
|
223
256
|
|
|
224
257
|
For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
@@ -241,13 +274,17 @@ For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
|
241
274
|
|
|
242
275
|
## Roadmap
|
|
243
276
|
|
|
277
|
+
`beaver` is roughly feature-complete, but there are still some features and improvements planned for future releases, mostly directed to improving developer experience.
|
|
278
|
+
|
|
244
279
|
These are some of the features and improvements planned for future releases:
|
|
245
280
|
|
|
246
|
-
- **
|
|
247
|
-
- **Type Hints**:
|
|
281
|
+
- **Async API**: Extend the async support with on-demand wrappers for all features besides channels.
|
|
282
|
+
- **Type Hints**: Extend type hints for channels and documents.
|
|
248
283
|
|
|
249
284
|
Check out the [roadmap](roadmap.md) for a detailed list of upcoming features and design ideas.
|
|
250
285
|
|
|
286
|
+
If you think of something that would make `beaver` more useful for your use case, please open an issue and/or submit a pull request.
|
|
287
|
+
|
|
251
288
|
## License
|
|
252
289
|
|
|
253
290
|
This project is licensed under the MIT License.
|
|
@@ -208,6 +208,39 @@ attachments.put(
|
|
|
208
208
|
avatar = attachments.get("user_123_avatar.png")
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
+
|
|
212
|
+
## Type-Safe Data Models
|
|
213
|
+
|
|
214
|
+
For enhanced data integrity and a better developer experience, BeaverDB supports type-safe operations for dictionaries, lists, and queues. By associating a model with these data structures, you get automatic serialization and deserialization, complete with autocompletion in your editor.
|
|
215
|
+
|
|
216
|
+
This feature is designed to be flexible and works seamlessly with two kinds of models:
|
|
217
|
+
|
|
218
|
+
* **Pydantic Models**: If you're already using Pydantic, your `BaseModel` classes will work out of the box.
|
|
219
|
+
* **Lightweight `beaver.Model`**: For a zero-dependency solution, you can inherit from the built-in `beaver.Model` class, which is a standard Python class with serialization methods automatically included.
|
|
220
|
+
|
|
221
|
+
Here’s a quick example of how to use it:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from beaver import BeaverDB, Model
|
|
225
|
+
|
|
226
|
+
# Inherit from beaver.Model for a lightweight, dependency-free model
|
|
227
|
+
class User(Model):
|
|
228
|
+
name: str
|
|
229
|
+
email: str
|
|
230
|
+
|
|
231
|
+
db = BeaverDB("user_data.db")
|
|
232
|
+
|
|
233
|
+
# Associate the User model with a dictionary
|
|
234
|
+
users = db.dict("user_profiles", model=User)
|
|
235
|
+
|
|
236
|
+
# BeaverDB now handles serialization automatically
|
|
237
|
+
users["alice"] = User(name="Alice", email="alice@example.com")
|
|
238
|
+
|
|
239
|
+
# The retrieved object is a proper instance of the User class
|
|
240
|
+
retrieved_user = users["alice"]
|
|
241
|
+
print(f"Retrieved: {retrieved_user.name}") # Your editor will provide autocompletion here
|
|
242
|
+
```
|
|
243
|
+
|
|
211
244
|
## More Examples
|
|
212
245
|
|
|
213
246
|
For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
@@ -230,13 +263,17 @@ For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
|
230
263
|
|
|
231
264
|
## Roadmap
|
|
232
265
|
|
|
266
|
+
`beaver` is roughly feature-complete, but there are still some features and improvements planned for future releases, mostly directed to improving developer experience.
|
|
267
|
+
|
|
233
268
|
These are some of the features and improvements planned for future releases:
|
|
234
269
|
|
|
235
|
-
- **
|
|
236
|
-
- **Type Hints**:
|
|
270
|
+
- **Async API**: Extend the async support with on-demand wrappers for all features besides channels.
|
|
271
|
+
- **Type Hints**: Extend type hints for channels and documents.
|
|
237
272
|
|
|
238
273
|
Check out the [roadmap](roadmap.md) for a detailed list of upcoming features and design ideas.
|
|
239
274
|
|
|
275
|
+
If you think of something that would make `beaver` more useful for your use case, please open an issue and/or submit a pull request.
|
|
276
|
+
|
|
240
277
|
## License
|
|
241
278
|
|
|
242
279
|
This project is licensed under the MIT License.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sqlite3
|
|
2
2
|
import threading
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
from .types import JsonSerializable
|
|
5
5
|
from .blobs import BlobManager
|
|
6
6
|
from .channels import ChannelManager
|
|
7
7
|
from .collections import CollectionManager
|
|
@@ -267,26 +267,44 @@ class BeaverDB:
|
|
|
267
267
|
|
|
268
268
|
# --- Factory and Passthrough Methods ---
|
|
269
269
|
|
|
270
|
-
def dict(self, name: str) -> DictManager:
|
|
271
|
-
"""
|
|
270
|
+
def dict[T](self, name: str, model: type[T] | None = None) -> DictManager[T]:
|
|
271
|
+
"""
|
|
272
|
+
Returns a wrapper object for interacting with a named dictionary.
|
|
273
|
+
If model is defined, it should be a type used for automatic (de)serialization.
|
|
274
|
+
"""
|
|
272
275
|
if not isinstance(name, str) or not name:
|
|
273
276
|
raise TypeError("Dictionary name must be a non-empty string.")
|
|
274
277
|
|
|
275
|
-
|
|
278
|
+
if model and not isinstance(model, JsonSerializable):
|
|
279
|
+
raise TypeError("The model parameter must be a JsonSerializable class.")
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
return DictManager(name, self._conn, model)
|
|
282
|
+
|
|
283
|
+
def list[T](self, name: str, model: type[T] | None = None) -> ListManager[T]:
|
|
284
|
+
"""
|
|
285
|
+
Returns a wrapper object for interacting with a named list.
|
|
286
|
+
If model is defined, it should be a type used for automatic (de)serialization.
|
|
287
|
+
"""
|
|
279
288
|
if not isinstance(name, str) or not name:
|
|
280
289
|
raise TypeError("List name must be a non-empty string.")
|
|
281
290
|
|
|
282
|
-
|
|
291
|
+
if model and not isinstance(model, JsonSerializable):
|
|
292
|
+
raise TypeError("The model parameter must be a JsonSerializable class.")
|
|
293
|
+
|
|
294
|
+
return ListManager(name, self._conn, model)
|
|
283
295
|
|
|
284
|
-
def queue(self, name: str) -> QueueManager:
|
|
285
|
-
"""
|
|
296
|
+
def queue[T](self, name: str, model: type[T] | None = None) -> QueueManager[T]:
|
|
297
|
+
"""
|
|
298
|
+
Returns a wrapper object for interacting with a persistent priority queue.
|
|
299
|
+
If model is defined, it should be a type used for automatic (de)serialization.
|
|
300
|
+
"""
|
|
286
301
|
if not isinstance(name, str) or not name:
|
|
287
302
|
raise TypeError("Queue name must be a non-empty string.")
|
|
288
303
|
|
|
289
|
-
|
|
304
|
+
if model and not isinstance(model, JsonSerializable):
|
|
305
|
+
raise TypeError("The model parameter must be a JsonSerializable class.")
|
|
306
|
+
|
|
307
|
+
return QueueManager(name, self._conn, model)
|
|
290
308
|
|
|
291
309
|
def collection(self, name: str) -> CollectionManager:
|
|
292
310
|
"""
|
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
|
-
import time
|
|
4
|
-
from typing import Any, Iterator, Tuple
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Iterator, Tuple, Type
|
|
5
5
|
|
|
6
|
+
from .types import JsonSerializable
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
class DictManager[T]:
|
|
8
10
|
"""A wrapper providing a Pythonic interface to a dictionary in the database."""
|
|
9
11
|
|
|
10
|
-
def __init__(self, name: str, conn: sqlite3.Connection):
|
|
12
|
+
def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
|
|
11
13
|
self._name = name
|
|
12
14
|
self._conn = conn
|
|
15
|
+
self._model = model
|
|
16
|
+
|
|
17
|
+
def _serialize(self, value: T) -> str:
|
|
18
|
+
"""Serializes the given value to a JSON string."""
|
|
19
|
+
if isinstance(value, JsonSerializable):
|
|
20
|
+
return value.model_dump_json()
|
|
21
|
+
|
|
22
|
+
return json.dumps(value)
|
|
23
|
+
|
|
24
|
+
def _deserialize(self, value: str) -> T:
|
|
25
|
+
"""Deserializes a JSON string into the specified model or a generic object."""
|
|
26
|
+
if self._model:
|
|
27
|
+
return self._model.model_validate_json(value)
|
|
28
|
+
|
|
29
|
+
return json.loads(value)
|
|
13
30
|
|
|
14
|
-
def set(self, key: str, value:
|
|
31
|
+
def set(self, key: str, value: T, ttl_seconds: int | None = None):
|
|
15
32
|
"""Sets a value for a key, with an optional TTL."""
|
|
16
33
|
self.__setitem__(key, value, ttl_seconds=ttl_seconds)
|
|
17
34
|
|
|
18
|
-
def __setitem__(self, key: str, value:
|
|
35
|
+
def __setitem__(self, key: str, value: T, ttl_seconds: int | None = None):
|
|
19
36
|
"""Sets a value for a key (e.g., `my_dict[key] = value`)."""
|
|
20
37
|
expires_at = None
|
|
21
38
|
if ttl_seconds is not None:
|
|
@@ -26,17 +43,17 @@ class DictManager:
|
|
|
26
43
|
with self._conn:
|
|
27
44
|
self._conn.execute(
|
|
28
45
|
"INSERT OR REPLACE INTO beaver_dicts (dict_name, key, value, expires_at) VALUES (?, ?, ?, ?)",
|
|
29
|
-
(self._name, key,
|
|
46
|
+
(self._name, key, self._serialize(value), expires_at),
|
|
30
47
|
)
|
|
31
48
|
|
|
32
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
49
|
+
def get(self, key: str, default: Any = None) -> T | Any:
|
|
33
50
|
"""Gets a value for a key, with a default if it doesn't exist or is expired."""
|
|
34
51
|
try:
|
|
35
52
|
return self[key]
|
|
36
53
|
except KeyError:
|
|
37
54
|
return default
|
|
38
55
|
|
|
39
|
-
def __getitem__(self, key: str) ->
|
|
56
|
+
def __getitem__(self, key: str) -> T:
|
|
40
57
|
"""Retrieves a value for a given key, raising KeyError if expired."""
|
|
41
58
|
cursor = self._conn.cursor()
|
|
42
59
|
cursor.execute(
|
|
@@ -53,20 +70,20 @@ class DictManager:
|
|
|
53
70
|
|
|
54
71
|
if expires_at is not None and time.time() > expires_at:
|
|
55
72
|
# Expired: delete the key and raise KeyError
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
with self._conn:
|
|
74
|
+
cursor.execute(
|
|
75
|
+
"DELETE FROM beaver_dicts WHERE dict_name = ? AND key = ?",
|
|
76
|
+
(self._name, key),
|
|
77
|
+
)
|
|
61
78
|
cursor.close()
|
|
62
79
|
raise KeyError(
|
|
63
80
|
f"Key '{key}' not found in dictionary '{self._name}' (expired)"
|
|
64
81
|
)
|
|
65
82
|
|
|
66
83
|
cursor.close()
|
|
67
|
-
return
|
|
84
|
+
return self._deserialize(value)
|
|
68
85
|
|
|
69
|
-
def pop(self, key: str, default: Any = None):
|
|
86
|
+
def pop(self, key: str, default: Any = None) -> T | Any:
|
|
70
87
|
"""Deletes an item if it exists and returns its value."""
|
|
71
88
|
try:
|
|
72
89
|
value = self[key]
|
|
@@ -110,25 +127,25 @@ class DictManager:
|
|
|
110
127
|
yield row["key"]
|
|
111
128
|
cursor.close()
|
|
112
129
|
|
|
113
|
-
def values(self) -> Iterator[
|
|
130
|
+
def values(self) -> Iterator[T]:
|
|
114
131
|
"""Returns an iterator over the dictionary's values."""
|
|
115
132
|
cursor = self._conn.cursor()
|
|
116
133
|
cursor.execute(
|
|
117
134
|
"SELECT value FROM beaver_dicts WHERE dict_name = ?", (self._name,)
|
|
118
135
|
)
|
|
119
136
|
for row in cursor:
|
|
120
|
-
yield
|
|
137
|
+
yield self._deserialize(row["value"])
|
|
121
138
|
cursor.close()
|
|
122
139
|
|
|
123
|
-
def items(self) -> Iterator[Tuple[str,
|
|
140
|
+
def items(self) -> Iterator[Tuple[str, T]]:
|
|
124
141
|
"""Returns an iterator over the dictionary's items (key-value pairs)."""
|
|
125
142
|
cursor = self._conn.cursor()
|
|
126
143
|
cursor.execute(
|
|
127
144
|
"SELECT key, value FROM beaver_dicts WHERE dict_name = ?", (self._name,)
|
|
128
145
|
)
|
|
129
146
|
for row in cursor:
|
|
130
|
-
yield (row["key"],
|
|
147
|
+
yield (row["key"], self._deserialize(row["value"]))
|
|
131
148
|
cursor.close()
|
|
132
149
|
|
|
133
150
|
def __repr__(self) -> str:
|
|
134
|
-
return f"
|
|
151
|
+
return f"DictManager(name='{self._name}')"
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Iterator, Type, Union
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
from .types import JsonSerializable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ListManager[T]:
|
|
6
9
|
"""A wrapper providing a Pythonic, full-featured interface to a list in the database."""
|
|
7
10
|
|
|
8
|
-
def __init__(self, name: str, conn: sqlite3.Connection):
|
|
11
|
+
def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
|
|
9
12
|
self._name = name
|
|
10
13
|
self._conn = conn
|
|
14
|
+
self._model = model
|
|
15
|
+
|
|
16
|
+
def _serialize(self, value: T) -> str:
|
|
17
|
+
"""Serializes the given value to a JSON string."""
|
|
18
|
+
if isinstance(value, JsonSerializable):
|
|
19
|
+
return value.model_dump_json()
|
|
20
|
+
return json.dumps(value)
|
|
21
|
+
|
|
22
|
+
def _deserialize(self, value: str) -> T:
|
|
23
|
+
"""Deserializes a JSON string into the specified model or a generic object."""
|
|
24
|
+
if self._model:
|
|
25
|
+
return self._model.model_validate_json(value)
|
|
26
|
+
return json.loads(value)
|
|
11
27
|
|
|
12
28
|
def __len__(self) -> int:
|
|
13
29
|
"""Returns the number of items in the list (e.g., `len(my_list)`)."""
|
|
@@ -19,7 +35,7 @@ class ListManager:
|
|
|
19
35
|
cursor.close()
|
|
20
36
|
return count
|
|
21
37
|
|
|
22
|
-
def __getitem__(self, key: Union[int, slice]) ->
|
|
38
|
+
def __getitem__(self, key: Union[int, slice]) -> T | list[T]:
|
|
23
39
|
"""
|
|
24
40
|
Retrieves an item or slice from the list (e.g., `my_list[0]`, `my_list[1:3]`).
|
|
25
41
|
"""
|
|
@@ -37,7 +53,7 @@ class ListManager:
|
|
|
37
53
|
"SELECT item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT ? OFFSET ?",
|
|
38
54
|
(self._name, limit, start),
|
|
39
55
|
)
|
|
40
|
-
results = [
|
|
56
|
+
results = [self._deserialize(row["item_value"]) for row in cursor.fetchall()]
|
|
41
57
|
cursor.close()
|
|
42
58
|
return results
|
|
43
59
|
|
|
@@ -55,12 +71,12 @@ class ListManager:
|
|
|
55
71
|
)
|
|
56
72
|
result = cursor.fetchone()
|
|
57
73
|
cursor.close()
|
|
58
|
-
return
|
|
74
|
+
return self._deserialize(result["item_value"])
|
|
59
75
|
|
|
60
76
|
else:
|
|
61
77
|
raise TypeError("List indices must be integers or slices.")
|
|
62
78
|
|
|
63
|
-
def __setitem__(self, key: int, value:
|
|
79
|
+
def __setitem__(self, key: int, value: T):
|
|
64
80
|
"""Sets the value of an item at a specific index (e.g., `my_list[0] = 'new'`)."""
|
|
65
81
|
if not isinstance(key, int):
|
|
66
82
|
raise TypeError("List indices must be integers.")
|
|
@@ -86,7 +102,7 @@ class ListManager:
|
|
|
86
102
|
# Update the value for that specific row
|
|
87
103
|
cursor.execute(
|
|
88
104
|
"UPDATE beaver_lists SET item_value = ? WHERE rowid = ?",
|
|
89
|
-
(
|
|
105
|
+
(self._serialize(value), rowid_to_update)
|
|
90
106
|
)
|
|
91
107
|
|
|
92
108
|
def __delitem__(self, key: int):
|
|
@@ -115,7 +131,7 @@ class ListManager:
|
|
|
115
131
|
# Delete that specific row
|
|
116
132
|
cursor.execute("DELETE FROM beaver_lists WHERE rowid = ?", (rowid_to_delete,))
|
|
117
133
|
|
|
118
|
-
def __iter__(self) -> Iterator[
|
|
134
|
+
def __iter__(self) -> Iterator[T]:
|
|
119
135
|
"""Returns an iterator for the list."""
|
|
120
136
|
cursor = self._conn.cursor()
|
|
121
137
|
cursor.execute(
|
|
@@ -123,15 +139,15 @@ class ListManager:
|
|
|
123
139
|
(self._name,)
|
|
124
140
|
)
|
|
125
141
|
for row in cursor:
|
|
126
|
-
yield
|
|
142
|
+
yield self._deserialize(row['item_value'])
|
|
127
143
|
cursor.close()
|
|
128
144
|
|
|
129
|
-
def __contains__(self, value:
|
|
145
|
+
def __contains__(self, value: T) -> bool:
|
|
130
146
|
"""Checks for the existence of an item in the list (e.g., `'item' in my_list`)."""
|
|
131
147
|
cursor = self._conn.cursor()
|
|
132
148
|
cursor.execute(
|
|
133
149
|
"SELECT 1 FROM beaver_lists WHERE list_name = ? AND item_value = ? LIMIT 1",
|
|
134
|
-
(self._name,
|
|
150
|
+
(self._name, self._serialize(value))
|
|
135
151
|
)
|
|
136
152
|
result = cursor.fetchone()
|
|
137
153
|
cursor.close()
|
|
@@ -139,7 +155,7 @@ class ListManager:
|
|
|
139
155
|
|
|
140
156
|
def __repr__(self) -> str:
|
|
141
157
|
"""Returns a developer-friendly representation of the object."""
|
|
142
|
-
return f"
|
|
158
|
+
return f"ListManager(name='{self._name}')"
|
|
143
159
|
|
|
144
160
|
def _get_order_at_index(self, index: int) -> float:
|
|
145
161
|
"""Helper to get the float `item_order` at a specific index."""
|
|
@@ -156,7 +172,7 @@ class ListManager:
|
|
|
156
172
|
|
|
157
173
|
raise IndexError(f"{index} out of range.")
|
|
158
174
|
|
|
159
|
-
def push(self, value:
|
|
175
|
+
def push(self, value: T):
|
|
160
176
|
"""Pushes an item to the end of the list."""
|
|
161
177
|
with self._conn:
|
|
162
178
|
cursor = self._conn.cursor()
|
|
@@ -169,10 +185,10 @@ class ListManager:
|
|
|
169
185
|
|
|
170
186
|
cursor.execute(
|
|
171
187
|
"INSERT INTO beaver_lists (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
172
|
-
(self._name, new_order,
|
|
188
|
+
(self._name, new_order, self._serialize(value)),
|
|
173
189
|
)
|
|
174
190
|
|
|
175
|
-
def prepend(self, value:
|
|
191
|
+
def prepend(self, value: T):
|
|
176
192
|
"""Prepends an item to the beginning of the list."""
|
|
177
193
|
with self._conn:
|
|
178
194
|
cursor = self._conn.cursor()
|
|
@@ -185,10 +201,10 @@ class ListManager:
|
|
|
185
201
|
|
|
186
202
|
cursor.execute(
|
|
187
203
|
"INSERT INTO beaver_lists (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
188
|
-
(self._name, new_order,
|
|
204
|
+
(self._name, new_order, self._serialize(value)),
|
|
189
205
|
)
|
|
190
206
|
|
|
191
|
-
def insert(self, index: int, value:
|
|
207
|
+
def insert(self, index: int, value: T):
|
|
192
208
|
"""Inserts an item at a specific index."""
|
|
193
209
|
list_len = len(self)
|
|
194
210
|
if index <= 0:
|
|
@@ -206,10 +222,10 @@ class ListManager:
|
|
|
206
222
|
with self._conn:
|
|
207
223
|
self._conn.execute(
|
|
208
224
|
"INSERT INTO beaver_lists (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
209
|
-
(self._name, new_order,
|
|
225
|
+
(self._name, new_order, self._serialize(value)),
|
|
210
226
|
)
|
|
211
227
|
|
|
212
|
-
def pop(self) ->
|
|
228
|
+
def pop(self) -> T | None:
|
|
213
229
|
"""Removes and returns the last item from the list."""
|
|
214
230
|
with self._conn:
|
|
215
231
|
cursor = self._conn.cursor()
|
|
@@ -225,9 +241,9 @@ class ListManager:
|
|
|
225
241
|
cursor.execute(
|
|
226
242
|
"DELETE FROM beaver_lists WHERE rowid = ?", (rowid_to_delete,)
|
|
227
243
|
)
|
|
228
|
-
return
|
|
244
|
+
return self._deserialize(value_to_return)
|
|
229
245
|
|
|
230
|
-
def deque(self) ->
|
|
246
|
+
def deque(self) -> T | None:
|
|
231
247
|
"""Removes and returns the first item from the list."""
|
|
232
248
|
with self._conn:
|
|
233
249
|
cursor = self._conn.cursor()
|
|
@@ -243,4 +259,4 @@ class ListManager:
|
|
|
243
259
|
cursor.execute(
|
|
244
260
|
"DELETE FROM beaver_lists WHERE rowid = ?", (rowid_to_delete,)
|
|
245
261
|
)
|
|
246
|
-
return
|
|
262
|
+
return self._deserialize(value_to_return)
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any, NamedTuple
|
|
4
|
+
from typing import Any, Literal, NamedTuple, Type, overload
|
|
5
5
|
|
|
6
|
+
from .types import JsonSerializable
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
class QueueItem[T](NamedTuple):
|
|
8
10
|
"""A data class representing a single item retrieved from the queue."""
|
|
9
11
|
|
|
10
12
|
priority: float
|
|
11
13
|
timestamp: float
|
|
12
|
-
data:
|
|
14
|
+
data: T
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
class QueueManager:
|
|
17
|
+
class QueueManager[T]:
|
|
16
18
|
"""A wrapper providing a Pythonic interface to a persistent priority queue."""
|
|
17
19
|
|
|
18
|
-
def __init__(self, name: str, conn: sqlite3.Connection):
|
|
20
|
+
def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
|
|
19
21
|
self._name = name
|
|
20
22
|
self._conn = conn
|
|
23
|
+
self._model = model
|
|
24
|
+
|
|
25
|
+
def _serialize(self, value: T) -> str:
|
|
26
|
+
"""Serializes the given value to a JSON string."""
|
|
27
|
+
if isinstance(value, JsonSerializable):
|
|
28
|
+
return value.model_dump_json()
|
|
29
|
+
|
|
30
|
+
return json.dumps(value)
|
|
31
|
+
|
|
32
|
+
def _deserialize(self, value: str) -> T:
|
|
33
|
+
"""Deserializes a JSON string into the specified model or a generic object."""
|
|
34
|
+
if self._model is not None:
|
|
35
|
+
return self._model.model_validate_json(value) # type: ignore
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
return json.loads(value)
|
|
38
|
+
|
|
39
|
+
def put(self, data: T, priority: float):
|
|
23
40
|
"""
|
|
24
41
|
Adds an item to the queue with a specific priority.
|
|
25
42
|
|
|
@@ -30,17 +47,18 @@ class QueueManager:
|
|
|
30
47
|
with self._conn:
|
|
31
48
|
self._conn.execute(
|
|
32
49
|
"INSERT INTO beaver_priority_queues (queue_name, priority, timestamp, data) VALUES (?, ?, ?, ?)",
|
|
33
|
-
(self._name, priority, time.time(),
|
|
50
|
+
(self._name, priority, time.time(), self._serialize(data)),
|
|
34
51
|
)
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
@overload
|
|
54
|
+
def get(self, safe:Literal[True]) -> QueueItem[T] | None: ...
|
|
55
|
+
@overload
|
|
56
|
+
def get(self) -> QueueItem[T]: ...
|
|
57
|
+
|
|
58
|
+
def get(self, safe:bool=False) -> QueueItem[T] | None:
|
|
37
59
|
"""
|
|
38
60
|
Atomically retrieves and removes the highest-priority item from the queue.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
A QueueItem containing the data and its metadata.
|
|
42
|
-
|
|
43
|
-
Raises IndexError if queue is empty.
|
|
61
|
+
If the queue is empty, returns None if safe is True, otherwise (the default) raises IndexError.
|
|
44
62
|
"""
|
|
45
63
|
with self._conn:
|
|
46
64
|
cursor = self._conn.cursor()
|
|
@@ -58,14 +76,17 @@ class QueueManager:
|
|
|
58
76
|
result = cursor.fetchone()
|
|
59
77
|
|
|
60
78
|
if result is None:
|
|
61
|
-
|
|
79
|
+
if safe:
|
|
80
|
+
return None
|
|
81
|
+
else:
|
|
82
|
+
raise IndexError("No item available.")
|
|
62
83
|
|
|
63
84
|
rowid, priority, timestamp, data = result
|
|
64
85
|
# Delete the retrieved item to ensure it's processed only once.
|
|
65
86
|
cursor.execute("DELETE FROM beaver_priority_queues WHERE rowid = ?", (rowid,))
|
|
66
87
|
|
|
67
88
|
return QueueItem(
|
|
68
|
-
priority=priority, timestamp=timestamp, data=
|
|
89
|
+
priority=priority, timestamp=timestamp, data=self._deserialize(data)
|
|
69
90
|
)
|
|
70
91
|
|
|
71
92
|
def __len__(self) -> int:
|
|
@@ -79,7 +100,7 @@ class QueueManager:
|
|
|
79
100
|
cursor.close()
|
|
80
101
|
return count
|
|
81
102
|
|
|
82
|
-
def
|
|
103
|
+
def __bool__(self) -> bool:
|
|
83
104
|
"""Returns True if the queue is not empty."""
|
|
84
105
|
return len(self) > 0
|
|
85
106
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Protocol, Type, runtime_checkable, Self
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@runtime_checkable
|
|
6
|
+
class JsonSerializable[T](Protocol):
|
|
7
|
+
"""
|
|
8
|
+
A protocol for objects that can be serialized to and from JSON,
|
|
9
|
+
compatible with Pydantic's `BaseModel`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def model_dump_json(self) -> str:
|
|
13
|
+
"""Serializes the model to a JSON string."""
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def model_validate_json(cls: Type[T], json_data: str | bytes) -> T:
|
|
18
|
+
"""Deserializes a JSON string into a model instance."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Model:
|
|
23
|
+
"""A lightweight base model that automatically provides JSON serialization."""
|
|
24
|
+
def __init__(self, **kwargs) -> None:
|
|
25
|
+
for k,v in kwargs.items():
|
|
26
|
+
setattr(self, k, v)
|
|
27
|
+
|
|
28
|
+
def model_dump_json(self) -> str:
|
|
29
|
+
"""Serializes the dataclass instance to a JSON string."""
|
|
30
|
+
return json.dumps(self.__dict__)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def model_validate_json(cls, json_data: str | bytes) -> Self:
|
|
34
|
+
"""Deserializes a JSON string into a new instance of the dataclass."""
|
|
35
|
+
return cls(**json.loads(json_data))
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
attrs = ", ".join(f"{k}={repr(v)}" for k,v in self.__dict__.items())
|
|
39
|
+
return f"{self.__class__.__name__}({attrs})"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beaver-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Fast, embedded, and multi-modal DB based on SQLite for AI-powered applications.
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -219,6 +219,39 @@ attachments.put(
|
|
|
219
219
|
avatar = attachments.get("user_123_avatar.png")
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
|
|
223
|
+
## Type-Safe Data Models
|
|
224
|
+
|
|
225
|
+
For enhanced data integrity and a better developer experience, BeaverDB supports type-safe operations for dictionaries, lists, and queues. By associating a model with these data structures, you get automatic serialization and deserialization, complete with autocompletion in your editor.
|
|
226
|
+
|
|
227
|
+
This feature is designed to be flexible and works seamlessly with two kinds of models:
|
|
228
|
+
|
|
229
|
+
* **Pydantic Models**: If you're already using Pydantic, your `BaseModel` classes will work out of the box.
|
|
230
|
+
* **Lightweight `beaver.Model`**: For a zero-dependency solution, you can inherit from the built-in `beaver.Model` class, which is a standard Python class with serialization methods automatically included.
|
|
231
|
+
|
|
232
|
+
Here’s a quick example of how to use it:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from beaver import BeaverDB, Model
|
|
236
|
+
|
|
237
|
+
# Inherit from beaver.Model for a lightweight, dependency-free model
|
|
238
|
+
class User(Model):
|
|
239
|
+
name: str
|
|
240
|
+
email: str
|
|
241
|
+
|
|
242
|
+
db = BeaverDB("user_data.db")
|
|
243
|
+
|
|
244
|
+
# Associate the User model with a dictionary
|
|
245
|
+
users = db.dict("user_profiles", model=User)
|
|
246
|
+
|
|
247
|
+
# BeaverDB now handles serialization automatically
|
|
248
|
+
users["alice"] = User(name="Alice", email="alice@example.com")
|
|
249
|
+
|
|
250
|
+
# The retrieved object is a proper instance of the User class
|
|
251
|
+
retrieved_user = users["alice"]
|
|
252
|
+
print(f"Retrieved: {retrieved_user.name}") # Your editor will provide autocompletion here
|
|
253
|
+
```
|
|
254
|
+
|
|
222
255
|
## More Examples
|
|
223
256
|
|
|
224
257
|
For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
@@ -241,13 +274,17 @@ For more in-depth examples, check out the scripts in the `examples/` directory:
|
|
|
241
274
|
|
|
242
275
|
## Roadmap
|
|
243
276
|
|
|
277
|
+
`beaver` is roughly feature-complete, but there are still some features and improvements planned for future releases, mostly directed to improving developer experience.
|
|
278
|
+
|
|
244
279
|
These are some of the features and improvements planned for future releases:
|
|
245
280
|
|
|
246
|
-
- **
|
|
247
|
-
- **Type Hints**:
|
|
281
|
+
- **Async API**: Extend the async support with on-demand wrappers for all features besides channels.
|
|
282
|
+
- **Type Hints**: Extend type hints for channels and documents.
|
|
248
283
|
|
|
249
284
|
Check out the [roadmap](roadmap.md) for a detailed list of upcoming features and design ideas.
|
|
250
285
|
|
|
286
|
+
If you think of something that would make `beaver` more useful for your use case, please open an issue and/or submit a pull request.
|
|
287
|
+
|
|
251
288
|
## License
|
|
252
289
|
|
|
253
290
|
This project is licensed under the MIT License.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|