beaver-db 0.17.4__tar.gz → 0.17.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of beaver-db might be problematic. Click here for more details.
- {beaver_db-0.17.4 → beaver_db-0.17.5}/PKG-INFO +1 -1
- beaver_db-0.17.5/beaver/server.py +339 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/pyproject.toml +1 -1
- beaver_db-0.17.4/beaver/server.py +0 -132
- {beaver_db-0.17.4 → beaver_db-0.17.5}/.gitignore +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/.python-version +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/LICENSE +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/README.md +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/__init__.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/blobs.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/channels.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/cli.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/collections.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/core.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/dicts.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/lists.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/logs.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/queues.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/types.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/beaver/vectors.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/design.md +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/async_pubsub.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/blobs.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/cache.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/fts.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/fuzzy.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/general_test.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/graph.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/kvstore.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/list.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/logs.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/pqueue.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/producer_consumer.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/publisher.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/pubsub.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/rerank.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/stress_vectors.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/subscriber.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/textual_chat.css +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/textual_chat.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/type_hints.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/examples/vector.py +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/makefile +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/roadmap.md +0 -0
- {beaver_db-0.17.4 → beaver_db-0.17.5}/uv.lock +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from typing import Any, Optional, List
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Body, UploadFile, File, Form, Response, WebSocket, WebSocketDisconnect
|
|
6
|
+
import uvicorn
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise ImportError("Please install server dependencies with: pip install \"beaver-db[server]\"")
|
|
10
|
+
|
|
11
|
+
from .core import BeaverDB
|
|
12
|
+
from .collections import Document, WalkDirection
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --- Pydantic Models for Collections ---
|
|
16
|
+
|
|
17
|
+
class IndexRequest(BaseModel):
|
|
18
|
+
id: Optional[str] = None
|
|
19
|
+
embedding: Optional[List[float]] = None
|
|
20
|
+
metadata: dict = Field(default_factory=dict)
|
|
21
|
+
fts: bool = True
|
|
22
|
+
fuzzy: bool = False
|
|
23
|
+
|
|
24
|
+
class SearchRequest(BaseModel):
|
|
25
|
+
vector: List[float]
|
|
26
|
+
top_k: int = 10
|
|
27
|
+
|
|
28
|
+
class MatchRequest(BaseModel):
|
|
29
|
+
query: str
|
|
30
|
+
on: Optional[List[str]] = None
|
|
31
|
+
top_k: int = 10
|
|
32
|
+
fuzziness: int = 0
|
|
33
|
+
|
|
34
|
+
class ConnectRequest(BaseModel):
|
|
35
|
+
source_id: str
|
|
36
|
+
target_id: str
|
|
37
|
+
label: str
|
|
38
|
+
metadata: Optional[dict] = None
|
|
39
|
+
|
|
40
|
+
class WalkRequest(BaseModel):
|
|
41
|
+
labels: List[str]
|
|
42
|
+
depth: int
|
|
43
|
+
direction: WalkDirection = WalkDirection.OUTGOING
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build(db: BeaverDB) -> FastAPI:
|
|
47
|
+
"""Constructs a FastAPI instance for a given BeaverDB."""
|
|
48
|
+
app = FastAPI(title="BeaverDB Server")
|
|
49
|
+
|
|
50
|
+
# --- Dicts Endpoints ---
|
|
51
|
+
|
|
52
|
+
@app.get("/dicts/{name}/{key}", tags=["Dicts"])
|
|
53
|
+
def get_dict_item(name: str, key: str) -> Any:
|
|
54
|
+
"""Retrieves the value for a specific key."""
|
|
55
|
+
d = db.dict(name)
|
|
56
|
+
value = d.get(key)
|
|
57
|
+
if value is None:
|
|
58
|
+
raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
@app.put("/dicts/{name}/{key}", tags=["Dicts"])
|
|
62
|
+
def set_dict_item(name: str, key: str, value: Any = Body(...)):
|
|
63
|
+
"""Sets or updates the value for a specific key."""
|
|
64
|
+
d = db.dict(name)
|
|
65
|
+
d[key] = value
|
|
66
|
+
return {"status": "ok"}
|
|
67
|
+
|
|
68
|
+
@app.delete("/dicts/{name}/{key}", tags=["Dicts"])
|
|
69
|
+
def delete_dict_item(name: str, key: str):
|
|
70
|
+
"""Deletes a key-value pair."""
|
|
71
|
+
d = db.dict(name)
|
|
72
|
+
try:
|
|
73
|
+
del d[key]
|
|
74
|
+
return {"status": "ok"}
|
|
75
|
+
except KeyError:
|
|
76
|
+
raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- Lists Endpoints ---
|
|
80
|
+
|
|
81
|
+
@app.get("/lists/{name}", tags=["Lists"])
|
|
82
|
+
def get_list(name: str) -> list:
|
|
83
|
+
"""Retrieves all items in the list."""
|
|
84
|
+
l = db.list(name)
|
|
85
|
+
return l[:]
|
|
86
|
+
|
|
87
|
+
@app.get("/lists/{name}/{index}", tags=["Lists"])
|
|
88
|
+
def get_list_item(name: str, index: int) -> Any:
|
|
89
|
+
"""Retrieves the item at a specific index."""
|
|
90
|
+
l = db.list(name)
|
|
91
|
+
try:
|
|
92
|
+
return l[index]
|
|
93
|
+
except IndexError:
|
|
94
|
+
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
95
|
+
|
|
96
|
+
@app.post("/lists/{name}", tags=["Lists"])
|
|
97
|
+
def push_list_item(name: str, value: Any = Body(...)):
|
|
98
|
+
"""Adds an item to the end of the list."""
|
|
99
|
+
l = db.list(name)
|
|
100
|
+
l.push(value)
|
|
101
|
+
return {"status": "ok"}
|
|
102
|
+
|
|
103
|
+
@app.put("/lists/{name}/{index}", tags=["Lists"])
|
|
104
|
+
def update_list_item(name: str, index: int, value: Any = Body(...)):
|
|
105
|
+
"""Updates the item at a specific index."""
|
|
106
|
+
l = db.list(name)
|
|
107
|
+
try:
|
|
108
|
+
l[index] = value
|
|
109
|
+
return {"status": "ok"}
|
|
110
|
+
except IndexError:
|
|
111
|
+
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
112
|
+
|
|
113
|
+
@app.delete("/lists/{name}/{index}", tags=["Lists"])
|
|
114
|
+
def delete_list_item(name: str, index: int):
|
|
115
|
+
"""Deletes the item at a specific index."""
|
|
116
|
+
l = db.list(name)
|
|
117
|
+
try:
|
|
118
|
+
del l[index]
|
|
119
|
+
return {"status": "ok"}
|
|
120
|
+
except IndexError:
|
|
121
|
+
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
122
|
+
|
|
123
|
+
# --- Queues Endpoints ---
|
|
124
|
+
|
|
125
|
+
@app.get("/queues/{name}/peek", tags=["Queues"])
|
|
126
|
+
def peek_queue_item(name: str) -> Any:
|
|
127
|
+
"""Retrieves the highest-priority item from the queue without removing it."""
|
|
128
|
+
q = db.queue(name)
|
|
129
|
+
item = q.peek()
|
|
130
|
+
if item is None:
|
|
131
|
+
raise HTTPException(status_code=404, detail=f"Queue '{name}' is empty")
|
|
132
|
+
return item
|
|
133
|
+
|
|
134
|
+
@app.post("/queues/{name}/put", tags=["Queues"])
|
|
135
|
+
def put_queue_item(name: str, data: Any = Body(...), priority: float = Body(...)):
|
|
136
|
+
"""Adds an item to the queue with a specific priority."""
|
|
137
|
+
q = db.queue(name)
|
|
138
|
+
q.put(data=data, priority=priority)
|
|
139
|
+
return {"status": "ok"}
|
|
140
|
+
|
|
141
|
+
@app.delete("/queues/{name}/get", tags=["Queues"])
|
|
142
|
+
def get_queue_item(name: str, timeout: float = 5.0) -> Any:
|
|
143
|
+
"""
|
|
144
|
+
Atomically retrieves and removes the highest-priority item from the queue,
|
|
145
|
+
blocking until an item is available or the timeout is reached.
|
|
146
|
+
"""
|
|
147
|
+
q = db.queue(name)
|
|
148
|
+
try:
|
|
149
|
+
item = q.get(block=True, timeout=timeout)
|
|
150
|
+
return item
|
|
151
|
+
except TimeoutError:
|
|
152
|
+
raise HTTPException(status_code=408, detail=f"Request timed out after {timeout}s waiting for an item in queue '{name}'")
|
|
153
|
+
except IndexError:
|
|
154
|
+
# This case is less likely with block=True but good to handle
|
|
155
|
+
raise HTTPException(status_code=404, detail=f"Queue '{name}' is empty")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --- Blobs Endpoints ---
|
|
159
|
+
|
|
160
|
+
@app.get("/blobs/{name}/{key}", response_class=Response, tags=["Blobs"])
|
|
161
|
+
def get_blob(name: str, key: str):
|
|
162
|
+
"""Retrieves a blob as a binary file."""
|
|
163
|
+
blobs = db.blobs(name)
|
|
164
|
+
blob = blobs.get(key)
|
|
165
|
+
if blob is None:
|
|
166
|
+
raise HTTPException(status_code=404, detail=f"Blob with key '{key}' not found in store '{name}'")
|
|
167
|
+
# Return the raw bytes with a generic binary content type
|
|
168
|
+
return Response(content=blob.data, media_type="application/octet-stream")
|
|
169
|
+
|
|
170
|
+
@app.put("/blobs/{name}/{key}", tags=["Blobs"])
|
|
171
|
+
async def put_blob(name: str, key: str, data: UploadFile = File(...), metadata: Optional[str] = Form(None)):
|
|
172
|
+
"""Stores a blob (binary file) with optional JSON metadata."""
|
|
173
|
+
blobs = db.blobs(name)
|
|
174
|
+
file_bytes = await data.read()
|
|
175
|
+
|
|
176
|
+
meta_dict = None
|
|
177
|
+
if metadata:
|
|
178
|
+
try:
|
|
179
|
+
meta_dict = json.loads(metadata)
|
|
180
|
+
except json.JSONDecodeError:
|
|
181
|
+
raise HTTPException(status_code=400, detail="Invalid JSON format for metadata.")
|
|
182
|
+
|
|
183
|
+
blobs.put(key=key, data=file_bytes, metadata=meta_dict)
|
|
184
|
+
return {"status": "ok"}
|
|
185
|
+
|
|
186
|
+
@app.delete("/blobs/{name}/{key}", tags=["Blobs"])
|
|
187
|
+
def delete_blob(name: str, key: str):
|
|
188
|
+
"""Deletes a blob from the store."""
|
|
189
|
+
blobs = db.blobs(name)
|
|
190
|
+
try:
|
|
191
|
+
blobs.delete(key)
|
|
192
|
+
return {"status": "ok"}
|
|
193
|
+
except KeyError:
|
|
194
|
+
raise HTTPException(status_code=404, detail=f"Blob with key '{key}' not found in store '{name}'")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# --- Logs Endpoints ---
|
|
198
|
+
|
|
199
|
+
@app.post("/logs/{name}", tags=["Logs"])
|
|
200
|
+
def create_log_entry(name: str, data: Any = Body(...)):
|
|
201
|
+
"""Adds a new entry to the log."""
|
|
202
|
+
log = db.log(name)
|
|
203
|
+
log.log(data)
|
|
204
|
+
return {"status": "ok"}
|
|
205
|
+
|
|
206
|
+
@app.get("/logs/{name}/range", tags=["Logs"])
|
|
207
|
+
def get_log_range(name: str, start: datetime, end: datetime) -> list:
|
|
208
|
+
"""Retrieves log entries within a specific time window."""
|
|
209
|
+
log = db.log(name)
|
|
210
|
+
# 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)
|
|
213
|
+
return log.range(start=start_utc, end=end_utc)
|
|
214
|
+
|
|
215
|
+
@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):
|
|
217
|
+
"""Streams live, aggregated log data over a WebSocket."""
|
|
218
|
+
await websocket.accept()
|
|
219
|
+
|
|
220
|
+
async_logs = db.log(name).as_async()
|
|
221
|
+
|
|
222
|
+
# This simple aggregator function runs in the background and returns a
|
|
223
|
+
# JSON-serializable summary of the data in the current window.
|
|
224
|
+
def simple_aggregator(window):
|
|
225
|
+
return {"count": len(window), "latest_timestamp": window[-1]["timestamp"] if window else None}
|
|
226
|
+
|
|
227
|
+
live_stream = async_logs.live(
|
|
228
|
+
window=timedelta(seconds=window_seconds),
|
|
229
|
+
period=timedelta(seconds=period_seconds),
|
|
230
|
+
aggregator=simple_aggregator,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
async for summary in live_stream:
|
|
235
|
+
await websocket.send_json(summary)
|
|
236
|
+
except WebSocketDisconnect:
|
|
237
|
+
print(f"Client disconnected from log '{name}' live feed.")
|
|
238
|
+
finally:
|
|
239
|
+
# Cleanly close the underlying iterator and its background thread.
|
|
240
|
+
live_stream.close()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# --- Channels Endpoints ---
|
|
244
|
+
|
|
245
|
+
@app.post("/channels/{name}/publish", tags=["Channels"])
|
|
246
|
+
def publish_to_channel(name: str, payload: Any = Body(...)):
|
|
247
|
+
"""Publishes a message to the specified channel."""
|
|
248
|
+
channel = db.channel(name)
|
|
249
|
+
channel.publish(payload)
|
|
250
|
+
return {"status": "ok"}
|
|
251
|
+
|
|
252
|
+
@app.websocket("/channels/{name}/subscribe", name="Channels")
|
|
253
|
+
async def subscribe_to_channel(websocket: WebSocket, name: str):
|
|
254
|
+
"""Subscribes to a channel and streams messages over a WebSocket."""
|
|
255
|
+
await websocket.accept()
|
|
256
|
+
|
|
257
|
+
async_channel = db.channel(name).as_async()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
async with async_channel.subscribe() as listener:
|
|
261
|
+
async for message in listener.listen():
|
|
262
|
+
await websocket.send_json(message)
|
|
263
|
+
except WebSocketDisconnect:
|
|
264
|
+
print(f"Client disconnected from channel '{name}' subscription.")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# --- Collections Endpoints ---
|
|
268
|
+
|
|
269
|
+
@app.get("/collections/{name}", tags=["Collections"])
|
|
270
|
+
def get_all_documents(name: str) -> List[dict]:
|
|
271
|
+
"""Retrieves all documents in the collection."""
|
|
272
|
+
collection = db.collection(name)
|
|
273
|
+
return [doc.model_dump() for doc in collection]
|
|
274
|
+
|
|
275
|
+
@app.post("/collections/{name}/index", tags=["Collections"])
|
|
276
|
+
def index_document(name: str, req: IndexRequest):
|
|
277
|
+
"""Indexes a document in the specified collection."""
|
|
278
|
+
collection = db.collection(name)
|
|
279
|
+
doc = Document(id=req.id, embedding=req.embedding, **req.metadata)
|
|
280
|
+
try:
|
|
281
|
+
collection.index(doc, fts=req.fts, fuzzy=req.fuzzy)
|
|
282
|
+
return {"status": "ok", "id": doc.id}
|
|
283
|
+
except TypeError as e:
|
|
284
|
+
if "faiss" in str(e):
|
|
285
|
+
raise HTTPException(status_code=501, detail="Vector indexing requires the '[faiss]' extra. Install with: pip install \"beaver-db[faiss]\"")
|
|
286
|
+
raise e
|
|
287
|
+
|
|
288
|
+
@app.post("/collections/{name}/search", tags=["Collections"])
|
|
289
|
+
def search_collection(name: str, req: SearchRequest) -> List[dict]:
|
|
290
|
+
"""Performs a vector search on the collection."""
|
|
291
|
+
collection = db.collection(name)
|
|
292
|
+
try:
|
|
293
|
+
results = collection.search(vector=req.vector, top_k=req.top_k)
|
|
294
|
+
return [{"document": doc.model_dump(), "distance": dist} for doc, dist in results]
|
|
295
|
+
except TypeError as e:
|
|
296
|
+
if "faiss" in str(e):
|
|
297
|
+
raise HTTPException(status_code=501, detail="Vector search requires the '[faiss]' extra. Install with: pip install \"beaver-db[faiss]\"")
|
|
298
|
+
raise e
|
|
299
|
+
|
|
300
|
+
@app.post("/collections/{name}/match", tags=["Collections"])
|
|
301
|
+
def match_collection(name: str, req: MatchRequest) -> List[dict]:
|
|
302
|
+
"""Performs a full-text or fuzzy search on the collection."""
|
|
303
|
+
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.model_dump(), "score": score} for doc, score in results]
|
|
306
|
+
|
|
307
|
+
@app.post("/collections/{name}/connect", tags=["Collections"])
|
|
308
|
+
def connect_documents(name: str, req: ConnectRequest):
|
|
309
|
+
"""Creates a directed edge between two documents."""
|
|
310
|
+
collection = db.collection(name)
|
|
311
|
+
source_doc = Document(id=req.source_id)
|
|
312
|
+
target_doc = Document(id=req.target_id)
|
|
313
|
+
collection.connect(source=source_doc, target=target_doc, label=req.label, metadata=req.metadata)
|
|
314
|
+
return {"status": "ok"}
|
|
315
|
+
|
|
316
|
+
@app.get("/collections/{name}/{doc_id}/neighbors", tags=["Collections"])
|
|
317
|
+
def get_neighbors(name: str, doc_id: str, label: Optional[str] = None) -> List[dict]:
|
|
318
|
+
"""Retrieves the neighboring documents for a given document."""
|
|
319
|
+
collection = db.collection(name)
|
|
320
|
+
doc = Document(id=doc_id)
|
|
321
|
+
neighbors = collection.neighbors(doc, label=label)
|
|
322
|
+
return [n.model_dump() for n in neighbors]
|
|
323
|
+
|
|
324
|
+
@app.post("/collections/{name}/{doc_id}/walk", tags=["Collections"])
|
|
325
|
+
def walk_graph(name: str, doc_id: str, req: WalkRequest) -> List[dict]:
|
|
326
|
+
"""Performs a graph traversal (BFS) from a starting document."""
|
|
327
|
+
collection = db.collection(name)
|
|
328
|
+
source_doc = Document(id=doc_id)
|
|
329
|
+
results = collection.walk(source=source_doc, labels=req.labels, depth=req.depth, direction=req.direction)
|
|
330
|
+
return [doc.model_dump() for doc in results]
|
|
331
|
+
|
|
332
|
+
return app
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def serve(db_path: str, host: str, port: int):
|
|
336
|
+
"""Initializes and runs the Uvicorn server."""
|
|
337
|
+
db = BeaverDB(db_path)
|
|
338
|
+
app = build(db)
|
|
339
|
+
uvicorn.run(app, host=host, port=port)
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
try:
|
|
2
|
-
from fastapi import FastAPI, HTTPException, Body
|
|
3
|
-
import uvicorn
|
|
4
|
-
except ImportError:
|
|
5
|
-
raise ImportError(
|
|
6
|
-
"FastAPI and Uvicorn are required to serve the database. "
|
|
7
|
-
'Please install them with `pip install "beaver-db[server]"`'
|
|
8
|
-
)
|
|
9
|
-
from typing import Any
|
|
10
|
-
from .core import BeaverDB
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def build(db: BeaverDB) -> FastAPI:
|
|
14
|
-
"""
|
|
15
|
-
Constructs a FastAPI application instance for a given BeaverDB instance.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
db: An active BeaverDB instance.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
A FastAPI application with all endpoints configured.
|
|
22
|
-
"""
|
|
23
|
-
app = FastAPI(
|
|
24
|
-
title="BeaverDB",
|
|
25
|
-
description="A RESTful API for a BeaverDB instance.",
|
|
26
|
-
version="0.1.0",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# --- Dicts Endpoints ---
|
|
30
|
-
|
|
31
|
-
@app.get("/dicts/{name}")
|
|
32
|
-
def get_all_dict_items(name: str) -> dict:
|
|
33
|
-
"""Retrieves all key-value pairs in a dictionary."""
|
|
34
|
-
d = db.dict(name)
|
|
35
|
-
return {k: v for k, v in d.items()}
|
|
36
|
-
|
|
37
|
-
@app.get("/dicts/{name}/{key}")
|
|
38
|
-
def get_dict_item(name: str, key: str) -> Any:
|
|
39
|
-
"""Retrieves the value for a specific key."""
|
|
40
|
-
d = db.dict(name)
|
|
41
|
-
try:
|
|
42
|
-
return d[key]
|
|
43
|
-
except KeyError:
|
|
44
|
-
raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
|
|
45
|
-
|
|
46
|
-
@app.post("/dicts/{name}/{key}")
|
|
47
|
-
def set_dict_item(name: str, key: str, value: Any = Body(...)):
|
|
48
|
-
"""Sets the value for a specific key."""
|
|
49
|
-
d = db.dict(name)
|
|
50
|
-
d[key] = value
|
|
51
|
-
return {"status": "ok"}
|
|
52
|
-
|
|
53
|
-
@app.delete("/dicts/{name}/{key}")
|
|
54
|
-
def delete_dict_item(name: str, key: str):
|
|
55
|
-
"""Deletes a key-value pair."""
|
|
56
|
-
d = db.dict(name)
|
|
57
|
-
try:
|
|
58
|
-
del d[key]
|
|
59
|
-
return {"status": "ok"}
|
|
60
|
-
except KeyError:
|
|
61
|
-
raise HTTPException(status_code=404, detail=f"Key '{key}' not found in dictionary '{name}'")
|
|
62
|
-
|
|
63
|
-
# --- Lists Endpoints ---
|
|
64
|
-
|
|
65
|
-
@app.get("/lists/{name}")
|
|
66
|
-
def get_list(name: str) -> list:
|
|
67
|
-
"""Retrieves all items in the list."""
|
|
68
|
-
l = db.list(name)
|
|
69
|
-
return l[:]
|
|
70
|
-
|
|
71
|
-
@app.get("/lists/{name}/{index}")
|
|
72
|
-
def get_list_item(name: str, index: int) -> Any:
|
|
73
|
-
"""Retrieves the item at a specific index."""
|
|
74
|
-
l = db.list(name)
|
|
75
|
-
try:
|
|
76
|
-
return l[index]
|
|
77
|
-
except IndexError:
|
|
78
|
-
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
79
|
-
|
|
80
|
-
@app.post("/lists/{name}")
|
|
81
|
-
def push_list_item(name: str, value: Any = Body(...)):
|
|
82
|
-
"""Adds an item to the end of the list."""
|
|
83
|
-
l = db.list(name)
|
|
84
|
-
l.push(value)
|
|
85
|
-
return {"status": "ok"}
|
|
86
|
-
|
|
87
|
-
@app.put("/lists/{name}/{index}")
|
|
88
|
-
def update_list_item(name: str, index: int, value: Any = Body(...)):
|
|
89
|
-
"""Updates the item at a specific index."""
|
|
90
|
-
l = db.list(name)
|
|
91
|
-
try:
|
|
92
|
-
l[index] = value
|
|
93
|
-
return {"status": "ok"}
|
|
94
|
-
except IndexError:
|
|
95
|
-
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
96
|
-
|
|
97
|
-
@app.delete("/lists/{name}/{index}")
|
|
98
|
-
def delete_list_item(name: str, index: int):
|
|
99
|
-
"""Deletes the item at a specific index."""
|
|
100
|
-
l = db.list(name)
|
|
101
|
-
try:
|
|
102
|
-
del l[index]
|
|
103
|
-
return {"status": "ok"}
|
|
104
|
-
except IndexError:
|
|
105
|
-
raise HTTPException(status_code=404, detail=f"Index {index} out of bounds for list '{name}'")
|
|
106
|
-
|
|
107
|
-
# TODO: Add endpoints for all BeaverDB modalities
|
|
108
|
-
# - Queues
|
|
109
|
-
# - Collections
|
|
110
|
-
# - Channels
|
|
111
|
-
# - Logs
|
|
112
|
-
# - Blobs
|
|
113
|
-
|
|
114
|
-
@app.get("/")
|
|
115
|
-
def read_root():
|
|
116
|
-
return {"message": "Welcome to the BeaverDB API"}
|
|
117
|
-
|
|
118
|
-
return app
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def serve(db_path: str, host: str, port: int):
|
|
122
|
-
"""
|
|
123
|
-
Initializes a BeaverDB instance and runs a Uvicorn server for it.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
db_path: The path to the SQLite database file.
|
|
127
|
-
host: The host to bind the server to.
|
|
128
|
-
port: The port to run the server on.
|
|
129
|
-
"""
|
|
130
|
-
db = BeaverDB(db_path)
|
|
131
|
-
app = build(db)
|
|
132
|
-
uvicorn.run(app, host=host, port=port)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|