beaver-db 0.19.1__py3-none-any.whl → 0.19.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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.1"
5
+ __version__ = "0.19.3"
beaver/blobs.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import json
2
2
  import sqlite3
3
3
  from typing import Any, Dict, Iterator, NamedTuple, Optional, Type, TypeVar
4
-
5
4
  from .types import JsonSerializable, IDatabase
5
+ from .locks import LockManager
6
6
 
7
7
 
8
8
  class Blob[M](NamedTuple):
@@ -16,10 +16,13 @@ class Blob[M](NamedTuple):
16
16
  class BlobManager[M]:
17
17
  """A wrapper providing a Pythonic interface to a blob store in the database."""
18
18
 
19
+ # In beaver/blobs.py, inside class BlobManager[M]:
19
20
  def __init__(self, name: str, db: IDatabase, model: Type[M] | None = None):
20
21
  self._name = name
21
22
  self._db = db
22
23
  self._model = model
24
+ lock_name = f"__lock__blob__{name}"
25
+ self._lock = LockManager(db, lock_name)
23
26
 
24
27
  def _serialize(self, value: M) -> str | None:
25
28
  """Serializes the given value to a JSON string."""
@@ -122,5 +125,48 @@ class BlobManager[M]:
122
125
  yield row["key"]
123
126
  cursor.close()
124
127
 
128
+ def __len__(self) -> int:
129
+ """Returns the number of blobs in the store."""
130
+ cursor = self._db.connection.cursor()
131
+ cursor.execute(
132
+ "SELECT COUNT(*) FROM beaver_blobs WHERE store_name = ?",
133
+ (self._name,)
134
+ )
135
+ count = cursor.fetchone()[0]
136
+ cursor.close()
137
+ return count
138
+
125
139
  def __repr__(self) -> str:
126
140
  return f"BlobManager(name='{self._name}')"
141
+
142
+ def acquire(
143
+ self,
144
+ timeout: Optional[float] = None,
145
+ lock_ttl: Optional[float] = None,
146
+ poll_interval: Optional[float] = None,
147
+ ) -> "BlobManager[M]":
148
+ """
149
+ Acquires an inter-process lock on this blob store, blocking until acquired.
150
+
151
+ Parameters override the default settings of the underlying LockManager.
152
+ """
153
+ self._lock.acquire(
154
+ timeout=timeout,
155
+ lock_ttl=lock_ttl,
156
+ poll_interval=poll_interval
157
+ )
158
+ return self
159
+
160
+ def release(self):
161
+ """
162
+ Releases the inter-process lock on this blob store.
163
+ """
164
+ self._lock.release()
165
+
166
+ def __enter__(self) -> "BlobManager[M]":
167
+ """Acquires the lock upon entering a 'with' statement."""
168
+ return self.acquire()
169
+
170
+ def __exit__(self, exc_type, exc_val, exc_tb):
171
+ """Releases the lock when exiting a 'with' statement."""
172
+ self.release()
beaver/collections.py CHANGED
@@ -3,15 +3,16 @@ import sqlite3
3
3
  import threading
4
4
  import uuid
5
5
  from enum import Enum
6
- from typing import Any, Iterator, List, Literal, Tuple, Type, TypeVar
6
+ from typing import Any, Iterator, List, Literal, Tuple, Type, TypeVar, Optional
7
7
  from .types import Model, stub, IDatabase
8
+ from .locks import LockManager
8
9
 
9
10
  try:
10
11
  import numpy as np
11
12
  from .vectors import VectorIndex
12
13
  except ImportError:
13
- np = stub("This feature requires to install beaver-db[faiss]")()
14
- VectorIndex = stub("This feature requires to install beaver-db[faiss]")
14
+ np = stub("This feature requires to install beaver-db[vector]")()
15
+ VectorIndex = stub("This feature requires to install beaver-db[vector]")
15
16
 
16
17
  # --- Fuzzy Search Helper Functions ---
17
18
 
@@ -135,6 +136,9 @@ class CollectionManager[D: Document]:
135
136
  self._compaction_lock = threading.Lock()
136
137
  self._compaction_thread: threading.Thread | None = None
137
138
 
139
+ lock_name = f"__lock__collection__{name}"
140
+ self._lock = LockManager(db, lock_name)
141
+
138
142
  def _flatten_metadata(self, metadata: dict, prefix: str = "") -> dict[str, Any]:
139
143
  """Flattens a nested dictionary for indexing."""
140
144
  flat_dict = {}
@@ -643,6 +647,38 @@ class CollectionManager[D: Document]:
643
647
  cursor.close()
644
648
  return count
645
649
 
650
+ def acquire(
651
+ self,
652
+ timeout: Optional[float] = None,
653
+ lock_ttl: Optional[float] = None,
654
+ poll_interval: Optional[float] = None,
655
+ ) -> "CollectionManager[D]":
656
+ """
657
+ Acquires an inter-process lock on this collection, blocking until acquired.
658
+ This guarantees exclusive access for multi-step atomic operations
659
+ (e.g., index + connect).
660
+
661
+ Parameters override the default settings of the underlying LockManager.
662
+ """
663
+ self._lock.acquire(
664
+ timeout=timeout, lock_ttl=lock_ttl, poll_interval=poll_interval
665
+ )
666
+ return self
667
+
668
+ def release(self):
669
+ """
670
+ Releases the inter-process lock on this collection.
671
+ """
672
+ self._lock.release()
673
+
674
+ def __enter__(self) -> "CollectionManager[D]":
675
+ """Acquires the lock upon entering a 'with' statement."""
676
+ return self.acquire()
677
+
678
+ def __exit__(self, exc_type, exc_val, exc_tb):
679
+ """Releases the lock when exiting a 'with' statement."""
680
+ self.release()
681
+
646
682
 
647
683
  def rerank[D: Document](
648
684
  *results: list[D], weights: list[float] | None = None, k: int = 60
beaver/dicts.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  import sqlite3
3
3
  import time
4
- from typing import Any, Iterator, Tuple, Type
5
-
4
+ from typing import Any, Iterator, Tuple, Type, Optional
6
5
  from .types import JsonSerializable, IDatabase
6
+ from .locks import LockManager
7
7
 
8
8
  class DictManager[T]:
9
9
  """A wrapper providing a Pythonic interface to a dictionary in the database."""
@@ -12,6 +12,8 @@ class DictManager[T]:
12
12
  self._name = name
13
13
  self._db = db
14
14
  self._model = model
15
+ lock_name = f"__lock__dict__{name}"
16
+ self._lock = LockManager(db, lock_name)
15
17
 
16
18
  def _serialize(self, value: T) -> str:
17
19
  """Serializes the given value to a JSON string."""
@@ -147,4 +149,35 @@ class DictManager[T]:
147
149
  cursor.close()
148
150
 
149
151
  def __repr__(self) -> str:
150
- return f"DictManager(name='{self._name}')"
152
+ return f"DictManager(name='{self._name}')"
153
+
154
+ def acquire(
155
+ self,
156
+ timeout: Optional[float] = None,
157
+ lock_ttl: Optional[float] = None,
158
+ poll_interval: Optional[float] = None,
159
+ ) -> "DictManager[T]":
160
+ """
161
+ Acquires an inter-process lock on this dictionary, blocking until acquired.
162
+ Parameters override the default settings of the underlying LockManager.
163
+ """
164
+ self._lock.acquire(
165
+ timeout=timeout,
166
+ lock_ttl=lock_ttl,
167
+ poll_interval=poll_interval
168
+ )
169
+ return self
170
+
171
+ def release(self):
172
+ """
173
+ Releases the inter-process lock on this dictionary.
174
+ """
175
+ self._lock.release()
176
+
177
+ def __enter__(self) -> "DictManager[T]":
178
+ """Acquires the lock upon entering a 'with' statement."""
179
+ return self.acquire()
180
+
181
+ def __exit__(self, exc_type, exc_val, exc_tb):
182
+ """Releases the lock when exiting a 'with' statement."""
183
+ self.release()
beaver/lists.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import json
2
2
  import sqlite3
3
- from typing import Any, Iterator, Type, Union
4
-
3
+ from typing import Any, Iterator, Type, Union, Optional
5
4
  from .types import JsonSerializable, IDatabase
5
+ from .locks import LockManager
6
6
 
7
7
 
8
8
  class ListManager[T]:
@@ -12,6 +12,8 @@ class ListManager[T]:
12
12
  self._name = name
13
13
  self._db = db
14
14
  self._model = model
15
+ lock_name = f"__lock__list__{name}"
16
+ self._lock = LockManager(db, lock_name)
15
17
 
16
18
  def _serialize(self, value: T) -> str:
17
19
  """Serializes the given value to a JSON string."""
@@ -261,4 +263,36 @@ class ListManager[T]:
261
263
  cursor.execute(
262
264
  "DELETE FROM beaver_lists WHERE rowid = ?", (rowid_to_delete,)
263
265
  )
264
- return self._deserialize(value_to_return)
266
+ return self._deserialize(value_to_return)
267
+
268
+ def acquire(
269
+ self,
270
+ timeout: Optional[float] = None,
271
+ lock_ttl: Optional[float] = None,
272
+ poll_interval: Optional[float] = None,
273
+ ) -> "ListManager[T]":
274
+ """
275
+ Acquires an inter-process lock on this list, blocking until acquired.
276
+
277
+ Parameters override the default settings of the underlying LockManager.
278
+ """
279
+ self._lock.acquire(
280
+ timeout=timeout,
281
+ lock_ttl=lock_ttl,
282
+ poll_interval=poll_interval
283
+ )
284
+ return self
285
+
286
+ def release(self):
287
+ """
288
+ Releases the inter-process lock on this list, allowing the next waiting process access.
289
+ """
290
+ self._lock.release()
291
+
292
+ def __enter__(self) -> "ListManager[T]":
293
+ """Acquires the lock upon entering a 'with' statement."""
294
+ return self.acquire()
295
+
296
+ def __exit__(self, exc_type, exc_val, exc_tb):
297
+ """Releases the lock when exiting a 'with' statement."""
298
+ self.release()
beaver/locks.py CHANGED
@@ -55,7 +55,11 @@ class LockManager:
55
55
  self._waiter_id = f"pid:{os.getpid()}:id:{uuid.uuid4()}"
56
56
  self._acquired = False # State to track if this instance holds the lock
57
57
 
58
- def acquire(self) -> "LockManager":
58
+ def acquire(self,
59
+ timeout: float|None = None,
60
+ lock_ttl: float |None = None,
61
+ poll_interval: float |None = None,
62
+ ) -> "LockManager":
59
63
  """
60
64
  Blocks until the lock is acquired or the timeout expires.
61
65
 
@@ -66,9 +70,18 @@ class LockManager:
66
70
  # This instance already holds the lock
67
71
  return self
68
72
 
73
+ if timeout is None:
74
+ timeout = self._timeout
75
+
76
+ if lock_ttl is None:
77
+ lock_ttl = self._lock_ttl
78
+
79
+ if poll_interval is None:
80
+ poll_interval = self._poll_interval
81
+
69
82
  start_time = time.time()
70
83
  requested_at = time.time()
71
- expires_at = requested_at + self._lock_ttl
84
+ expires_at = requested_at + lock_ttl
72
85
 
73
86
  conn = self._db.connection
74
87
 
@@ -113,19 +126,19 @@ class LockManager:
113
126
  return self
114
127
 
115
128
  # 5. Check for timeout
116
- if self._timeout is not None:
117
- if (time.time() - start_time) > self._timeout:
129
+ if timeout is not None:
130
+ if (time.time() - start_time) > timeout:
118
131
  # We timed out. Remove ourselves from the queue and raise.
119
132
  self._release_from_queue()
120
133
  raise TimeoutError(
121
- f"Failed to acquire lock '{self._lock_name}' within {self._timeout}s."
134
+ f"Failed to acquire lock '{self._lock_name}' within {timeout}s."
122
135
  )
123
136
 
124
137
  # 6. Wait politely before polling again
125
138
  # Add +/- 10% jitter to the poll interval to avoid thundering herd
126
- jitter = self._poll_interval * 0.1
139
+ jitter = poll_interval * 0.1
127
140
  sleep_time = random.uniform(
128
- self._poll_interval - jitter, self._poll_interval + jitter
141
+ poll_interval - jitter, poll_interval + jitter
129
142
  )
130
143
  time.sleep(sleep_time)
131
144
 
beaver/queues.py CHANGED
@@ -2,10 +2,9 @@ import asyncio
2
2
  import json
3
3
  import sqlite3
4
4
  import time
5
- from typing import Any, Literal, NamedTuple, Type, overload
6
-
5
+ from typing import Any, Literal, NamedTuple, Type, overload, Optional
7
6
  from .types import JsonSerializable, IDatabase
8
-
7
+ from .locks import LockManager
9
8
 
10
9
  class QueueItem[T](NamedTuple):
11
10
  """A data class representing a single item retrieved from the queue."""
@@ -54,6 +53,8 @@ class QueueManager[T]:
54
53
  self._name = name
55
54
  self._db = db
56
55
  self._model = model
56
+ lock_name = f"__lock__queue__{name}"
57
+ self._lock = LockManager(db, lock_name)
57
58
 
58
59
  def _serialize(self, value: T) -> str:
59
60
  """Serializes the given value to a JSON string."""
@@ -184,3 +185,37 @@ class QueueManager[T]:
184
185
 
185
186
  def __repr__(self) -> str:
186
187
  return f"QueueManager(name='{self._name}')"
188
+
189
+ def acquire(
190
+ self,
191
+ timeout: Optional[float] = None,
192
+ lock_ttl: Optional[float] = None,
193
+ poll_interval: Optional[float] = None,
194
+ ) -> "QueueManager[T]":
195
+ """
196
+ Acquires an inter-process lock on this queue, blocking until acquired.
197
+ This ensures that a sequence of operations (e.g., batch-getting tasks)
198
+ is performed atomically without interruption from other processes.
199
+
200
+ Parameters override the default settings of the underlying LockManager.
201
+ """
202
+ self._lock.acquire(
203
+ timeout=timeout,
204
+ lock_ttl=lock_ttl,
205
+ poll_interval=poll_interval
206
+ )
207
+ return self
208
+
209
+ def release(self):
210
+ """
211
+ Releases the inter-process lock on this queue.
212
+ """
213
+ self._lock.release()
214
+
215
+ def __enter__(self) -> "QueueManager[T]":
216
+ """Acquires the lock upon entering a 'with' statement."""
217
+ return self.acquire()
218
+
219
+ def __exit__(self, exc_type, exc_val, exc_tb):
220
+ """Releases the lock when exiting a 'with' statement."""
221
+ self.release()
beaver/server.py CHANGED
@@ -2,11 +2,23 @@ try:
2
2
  from typing import Any, Optional, List
3
3
  import json
4
4
  from datetime import datetime, timedelta, timezone
5
- from fastapi import FastAPI, HTTPException, Body, UploadFile, File, Form, Response, WebSocket, WebSocketDisconnect
5
+ from fastapi import (
6
+ FastAPI,
7
+ HTTPException,
8
+ Body,
9
+ UploadFile,
10
+ File,
11
+ Form,
12
+ Response,
13
+ WebSocket,
14
+ WebSocketDisconnect,
15
+ )
6
16
  import uvicorn
7
17
  from pydantic import BaseModel, Field
8
18
  except ImportError:
9
- raise ImportError("Please install server dependencies with: pip install \"beaver-db[server]\"")
19
+ raise ImportError(
20
+ 'Please install server dependencies with: pip install "beaver-db[server]"'
21
+ )
10
22
 
11
23
  from .core import BeaverDB
12
24
  from .collections import Document, WalkDirection
@@ -14,6 +26,7 @@ from .collections import Document, WalkDirection
14
26
 
15
27
  # --- Pydantic Models for Collections ---
16
28
 
29
+
17
30
  class IndexRequest(BaseModel):
18
31
  id: Optional[str] = None
19
32
  embedding: Optional[List[float]] = None
@@ -21,28 +34,36 @@ class IndexRequest(BaseModel):
21
34
  fts: bool = True
22
35
  fuzzy: bool = False
23
36
 
37
+
24
38
  class SearchRequest(BaseModel):
25
39
  vector: List[float]
26
40
  top_k: int = 10
27
41
 
42
+
28
43
  class MatchRequest(BaseModel):
29
44
  query: str
30
45
  on: Optional[List[str]] = None
31
46
  top_k: int = 10
32
47
  fuzziness: int = 0
33
48
 
49
+
34
50
  class ConnectRequest(BaseModel):
35
51
  source_id: str
36
52
  target_id: str
37
53
  label: str
38
54
  metadata: Optional[dict] = None
39
55
 
56
+
40
57
  class WalkRequest(BaseModel):
41
58
  labels: List[str]
42
59
  depth: int
43
60
  direction: WalkDirection = WalkDirection.OUTGOING
44
61
 
45
62
 
63
+ class CountResponse(BaseModel):
64
+ count: int
65
+
66
+
46
67
  def build(db: BeaverDB) -> FastAPI:
47
68
  """Constructs a FastAPI instance for a given BeaverDB."""
48
69
  app = FastAPI(title="BeaverDB Server")
@@ -55,7 +76,9 @@ def build(db: BeaverDB) -> FastAPI:
55
76
  d = db.dict(name)
56
77
  value = d.get(key)
57
78
  if value is None:
58
- raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
79
+ raise HTTPException(
80
+ status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'"
81
+ )
59
82
  return value
60
83
 
61
84
  @app.put("/dicts/{name}/{key}", tags=["Dicts"])
@@ -73,8 +96,15 @@ def build(db: BeaverDB) -> FastAPI:
73
96
  del d[key]
74
97
  return {"status": "ok"}
75
98
  except KeyError:
76
- raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
99
+ raise HTTPException(
100
+ status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'"
101
+ )
77
102
 
103
+ @app.get("/dicts/{name}/count", tags=["Dicts"], response_model=CountResponse)
104
+ def get_dict_count(name: str) -> dict:
105
+ """Retrieves the number of key-value pairs in the dictionary."""
106
+ d = db.dict(name)
107
+ return {"count": len(d)}
78
108
 
79
109
  # --- Lists Endpoints ---
80
110
 
@@ -91,7 +121,9 @@ def build(db: BeaverDB) -> FastAPI:
91
121
  try:
92
122
  return l[index]
93
123
  except IndexError:
94
- raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
124
+ raise HTTPException(
125
+ status_code=404, detail=f"Index {index} out of bounds for list '{name}'"
126
+ )
95
127
 
96
128
  @app.post("/lists/{name}", tags=["Lists"])
97
129
  def push_list_item(name: str, value: Any = Body(...)):
@@ -108,7 +140,9 @@ def build(db: BeaverDB) -> FastAPI:
108
140
  l[index] = value
109
141
  return {"status": "ok"}
110
142
  except IndexError:
111
- raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
143
+ raise HTTPException(
144
+ status_code=404, detail=f"Index {index} out of bounds for list '{name}'"
145
+ )
112
146
 
113
147
  @app.delete("/lists/{name}/{index}", tags=["Lists"])
114
148
  def delete_list_item(name: str, index: int):
@@ -118,7 +152,15 @@ def build(db: BeaverDB) -> FastAPI:
118
152
  del l[index]
119
153
  return {"status": "ok"}
120
154
  except IndexError:
121
- raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
155
+ raise HTTPException(
156
+ status_code=404, detail=f"Index {index} out of bounds for list '{name}'"
157
+ )
158
+
159
+ @app.get("/lists/{name}/count", tags=["Lists"], response_model=CountResponse)
160
+ def get_list_count(name: str) -> dict:
161
+ """Retrieves the number of items in the list."""
162
+ l = db.list(name)
163
+ return {"count": len(l)}
122
164
 
123
165
  # --- Queues Endpoints ---
124
166
 
@@ -149,11 +191,19 @@ def build(db: BeaverDB) -> FastAPI:
149
191
  item = q.get(block=True, timeout=timeout)
150
192
  return item
151
193
  except TimeoutError:
152
- raise HTTPException(status_code=408, detail=f"Request timed out after {timeout}s waiting for an item in queue '{name}'")
194
+ raise HTTPException(
195
+ status_code=408,
196
+ detail=f"Request timed out after {timeout}s waiting for an item in queue '{name}'",
197
+ )
153
198
  except IndexError:
154
199
  # This case is less likely with block=True but good to handle
155
200
  raise HTTPException(status_code=404, detail=f"Queue '{name}' is empty")
156
201
 
202
+ @app.get("/queues/{name}/count", tags=["Queues"], response_model=CountResponse)
203
+ def get_queue_count(name: str) -> dict:
204
+ """RetrieVIes the number of items currently in the queue."""
205
+ q = db.queue(name)
206
+ return {"count": len(q)}
157
207
 
158
208
  # --- Blobs Endpoints ---
159
209
 
@@ -163,12 +213,20 @@ def build(db: BeaverDB) -> FastAPI:
163
213
  blobs = db.blobs(name)
164
214
  blob = blobs.get(key)
165
215
  if blob is None:
166
- raise HTTPException(status_code=404, detail=f"Blob with key '{key}' not found in store '{name}'")
216
+ raise HTTPException(
217
+ status_code=404,
218
+ detail=f"Blob with key '{key}' not found in store '{name}'",
219
+ )
167
220
  # Return the raw bytes with a generic binary content type
168
221
  return Response(content=blob.data, media_type="application/octet-stream")
169
222
 
170
223
  @app.put("/blobs/{name}/{key}", tags=["Blobs"])
171
- async def put_blob(name: str, key: str, data: UploadFile = File(...), metadata: Optional[str] = Form(None)):
224
+ async def put_blob(
225
+ name: str,
226
+ key: str,
227
+ data: UploadFile = File(...),
228
+ metadata: Optional[str] = Form(None),
229
+ ):
172
230
  """Stores a blob (binary file) with optional JSON metadata."""
173
231
  blobs = db.blobs(name)
174
232
  file_bytes = await data.read()
@@ -178,7 +236,9 @@ def build(db: BeaverDB) -> FastAPI:
178
236
  try:
179
237
  meta_dict = json.loads(metadata)
180
238
  except json.JSONDecodeError:
181
- raise HTTPException(status_code=400, detail="Invalid JSON format for metadata.")
239
+ raise HTTPException(
240
+ status_code=400, detail="Invalid JSON format for metadata."
241
+ )
182
242
 
183
243
  blobs.put(key=key, data=file_bytes, metadata=meta_dict)
184
244
  return {"status": "ok"}
@@ -191,8 +251,16 @@ def build(db: BeaverDB) -> FastAPI:
191
251
  blobs.delete(key)
192
252
  return {"status": "ok"}
193
253
  except KeyError:
194
- raise HTTPException(status_code=404, detail=f"Blob with key '{key}' not found in store '{name}'")
254
+ raise HTTPException(
255
+ status_code=404,
256
+ detail=f"Blob with key '{key}' not found in store '{name}'",
257
+ )
195
258
 
259
+ @app.get("/blobs/{name}/count", tags=["Blobs"], response_model=CountResponse)
260
+ def get_blob_count(name: str) -> dict:
261
+ """Retrieves the number of blobs in the store."""
262
+ b = db.blobs(name)
263
+ return {"count": len(b)}
196
264
 
197
265
  # --- Logs Endpoints ---
198
266
 
@@ -208,12 +276,25 @@ def build(db: BeaverDB) -> FastAPI:
208
276
  """Retrieves log entries within a specific time window."""
209
277
  log = db.log(name)
210
278
  # Ensure datetimes are timezone-aware (UTC) for correct comparison
211
- start_utc = start.astimezone(timezone.utc) if start.tzinfo else start.replace(tzinfo=timezone.utc)
212
- end_utc = end.astimezone(timezone.utc) if end.tzinfo else end.replace(tzinfo=timezone.utc)
279
+ start_utc = (
280
+ start.astimezone(timezone.utc)
281
+ if start.tzinfo
282
+ else start.replace(tzinfo=timezone.utc)
283
+ )
284
+ end_utc = (
285
+ end.astimezone(timezone.utc)
286
+ if end.tzinfo
287
+ else end.replace(tzinfo=timezone.utc)
288
+ )
213
289
  return log.range(start=start_utc, end=end_utc)
214
290
 
215
291
  @app.websocket("/logs/{name}/live", name="Logs")
216
- async def live_log_feed(websocket: WebSocket, name: str, window_seconds: int = 5, period_seconds: int = 1):
292
+ async def live_log_feed(
293
+ websocket: WebSocket,
294
+ name: str,
295
+ window_seconds: int = 5,
296
+ period_seconds: int = 1,
297
+ ):
217
298
  """Streams live, aggregated log data over a WebSocket."""
218
299
  await websocket.accept()
219
300
 
@@ -222,7 +303,10 @@ def build(db: BeaverDB) -> FastAPI:
222
303
  # This simple aggregator function runs in the background and returns a
223
304
  # JSON-serializable summary of the data in the current window.
224
305
  def simple_aggregator(window):
225
- return {"count": len(window), "latest_timestamp": window[-1]["timestamp"] if window else None}
306
+ return {
307
+ "count": len(window),
308
+ "latest_timestamp": window[-1]["timestamp"] if window else None,
309
+ }
226
310
 
227
311
  live_stream = async_logs.live(
228
312
  window=timedelta(seconds=window_seconds),
@@ -239,7 +323,6 @@ def build(db: BeaverDB) -> FastAPI:
239
323
  # Cleanly close the underlying iterator and its background thread.
240
324
  live_stream.close()
241
325
 
242
-
243
326
  # --- Channels Endpoints ---
244
327
 
245
328
  @app.post("/channels/{name}/publish", tags=["Channels"])
@@ -263,7 +346,6 @@ def build(db: BeaverDB) -> FastAPI:
263
346
  except WebSocketDisconnect:
264
347
  print(f"Client disconnected from channel '{name}' subscription.")
265
348
 
266
-
267
349
  # --- Collections Endpoints ---
268
350
 
269
351
  @app.get("/collections/{name}", tags=["Collections"])
@@ -282,7 +364,10 @@ def build(db: BeaverDB) -> FastAPI:
282
364
  return {"status": "ok", "id": doc.id}
283
365
  except TypeError as e:
284
366
  if "vector" in str(e):
285
- raise HTTPException(status_code=501, detail="Vector indexing requires the '[vector]' extra. Install with: pip install \"beaver-db[vector]\"")
367
+ raise HTTPException(
368
+ status_code=501,
369
+ detail="Vector indexing requires the '[vector]' extra. Install with: pip install \"beaver-db[vector]\"",
370
+ )
286
371
  raise e
287
372
 
288
373
  @app.post("/collections/{name}/search", tags=["Collections"])
@@ -291,18 +376,29 @@ def build(db: BeaverDB) -> FastAPI:
291
376
  collection = db.collection(name)
292
377
  try:
293
378
  results = collection.search(vector=req.vector, top_k=req.top_k)
294
- return [{"document": doc.to_dict(metadata_only=False), "distance": dist} for doc, dist in results]
379
+ return [
380
+ {"document": doc.to_dict(metadata_only=False), "distance": dist}
381
+ for doc, dist in results
382
+ ]
295
383
  except TypeError as e:
296
384
  if "vector" in str(e):
297
- raise HTTPException(status_code=501, detail="Vector search requires the '[vector]' extra. Install with: pip install \"beaver-db[vector]\"")
385
+ raise HTTPException(
386
+ status_code=501,
387
+ detail="Vector search requires the '[vector]' extra. Install with: pip install \"beaver-db[vector]\"",
388
+ )
298
389
  raise e
299
390
 
300
391
  @app.post("/collections/{name}/match", tags=["Collections"])
301
392
  def match_collection(name: str, req: MatchRequest) -> List[dict]:
302
393
  """Performs a full-text or fuzzy search on the collection."""
303
394
  collection = db.collection(name)
304
- results = collection.match(query=req.query, on=req.on, top_k=req.top_k, fuzziness=req.fuzziness)
305
- return [{"document": doc.to_dict(metadata_only=False), "score": score} for doc, score in results]
395
+ results = collection.match(
396
+ query=req.query, on=req.on, top_k=req.top_k, fuzziness=req.fuzziness
397
+ )
398
+ return [
399
+ {"document": doc.to_dict(metadata_only=False), "score": score}
400
+ for doc, score in results
401
+ ]
306
402
 
307
403
  @app.post("/collections/{name}/connect", tags=["Collections"])
308
404
  def connect_documents(name: str, req: ConnectRequest):
@@ -310,11 +406,15 @@ def build(db: BeaverDB) -> FastAPI:
310
406
  collection = db.collection(name)
311
407
  source_doc = Document(id=req.source_id)
312
408
  target_doc = Document(id=req.target_id)
313
- collection.connect(source=source_doc, target=target_doc, label=req.label, metadata=req.metadata)
409
+ collection.connect(
410
+ source=source_doc, target=target_doc, label=req.label, metadata=req.metadata
411
+ )
314
412
  return {"status": "ok"}
315
413
 
316
414
  @app.get("/collections/{name}/{doc_id}/neighbors", tags=["Collections"])
317
- def get_neighbors(name: str, doc_id: str, label: Optional[str] = None) -> List[dict]:
415
+ def get_neighbors(
416
+ name: str, doc_id: str, label: Optional[str] = None
417
+ ) -> List[dict]:
318
418
  """Retrieves the neighboring documents for a given document."""
319
419
  collection = db.collection(name)
320
420
  doc = Document(id=doc_id)
@@ -326,9 +426,22 @@ def build(db: BeaverDB) -> FastAPI:
326
426
  """Performs a graph traversal (BFS) from a starting document."""
327
427
  collection = db.collection(name)
328
428
  source_doc = Document(id=doc_id)
329
- results = collection.walk(source=source_doc, labels=req.labels, depth=req.depth, direction=req.direction)
429
+ results = collection.walk(
430
+ source=source_doc,
431
+ labels=req.labels,
432
+ depth=req.depth,
433
+ direction=req.direction,
434
+ )
330
435
  return [doc.to_dict(metadata_only=False) for doc in results]
331
436
 
437
+ @app.get(
438
+ "/collections/{name}/count", tags=["Collections"], response_model=CountResponse
439
+ )
440
+ def get_collection_count(name: str) -> dict:
441
+ """RetrieRetrieves the number of documents in the collection."""
442
+ c = db.collection(name)
443
+ return {"count": len(c)}
444
+
332
445
  return app
333
446
 
334
447
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaver-db
3
- Version: 0.19.1
3
+ Version: 0.19.3
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
@@ -23,7 +23,11 @@ Provides-Extra: vector
23
23
  Requires-Dist: faiss-cpu>=1.12.0; extra == 'vector'
24
24
  Description-Content-Type: text/markdown
25
25
 
26
- # beaver 🦫
26
+ <div style="text-align: center;">
27
+ <img src="https://github.com/syalia-srl/beaver/blob/main/logo.png?raw=true" width="256px">
28
+ </div>
29
+
30
+ ---
27
31
 
28
32
  <!-- Project badges -->
29
33
  ![PyPI - Version](https://img.shields.io/pypi/v/beaver-db)
@@ -32,7 +36,9 @@ Description-Content-Type: text/markdown
32
36
  ![PyPi - Downloads (Monthly)](https://img.shields.io/pypi/dm/beaver-db)
33
37
  ![Github - Commits](https://img.shields.io/github/commit-activity/m/apiad/beaver)
34
38
 
35
- A fast, single-file, multi-modal database for Python, built with the standard `sqlite3` library.
39
+ > A fast, single-file, multi-modal database for Python, built with the standard `sqlite3` library.
40
+
41
+ ---
36
42
 
37
43
  `beaver` is the **B**ackend for **E**mbedded, **A**ll-in-one **V**ector, **E**ntity, and **R**elationship storage. It's a simple, local, and embedded database designed to manage complex, modern data types without requiring a database server, built on top of SQLite.
38
44
 
@@ -0,0 +1,19 @@
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,,
@@ -1,19 +0,0 @@
1
- beaver/__init__.py,sha256=dMWanhrt58thAIt9Qj4jzxy5Dg5KzvNQenZ_cNcz_Tk,125
2
- beaver/blobs.py,sha256=YkIEskHD6oHRaJTF0P25HrTT8LqM-REyV_UBPVQxeqQ,4055
3
- beaver/channels.py,sha256=kIuwKMDBdDQObaKT23znsMXzfpKfE7pXSxvf-u4LlpY,9554
4
- beaver/cli.py,sha256=Sxm-mYU3LGd4tIqw-5LHb0ektWebjV9vn51hm-CMJD0,2232
5
- beaver/collections.py,sha256=UAQAuRxJRCqY5PHfxJNm3CdKqMNuyY8DOLdodvY6jpk,26107
6
- beaver/core.py,sha256=JRkRvc0Sb3FT9KlR3YbmiPcqCQ686dFKmHSNZ_UJ_aE,17100
7
- beaver/dicts.py,sha256=Xp8lPfQt08O8zCbptQLWQLO79OxG6uAVER6ryj3SScQ,5495
8
- beaver/lists.py,sha256=rfJ8uTNLkMREYc0uGx0z1VKt2m3eR9hvbdvDD58EbmQ,10140
9
- beaver/locks.py,sha256=GWDSRkPw2lrAQfXIRqvkc5PK9zZ2eLYWKTuzHTs9j_A,6321
10
- beaver/logs.py,sha256=a5xenwl5NZeegIU0dWVEs67lvaHzzw-JRAZtEzNNO3E,9529
11
- beaver/queues.py,sha256=Fr3oie63EtceSoiC8EOEDSLu1tDI8q2MYLXd8MEeC3g,6476
12
- beaver/server.py,sha256=WoNcPXU9oh6hcHtb60IbEk5DfZT5J4Fb-yubJE3YLIc,13642
13
- beaver/types.py,sha256=m0ohT7A8r0Y1a7bJEx4VanLaOUWU2VYxaLHPsVPjrIw,1651
14
- beaver/vectors.py,sha256=EGZf1s364-rMubxkYoTcjBl72lRRxM1cUwypjsoC6ec,18499
15
- beaver_db-0.19.1.dist-info/METADATA,sha256=Nt_3RlU4UraPp-zJusitwvwE2yza_zUyhRvecRcEvrI,23299
16
- beaver_db-0.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- beaver_db-0.19.1.dist-info/entry_points.txt,sha256=bd5E2s45PoBdtdR9-ToKSdLNhmHp8naV1lWP5mOzlrc,42
18
- beaver_db-0.19.1.dist-info/licenses/LICENSE,sha256=1xrIY5JnMk_QDQzsqmVzPIIyCgZAkWCC8kF2Ddo1UT0,1071
19
- beaver_db-0.19.1.dist-info/RECORD,,