sqlite-anyio 0.2.10__tar.gz → 0.3.0__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.
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/PKG-INFO +2 -2
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/pyproject.toml +2 -2
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/src/sqlite_anyio/sqlite.py +89 -18
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/LICENSE +0 -0
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/README.md +0 -0
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/src/sqlite_anyio/__init__.py +0 -0
- {sqlite_anyio-0.2.10 → sqlite_anyio-0.3.0}/src/sqlite_anyio/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sqlite-anyio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Asynchronous client for SQLite using AnyIO
|
|
5
5
|
Author: Alex Grönholm, David Brochart
|
|
6
6
|
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>, David Brochart <david.brochart@gmail.com>
|
|
@@ -35,7 +35,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.12
|
|
36
36
|
Classifier: Programming Language :: Python :: 3.13
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.14
|
|
38
|
-
Requires-Dist: anyio>=4.
|
|
38
|
+
Requires-Dist: anyio>=4.14,<5.0
|
|
39
39
|
Requires-Dist: typing-extensions>=4.15.0
|
|
40
40
|
Requires-Python: >=3.10
|
|
41
41
|
Project-URL: Source, https://github.com/davidbrochart/sqlite-anyio
|
|
@@ -6,7 +6,7 @@ build-backend = "uv_build"
|
|
|
6
6
|
name = "sqlite-anyio"
|
|
7
7
|
description = "Asynchronous client for SQLite using AnyIO"
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.3.0"
|
|
10
10
|
authors = [
|
|
11
11
|
{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"},
|
|
12
12
|
{name = "David Brochart", email = "david.brochart@gmail.com"},
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
]
|
|
29
29
|
requires-python = ">= 3.10"
|
|
30
30
|
dependencies = [
|
|
31
|
-
"anyio >=4.
|
|
31
|
+
"anyio >=4.14,<5.0",
|
|
32
32
|
"typing-extensions >=4.15.0",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -4,18 +4,86 @@ __all__ = ["connect", "Connection", "Cursor"]
|
|
|
4
4
|
|
|
5
5
|
import sqlite3
|
|
6
6
|
import sys
|
|
7
|
+
import threading
|
|
7
8
|
from collections.abc import Callable, Sequence
|
|
8
9
|
from functools import partial
|
|
9
10
|
from logging import Logger, getLogger
|
|
10
11
|
from types import TracebackType
|
|
11
|
-
from typing import Any
|
|
12
|
+
from typing import Any, TypeVar
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
import anyio
|
|
15
|
+
from anyio import to_thread, from_thread
|
|
14
16
|
|
|
15
17
|
if sys.version_info >= (3, 11):
|
|
16
|
-
from typing import Self
|
|
17
|
-
else:
|
|
18
|
-
from
|
|
18
|
+
from typing import Self, TypeVarTuple, Unpack
|
|
19
|
+
else: # pragma: nocover
|
|
20
|
+
from exceptiongroup import BaseExceptionGroup
|
|
21
|
+
from typing_extensions import Self, TypeVarTuple, Unpack
|
|
22
|
+
|
|
23
|
+
T_Retval = TypeVar("T_Retval")
|
|
24
|
+
PosArgsT = TypeVarTuple("PosArgsT")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _interruptible_dispatch(
|
|
28
|
+
self: Connection | Cursor,
|
|
29
|
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
|
30
|
+
*args: Unpack[PosArgsT]
|
|
31
|
+
) -> T_Retval:
|
|
32
|
+
if isinstance(self, Connection):
|
|
33
|
+
real_connection = self._real_connection
|
|
34
|
+
elif isinstance(self, Cursor):
|
|
35
|
+
real_connection = self._real_cursor.connection
|
|
36
|
+
else: # pragma: nocover
|
|
37
|
+
raise AssertionError("Unknown type:", self)
|
|
38
|
+
|
|
39
|
+
ev = anyio.Event()
|
|
40
|
+
lock = threading.Lock()
|
|
41
|
+
need_interrupt = False
|
|
42
|
+
|
|
43
|
+
async def cancel_detector() -> None:
|
|
44
|
+
try:
|
|
45
|
+
await ev.wait()
|
|
46
|
+
except anyio.get_cancelled_exc_class():
|
|
47
|
+
# Block progress in the thread while checking this flag.
|
|
48
|
+
# Our guard_interrupt thread only ever holds the lock briefly,
|
|
49
|
+
# so there's no risk of blocking the event loop.
|
|
50
|
+
with lock:
|
|
51
|
+
# Due to race conditions, the first calls to interrupt may be
|
|
52
|
+
# ignored. This race is quick so this loop should not cycle much.
|
|
53
|
+
while need_interrupt:
|
|
54
|
+
real_connection.interrupt()
|
|
55
|
+
await anyio.lowlevel.cancel_shielded_checkpoint()
|
|
56
|
+
# we do NOT re-raise the cancellation so that the task group
|
|
57
|
+
# does not swallow our retval. If a Cancelled is to propagate,
|
|
58
|
+
# it should come out of to_thread.run_sync
|
|
59
|
+
|
|
60
|
+
def guard_interrupt() -> T_Retval:
|
|
61
|
+
nonlocal need_interrupt
|
|
62
|
+
|
|
63
|
+
with lock:
|
|
64
|
+
from_thread.check_cancelled()
|
|
65
|
+
need_interrupt = True
|
|
66
|
+
try:
|
|
67
|
+
return func(*args)
|
|
68
|
+
except sqlite3.OperationalError as e:
|
|
69
|
+
if str(e) == "interrupted":
|
|
70
|
+
from_thread.check_cancelled()
|
|
71
|
+
raise
|
|
72
|
+
finally:
|
|
73
|
+
need_interrupt = False
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
async with anyio.create_task_group() as g:
|
|
77
|
+
g.start_soon(cancel_detector)
|
|
78
|
+
retval = await to_thread.run_sync(guard_interrupt, limiter=self._limiter)
|
|
79
|
+
ev.set()
|
|
80
|
+
except BaseExceptionGroup as eg:
|
|
81
|
+
if len(eg.exceptions) == 1:
|
|
82
|
+
if isinstance(eg.exceptions[0], Exception):
|
|
83
|
+
raise eg.exceptions[0]
|
|
84
|
+
raise # pragma: nocover (would be an internal error that should fail other tests)
|
|
85
|
+
|
|
86
|
+
return retval
|
|
19
87
|
|
|
20
88
|
|
|
21
89
|
class Connection:
|
|
@@ -28,7 +96,7 @@ class Connection:
|
|
|
28
96
|
self._real_connection = _real_connection
|
|
29
97
|
self._exception_handler = _exception_handler
|
|
30
98
|
self._log = _log or getLogger(__name__)
|
|
31
|
-
self._limiter = CapacityLimiter(1)
|
|
99
|
+
self._limiter = anyio.CapacityLimiter(1)
|
|
32
100
|
|
|
33
101
|
async def __aenter__(self) -> Self:
|
|
34
102
|
return self
|
|
@@ -53,17 +121,19 @@ class Connection:
|
|
|
53
121
|
return exception_handled
|
|
54
122
|
|
|
55
123
|
async def execute(self, sql: str, parameters: Sequence[Any] = (), /) -> Cursor:
|
|
56
|
-
real_cursor = await
|
|
124
|
+
real_cursor = await _interruptible_dispatch(self, self._real_connection.execute, sql, parameters)
|
|
57
125
|
return Cursor(real_cursor, self._limiter, self._exception_handler, self._log)
|
|
58
126
|
|
|
59
127
|
async def close(self) -> None:
|
|
60
|
-
|
|
128
|
+
with anyio.CancelScope(shield=True):
|
|
129
|
+
await to_thread.run_sync(self._real_connection.close, limiter=self._limiter)
|
|
61
130
|
|
|
62
131
|
async def commit(self) -> None:
|
|
63
|
-
await
|
|
132
|
+
await _interruptible_dispatch(self, self._real_connection.commit)
|
|
64
133
|
|
|
65
134
|
async def rollback(self) -> None:
|
|
66
|
-
|
|
135
|
+
with anyio.CancelScope(shield=True):
|
|
136
|
+
await to_thread.run_sync(self._real_connection.rollback, limiter=self._limiter)
|
|
67
137
|
|
|
68
138
|
async def cursor(self, factory: Callable[[sqlite3.Connection], sqlite3.Cursor] = sqlite3.Cursor) -> Cursor:
|
|
69
139
|
real_cursor = await to_thread.run_sync(self._real_connection.cursor, factory, limiter=self._limiter)
|
|
@@ -74,7 +144,7 @@ class Cursor:
|
|
|
74
144
|
def __init__(
|
|
75
145
|
self,
|
|
76
146
|
real_cursor: sqlite3.Cursor,
|
|
77
|
-
limiter: CapacityLimiter,
|
|
147
|
+
limiter: anyio.CapacityLimiter,
|
|
78
148
|
_exception_handler: Callable[[type[BaseException], BaseException, TracebackType, Logger], bool] | None,
|
|
79
149
|
_log: Logger,
|
|
80
150
|
) -> None:
|
|
@@ -117,28 +187,29 @@ class Cursor:
|
|
|
117
187
|
return exception_handled
|
|
118
188
|
|
|
119
189
|
async def close(self) -> None:
|
|
120
|
-
|
|
190
|
+
with anyio.CancelScope(shield=True):
|
|
191
|
+
await to_thread.run_sync(self._real_cursor.close, limiter=self._limiter)
|
|
121
192
|
|
|
122
193
|
async def execute(self, sql: str, parameters: Sequence[Any] = (), /) -> Cursor:
|
|
123
|
-
await
|
|
194
|
+
await _interruptible_dispatch(self, self._real_cursor.execute, sql, parameters)
|
|
124
195
|
return self
|
|
125
196
|
|
|
126
197
|
async def executemany(self, sql: str, parameters: Sequence[Any], /) -> Cursor:
|
|
127
|
-
await
|
|
198
|
+
await _interruptible_dispatch(self, self._real_cursor.executemany, sql, parameters)
|
|
128
199
|
return self
|
|
129
200
|
|
|
130
201
|
async def executescript(self, sql_script: str, /) -> Cursor:
|
|
131
|
-
await
|
|
202
|
+
await _interruptible_dispatch(self, self._real_cursor.executescript, sql_script)
|
|
132
203
|
return self
|
|
133
204
|
|
|
134
205
|
async def fetchone(self) -> tuple[Any, ...] | None:
|
|
135
|
-
return await
|
|
206
|
+
return await _interruptible_dispatch(self, self._real_cursor.fetchone)
|
|
136
207
|
|
|
137
208
|
async def fetchmany(self, size: int) -> list[tuple[Any, ...]]:
|
|
138
|
-
return await
|
|
209
|
+
return await _interruptible_dispatch(self, self._real_cursor.fetchmany, size)
|
|
139
210
|
|
|
140
211
|
async def fetchall(self) -> list[tuple[Any, ...]]:
|
|
141
|
-
return await
|
|
212
|
+
return await _interruptible_dispatch(self, self._real_cursor.fetchall)
|
|
142
213
|
|
|
143
214
|
|
|
144
215
|
async def connect(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|