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 +1 -1
- beaver/blobs.py +47 -1
- beaver/collections.py +39 -3
- beaver/dicts.py +36 -3
- beaver/lists.py +37 -3
- beaver/locks.py +20 -7
- beaver/queues.py +38 -3
- beaver/server.py +139 -26
- {beaver_db-0.19.1.dist-info → beaver_db-0.19.3.dist-info}/METADATA +9 -3
- beaver_db-0.19.3.dist-info/RECORD +19 -0
- beaver_db-0.19.1.dist-info/RECORD +0 -19
- {beaver_db-0.19.1.dist-info → beaver_db-0.19.3.dist-info}/WHEEL +0 -0
- {beaver_db-0.19.1.dist-info → beaver_db-0.19.3.dist-info}/entry_points.txt +0 -0
- {beaver_db-0.19.1.dist-info → beaver_db-0.19.3.dist-info}/licenses/LICENSE +0 -0
beaver/__init__.py
CHANGED
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[
|
|
14
|
-
VectorIndex = stub("This feature requires to install beaver-db[
|
|
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
|
|
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 +
|
|
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
|
|
117
|
-
if (time.time() - start_time) >
|
|
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 {
|
|
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 =
|
|
139
|
+
jitter = poll_interval * 0.1
|
|
127
140
|
sleep_time = random.uniform(
|
|
128
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
212
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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 [
|
|
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(
|
|
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(
|
|
305
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|

|
|
@@ -32,7 +36,9 @@ Description-Content-Type: text/markdown
|
|
|
32
36
|

|
|
33
37
|

|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|