beaver-db 0.19.3__py3-none-any.whl → 0.20.2__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.

Potentially problematic release.


This version of beaver-db might be problematic. Click here for more details.

beaver/__init__.py CHANGED
@@ -2,4 +2,4 @@ from .core import BeaverDB
2
2
  from .types import Model
3
3
  from .collections import Document, WalkDirection
4
4
 
5
- __version__ = "0.19.3"
5
+ __version__ = "0.20.2"
beaver/blobs.py CHANGED
@@ -1,6 +1,8 @@
1
+ import base64
2
+ from datetime import datetime, timezone
1
3
  import json
2
4
  import sqlite3
3
- from typing import Any, Dict, Iterator, NamedTuple, Optional, Type, TypeVar
5
+ from typing import IO, Any, Dict, Iterator, NamedTuple, Optional, Type, TypeVar
4
6
  from .types import JsonSerializable, IDatabase
5
7
  from .locks import LockManager
6
8
 
@@ -170,3 +172,60 @@ class BlobManager[M]:
170
172
  def __exit__(self, exc_type, exc_val, exc_tb):
171
173
  """Releases the lock when exiting a 'with' statement."""
172
174
  self.release()
175
+
176
+ def _get_dump_object(self) -> dict:
177
+ """Builds the JSON-compatible dump object."""
178
+
179
+ items_list = []
180
+ # __iter__ yields keys, so we get each blob
181
+ for key in self:
182
+ blob = self.get(key)
183
+ if blob:
184
+ metadata = blob.metadata
185
+
186
+ # Handle model instances in metadata
187
+ if self._model and isinstance(metadata, JsonSerializable):
188
+ metadata = json.loads(metadata.model_dump_json())
189
+
190
+ # Encode binary data to a base64 string
191
+ data_b64 = base64.b64encode(blob.data).decode('utf-8')
192
+
193
+ items_list.append({
194
+ "key": blob.key,
195
+ "metadata": metadata,
196
+ "data_b64": data_b64
197
+ })
198
+
199
+ metadata = {
200
+ "type": "BlobStore",
201
+ "name": self._name,
202
+ "count": len(items_list),
203
+ "dump_date": datetime.now(timezone.utc).isoformat()
204
+ }
205
+
206
+ return {
207
+ "metadata": metadata,
208
+ "items": items_list
209
+ }
210
+
211
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
212
+ """
213
+ Dumps the entire contents of the blob store to a JSON-compatible
214
+ Python object or a file-like object.
215
+
216
+ Args:
217
+ fp: A file-like object opened in text mode (e.g., with 'w').
218
+ If provided, the JSON dump will be written to this file.
219
+ If None (default), the dump will be returned as a dictionary.
220
+
221
+ Returns:
222
+ A dictionary containing the dump if fp is None.
223
+ None if fp is provided.
224
+ """
225
+ dump_object = self._get_dump_object()
226
+
227
+ if fp:
228
+ json.dump(dump_object, fp, indent=2)
229
+ return None
230
+
231
+ return dump_object
beaver/client.py ADDED
@@ -0,0 +1,360 @@
1
+ """
2
+ This module implements the BeaverClient, a drop-in replacement for the
3
+ BeaverDB class that interacts with a remote BeaverDB server over HTTP.
4
+
5
+ This implementation relies on the 'httpx' library.
6
+ """
7
+
8
+ import threading
9
+ from typing import Generic, Type, TypeVar, Optional, Any
10
+ import httpx
11
+ from .types import JsonSerializable
12
+ from .collections import Document
13
+
14
+ # --- Base Remote Manager ---
15
+
16
+ class RemoteManager:
17
+ """Base class for all remote managers, holding the HTTP client."""
18
+ def __init__(
19
+ self,
20
+ client: httpx.Client,
21
+ name: str,
22
+ model: Type | None = None
23
+ ):
24
+ self._client = client
25
+ self._name = name
26
+ self._model = model
27
+ self._validate_model(model)
28
+
29
+ def _validate_model(self, model: Type | None):
30
+ """Helper to validate the model, mirroring BeaverDB.core"""
31
+ if model and not isinstance(model, JsonSerializable):
32
+ # This check might need to be refined if Pydantic isn't a direct dependency
33
+ pass
34
+
35
+ # --- Stub Remote Manager Implementations ---
36
+
37
+ class RemoteDictManager[T](RemoteManager):
38
+ """
39
+ Manages a remote dictionary. All methods will make HTTP requests.
40
+ (This is a skeleton implementation)
41
+ """
42
+ def __init__(
43
+ self,
44
+ client: httpx.Client,
45
+ name: str,
46
+ model: Type[T] | None = None
47
+ ):
48
+ super().__init__(client, name, model)
49
+ # Placeholder for manager-level locking, mirroring local implementation
50
+ self._lock = RemoteLockManager(client, f"__lock__dict__{name}", None)
51
+
52
+ def __setitem__(self, key: str, value: T):
53
+ raise NotImplementedError("This method will be implemented in a future milestone.")
54
+
55
+ def __getitem__(self, key: str) -> T:
56
+ raise NotImplementedError("This method will be implemented in a future milestone.")
57
+
58
+ # ... other dict methods (get, __delitem__, __len__, items, etc.) ...
59
+
60
+
61
+ class RemoteListManager[T](RemoteManager):
62
+ """
63
+ Manages a remote list. All methods will make HTTP requests.
64
+ (This is a skeleton implementation)
65
+ """
66
+ def __init__(
67
+ self,
68
+ client: httpx.Client,
69
+ name: str,
70
+ model: Type[T] | None = None
71
+ ):
72
+ super().__init__(client, name, model)
73
+ self._lock = RemoteLockManager(client, f"__lock__list__{name}", None)
74
+
75
+ def push(self, value: T):
76
+ raise NotImplementedError("This method will be implemented in a future milestone.")
77
+
78
+ # ... other list methods (pop, __getitem__, __setitem__, etc.) ...
79
+
80
+
81
+ class RemoteQueueManager[T](RemoteManager):
82
+ """
83
+ Manages a remote priority queue. All methods will make HTTP requests.
84
+ (This is a skeleton implementation)
85
+ """
86
+ def __init__(
87
+ self,
88
+ client: httpx.Client,
89
+ name: str,
90
+ model: Type[T] | None = None
91
+ ):
92
+ super().__init__(client, name, model)
93
+ self._lock = RemoteLockManager(client, f"__lock__queue__{name}", None)
94
+
95
+ def put(self, data: T, priority: float):
96
+ raise NotImplementedError("This method will be implemented in a future milestone.")
97
+
98
+ def get(self, block: bool = True, timeout: float | None = None):
99
+ raise NotImplementedError("This method will be implemented in a future milestone.")
100
+
101
+ # ... other queue methods (peek, __len__, etc.) ...
102
+
103
+
104
+ class RemoteCollectionManager[D](RemoteManager):
105
+ """
106
+ Manages a remote collection. All methods will make HTTP requests.
107
+ (This is a skeleton implementation)
108
+ """
109
+ def __init__(
110
+ self,
111
+ client: httpx.Client,
112
+ name: str,
113
+ model: Type[D] | None = None
114
+ ):
115
+ super().__init__(client, name, model)
116
+ self._lock = RemoteLockManager(client, f"__lock__collection__{name}", None)
117
+
118
+ def index(self, document: D, *, fts: bool | list[str] = True, fuzzy: bool = False):
119
+ raise NotImplementedError("This method will be implemented in a future milestone.")
120
+
121
+ def search(self, vector: list[float], top_k: int = 10):
122
+ raise NotImplementedError("This method will be implemented in a future milestone.")
123
+
124
+ # ... other collection methods (drop, match, connect, walk, etc.) ...
125
+
126
+
127
+ class RemoteChannelManager[T](RemoteManager):
128
+ """
129
+ Manages a remote pub/sub channel.
130
+ (This is a skeleton implementation)
131
+ """
132
+ def publish(self, payload: T):
133
+ raise NotImplementedError("This method will be implemented in a future milestone.")
134
+
135
+ def subscribe(self):
136
+ raise NotImplementedError("This method will be implemented in a future milestone.")
137
+
138
+
139
+ class RemoteBlobManager[M](RemoteManager):
140
+ """
141
+ Manages a remote blob store. All methods will make HTTP requests.
142
+ (This is a skeleton implementation)
143
+ """
144
+ def __init__(
145
+ self,
146
+ client: httpx.Client,
147
+ name: str,
148
+ model: Type[M] | None = None
149
+ ):
150
+ super().__init__(client, name, model)
151
+ self._lock = RemoteLockManager(client, f"__lock__blob__{name}", None)
152
+
153
+ def put(self, key: str, data: bytes, metadata: Optional[M] = None):
154
+ raise NotImplementedError("This method will be implemented in a future milestone.")
155
+
156
+ def get(self, key: str):
157
+ raise NotImplementedError("This method will be implemented in a future milestone.")
158
+
159
+ # ... other blob methods (delete, __len__, etc.) ...
160
+
161
+
162
+ class RemoteLogManager[T](RemoteManager):
163
+ """
164
+ Manages a remote time-indexed log.
165
+ (This is a skeleton implementation)
166
+ """
167
+ def log(self, data: T, timestamp: Any | None = None): # Using Any for datetime
168
+ raise NotImplementedError("This method will be implemented in a future milestone.")
169
+
170
+ def range(self, start: Any, end: Any): # Using Any for datetime
171
+ raise NotImplementedError("This method will be implemented in a future milestone.")
172
+
173
+ def live(self, window: Any, period: Any, aggregator: Any): # Using Any for timedelta/callable
174
+ raise NotImplementedError("This method will be implemented in a future milestone.")
175
+
176
+
177
+ class RemoteLockManager(RemoteManager):
178
+ """
179
+ Manages a remote inter-process lock.
180
+ (This is a skeleton implementation)
181
+ """
182
+ def __init__(
183
+ self,
184
+ client: httpx.Client,
185
+ name: str,
186
+ model: Type | None = None,
187
+ timeout: float | None = None,
188
+ lock_ttl: float = 60.0,
189
+ poll_interval: float = 0.1,
190
+ ):
191
+ super().__init__(client, name, model)
192
+ self._timeout = timeout
193
+ self._lock_ttl = lock_ttl
194
+ self._poll_interval = poll_interval
195
+
196
+ def acquire(self):
197
+ raise NotImplementedError("This method will be implemented in a future milestone.")
198
+
199
+ def release(self):
200
+ raise NotImplementedError("This method will be implemented in a future milestone.")
201
+
202
+ def __enter__(self):
203
+ self.acquire()
204
+ return self
205
+
206
+ def __exit__(self, exc_type, exc_val, exc_tb):
207
+ self.release()
208
+
209
+
210
+ # --- The Main Client Class ---
211
+
212
+ class BeaverClient:
213
+ """
214
+ A drop-in client for a remote BeaverDB server.
215
+
216
+ This class provides the same factory methods as the local BeaverDB class,
217
+ but all operations are performed over HTTP/WebSockets against a
218
+ server running 'beaver serve'.
219
+ """
220
+
221
+ def __init__(self, base_url: str, **httpx_args):
222
+ """
223
+ Initializes the client.
224
+
225
+ Args:
226
+ base_url: The base URL of the BeaverDB server (e.g., "http://127.0.0.1:8000").
227
+ **httpx_args: Additional keyword arguments to pass to the httpx.Client
228
+ (e.g., headers, timeouts).
229
+ """
230
+ self._client = httpx.Client(base_url=base_url, **httpx_args)
231
+
232
+ # Singleton managers for collections and channels, just like in BeaverDB.core
233
+ self._collections: dict[str, RemoteCollectionManager] = {}
234
+ self._collections_lock = threading.Lock()
235
+ self._channels: dict[str, RemoteChannelManager] = {}
236
+ self._channels_lock = threading.Lock()
237
+
238
+ def close(self):
239
+ """Closes the underlying HTTP client session."""
240
+ self._client.close()
241
+
242
+ def __enter__(self):
243
+ return self
244
+
245
+ def __exit__(self, exc_type, exc_val, exc_tb):
246
+ self.close()
247
+
248
+ def dict[T](self, name: str, model: type[T] | None = None) -> RemoteDictManager[T]:
249
+ """
250
+ Returns a wrapper for interacting with a remote named dictionary.
251
+ If model is defined, it should be a type used for automatic (de)serialization.
252
+ """
253
+ if not isinstance(name, str) or not name:
254
+ raise TypeError("Dictionary name must be a non-empty string.")
255
+
256
+ return RemoteDictManager(self._client, name, model)
257
+
258
+ def list[T](self, name: str, model: type[T] | None = None) -> RemoteListManager[T]:
259
+ """
260
+ Returns a wrapper for interacting with a remote named list.
261
+ If model is defined, it should be a type used for automatic (de)serialization.
262
+ """
263
+ if not isinstance(name, str) or not name:
264
+ raise TypeError("List name must be a non-empty string.")
265
+
266
+ return RemoteListManager(self._client, name, model)
267
+
268
+ def queue[T](self, name: str, model: type[T] | None = None) -> RemoteQueueManager[T]:
269
+ """
270
+ Returns a wrapper for interacting with a remote persistent priority queue.
271
+ If model is defined, it should be a type used for automatic (de)serialization.
272
+ """
273
+ if not isinstance(name, str) or not name:
274
+ raise TypeError("Queue name must be a non-empty string.")
275
+
276
+ return RemoteQueueManager(self._client, name, model)
277
+
278
+ def collection[D: Document](
279
+ self, name: str, model: Type[D] | None = None
280
+ ) -> RemoteCollectionManager[D]:
281
+ """
282
+ Returns a singleton wrapper for a remote document collection.
283
+ """
284
+ if not isinstance(name, str) or not name:
285
+ raise TypeError("Collection name must be a non-empty string.")
286
+
287
+ with self._collections_lock:
288
+ if name not in self._collections:
289
+ self._collections[name] = RemoteCollectionManager(self._client, name, model)
290
+ return self._collections[name] # type: ignore
291
+
292
+ def channel[T](self, name: str, model: type[T] | None = None) -> RemoteChannelManager[T]:
293
+ """
294
+ Returns a singleton wrapper for a remote pub/sub channel.
295
+ """
296
+ if not isinstance(name, str) or not name:
297
+ raise ValueError("Channel name must be a non-empty string.")
298
+
299
+ with self._channels_lock:
300
+ if name not in self._channels:
301
+ self._channels[name] = RemoteChannelManager(self._client, name, model)
302
+ return self._channels[name]
303
+
304
+ def blobs[M](self, name: str, model: type[M] | None = None) -> RemoteBlobManager[M]:
305
+ """Returns a wrapper for interacting with a remote blob store."""
306
+ if not isinstance(name, str) or not name:
307
+ raise TypeError("Blob store name must be a non-empty string.")
308
+
309
+ return RemoteBlobManager(self._client, name, model)
310
+
311
+ def log[T](self, name: str, model: type[T] | None = None) -> RemoteLogManager[T]:
312
+ """
313
+ Returns a wrapper for interacting with a remote, time-indexed log.
314
+ If model is defined, it should be a type used for automatic (de)serialization.
315
+ """
316
+ if not isinstance(name, str) or not name:
317
+ raise TypeError("Log name must be a non-empty string.")
318
+
319
+ return RemoteLogManager(self._client, name, model)
320
+
321
+ def lock(
322
+ self,
323
+ name: str,
324
+ timeout: float | None = None,
325
+ lock_ttl: float = 60.0,
326
+ poll_interval: float = 0.1,
327
+ ) -> RemoteLockManager:
328
+ """
329
+ Returns a wrapper for a remote inter-process lock.
330
+ """
331
+ return RemoteLockManager(
332
+ self._client,
333
+ name,
334
+ model=None,
335
+ timeout=timeout,
336
+ lock_ttl=lock_ttl,
337
+ poll_interval=poll_interval,
338
+ )
339
+
340
+ # --- Async API ---
341
+
342
+ def as_async(self) -> "AsyncBeaverClient":
343
+ """
344
+ Returns an async-compatible version of the client.
345
+ (This is a skeleton implementation)
346
+ """
347
+ raise NotImplementedError("AsyncBeaverClient will be implemented in a future milestone.")
348
+
349
+
350
+ class AsyncBeaverClient:
351
+ """
352
+ An async-compatible, drop-in client for a remote BeaverDB server.
353
+ (This is a skeleton implementation)
354
+ """
355
+ def __init__(self, base_url: str, **httpx_args):
356
+ self._client = httpx.AsyncClient(base_url=base_url, **httpx_args)
357
+ # ... async-compatible locks and manager caches ...
358
+
359
+ async def close(self):
360
+ await self._client.aclose()
beaver/collections.py CHANGED
@@ -1,9 +1,10 @@
1
+ from datetime import datetime, timezone
1
2
  import json
2
3
  import sqlite3
3
4
  import threading
4
5
  import uuid
5
6
  from enum import Enum
6
- from typing import Any, Iterator, List, Literal, Tuple, Type, TypeVar, Optional
7
+ from typing import IO, Any, Iterator, List, Literal, Tuple, Type, TypeVar, Optional
7
8
  from .types import Model, stub, IDatabase
8
9
  from .locks import LockManager
9
10
 
@@ -679,6 +680,47 @@ class CollectionManager[D: Document]:
679
680
  """Releases the lock when exiting a 'with' statement."""
680
681
  self.release()
681
682
 
683
+ def _get_dump_object(self) -> dict:
684
+ """Builds the JSON-compatible dump object."""
685
+
686
+ # The __iter__ method yields Document instances.
687
+ # doc.to_dict(metadata_only=False) correctly serializes
688
+ # the Document (including embedding) to a plain dictionary.
689
+ items_list = [doc.to_dict(metadata_only=False) for doc in self]
690
+
691
+ metadata = {
692
+ "type": "Collection",
693
+ "name": self._name,
694
+ "count": len(items_list),
695
+ "dump_date": datetime.now(timezone.utc).isoformat()
696
+ }
697
+
698
+ return {
699
+ "metadata": metadata,
700
+ "items": items_list
701
+ }
702
+
703
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
704
+ """
705
+ Dumps the entire contents of the collection to a JSON-compatible
706
+ Python object or a file-like object.
707
+
708
+ Args:
709
+ fp: A file-like object opened in text mode (e.g., with 'w').
710
+ If provided, the JSON dump will be written to this file.
711
+ If None (default), the dump will be returned as a dictionary.
712
+
713
+ Returns:
714
+ A dictionary containing the dump if fp is None.
715
+ None if fp is provided.
716
+ """
717
+ dump_object = self._get_dump_object()
718
+
719
+ if fp:
720
+ json.dump(dump_object, fp, indent=2)
721
+ return None
722
+
723
+ return dump_object
682
724
 
683
725
  def rerank[D: Document](
684
726
  *results: list[D], weights: list[float] | None = None, k: int = 60
beaver/core.py CHANGED
@@ -167,7 +167,7 @@ class BeaverDB:
167
167
  """Creates the table to store the serialized base ANN index."""
168
168
  self.connection.execute(
169
169
  """
170
- CREATE TABLE IF NOT EXISTS _beaver_ann_indexes (
170
+ CREATE TABLE IF NOT EXISTS beaver_ann_indexes (
171
171
  collection_name TEXT PRIMARY KEY,
172
172
  index_data BLOB,
173
173
  base_index_version INTEGER NOT NULL DEFAULT 0
@@ -179,7 +179,7 @@ class BeaverDB:
179
179
  """Creates the log for new vector additions."""
180
180
  self.connection.execute(
181
181
  """
182
- CREATE TABLE IF NOT EXISTS _beaver_ann_pending_log (
182
+ CREATE TABLE IF NOT EXISTS beaver_ann_pending_log (
183
183
  collection_name TEXT NOT NULL,
184
184
  str_id TEXT NOT NULL,
185
185
  PRIMARY KEY (collection_name, str_id)
@@ -191,7 +191,7 @@ class BeaverDB:
191
191
  """Creates the log for vector deletions (tombstones)."""
192
192
  self.connection.execute(
193
193
  """
194
- CREATE TABLE IF NOT EXISTS _beaver_ann_deletions_log (
194
+ CREATE TABLE IF NOT EXISTS beaver_ann_deletions_log (
195
195
  collection_name TEXT NOT NULL,
196
196
  int_id INTEGER NOT NULL,
197
197
  PRIMARY KEY (collection_name, int_id)
@@ -203,7 +203,7 @@ class BeaverDB:
203
203
  """Creates the table to map string IDs to integer IDs for Faiss."""
204
204
  self.connection.execute(
205
205
  """
206
- CREATE TABLE IF NOT EXISTS _beaver_ann_id_mapping (
206
+ CREATE TABLE IF NOT EXISTS beaver_ann_id_mapping (
207
207
  collection_name TEXT NOT NULL,
208
208
  str_id TEXT NOT NULL,
209
209
  int_id INTEGER PRIMARY KEY AUTOINCREMENT,
beaver/dicts.py CHANGED
@@ -1,7 +1,8 @@
1
+ from datetime import datetime, timezone
1
2
  import json
2
3
  import sqlite3
3
4
  import time
4
- from typing import Any, Iterator, Tuple, Type, Optional
5
+ from typing import IO, Any, Iterator, Tuple, Type, Optional
5
6
  from .types import JsonSerializable, IDatabase
6
7
  from .locks import LockManager
7
8
 
@@ -15,6 +16,55 @@ class DictManager[T]:
15
16
  lock_name = f"__lock__dict__{name}"
16
17
  self._lock = LockManager(db, lock_name)
17
18
 
19
+ def _get_dump_object(self) -> dict:
20
+ """Builds the JSON-compatible dump object."""
21
+ items = []
22
+
23
+ for k, v in self.items():
24
+ item_value = v
25
+ # Check if a model is defined and the value is a model instance
26
+ if self._model and isinstance(v, JsonSerializable):
27
+ # Use the model's serializer to get its string representation,
28
+ # then parse that string back into a dict.
29
+ # This ensures the dump contains serializable dicts, not model objects.
30
+ item_value = json.loads(v.model_dump_json())
31
+
32
+ items.append({"key": k, "value": item_value})
33
+
34
+ metadata = {
35
+ "type": "Dict",
36
+ "name": self._name,
37
+ "count": len(items),
38
+ "dump_date": datetime.now(timezone.utc).isoformat()
39
+ }
40
+
41
+ return {
42
+ "metadata": metadata,
43
+ "items": items
44
+ }
45
+
46
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
47
+ """
48
+ Dumps the entire contents of the dictionary to a JSON-compatible
49
+ Python object or a file-like object.
50
+
51
+ Args:
52
+ fp: A file-like object opened in text mode (e.g., with 'w').
53
+ If provided, the JSON dump will be written to this file.
54
+ If None (default), the dump will be returned as a dictionary.
55
+
56
+ Returns:
57
+ A dictionary containing the dump if fp is None.
58
+ None if fp is provided.
59
+ """
60
+ dump_object = self._get_dump_object()
61
+
62
+ if fp:
63
+ json.dump(dump_object, fp, indent=2)
64
+ return None
65
+
66
+ return dump_object
67
+
18
68
  def _serialize(self, value: T) -> str:
19
69
  """Serializes the given value to a JSON string."""
20
70
  if isinstance(value, JsonSerializable):
beaver/lists.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import json
2
2
  import sqlite3
3
- from typing import Any, Iterator, Type, Union, Optional
3
+ from typing import Any, Iterator, Type, Union, Optional, IO
4
+ from datetime import datetime, timezone
4
5
  from .types import JsonSerializable, IDatabase
5
6
  from .locks import LockManager
6
7
 
7
-
8
8
  class ListManager[T]:
9
9
  """A wrapper providing a Pythonic, full-featured interface to a list in the database."""
10
10
 
@@ -15,6 +15,53 @@ class ListManager[T]:
15
15
  lock_name = f"__lock__list__{name}"
16
16
  self._lock = LockManager(db, lock_name)
17
17
 
18
+ def _get_dump_object(self) -> dict:
19
+ """Builds the JSON-compatible dump object."""
20
+ items = []
21
+
22
+ for item in self:
23
+ item_value = item
24
+ # Check if a model is defined and the item is a model instance
25
+ if self._model and isinstance(item, JsonSerializable):
26
+ # Convert the model object to a serializable dict
27
+ item_value = json.loads(item.model_dump_json())
28
+
29
+ items.append(item_value)
30
+
31
+ metadata = {
32
+ "type": "List",
33
+ "name": self._name,
34
+ "count": len(items),
35
+ "dump_date": datetime.now(timezone.utc).isoformat()
36
+ }
37
+
38
+ return {
39
+ "metadata": metadata,
40
+ "items": items
41
+ }
42
+
43
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
44
+ """
45
+ Dumps the entire contents of the list to a JSON-compatible
46
+ Python object or a file-like object.
47
+
48
+ Args:
49
+ fp: A file-like object opened in text mode (e.g., with 'w').
50
+ If provided, the JSON dump will be written to this file.
51
+ If None (default), the dump will be returned as a dictionary.
52
+
53
+ Returns:
54
+ A dictionary containing the dump if fp is None.
55
+ None if fp is provided.
56
+ """
57
+ dump_object = self._get_dump_object()
58
+
59
+ if fp:
60
+ json.dump(dump_object, fp, indent=2)
61
+ return None
62
+
63
+ return dump_object
64
+
18
65
  def _serialize(self, value: T) -> str:
19
66
  """Serializes the given value to a JSON string."""
20
67
  if isinstance(value, JsonSerializable):
beaver/logs.py CHANGED
@@ -6,7 +6,7 @@ import threading
6
6
  import time
7
7
  from datetime import datetime, timedelta, timezone
8
8
  from queue import Empty, Queue
9
- from typing import Any, AsyncIterator, Callable, Iterator, Type, TypeVar
9
+ from typing import IO, Any, AsyncIterator, Callable, Iterator, Type, TypeVar
10
10
 
11
11
  from .types import JsonSerializable, IDatabase
12
12
 
@@ -15,7 +15,7 @@ from .types import JsonSerializable, IDatabase
15
15
  _SHUTDOWN_SENTINEL = object()
16
16
 
17
17
 
18
- class LiveIterator[T,R]:
18
+ class LiveIterator[T, R]:
19
19
  """
20
20
  A thread-safe, blocking iterator that yields aggregated results from a
21
21
  rolling window of log data.
@@ -119,10 +119,10 @@ class LiveIterator[T,R]:
119
119
  self._thread.join()
120
120
 
121
121
 
122
- class AsyncLiveIterator[T,R]:
122
+ class AsyncLiveIterator[T, R]:
123
123
  """An async wrapper for the LiveIterator."""
124
124
 
125
- def __init__(self, sync_iterator: LiveIterator[T,R]):
125
+ def __init__(self, sync_iterator: LiveIterator[T, R]):
126
126
  self._sync_iterator = sync_iterator
127
127
 
128
128
  async def __anext__(self) -> R:
@@ -162,9 +162,7 @@ class AsyncLogManager[T]:
162
162
  aggregator: Callable[[list[T]], R],
163
163
  ) -> AsyncIterator[R]:
164
164
  """Returns an async, infinite iterator for real-time log analysis."""
165
- sync_iterator = self._sync_manager.live(
166
- window, period, aggregator
167
- )
165
+ sync_iterator = self._sync_manager.live(window, period, aggregator)
168
166
  return AsyncLiveIterator(sync_iterator)
169
167
 
170
168
 
@@ -272,3 +270,70 @@ class LogManager[T]:
272
270
  def as_async(self) -> AsyncLogManager[T]:
273
271
  """Returns an async-compatible version of the log manager."""
274
272
  return AsyncLogManager(self)
273
+
274
+ def __iter__(self) -> Iterator[tuple[float, T]]:
275
+ """Returns an iterator over all log entries, in chronological order.
276
+
277
+ Yields:
278
+ A dictionary for each log entry with keys "timestamp" and "data".
279
+ The "data" is deserialized.
280
+ """
281
+ cursor = self._db.connection.cursor()
282
+ cursor.execute(
283
+ "SELECT timestamp, data FROM beaver_logs WHERE log_name = ? ORDER BY timestamp ASC",
284
+ (self._name,),
285
+ )
286
+ try:
287
+ for row in cursor:
288
+ yield (row["timestamp"], self._deserialize(row["data"]))
289
+ finally:
290
+ cursor.close()
291
+
292
+ def _get_dump_object(self) -> dict:
293
+ """Builds the JSON-compatible dump object."""
294
+
295
+ items_list = []
296
+ # Use the new __iter__ method
297
+ for timestamp, data in self:
298
+
299
+ # Handle model instances
300
+ if self._model and isinstance(data, JsonSerializable):
301
+ data = json.loads(data.model_dump_json())
302
+
303
+ items_list.append(
304
+ {
305
+ "timestamp": timestamp,
306
+ "data": data,
307
+ }
308
+ )
309
+
310
+ metadata = {
311
+ "type": "Log",
312
+ "name": self._name,
313
+ "count": len(items_list),
314
+ "dump_date": datetime.now(timezone.utc).isoformat(),
315
+ }
316
+
317
+ return {"metadata": metadata, "items": items_list}
318
+
319
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
320
+ """
321
+ Dumps the entire contents of the log to a JSON-compatible
322
+ Python object or a file-like object.
323
+
324
+ Args:
325
+ fp: A file-like object opened in text mode (e.g., with 'w').
326
+ If provided, the JSON dump will be written to this file.
327
+ If None (default), the dump will be returned as a dictionary.
328
+
329
+ Returns:
330
+ A dictionary containing the dump if fp is None.
331
+ None if fp is provided.
332
+ """
333
+ dump_object = self._get_dump_object()
334
+
335
+ if fp:
336
+ json.dump(dump_object, fp, indent=2)
337
+ return None
338
+
339
+ return dump_object
beaver/queues.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
+ from datetime import datetime, timezone
2
3
  import json
3
4
  import sqlite3
4
5
  import time
5
- from typing import Any, Literal, NamedTuple, Type, overload, Optional
6
+ from typing import IO, Any, Iterator, Literal, NamedTuple, Type, overload, Optional
6
7
  from .types import JsonSerializable, IDatabase
7
8
  from .locks import LockManager
8
9
 
@@ -219,3 +220,83 @@ class QueueManager[T]:
219
220
  def __exit__(self, exc_type, exc_val, exc_tb):
220
221
  """Releases the lock when exiting a 'with' statement."""
221
222
  self.release()
223
+
224
+ def __iter__(self) -> Iterator[QueueItem[T]]:
225
+ """
226
+ Returns an iterator over all items in the queue, in priority order,
227
+ without removing them.
228
+
229
+ Yields:
230
+ QueueItem: The next item in the queue (priority, timestamp, data).
231
+ """
232
+ cursor = self._db.connection.cursor()
233
+ cursor.execute(
234
+ """
235
+ SELECT priority, timestamp, data
236
+ FROM beaver_priority_queues
237
+ WHERE queue_name = ?
238
+ ORDER BY priority ASC, timestamp ASC
239
+ """,
240
+ (self._name,),
241
+ )
242
+ try:
243
+ for row in cursor:
244
+ yield QueueItem(
245
+ priority=row["priority"],
246
+ timestamp=row["timestamp"],
247
+ data=self._deserialize(row["data"])
248
+ )
249
+ finally:
250
+ cursor.close()
251
+
252
+ def _get_dump_object(self) -> dict:
253
+ """Builds the JSON-compatible dump object."""
254
+
255
+ items_list = []
256
+ # Use the new __iter__ method
257
+ for item in self:
258
+ data = item.data
259
+
260
+ # Handle model instances
261
+ if self._model and isinstance(data, JsonSerializable):
262
+ data = json.loads(data.model_dump_json())
263
+
264
+ items_list.append({
265
+ "priority": item.priority,
266
+ "timestamp": item.timestamp,
267
+ "data": data
268
+ })
269
+
270
+ metadata = {
271
+ "type": "Queue",
272
+ "name": self._name,
273
+ "count": len(items_list),
274
+ "dump_date": datetime.now(timezone.utc).isoformat()
275
+ }
276
+
277
+ return {
278
+ "metadata": metadata,
279
+ "items": items_list
280
+ }
281
+
282
+ def dump(self, fp: IO[str] | None = None) -> dict | None:
283
+ """
284
+ Dumps the entire contents of the queue to a JSON-compatible
285
+ Python object or a file-like object.
286
+
287
+ Args:
288
+ fp: A file-like object opened in text mode (e.g., with 'w').
289
+ If provided, the JSON dump will be written to this file.
290
+ If None (default), the dump will be returned as a dictionary.
291
+
292
+ Returns:
293
+ A dictionary containing the dump if fp is None.
294
+ None if fp is provided.
295
+ """
296
+ dump_object = self._get_dump_object()
297
+
298
+ if fp:
299
+ json.dump(dump_object, fp, indent=2)
300
+ return None
301
+
302
+ return dump_object
beaver/types.py CHANGED
@@ -20,6 +20,19 @@ class JsonSerializable[T](Protocol):
20
20
  ...
21
21
 
22
22
 
23
+ class _ModelEncoder(json.JSONEncoder):
24
+ """
25
+ Custom JSON encoder that recursively serializes Model instances.
26
+ """
27
+ def default(self, o):
28
+ if isinstance(o, Model):
29
+ # If the object is a Model instance, return its __dict__.
30
+ # The JSONEncoder will then recursively serialize this dict.
31
+ return o.__dict__
32
+ # Let the base class handle built-in types
33
+ return super().default(o)
34
+
35
+
23
36
  class Model:
24
37
  """A lightweight base model that automatically provides JSON serialization."""
25
38
  def __init__(self, **kwargs) -> None:
@@ -28,11 +41,13 @@ class Model:
28
41
 
29
42
  def model_dump_json(self) -> str:
30
43
  """Serializes the dataclass instance to a JSON string."""
31
- return json.dumps(self.__dict__)
44
+ # Use the custom _ModelEncoder to handle self and any nested Models
45
+ return json.dumps(self, cls=_ModelEncoder)
32
46
 
33
47
  @classmethod
34
48
  def model_validate_json(cls, json_data: str | bytes) -> Self:
35
49
  """Deserializes a JSON string into a new instance of the dataclass."""
50
+ # Note: This deserializes nested objects as dicts, not Model instances.
36
51
  return cls(**json.loads(json_data))
37
52
 
38
53
  def __repr__(self) -> str:
@@ -40,7 +55,6 @@ class Model:
40
55
  return f"{self.__class__.__name__}({attrs})"
41
56
 
42
57
 
43
-
44
58
  def stub(msg: str):
45
59
  class Stub:
46
60
  def __init__(self, *args, **kwargs) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaver-db
3
- Version: 0.19.3
3
+ Version: 0.20.2
4
4
  Summary: Fast, embedded, and multi-modal DB based on SQLite for AI-powered applications.
5
5
  License-File: LICENSE
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -71,6 +71,7 @@ Description-Content-Type: text/markdown
71
71
  - **Built-in REST API Server (Optional)**: Instantly serve your database over a RESTful API with automatic OpenAPI documentation using FastAPI.
72
72
  - **Full-Featured CLI Client (Optional)**: Interact with your database directly from the command line for administrative tasks and data exploration.
73
73
  - **Optional Type-Safety:** Although the database is schemaless, you can use a minimalistic typing system for automatic serialization and deserialization that is Pydantic-compatible out of the box.
74
+ - **Data Export & Backups:** Dump any dictionary, list, collection, queue, blob, or log structure to a portable JSON file with a single `.dump()` command.
74
75
 
75
76
  ## How Beaver is Implemented
76
77
 
@@ -203,6 +204,43 @@ beaver client --database data.db dict app_config get theme
203
204
  beaver client --database data.db list daily_tasks push "Review PRs"
204
205
  ```
205
206
 
207
+ ## Data Export for Backups
208
+
209
+ All data structures (`dict`, `list`, `collection`, `queue`, `log`, and `blobs`) support a `.dump()` method for easy backups and migration. You can either write the data directly to a JSON file or get it as a Python dictionary.
210
+
211
+ ```python
212
+ import json
213
+ from beaver import BeaverDB
214
+
215
+ db = BeaverDB("my_app.db")
216
+ config = db.dict("app_config")
217
+
218
+ # Add some data
219
+ config["theme"] = "dark"
220
+ config["user_id"] = 456
221
+
222
+ # Dump the dictionary's contents to a JSON file
223
+ with open("config_backup.json", "w") as f:
224
+ config.dump(f)
225
+
226
+ # 'config_backup.json' now contains:
227
+ # {
228
+ # "metadata": {
229
+ # "type": "Dict",
230
+ # "name": "app_config",
231
+ # "count": 2,
232
+ # "dump_date": "2025-11-02T09:05:10.123456Z"
233
+ # },
234
+ # "items": [
235
+ # {"key": "theme", "value": "dark"},
236
+ # {"key": "user_id", "value": 456}
237
+ # ]
238
+ # }
239
+
240
+ # You can also get the dump as a Python object
241
+ dump_data = config.dump()
242
+ ```
243
+
206
244
  ## Things You Can Build with Beaver
207
245
 
208
246
  Here are a few ideas to inspire your next project, showcasing how to combine Beaver's features to build powerful local applications.
@@ -446,9 +484,9 @@ For more in-depth examples, check out the scripts in the `examples/` directory:
446
484
  These are some of the features and improvements planned for future releases:
447
485
 
448
486
  - **[Issue #2](https://github.com/syalia-srl/beaver/issues/2) Comprehensive async wrappers**: Extend the async support with on-demand wrappers for all data structures, not just channels.
449
- - **[Issue #9](https://github.com/syalia-srl/beaver/issues/2) Type-safe wrappers based on Pydantic-compatible models**: Enhance the built-in `Model` to handle recursive and embedded types and provide Pydantic compatibility.
450
- - **[Issue #6](https://github.com/syalia-srl/beaver/issues/2) Drop-in replacement for Beaver REST server client**: Implement a `BeaverClient` class that acts as a drop-in replacement for `BeaverDB` but works against the REST API server.
451
- - **[Issue #7](https://github.com/syalia-srl/beaver/issues/2) Replace `faiss` with simpler, linear `numpy` vectorial search**: Investigate removing the heavy `faiss` dependency in favor of a pure `numpy` implementation to improve installation simplicity, accepting a trade-off in search performance for O(1) installation.
487
+ - **[Issue #6](https://github.com/syalia-srl/beaver/issues/6) Drop-in replacement for Beaver REST server client**: Implement a `BeaverClient` class that acts as a drop-in replacement for `BeaverDB` but works against the REST API server.
488
+ - **[Issue #7](https://github.com/syalia-srl/beaver/issues/7) Replace `faiss` with simpler, linear `numpy` vectorial search**: Investigate removing the heavy `faiss` dependency in favor of a pure `numpy` implementation to improve installation simplicity, accepting a trade-off in search performance for O(1) installation.
489
+ - **[Issue #9](https://github.com/syalia-srl/beaver/issues/9) Type-safe wrappers based on Pydantic-compatible models**: Enhance the built-in `Model` to handle recursive and embedded types and provide Pydantic compatibility.
452
490
 
453
491
 
454
492
  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.
@@ -0,0 +1,20 @@
1
+ beaver/__init__.py,sha256=p5ObXWW5sqLrfIbuacZw2Hmafe0p513_14fueaaTJCc,125
2
+ beaver/blobs.py,sha256=VdtjSIPrdjkmAlOZ8-wEkJsJ2KW4b9t8swHFu6d16c8,7394
3
+ beaver/channels.py,sha256=kIuwKMDBdDQObaKT23znsMXzfpKfE7pXSxvf-u4LlpY,9554
4
+ beaver/cli.py,sha256=Sxm-mYU3LGd4tIqw-5LHb0ektWebjV9vn51hm-CMJD0,2232
5
+ beaver/client.py,sha256=jDX2WJHZk1LL3R2sulM4JabZQJCcoZU4Hwfeqz-1KME,12572
6
+ beaver/collections.py,sha256=yn6Bc8zf2zYKBC0G0VsrSqSTGEia75VTIfg3NLUtKAc,28714
7
+ beaver/core.py,sha256=5LIZiPsv7KnKmsQucABWApqgyauBJJUtBIa6Cqh2o7U,17096
8
+ beaver/dicts.py,sha256=PhmQ_1Z0gyBlIFSCKDsnAHMA3t8BYJJCLKeQNSF_32k,8287
9
+ beaver/lists.py,sha256=0k-5qZrO38ipYHCyYeZ8rrzkcuzbgGasUCmlhyQiTzw,12775
10
+ beaver/locks.py,sha256=b6cHgAI1YQxKmVdLLzNw9fcGvk4V3SQS_wUaQTHSsUk,6630
11
+ beaver/logs.py,sha256=oKbJnDNb3KPH4JcwcaXzgG9pqSAZ_gpTn23xwFlVw9k,11643
12
+ beaver/queues.py,sha256=KRbEKsCcZ4z0poO4rwXhpjwIvLEbvSSr1Nyp1EQM0mE,10183
13
+ beaver/server.py,sha256=At3BoEV7JfpYjNtyHMdPUF8shj4V4D5nStXWb6Bv53A,15947
14
+ beaver/types.py,sha256=TKUW5lnqb7Q0xb51iyKGtANJ6-j9XghlhMM3PPIyFN8,2259
15
+ beaver/vectors.py,sha256=EGZf1s364-rMubxkYoTcjBl72lRRxM1cUwypjsoC6ec,18499
16
+ beaver_db-0.20.2.dist-info/METADATA,sha256=IzMrPGWyYSIMGl-hVh6lKNOOAl-mepoTtqBu9p-LGvw,24496
17
+ beaver_db-0.20.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ beaver_db-0.20.2.dist-info/entry_points.txt,sha256=bd5E2s45PoBdtdR9-ToKSdLNhmHp8naV1lWP5mOzlrc,42
19
+ beaver_db-0.20.2.dist-info/licenses/LICENSE,sha256=1xrIY5JnMk_QDQzsqmVzPIIyCgZAkWCC8kF2Ddo1UT0,1071
20
+ beaver_db-0.20.2.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- beaver/__init__.py,sha256=DBnqxMZ-ZIvX14_FoC_ZGmYqcqhQtpLG1VXjNVCcPdc,125
2
- beaver/blobs.py,sha256=gauxfWOCMoP6mq61JRj-TVqqkXMAcVI2taBGYnJXsxg,5527
3
- beaver/channels.py,sha256=kIuwKMDBdDQObaKT23znsMXzfpKfE7pXSxvf-u4LlpY,9554
4
- beaver/cli.py,sha256=Sxm-mYU3LGd4tIqw-5LHb0ektWebjV9vn51hm-CMJD0,2232
5
- beaver/collections.py,sha256=Vc5ZF5gVIACx2XeL25PxgOcW7iF6lBInO-cnfyBBUeo,27299
6
- beaver/core.py,sha256=JRkRvc0Sb3FT9KlR3YbmiPcqCQ686dFKmHSNZ_UJ_aE,17100
7
- beaver/dicts.py,sha256=0csA1fvQb3rB2yQvwuq_kzgJ0YPp-wifBLkNcuiUltI,6582
8
- beaver/lists.py,sha256=vKZPVRiw0BRSx4JKaboGnWXfwNGkHX8cBFH59qQX1yY,11258
9
- beaver/locks.py,sha256=b6cHgAI1YQxKmVdLLzNw9fcGvk4V3SQS_wUaQTHSsUk,6630
10
- beaver/logs.py,sha256=a5xenwl5NZeegIU0dWVEs67lvaHzzw-JRAZtEzNNO3E,9529
11
- beaver/queues.py,sha256=wTlxsu12grcIPbosA1S0_23Tot4Wv3BWXeKyq4dRPKk,7709
12
- beaver/server.py,sha256=At3BoEV7JfpYjNtyHMdPUF8shj4V4D5nStXWb6Bv53A,15947
13
- beaver/types.py,sha256=m0ohT7A8r0Y1a7bJEx4VanLaOUWU2VYxaLHPsVPjrIw,1651
14
- beaver/vectors.py,sha256=EGZf1s364-rMubxkYoTcjBl72lRRxM1cUwypjsoC6ec,18499
15
- beaver_db-0.19.3.dist-info/METADATA,sha256=gRJetTcWICFhi0kQUFeNt773vTZkZYF-iRFqCtUXqjE,23431
16
- beaver_db-0.19.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- beaver_db-0.19.3.dist-info/entry_points.txt,sha256=bd5E2s45PoBdtdR9-ToKSdLNhmHp8naV1lWP5mOzlrc,42
18
- beaver_db-0.19.3.dist-info/licenses/LICENSE,sha256=1xrIY5JnMk_QDQzsqmVzPIIyCgZAkWCC8kF2Ddo1UT0,1071
19
- beaver_db-0.19.3.dist-info/RECORD,,