pyturso 0.1.5rc5__cp310-cp310-macosx_11_0_arm64.whl → 0.4.0rc9__cp310-cp310-macosx_11_0_arm64.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 pyturso might be problematic. Click here for more details.

turso/lib_aio.py ADDED
@@ -0,0 +1,351 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from queue import SimpleQueue
5
+ from typing import Any, Callable, Iterable, Mapping, Optional, Sequence
6
+
7
+ from .lib import (
8
+ Connection as BlockingConnection,
9
+ )
10
+ from .lib import (
11
+ Cursor as BlockingCursor,
12
+ )
13
+ from .lib import (
14
+ ProgrammingError,
15
+ )
16
+ from .lib import (
17
+ connect as blocking_connect,
18
+ )
19
+ from .worker import STOP_RUNNING_SENTINEL, Worker
20
+
21
+
22
+ # Connection goes FIRST
23
+ class Connection:
24
+ def __init__(self, connector: Callable[[], BlockingConnection]) -> None:
25
+ # Event loop and per-connection worker thread state
26
+ self._loop = asyncio.get_event_loop()
27
+ self._queue: SimpleQueue[tuple[asyncio.Future, Callable[[], Any]]] = SimpleQueue()
28
+ self._worker = Worker(self._queue, self._loop)
29
+ self._connector = connector
30
+
31
+ # Underlying blocking connection created in worker thread
32
+ self._conn: Optional[BlockingConnection] = None
33
+ self._closed: bool = False
34
+
35
+ # Schedule connection creation as the very first job in the worker
36
+ self._open_future: asyncio.Future[Connection] = self._loop.create_future()
37
+
38
+ def _open() -> Connection:
39
+ # Create the blocking connection inside the worker thread once
40
+ if self._conn is None:
41
+ self._conn = self._connector()
42
+ return self
43
+
44
+ self._queue.put_nowait((self._open_future, _open))
45
+ self._worker.start()
46
+
47
+ # Cached properties mirrored to the underlying connection.
48
+ # Setters will enqueue mutation jobs; we keep local cache for getters.
49
+ self._isolation_level_cache: Optional[str] = None
50
+ self._row_factory_cache: Any = None
51
+ self._text_factory_cache: Any = None
52
+ self._autocommit_cache: object | bool | None = None
53
+
54
+ async def close(self) -> None:
55
+ if self._closed:
56
+ return
57
+
58
+ # Ensure underlying Connection.close() is called in worker thread
59
+ def _do_close() -> None:
60
+ if self._conn is not None:
61
+ self._conn.close()
62
+
63
+ await self._run(lambda: (_do_close(), None)[1]) # schedule and await completion of close
64
+
65
+ # Request worker stop; we must not block the event loop while waiting.
66
+ # Note: STOP_RUNNING_SENTINEL item will terminate the worker loop.
67
+ stop_future = self._loop.create_future()
68
+ self._queue.put_nowait((stop_future, lambda: STOP_RUNNING_SENTINEL))
69
+
70
+ # Wait for the worker thread to terminate without blocking the loop
71
+ await self._loop.run_in_executor(None, self._worker.join)
72
+ self._closed = True
73
+
74
+ def __await__(self):
75
+ async def _await_open() -> "Connection":
76
+ await self._open_future
77
+ return self
78
+
79
+ return _await_open().__await__()
80
+
81
+ async def __aenter__(self) -> "Connection":
82
+ await self
83
+ return self
84
+
85
+ async def __aexit__(self, exc_type, exc, tb) -> None:
86
+ # Just close the connection - do not add any extra logic
87
+ await self.close()
88
+
89
+ # Internal helper: schedule a callable to run in the worker thread and await its result.
90
+ async def _run(self, func: Callable[[], Any]) -> Any:
91
+ if self._closed:
92
+ raise ProgrammingError("Cannot operate on a closed connection")
93
+ fut = self._loop.create_future()
94
+ self._queue.put_nowait((fut, func))
95
+ return await fut
96
+
97
+ # Internal helper: enqueue a callable but do not await completion (used for property setters).
98
+ def _run_nowait(self, func: Callable[[], Any]) -> None:
99
+ if self._closed:
100
+ raise ProgrammingError("Cannot operate on a closed connection")
101
+ fut = self._loop.create_future()
102
+ self._queue.put_nowait((fut, func))
103
+
104
+ # Cursor factory returning async Cursor wrapper
105
+ def cursor(self, factory: Optional[Callable[[BlockingConnection], BlockingCursor]] = None) -> "Cursor":
106
+ # Creation of the underlying blocking cursor is enqueued to preserve thread affinity.
107
+ return Cursor(self, factory=factory)
108
+
109
+ # Helpers similar to aiosqlite
110
+ async def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor":
111
+ cur = self.cursor()
112
+ await cur.execute(sql, parameters)
113
+ return cur
114
+
115
+ async def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor":
116
+ cur = self.cursor()
117
+ await cur.executemany(sql, parameters)
118
+ return cur
119
+
120
+ async def executescript(self, sql_script: str) -> "Cursor":
121
+ cur = self.cursor()
122
+ await cur.executescript(sql_script)
123
+ return cur
124
+
125
+ async def commit(self) -> None:
126
+ await self._run(lambda: self._conn.commit()) # type: ignore[union-attr]
127
+
128
+ async def rollback(self) -> None:
129
+ await self._run(lambda: self._conn.rollback()) # type: ignore[union-attr]
130
+
131
+ # Read/write properties mirrored to the underlying blocking connection.
132
+ # DB-API hints:
133
+ # - isolation_level controls implicit transactions (BEGIN DEFERRED/IMMEDIATE/EXCLUSIVE or None for autocommit).
134
+ @property
135
+ def isolation_level(self) -> Optional[str]:
136
+ return self._isolation_level_cache
137
+
138
+ @isolation_level.setter
139
+ def isolation_level(self, value: Optional[str]) -> None:
140
+ self._isolation_level_cache = value
141
+
142
+ def _set() -> None:
143
+ # Will be applied in the worker thread before subsequent operations
144
+ self._conn.isolation_level = value # type: ignore[union-attr]
145
+
146
+ self._run_nowait(_set)
147
+
148
+ @property
149
+ def row_factory(self) -> Any:
150
+ return self._row_factory_cache
151
+
152
+ @row_factory.setter
153
+ def row_factory(self, value: Any) -> None:
154
+ self._row_factory_cache = value
155
+
156
+ def _set() -> None:
157
+ self._conn.row_factory = value # type: ignore[union-attr]
158
+
159
+ self._run_nowait(_set)
160
+
161
+ @property
162
+ def text_factory(self) -> Any:
163
+ return self._text_factory_cache
164
+
165
+ @text_factory.setter
166
+ def text_factory(self, value: Any) -> None:
167
+ self._text_factory_cache = value
168
+
169
+ def _set() -> None:
170
+ self._conn.text_factory = value # type: ignore[union-attr]
171
+
172
+ self._run_nowait(_set)
173
+
174
+ @property
175
+ def autocommit(self) -> object | bool | None:
176
+ return self._autocommit_cache
177
+
178
+ @autocommit.setter
179
+ def autocommit(self, value: object | bool) -> None:
180
+ self._autocommit_cache = value
181
+
182
+ def _set() -> None:
183
+ self._conn.autocommit = value # type: ignore[union-attr]
184
+
185
+ self._run_nowait(_set)
186
+
187
+
188
+ # Cursor goes SECOND
189
+ class Cursor:
190
+ def __init__(
191
+ self,
192
+ connection: Connection,
193
+ factory: Optional[Callable[[BlockingConnection], BlockingCursor]] = None
194
+ ):
195
+ self._connection: Connection = connection
196
+ self._loop = asyncio.get_event_loop()
197
+
198
+ # Underlying blocking cursor and its creation job
199
+ self._cursor_created: bool = False
200
+ self._bcursor: Optional[BlockingCursor] = None
201
+
202
+ # Cursor attributes (DB-API)
203
+ self.arraysize: int = 1
204
+ self._description: Optional[tuple[tuple[str, None, None, None, None, None, None], ...]] = None
205
+ self._lastrowid: Optional[int] = None
206
+ self._rowcount: int = -1
207
+ self._closed: bool = False
208
+
209
+ # Enqueue creation of the underlying blocking cursor in the worker thread
210
+ def _create() -> None:
211
+ if self._cursor_created:
212
+ return
213
+ if factory is None:
214
+ self._bcursor = self._connection._conn.cursor() # type: ignore[union-attr]
215
+ else:
216
+ # Use provided factory to create BlockingCursor from BlockingConnection
217
+ self._bcursor = factory(self._connection._conn) # type: ignore[union-attr]
218
+ self._cursor_created = True
219
+ # Apply initial arraysize if any
220
+ self._bcursor.arraysize = self.arraysize # type: ignore[union-attr]
221
+
222
+ self._connection._run_nowait(_create)
223
+
224
+ @property
225
+ def connection(self) -> Connection:
226
+ return self._connection
227
+
228
+ async def close(self) -> None:
229
+ if self._closed:
230
+ return
231
+
232
+ def _close() -> None:
233
+ if self._bcursor is not None:
234
+ self._bcursor.close()
235
+
236
+ await self._connection._run(_close)
237
+ self._closed = True
238
+
239
+ # Internal helpers for updating cached metadata after execute-like calls
240
+ def _update_meta_cache(self, description, lastrowid, rowcount) -> None:
241
+ self._description = description
242
+ self._lastrowid = lastrowid
243
+ self._rowcount = rowcount if rowcount is not None else -1
244
+
245
+ async def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor":
246
+ self._ensure_open()
247
+
248
+ def _exec() -> tuple[Any, Any, Any]:
249
+ # Perform the execute and collect metadata
250
+ cur = self._bcursor # type: ignore[assignment]
251
+ cur.execute(sql, parameters)
252
+ return (cur.description, cur.lastrowid, cur.rowcount)
253
+
254
+ description, lastrowid, rowcount = await self._connection._run(_exec)
255
+ self._update_meta_cache(description, lastrowid, rowcount)
256
+ return self
257
+
258
+ async def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor":
259
+ self._ensure_open()
260
+
261
+ def _execm() -> tuple[Any, Any, Any]:
262
+ cur = self._bcursor # type: ignore[assignment]
263
+ cur.executemany(sql, parameters)
264
+ return (cur.description, cur.lastrowid, cur.rowcount)
265
+
266
+ description, lastrowid, rowcount = await self._connection._run(_execm)
267
+ self._update_meta_cache(description, lastrowid, rowcount)
268
+ return self
269
+
270
+ async def executescript(self, sql_script: str) -> "Cursor":
271
+ self._ensure_open()
272
+
273
+ def _execs() -> tuple[Any, Any, Any]:
274
+ cur = self._bcursor # type: ignore[assignment]
275
+ cur.executescript(sql_script)
276
+ return (cur.description, cur.lastrowid, cur.rowcount)
277
+
278
+ description, lastrowid, rowcount = await self._connection._run(_execs)
279
+ self._update_meta_cache(description, lastrowid, rowcount)
280
+ return self
281
+
282
+ async def fetchone(self) -> Any:
283
+ self._ensure_open()
284
+
285
+ def _one() -> Any:
286
+ return self._bcursor.fetchone() # type: ignore[union-attr]
287
+
288
+ return await self._connection._run(_one)
289
+
290
+ async def fetchmany(self, size: Optional[int] = None) -> list[Any]:
291
+ self._ensure_open()
292
+
293
+ def _many() -> list[Any]:
294
+ n = self.arraysize if size is None else size
295
+ return list(self._bcursor.fetchmany(n)) # type: ignore[union-attr]
296
+
297
+ return await self._connection._run(_many)
298
+
299
+ async def fetchall(self) -> list[Any]:
300
+ self._ensure_open()
301
+
302
+ def _all() -> list[Any]:
303
+ return list(self._bcursor.fetchall()) # type: ignore[union-attr]
304
+
305
+ return await self._connection._run(_all)
306
+
307
+ def _ensure_open(self) -> None:
308
+ if self._closed:
309
+ raise ProgrammingError("Cannot operate on a closed cursor")
310
+
311
+ # Properties reflecting DB-API attributes of the last executed statement
312
+ @property
313
+ def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:
314
+ return self._description
315
+
316
+ @property
317
+ def lastrowid(self) -> int | None:
318
+ return self._lastrowid
319
+
320
+ @property
321
+ def rowcount(self) -> int:
322
+ return self._rowcount
323
+
324
+ # Make cursor usable as async context manager, similar to aiosqlite
325
+ async def __aenter__(self) -> "Cursor":
326
+ return self
327
+
328
+ async def __aexit__(self, exc_type, exc, tb) -> None:
329
+ await self.close()
330
+
331
+
332
+ # connect is not async because it returns awaitable Connection
333
+ # same signature as in the lib.py
334
+ def connect(
335
+ database: str,
336
+ *,
337
+ experimental_features: Optional[str] = None,
338
+ isolation_level: Optional[str] = "DEFERRED",
339
+ extra_io: Optional[Callable[[], None]] = None,
340
+ ) -> Connection:
341
+ # Create a connector that opens a blocking Connection using the existing driver.
342
+ def _connector() -> BlockingConnection:
343
+ conn = blocking_connect(
344
+ database,
345
+ experimental_features=experimental_features,
346
+ isolation_level=isolation_level,
347
+ extra_io=extra_io,
348
+ )
349
+ return conn
350
+
351
+ return Connection(_connector)