beaver-db 2.0rc2__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.
- beaver/__init__.py +16 -0
- beaver/blobs.py +223 -0
- beaver/bridge.py +167 -0
- beaver/cache.py +274 -0
- beaver/channels.py +249 -0
- beaver/cli/__init__.py +133 -0
- beaver/cli/blobs.py +225 -0
- beaver/cli/channels.py +166 -0
- beaver/cli/collections.py +500 -0
- beaver/cli/dicts.py +171 -0
- beaver/cli/lists.py +244 -0
- beaver/cli/locks.py +202 -0
- beaver/cli/logs.py +248 -0
- beaver/cli/queues.py +215 -0
- beaver/client.py +392 -0
- beaver/core.py +646 -0
- beaver/dicts.py +314 -0
- beaver/docs.py +459 -0
- beaver/events.py +155 -0
- beaver/graphs.py +212 -0
- beaver/lists.py +337 -0
- beaver/locks.py +186 -0
- beaver/logs.py +187 -0
- beaver/manager.py +203 -0
- beaver/queries.py +66 -0
- beaver/queues.py +215 -0
- beaver/security.py +144 -0
- beaver/server.py +452 -0
- beaver/sketches.py +307 -0
- beaver/types.py +32 -0
- beaver/vectors.py +198 -0
- beaver_db-2.0rc2.dist-info/METADATA +149 -0
- beaver_db-2.0rc2.dist-info/RECORD +36 -0
- beaver_db-2.0rc2.dist-info/WHEEL +4 -0
- beaver_db-2.0rc2.dist-info/entry_points.txt +2 -0
- beaver_db-2.0rc2.dist-info/licenses/LICENSE +21 -0
beaver/graphs.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import (
|
|
3
|
+
Iterator,
|
|
4
|
+
AsyncIterator,
|
|
5
|
+
Protocol,
|
|
6
|
+
runtime_checkable,
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Tuple,
|
|
9
|
+
Any,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from .manager import AsyncBeaverBase, atomic, emits
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .core import AsyncBeaverDB
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Edge[T](BaseModel):
|
|
21
|
+
"""
|
|
22
|
+
Represents a directed edge in the graph with typed metadata.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
source: str
|
|
26
|
+
target: str
|
|
27
|
+
label: str
|
|
28
|
+
metadata: T | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class IBeaverGraph[T: BaseModel](Protocol):
|
|
33
|
+
"""Protocol exposed to the user via BeaverBridge."""
|
|
34
|
+
|
|
35
|
+
def link(
|
|
36
|
+
self, source: str, target: str, label: str, metadata: T | None = None
|
|
37
|
+
) -> None: ...
|
|
38
|
+
def unlink(self, source: str, target: str, label: str) -> None: ...
|
|
39
|
+
|
|
40
|
+
def linked(self, source: str, target: str, label: str) -> bool: ...
|
|
41
|
+
def get(self, source: str, target: str, label: str) -> Edge[T]: ...
|
|
42
|
+
|
|
43
|
+
def children(self, source: str, label: str | None = None) -> Iterator[str]: ...
|
|
44
|
+
def parents(self, target: str, label: str | None = None) -> Iterator[str]: ...
|
|
45
|
+
def edges(self, source: str, label: str | None = None) -> Iterator[Edge[T]]: ...
|
|
46
|
+
|
|
47
|
+
def clear(self) -> None: ...
|
|
48
|
+
def count(self) -> int: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AsyncBeaverGraph[T: BaseModel](AsyncBeaverBase[T]):
|
|
52
|
+
"""
|
|
53
|
+
Manages directed relationships between entities.
|
|
54
|
+
|
|
55
|
+
The generic type T refers to the type of the Edge Metadata.
|
|
56
|
+
|
|
57
|
+
Table managed:
|
|
58
|
+
- __beaver_edges__ (collection, source_item_id, target_item_id, label, metadata)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, name: str, db: "AsyncBeaverDB", model: type[T] | None = None):
|
|
62
|
+
super().__init__(name, db, model)
|
|
63
|
+
# Construct the concrete Edge model for this manager
|
|
64
|
+
self._edge_model = Edge[model] if model else Edge
|
|
65
|
+
|
|
66
|
+
@emits(
|
|
67
|
+
"link",
|
|
68
|
+
payload=lambda s, t, l, *args, **kwargs: dict(source=s, target=t, label=l),
|
|
69
|
+
)
|
|
70
|
+
@atomic
|
|
71
|
+
async def link(
|
|
72
|
+
self, source: str, target: str, label: str, metadata: T | None = None
|
|
73
|
+
):
|
|
74
|
+
"""Creates or updates a directed edge."""
|
|
75
|
+
# Use the base manager's serializer to handle Pydantic models vs dicts
|
|
76
|
+
# Default to empty dict if None (and no model enforced)
|
|
77
|
+
meta_json = self._serialize(metadata) if metadata else None
|
|
78
|
+
|
|
79
|
+
await self.connection.execute(
|
|
80
|
+
"""
|
|
81
|
+
INSERT OR REPLACE INTO __beaver_edges__
|
|
82
|
+
(collection, source_item_id, target_item_id, label, metadata)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?)
|
|
84
|
+
""",
|
|
85
|
+
(self._name, source, target, label, meta_json),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@emits(
|
|
89
|
+
"unlink",
|
|
90
|
+
payload=lambda s, t, l, *args, **kwargs: dict(source=s, target=t, label=l),
|
|
91
|
+
)
|
|
92
|
+
@atomic
|
|
93
|
+
async def unlink(self, source: str, target: str, label: str):
|
|
94
|
+
"""Removes a directed edge."""
|
|
95
|
+
await self.connection.execute(
|
|
96
|
+
"""
|
|
97
|
+
DELETE FROM __beaver_edges__
|
|
98
|
+
WHERE collection = ? AND source_item_id = ? AND target_item_id = ? AND label = ?
|
|
99
|
+
""",
|
|
100
|
+
(self._name, source, target, label),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def linked(self, source: str, target: str, label: str) -> bool:
|
|
104
|
+
"""Checks if a specific edge exists."""
|
|
105
|
+
cursor = await self.connection.execute(
|
|
106
|
+
"""
|
|
107
|
+
SELECT 1 FROM __beaver_edges__
|
|
108
|
+
WHERE collection = ? AND source_item_id = ? AND target_item_id = ? AND label = ?
|
|
109
|
+
LIMIT 1
|
|
110
|
+
""",
|
|
111
|
+
(self._name, source, target, label),
|
|
112
|
+
)
|
|
113
|
+
return await cursor.fetchone() is not None
|
|
114
|
+
|
|
115
|
+
async def get(self, source: str, target: str, label: str) -> Edge[T]:
|
|
116
|
+
"""
|
|
117
|
+
Retrieves a specific edge with metadata.
|
|
118
|
+
Raises KeyError if not found.
|
|
119
|
+
"""
|
|
120
|
+
cursor = await self.connection.execute(
|
|
121
|
+
"""
|
|
122
|
+
SELECT metadata FROM __beaver_edges__
|
|
123
|
+
WHERE collection = ? AND source_item_id = ? AND target_item_id = ? AND label = ?
|
|
124
|
+
""",
|
|
125
|
+
(self._name, source, target, label),
|
|
126
|
+
)
|
|
127
|
+
row = await cursor.fetchone()
|
|
128
|
+
|
|
129
|
+
if not row:
|
|
130
|
+
raise KeyError(f"Edge not found: {source} -[{label}]-> {target}")
|
|
131
|
+
|
|
132
|
+
# Deserialize using the base manager (handles T validation)
|
|
133
|
+
meta_str = row["metadata"]
|
|
134
|
+
meta_val = self._deserialize(meta_str)
|
|
135
|
+
|
|
136
|
+
return self._edge_model(
|
|
137
|
+
source=source, target=target, label=label, metadata=meta_val
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def children(
|
|
141
|
+
self, source: str, label: str | None = None
|
|
142
|
+
) -> AsyncIterator[str]:
|
|
143
|
+
"""
|
|
144
|
+
Yields target IDs connected by outgoing edges from 'source'.
|
|
145
|
+
(Forward traversal: source -> target)
|
|
146
|
+
"""
|
|
147
|
+
query = "SELECT target_item_id FROM __beaver_edges__ WHERE collection = ? AND source_item_id = ?"
|
|
148
|
+
params = [self._name, source]
|
|
149
|
+
|
|
150
|
+
if label:
|
|
151
|
+
query += " AND label = ?"
|
|
152
|
+
params.append(label)
|
|
153
|
+
|
|
154
|
+
cursor = await self.connection.execute(query, tuple(params))
|
|
155
|
+
async for row in cursor:
|
|
156
|
+
yield row["target_item_id"]
|
|
157
|
+
|
|
158
|
+
async def parents(
|
|
159
|
+
self, target: str, label: str | None = None
|
|
160
|
+
) -> AsyncIterator[str]:
|
|
161
|
+
"""
|
|
162
|
+
Yields source IDs connected by incoming edges to 'target'.
|
|
163
|
+
(Reverse traversal: source -> target)
|
|
164
|
+
"""
|
|
165
|
+
# This relies on the index (collection, target_item_id) for performance
|
|
166
|
+
query = "SELECT source_item_id FROM __beaver_edges__ WHERE collection = ? AND target_item_id = ?"
|
|
167
|
+
params = [self._name, target]
|
|
168
|
+
|
|
169
|
+
if label:
|
|
170
|
+
query += " AND label = ?"
|
|
171
|
+
params.append(label)
|
|
172
|
+
|
|
173
|
+
cursor = await self.connection.execute(query, tuple(params))
|
|
174
|
+
async for row in cursor:
|
|
175
|
+
yield row["source_item_id"]
|
|
176
|
+
|
|
177
|
+
async def edges(
|
|
178
|
+
self, source: str, label: str | None = None
|
|
179
|
+
) -> AsyncIterator[Edge[T]]:
|
|
180
|
+
"""
|
|
181
|
+
Yields full Edge objects (including metadata) originating from 'source'.
|
|
182
|
+
"""
|
|
183
|
+
query = "SELECT target_item_id, label, metadata FROM __beaver_edges__ WHERE collection = ? AND source_item_id = ?"
|
|
184
|
+
params = [self._name, source]
|
|
185
|
+
|
|
186
|
+
if label:
|
|
187
|
+
query += " AND label = ?"
|
|
188
|
+
params.append(label)
|
|
189
|
+
|
|
190
|
+
cursor = await self.connection.execute(query, tuple(params))
|
|
191
|
+
async for row in cursor:
|
|
192
|
+
meta_str = row["metadata"]
|
|
193
|
+
meta_val = self._deserialize(meta_str) if meta_str else None
|
|
194
|
+
|
|
195
|
+
yield self._edge_model(
|
|
196
|
+
source=source,
|
|
197
|
+
target=row["target_item_id"],
|
|
198
|
+
label=row["label"],
|
|
199
|
+
metadata=meta_val,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def count(self) -> int:
|
|
203
|
+
cursor = await self.connection.execute(
|
|
204
|
+
"SELECT COUNT(*) FROM __beaver_edges__ WHERE collection = ?", (self._name,)
|
|
205
|
+
)
|
|
206
|
+
return (await cursor.fetchone())[0]
|
|
207
|
+
|
|
208
|
+
@atomic
|
|
209
|
+
async def clear(self):
|
|
210
|
+
await self.connection.execute(
|
|
211
|
+
"DELETE FROM __beaver_edges__ WHERE collection = ?", (self._name,)
|
|
212
|
+
)
|
beaver/lists.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import (
|
|
3
|
+
Iterator,
|
|
4
|
+
Union,
|
|
5
|
+
IO,
|
|
6
|
+
overload,
|
|
7
|
+
Protocol,
|
|
8
|
+
runtime_checkable,
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
List,
|
|
11
|
+
)
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from .manager import AsyncBeaverBase, atomic, emits
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .core import AsyncBeaverDB
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class IBeaverList[T: BaseModel](Protocol):
|
|
24
|
+
"""
|
|
25
|
+
The Synchronous Protocol exposed to the user via BeaverBridge.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, index: int | slice) -> T | List[T]: ...
|
|
29
|
+
def __setitem__(self, index: int, value: T) -> None: ...
|
|
30
|
+
def __delitem__(self, index: int) -> None: ...
|
|
31
|
+
def __len__(self) -> int: ...
|
|
32
|
+
def __iter__(self) -> Iterator[T]: ...
|
|
33
|
+
def __contains__(self, value: T) -> bool: ...
|
|
34
|
+
|
|
35
|
+
def get(self, index: int | slice) -> T | List[T]: ...
|
|
36
|
+
def set(self, index: int, value: T) -> None: ...
|
|
37
|
+
def delete(self, index: int) -> None: ...
|
|
38
|
+
|
|
39
|
+
def count(self) -> int: ...
|
|
40
|
+
def push(self, value: T) -> None: ...
|
|
41
|
+
def prepend(self, value: T) -> None: ...
|
|
42
|
+
def insert(self, index: int, value: T) -> None: ...
|
|
43
|
+
def pop(self) -> T | None: ...
|
|
44
|
+
def deque(self) -> T | None: ...
|
|
45
|
+
def clear(self) -> None: ...
|
|
46
|
+
def dump(self, fp: IO[str] | None = None) -> dict | None: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AsyncBeaverList[T: BaseModel](AsyncBeaverBase[T]):
|
|
50
|
+
"""
|
|
51
|
+
A wrapper providing a Pythonic, persistent list in the database.
|
|
52
|
+
Refactored for Async-First architecture (v2.0).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
async def _get_dump_object(self) -> dict:
|
|
56
|
+
items = []
|
|
57
|
+
async for item in self:
|
|
58
|
+
item_value = item
|
|
59
|
+
if self._model and isinstance(item, BaseModel):
|
|
60
|
+
item_value = json.loads(item.model_dump_json())
|
|
61
|
+
items.append(item_value)
|
|
62
|
+
|
|
63
|
+
metadata = {
|
|
64
|
+
"type": "List",
|
|
65
|
+
"name": self._name,
|
|
66
|
+
"count": len(items),
|
|
67
|
+
"dump_date": datetime.now(timezone.utc).isoformat(),
|
|
68
|
+
}
|
|
69
|
+
return {"metadata": metadata, "items": items}
|
|
70
|
+
|
|
71
|
+
async def dump(self, fp: IO[str] | None = None) -> dict | None:
|
|
72
|
+
"""
|
|
73
|
+
Dumps the entire contents of the list to a JSON-compatible object.
|
|
74
|
+
"""
|
|
75
|
+
# We can acquire the public lock for consistency during dump
|
|
76
|
+
async with self:
|
|
77
|
+
dump_object = await self._get_dump_object()
|
|
78
|
+
|
|
79
|
+
if fp:
|
|
80
|
+
json.dump(dump_object, fp, indent=2)
|
|
81
|
+
return None
|
|
82
|
+
return dump_object
|
|
83
|
+
|
|
84
|
+
async def count(self) -> int:
|
|
85
|
+
"""Returns the number of items in the list."""
|
|
86
|
+
cursor = await self.connection.execute(
|
|
87
|
+
"SELECT COUNT(*) FROM __beaver_lists__ WHERE list_name = ?", (self._name,)
|
|
88
|
+
)
|
|
89
|
+
row = await cursor.fetchone()
|
|
90
|
+
return row[0] if row else 0
|
|
91
|
+
|
|
92
|
+
@atomic
|
|
93
|
+
async def get(self, index: Union[int, slice]) -> T | list[T]:
|
|
94
|
+
"""
|
|
95
|
+
Retrieves an item or slice from the list.
|
|
96
|
+
Mapped from __getitem__ by the Bridge.
|
|
97
|
+
"""
|
|
98
|
+
# Handle Slice
|
|
99
|
+
if isinstance(index, slice):
|
|
100
|
+
# We need the length to calculate indices
|
|
101
|
+
list_len = await self.count()
|
|
102
|
+
start, stop, step = index.indices(list_len)
|
|
103
|
+
|
|
104
|
+
if step != 1:
|
|
105
|
+
raise ValueError("Slicing with a step is not supported.")
|
|
106
|
+
|
|
107
|
+
limit = stop - start
|
|
108
|
+
if limit <= 0:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
cursor = await self.connection.execute(
|
|
112
|
+
"SELECT item_value FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT ? OFFSET ?",
|
|
113
|
+
(self._name, limit, start),
|
|
114
|
+
)
|
|
115
|
+
results = [
|
|
116
|
+
self._deserialize(row["item_value"]) for row in await cursor.fetchall()
|
|
117
|
+
]
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
# Handle Integer
|
|
121
|
+
elif isinstance(index, int):
|
|
122
|
+
list_len = await self.count()
|
|
123
|
+
if index < -list_len or index >= list_len:
|
|
124
|
+
raise IndexError("List index out of range.")
|
|
125
|
+
|
|
126
|
+
offset = index if index >= 0 else list_len + index
|
|
127
|
+
|
|
128
|
+
cursor = await self.connection.execute(
|
|
129
|
+
"SELECT item_value FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
|
|
130
|
+
(self._name, offset),
|
|
131
|
+
)
|
|
132
|
+
result = await cursor.fetchone()
|
|
133
|
+
if not result:
|
|
134
|
+
raise IndexError("List index out of range.")
|
|
135
|
+
|
|
136
|
+
return self._deserialize(result["item_value"])
|
|
137
|
+
|
|
138
|
+
else:
|
|
139
|
+
raise TypeError("List indices must be integers or slices.")
|
|
140
|
+
|
|
141
|
+
@emits("set", payload=lambda index, *args, **kwargs: dict(index=index))
|
|
142
|
+
@atomic
|
|
143
|
+
async def set(self, index: int, value: T):
|
|
144
|
+
"""
|
|
145
|
+
Sets the value of an item at a specific index.
|
|
146
|
+
Mapped from __setitem__ by the Bridge.
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(index, int):
|
|
149
|
+
raise TypeError("List indices must be integers.")
|
|
150
|
+
|
|
151
|
+
list_len = await self.count()
|
|
152
|
+
if index < -list_len or index >= list_len:
|
|
153
|
+
raise IndexError("List index out of range.")
|
|
154
|
+
|
|
155
|
+
offset = index if index >= 0 else list_len + index
|
|
156
|
+
|
|
157
|
+
# Find the rowid of the item to update
|
|
158
|
+
cursor = await self.connection.execute(
|
|
159
|
+
"SELECT rowid FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
|
|
160
|
+
(self._name, offset),
|
|
161
|
+
)
|
|
162
|
+
result = await cursor.fetchone()
|
|
163
|
+
if not result:
|
|
164
|
+
raise IndexError("List index out of range during update.")
|
|
165
|
+
|
|
166
|
+
rowid_to_update = result["rowid"]
|
|
167
|
+
|
|
168
|
+
# Update the value
|
|
169
|
+
await self.connection.execute(
|
|
170
|
+
"UPDATE __beaver_lists__ SET item_value = ? WHERE rowid = ?",
|
|
171
|
+
(self._serialize(value), rowid_to_update),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@emits("del", payload=lambda index, *args, **kwargs: dict(index=index))
|
|
175
|
+
@atomic
|
|
176
|
+
async def delete(self, index: int):
|
|
177
|
+
"""
|
|
178
|
+
Deletes an item at a specific index.
|
|
179
|
+
Mapped from __delitem__ by the Bridge.
|
|
180
|
+
"""
|
|
181
|
+
if not isinstance(index, int):
|
|
182
|
+
raise TypeError("List indices must be integers.")
|
|
183
|
+
|
|
184
|
+
list_len = await self.count()
|
|
185
|
+
if index < -list_len or index >= list_len:
|
|
186
|
+
raise IndexError("List index out of range.")
|
|
187
|
+
|
|
188
|
+
offset = index if index >= 0 else list_len + index
|
|
189
|
+
|
|
190
|
+
# Find rowid
|
|
191
|
+
cursor = await self.connection.execute(
|
|
192
|
+
"SELECT rowid FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
|
|
193
|
+
(self._name, offset),
|
|
194
|
+
)
|
|
195
|
+
result = await cursor.fetchone()
|
|
196
|
+
if not result:
|
|
197
|
+
raise IndexError("List index out of range during delete.")
|
|
198
|
+
|
|
199
|
+
rowid_to_delete = result["rowid"]
|
|
200
|
+
|
|
201
|
+
await self.connection.execute(
|
|
202
|
+
"DELETE FROM __beaver_lists__ WHERE rowid = ?", (rowid_to_delete,)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# --- Iterators ---
|
|
206
|
+
|
|
207
|
+
async def __aiter__(self):
|
|
208
|
+
"""Async iterator for the list."""
|
|
209
|
+
cursor = await self.connection.execute(
|
|
210
|
+
"SELECT item_value FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC",
|
|
211
|
+
(self._name,),
|
|
212
|
+
)
|
|
213
|
+
async for row in cursor:
|
|
214
|
+
yield self._deserialize(row["item_value"])
|
|
215
|
+
|
|
216
|
+
async def contains(self, value: T) -> bool:
|
|
217
|
+
"""Checks for existence of an item."""
|
|
218
|
+
serialized = self._serialize(value)
|
|
219
|
+
cursor = await self.connection.execute(
|
|
220
|
+
"SELECT 1 FROM __beaver_lists__ WHERE list_name = ? AND item_value = ? LIMIT 1",
|
|
221
|
+
(self._name, serialized),
|
|
222
|
+
)
|
|
223
|
+
return await cursor.fetchone() is not None
|
|
224
|
+
|
|
225
|
+
async def _get_order_at_index(self, index: int) -> float:
|
|
226
|
+
"""Helper to get the float item_order at a specific index."""
|
|
227
|
+
cursor = await self.connection.execute(
|
|
228
|
+
"SELECT item_order FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
|
|
229
|
+
(self._name, index),
|
|
230
|
+
)
|
|
231
|
+
result = await cursor.fetchone()
|
|
232
|
+
|
|
233
|
+
if result:
|
|
234
|
+
return result[0]
|
|
235
|
+
raise IndexError(f"{index} out of range.")
|
|
236
|
+
|
|
237
|
+
@emits("push", payload=lambda *args, **kwargs: dict())
|
|
238
|
+
@atomic
|
|
239
|
+
async def push(self, value: T):
|
|
240
|
+
"""Pushes an item to the end of the list."""
|
|
241
|
+
cursor = await self.connection.execute(
|
|
242
|
+
"SELECT MAX(item_order) FROM __beaver_lists__ WHERE list_name = ?",
|
|
243
|
+
(self._name,),
|
|
244
|
+
)
|
|
245
|
+
row = await cursor.fetchone()
|
|
246
|
+
max_order = row[0] if row and row[0] is not None else 0.0
|
|
247
|
+
new_order = max_order + 1.0
|
|
248
|
+
|
|
249
|
+
await self.connection.execute(
|
|
250
|
+
"INSERT INTO __beaver_lists__ (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
251
|
+
(self._name, new_order, self._serialize(value)),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@emits("prepend", payload=lambda *args, **kwargs: dict())
|
|
255
|
+
@atomic
|
|
256
|
+
async def prepend(self, value: T):
|
|
257
|
+
"""Prepends an item to the beginning of the list."""
|
|
258
|
+
cursor = await self.connection.execute(
|
|
259
|
+
"SELECT MIN(item_order) FROM __beaver_lists__ WHERE list_name = ?",
|
|
260
|
+
(self._name,),
|
|
261
|
+
)
|
|
262
|
+
row = await cursor.fetchone()
|
|
263
|
+
min_order = row[0] if row and row[0] is not None else 0.0
|
|
264
|
+
new_order = min_order - 1.0
|
|
265
|
+
|
|
266
|
+
await self.connection.execute(
|
|
267
|
+
"INSERT INTO __beaver_lists__ (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
268
|
+
(self._name, new_order, self._serialize(value)),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@emits("insert", payload=lambda index, *args, **kwargs: dict(index=index))
|
|
272
|
+
@atomic
|
|
273
|
+
async def insert(self, index: int, value: T):
|
|
274
|
+
"""Inserts an item at a specific index using float order logic."""
|
|
275
|
+
list_len = await self.count()
|
|
276
|
+
|
|
277
|
+
if index <= 0:
|
|
278
|
+
await self.prepend(value)
|
|
279
|
+
return
|
|
280
|
+
if index >= list_len:
|
|
281
|
+
await self.push(value)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Midpoint insertion
|
|
285
|
+
order_before = await self._get_order_at_index(index - 1)
|
|
286
|
+
order_after = await self._get_order_at_index(index)
|
|
287
|
+
new_order = order_before + (order_after - order_before) / 2.0
|
|
288
|
+
|
|
289
|
+
await self.connection.execute(
|
|
290
|
+
"INSERT INTO __beaver_lists__ (list_name, item_order, item_value) VALUES (?, ?, ?)",
|
|
291
|
+
(self._name, new_order, self._serialize(value)),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
@emits("pop", payload=lambda *args, **kwargs: dict())
|
|
295
|
+
@atomic
|
|
296
|
+
async def pop(self) -> T | None:
|
|
297
|
+
"""Removes and returns the last item."""
|
|
298
|
+
cursor = await self.connection.execute(
|
|
299
|
+
"SELECT rowid, item_value FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order DESC LIMIT 1",
|
|
300
|
+
(self._name,),
|
|
301
|
+
)
|
|
302
|
+
result = await cursor.fetchone()
|
|
303
|
+
if not result:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
rowid_to_delete, value_to_return = result
|
|
307
|
+
await self.connection.execute(
|
|
308
|
+
"DELETE FROM __beaver_lists__ WHERE rowid = ?", (rowid_to_delete,)
|
|
309
|
+
)
|
|
310
|
+
return self._deserialize(value_to_return)
|
|
311
|
+
|
|
312
|
+
@emits("deque", payload=lambda *args, **kwargs: dict())
|
|
313
|
+
@atomic
|
|
314
|
+
async def deque(self) -> T | None:
|
|
315
|
+
"""Removes and returns the first item."""
|
|
316
|
+
cursor = await self.connection.execute(
|
|
317
|
+
"SELECT rowid, item_value FROM __beaver_lists__ WHERE list_name = ? ORDER BY item_order ASC LIMIT 1",
|
|
318
|
+
(self._name,),
|
|
319
|
+
)
|
|
320
|
+
result = await cursor.fetchone()
|
|
321
|
+
if not result:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
rowid_to_delete, value_to_return = result
|
|
325
|
+
await self.connection.execute(
|
|
326
|
+
"DELETE FROM __beaver_lists__ WHERE rowid = ?", (rowid_to_delete,)
|
|
327
|
+
)
|
|
328
|
+
return self._deserialize(value_to_return)
|
|
329
|
+
|
|
330
|
+
@emits("clear", payload=lambda *args, **kwargs: dict())
|
|
331
|
+
@atomic
|
|
332
|
+
async def clear(self):
|
|
333
|
+
"""Atomically removes all items."""
|
|
334
|
+
await self.connection.execute(
|
|
335
|
+
"DELETE FROM __beaver_lists__ WHERE list_name = ?",
|
|
336
|
+
(self._name,),
|
|
337
|
+
)
|