beaver-db 0.16.7__py3-none-any.whl → 0.17.0__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/dicts.py CHANGED
@@ -3,15 +3,14 @@ import sqlite3
3
3
  import time
4
4
  from typing import Any, Iterator, Tuple, Type
5
5
 
6
- from .types import JsonSerializable
7
-
6
+ from .types import JsonSerializable, IDatabase
8
7
 
9
8
  class DictManager[T]:
10
9
  """A wrapper providing a Pythonic interface to a dictionary in the database."""
11
10
 
12
- def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
11
+ def __init__(self, name: str, db: IDatabase, model: Type[T] | None = None):
13
12
  self._name = name
14
- self._conn = conn
13
+ self._db = db
15
14
  self._model = model
16
15
 
17
16
  def _serialize(self, value: T) -> str:
@@ -40,8 +39,8 @@ class DictManager[T]:
40
39
  raise ValueError("ttl_seconds must be a positive integer.")
41
40
  expires_at = time.time() + ttl_seconds
42
41
 
43
- with self._conn:
44
- self._conn.execute(
42
+ with self._db.connection:
43
+ self._db.connection.execute(
45
44
  "INSERT OR REPLACE INTO beaver_dicts (dict_name, key, value, expires_at) VALUES (?, ?, ?, ?)",
46
45
  (self._name, key, self._serialize(value), expires_at),
47
46
  )
@@ -55,7 +54,7 @@ class DictManager[T]:
55
54
 
56
55
  def __getitem__(self, key: str) -> T:
57
56
  """Retrieves a value for a given key, raising KeyError if expired."""
58
- cursor = self._conn.cursor()
57
+ cursor = self._db.connection.cursor()
59
58
  cursor.execute(
60
59
  "SELECT value, expires_at FROM beaver_dicts WHERE dict_name = ? AND key = ?",
61
60
  (self._name, key),
@@ -70,7 +69,7 @@ class DictManager[T]:
70
69
 
71
70
  if expires_at is not None and time.time() > expires_at:
72
71
  # Expired: delete the key and raise KeyError
73
- with self._conn:
72
+ with self._db.connection:
74
73
  cursor.execute(
75
74
  "DELETE FROM beaver_dicts WHERE dict_name = ? AND key = ?",
76
75
  (self._name, key),
@@ -94,8 +93,8 @@ class DictManager[T]:
94
93
 
95
94
  def __delitem__(self, key: str):
96
95
  """Deletes a key-value pair (e.g., `del my_dict[key]`)."""
97
- with self._conn:
98
- cursor = self._conn.cursor()
96
+ with self._db.connection:
97
+ cursor = self._db.connection.cursor()
99
98
  cursor.execute(
100
99
  "DELETE FROM beaver_dicts WHERE dict_name = ? AND key = ?",
101
100
  (self._name, key),
@@ -105,7 +104,7 @@ class DictManager[T]:
105
104
 
106
105
  def __len__(self) -> int:
107
106
  """Returns the number of items in the dictionary."""
108
- cursor = self._conn.cursor()
107
+ cursor = self._db.connection.cursor()
109
108
  cursor.execute(
110
109
  "SELECT COUNT(*) FROM beaver_dicts WHERE dict_name = ?", (self._name,)
111
110
  )
@@ -119,7 +118,7 @@ class DictManager[T]:
119
118
 
120
119
  def keys(self) -> Iterator[str]:
121
120
  """Returns an iterator over the dictionary's keys."""
122
- cursor = self._conn.cursor()
121
+ cursor = self._db.connection.cursor()
123
122
  cursor.execute(
124
123
  "SELECT key FROM beaver_dicts WHERE dict_name = ?", (self._name,)
125
124
  )
@@ -129,7 +128,7 @@ class DictManager[T]:
129
128
 
130
129
  def values(self) -> Iterator[T]:
131
130
  """Returns an iterator over the dictionary's values."""
132
- cursor = self._conn.cursor()
131
+ cursor = self._db.connection.cursor()
133
132
  cursor.execute(
134
133
  "SELECT value FROM beaver_dicts WHERE dict_name = ?", (self._name,)
135
134
  )
@@ -139,7 +138,7 @@ class DictManager[T]:
139
138
 
140
139
  def items(self) -> Iterator[Tuple[str, T]]:
141
140
  """Returns an iterator over the dictionary's items (key-value pairs)."""
142
- cursor = self._conn.cursor()
141
+ cursor = self._db.connection.cursor()
143
142
  cursor.execute(
144
143
  "SELECT key, value FROM beaver_dicts WHERE dict_name = ?", (self._name,)
145
144
  )
beaver/lists.py CHANGED
@@ -2,15 +2,15 @@ import json
2
2
  import sqlite3
3
3
  from typing import Any, Iterator, Type, Union
4
4
 
5
- from .types import JsonSerializable
5
+ from .types import JsonSerializable, IDatabase
6
6
 
7
7
 
8
8
  class ListManager[T]:
9
9
  """A wrapper providing a Pythonic, full-featured interface to a list in the database."""
10
10
 
11
- def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
11
+ def __init__(self, name: str, db: IDatabase, model: Type[T] | None = None):
12
12
  self._name = name
13
- self._conn = conn
13
+ self._db = db
14
14
  self._model = model
15
15
 
16
16
  def _serialize(self, value: T) -> str:
@@ -27,7 +27,7 @@ class ListManager[T]:
27
27
 
28
28
  def __len__(self) -> int:
29
29
  """Returns the number of items in the list (e.g., `len(my_list)`)."""
30
- cursor = self._conn.cursor()
30
+ cursor = self._db.connection.cursor()
31
31
  cursor.execute(
32
32
  "SELECT COUNT(*) FROM beaver_lists WHERE list_name = ?", (self._name,)
33
33
  )
@@ -40,7 +40,7 @@ class ListManager[T]:
40
40
  Retrieves an item or slice from the list (e.g., `my_list[0]`, `my_list[1:3]`).
41
41
  """
42
42
  if isinstance(key, slice):
43
- with self._conn:
43
+ with self._db.connection:
44
44
  start, stop, step = key.indices(len(self))
45
45
  if step != 1:
46
46
  raise ValueError("Slicing with a step is not supported.")
@@ -49,7 +49,7 @@ class ListManager[T]:
49
49
  if limit <= 0:
50
50
  return []
51
51
 
52
- cursor = self._conn.cursor()
52
+ cursor = self._db.connection.cursor()
53
53
  cursor.execute(
54
54
  "SELECT item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT ? OFFSET ?",
55
55
  (self._name, limit, start),
@@ -59,14 +59,14 @@ class ListManager[T]:
59
59
  return results
60
60
 
61
61
  elif isinstance(key, int):
62
- with self._conn:
62
+ with self._db.connection:
63
63
  list_len = len(self)
64
64
  if key < -list_len or key >= list_len:
65
65
  raise IndexError("List index out of range.")
66
66
 
67
67
  offset = key if key >= 0 else list_len + key
68
68
 
69
- cursor = self._conn.cursor()
69
+ cursor = self._db.connection.cursor()
70
70
  cursor.execute(
71
71
  "SELECT item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
72
72
  (self._name, offset),
@@ -83,14 +83,14 @@ class ListManager[T]:
83
83
  if not isinstance(key, int):
84
84
  raise TypeError("List indices must be integers.")
85
85
 
86
- with self._conn:
86
+ with self._db.connection:
87
87
  list_len = len(self)
88
88
  if key < -list_len or key >= list_len:
89
89
  raise IndexError("List index out of range.")
90
90
 
91
91
  offset = key if key >= 0 else list_len + key
92
92
 
93
- cursor = self._conn.cursor()
93
+ cursor = self._db.connection.cursor()
94
94
  # Find the rowid of the item to update
95
95
  cursor.execute(
96
96
  "SELECT rowid FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
@@ -112,14 +112,14 @@ class ListManager[T]:
112
112
  if not isinstance(key, int):
113
113
  raise TypeError("List indices must be integers.")
114
114
 
115
- with self._conn:
115
+ with self._db.connection:
116
116
  list_len = len(self)
117
117
  if key < -list_len or key >= list_len:
118
118
  raise IndexError("List index out of range.")
119
119
 
120
120
  offset = key if key >= 0 else list_len + key
121
121
 
122
- cursor = self._conn.cursor()
122
+ cursor = self._db.connection.cursor()
123
123
  # Find the rowid of the item to delete
124
124
  cursor.execute(
125
125
  "SELECT rowid FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
@@ -135,7 +135,7 @@ class ListManager[T]:
135
135
 
136
136
  def __iter__(self) -> Iterator[T]:
137
137
  """Returns an iterator for the list."""
138
- cursor = self._conn.cursor()
138
+ cursor = self._db.connection.cursor()
139
139
  cursor.execute(
140
140
  "SELECT item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC",
141
141
  (self._name,)
@@ -146,7 +146,7 @@ class ListManager[T]:
146
146
 
147
147
  def __contains__(self, value: T) -> bool:
148
148
  """Checks for the existence of an item in the list (e.g., `'item' in my_list`)."""
149
- cursor = self._conn.cursor()
149
+ cursor = self._db.connection.cursor()
150
150
  cursor.execute(
151
151
  "SELECT 1 FROM beaver_lists WHERE list_name = ? AND item_value = ? LIMIT 1",
152
152
  (self._name, self._serialize(value))
@@ -161,7 +161,7 @@ class ListManager[T]:
161
161
 
162
162
  def _get_order_at_index(self, index: int) -> float:
163
163
  """Helper to get the float `item_order` at a specific index."""
164
- cursor = self._conn.cursor()
164
+ cursor = self._db.connection.cursor()
165
165
  cursor.execute(
166
166
  "SELECT item_order FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT 1 OFFSET ?",
167
167
  (self._name, index),
@@ -176,8 +176,8 @@ class ListManager[T]:
176
176
 
177
177
  def push(self, value: T):
178
178
  """Pushes an item to the end of the list."""
179
- with self._conn:
180
- cursor = self._conn.cursor()
179
+ with self._db.connection:
180
+ cursor = self._db.connection.cursor()
181
181
  cursor.execute(
182
182
  "SELECT MAX(item_order) FROM beaver_lists WHERE list_name = ?",
183
183
  (self._name,),
@@ -192,8 +192,8 @@ class ListManager[T]:
192
192
 
193
193
  def prepend(self, value: T):
194
194
  """Prepends an item to the beginning of the list."""
195
- with self._conn:
196
- cursor = self._conn.cursor()
195
+ with self._db.connection:
196
+ cursor = self._db.connection.cursor()
197
197
  cursor.execute(
198
198
  "SELECT MIN(item_order) FROM beaver_lists WHERE list_name = ?",
199
199
  (self._name,),
@@ -208,7 +208,7 @@ class ListManager[T]:
208
208
 
209
209
  def insert(self, index: int, value: T):
210
210
  """Inserts an item at a specific index."""
211
- with self._conn:
211
+ with self._db.connection:
212
212
  list_len = len(self)
213
213
  if index <= 0:
214
214
  self.prepend(value)
@@ -222,15 +222,15 @@ class ListManager[T]:
222
222
  order_after = self._get_order_at_index(index)
223
223
  new_order = order_before + (order_after - order_before) / 2.0
224
224
 
225
- self._conn.execute(
225
+ self._db.connection.execute(
226
226
  "INSERT INTO beaver_lists (list_name, item_order, item_value) VALUES (?, ?, ?)",
227
227
  (self._name, new_order, self._serialize(value)),
228
228
  )
229
229
 
230
230
  def pop(self) -> T | None:
231
231
  """Removes and returns the last item from the list."""
232
- with self._conn:
233
- cursor = self._conn.cursor()
232
+ with self._db.connection:
233
+ cursor = self._db.connection.cursor()
234
234
  cursor.execute(
235
235
  "SELECT rowid, item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order DESC LIMIT 1",
236
236
  (self._name,),
@@ -247,8 +247,8 @@ class ListManager[T]:
247
247
 
248
248
  def deque(self) -> T | None:
249
249
  """Removes and returns the first item from the list."""
250
- with self._conn:
251
- cursor = self._conn.cursor()
250
+ with self._db.connection:
251
+ cursor = self._db.connection.cursor()
252
252
  cursor.execute(
253
253
  "SELECT rowid, item_value FROM beaver_lists WHERE list_name = ? ORDER BY item_order ASC LIMIT 1",
254
254
  (self._name,),
beaver/logs.py CHANGED
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone
8
8
  from queue import Empty, Queue
9
9
  from typing import Any, AsyncIterator, Callable, Iterator, Type, TypeVar
10
10
 
11
- from .types import JsonSerializable
11
+ from .types import JsonSerializable, IDatabase
12
12
 
13
13
 
14
14
  # A special message object used to signal the iterator to gracefully shut down.
@@ -23,14 +23,14 @@ class LiveIterator[T,R]:
23
23
 
24
24
  def __init__(
25
25
  self,
26
- db_path: str,
26
+ db: IDatabase,
27
27
  log_name: str,
28
28
  window: timedelta,
29
29
  period: timedelta,
30
30
  aggregator: Callable[[list[T]], R],
31
31
  deserializer: Callable[[str], T],
32
32
  ):
33
- self._db_path = db_path
33
+ self._db = db
34
34
  self._log_name = log_name
35
35
  self._window_duration_seconds = window.total_seconds()
36
36
  self._sampling_period_seconds = period.total_seconds()
@@ -43,9 +43,7 @@ class LiveIterator[T,R]:
43
43
  def _polling_loop(self):
44
44
  """The main loop for the background thread that queries and aggregates data."""
45
45
  # Each thread needs its own database connection.
46
- thread_conn = sqlite3.connect(self._db_path, check_same_thread=False)
47
- thread_conn.row_factory = sqlite3.Row
48
-
46
+ thread_conn = self._db.connection
49
47
  window_deque: collections.deque[tuple[float, T]] = collections.deque()
50
48
  last_seen_timestamp = 0.0
51
49
 
@@ -179,13 +177,11 @@ class LogManager[T]:
179
177
  def __init__(
180
178
  self,
181
179
  name: str,
182
- conn: sqlite3.Connection,
183
- db_path: str,
180
+ db,
184
181
  model: Type[T] | None = None,
185
182
  ):
186
183
  self._name = name
187
- self._conn = conn
188
- self._db_path = db_path
184
+ self._db = db
189
185
  self._model = model
190
186
 
191
187
  def _serialize(self, value: T) -> str:
@@ -215,8 +211,8 @@ class LogManager[T]:
215
211
  ts = timestamp or datetime.now(timezone.utc)
216
212
  ts_float = ts.timestamp()
217
213
 
218
- with self._conn:
219
- self._conn.execute(
214
+ with self._db.connection:
215
+ self._db.connection.execute(
220
216
  "INSERT INTO beaver_logs (log_name, timestamp, data) VALUES (?, ?, ?)",
221
217
  (self._name, ts_float, self._serialize(data)),
222
218
  )
@@ -235,7 +231,7 @@ class LogManager[T]:
235
231
  start_ts = start.timestamp()
236
232
  end_ts = end.timestamp()
237
233
 
238
- cursor = self._conn.cursor()
234
+ cursor = self._db.connection.cursor()
239
235
  cursor.execute(
240
236
  "SELECT data FROM beaver_logs WHERE log_name = ? AND timestamp BETWEEN ? AND ? ORDER BY timestamp ASC",
241
237
  (self._name, start_ts, end_ts),
@@ -265,7 +261,7 @@ class LogManager[T]:
265
261
  An iterator that yields the results of the aggregator.
266
262
  """
267
263
  return LiveIterator(
268
- db_path=self._db_path,
264
+ db=self._db,
269
265
  log_name=self._name,
270
266
  window=window,
271
267
  period=period,
beaver/queues.py CHANGED
@@ -4,7 +4,7 @@ import sqlite3
4
4
  import time
5
5
  from typing import Any, Literal, NamedTuple, Type, overload
6
6
 
7
- from .types import JsonSerializable
7
+ from .types import JsonSerializable, IDatabase
8
8
 
9
9
 
10
10
  class QueueItem[T](NamedTuple):
@@ -50,9 +50,9 @@ class QueueManager[T]:
50
50
  producer-consumer priority queue.
51
51
  """
52
52
 
53
- def __init__(self, name: str, conn: sqlite3.Connection, model: Type[T] | None = None):
53
+ def __init__(self, name: str, db: IDatabase, model: Type[T] | None = None):
54
54
  self._name = name
55
- self._conn = conn
55
+ self._db = db
56
56
  self._model = model
57
57
 
58
58
  def _serialize(self, value: T) -> str:
@@ -77,8 +77,8 @@ class QueueManager[T]:
77
77
  data: The JSON-serializable data to store.
78
78
  priority: The priority of the item (lower numbers are higher priority).
79
79
  """
80
- with self._conn:
81
- self._conn.execute(
80
+ with self._db.connection:
81
+ self._db.connection.execute(
82
82
  "INSERT INTO beaver_priority_queues (queue_name, priority, timestamp, data) VALUES (?, ?, ?, ?)",
83
83
  (self._name, priority, time.time(), self._serialize(data)),
84
84
  )
@@ -88,8 +88,8 @@ class QueueManager[T]:
88
88
  Performs a single, atomic attempt to retrieve and remove the
89
89
  highest-priority item from the queue. Returns None if the queue is empty.
90
90
  """
91
- with self._conn:
92
- cursor = self._conn.cursor()
91
+ with self._db.connection:
92
+ cursor = self._db.connection.cursor()
93
93
  cursor.execute(
94
94
  """
95
95
  SELECT rowid, priority, timestamp, data
@@ -108,11 +108,11 @@ class QueueManager[T]:
108
108
  rowid, priority, timestamp, data = result
109
109
 
110
110
  if pop:
111
- cursor.execute("DELETE FROM beaver_priority_queues WHERE rowid = ?", (rowid,))
111
+ self._db.connection.execute("DELETE FROM beaver_priority_queues WHERE rowid = ?", (rowid,))
112
112
 
113
- return QueueItem(
114
- priority=priority, timestamp=timestamp, data=self._deserialize(data)
115
- )
113
+ return QueueItem(
114
+ priority=priority, timestamp=timestamp, data=self._deserialize(data)
115
+ )
116
116
 
117
117
  def peek(self) -> QueueItem[T] | None:
118
118
  """
@@ -169,7 +169,7 @@ class QueueManager[T]:
169
169
 
170
170
  def __len__(self) -> int:
171
171
  """Returns the current number of items in the queue."""
172
- cursor = self._conn.cursor()
172
+ cursor = self._db.connection.cursor()
173
173
  cursor.execute(
174
174
  "SELECT COUNT(*) FROM beaver_priority_queues WHERE queue_name = ?",
175
175
  (self._name,),
beaver/server.py ADDED
@@ -0,0 +1,132 @@
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)
beaver/types.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import sqlite3
2
3
  from typing import Protocol, Type, runtime_checkable, Self
3
4
 
4
5
 
@@ -49,3 +50,9 @@ def stub(msg: str):
49
50
  raise TypeError(msg)
50
51
 
51
52
  return Stub()
53
+
54
+
55
+ class IDatabase(Protocol):
56
+ @property
57
+ def connection(self) -> sqlite3.Connection:
58
+ ...