anyio-cysqlite 0.1.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.
- anyio_cysqlite-0.1.0/LICENSE +21 -0
- anyio_cysqlite-0.1.0/PKG-INFO +63 -0
- anyio_cysqlite-0.1.0/README.md +49 -0
- anyio_cysqlite-0.1.0/pyproject.toml +38 -0
- anyio_cysqlite-0.1.0/setup.cfg +4 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite/__init__.py +23 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite/db.py +499 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite/py.typed +0 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite/typedefs.py +116 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite.egg-info/PKG-INFO +63 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite.egg-info/SOURCES.txt +13 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite.egg-info/dependency_links.txt +1 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite.egg-info/requires.txt +5 -0
- anyio_cysqlite-0.1.0/src/anyio_cysqlite.egg-info/top_level.txt +1 -0
- anyio_cysqlite-0.1.0/tests/test_context_manager.py +108 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vizonex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anyio-cysqlite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: a anyio and cysqlite library
|
|
5
|
+
Author-email: Vizonex <VizonexBusiness@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: anyio>=4.13.0
|
|
10
|
+
Requires-Dist: cysqlite>=0.3.2
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest; extra == "test"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
<img src="https://raw.githubusercontent.com/Vizonex/anyio-cysqlite/main/anyio-cysqlite-logo.PNG"/>
|
|
16
|
+
|
|
17
|
+
[](https://opensource.org/licenses/MIT)
|
|
18
|
+
|
|
19
|
+
# anyio-cysqlite
|
|
20
|
+
A Bidning for cysqlite that makes the database asynchronous
|
|
21
|
+
with any asynchronous library that can be binded to anyio
|
|
22
|
+
meaning that it will work with many server implementations and applications including FastAPI, Starlette and Litestar to name a few server libraries.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from anyio_cysqlite import connect
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
async with await connect("cysqlite.db") as db:
|
|
30
|
+
await db.execute('create table IF NOT EXISTS data (k, v)')
|
|
31
|
+
|
|
32
|
+
async with db.atomic():
|
|
33
|
+
await db.executemany('insert into data (k, v) values (?, ?)',
|
|
34
|
+
[(f'k{i:02d}', f'v{i:02d}') for i in range(10)])
|
|
35
|
+
print(await db.last_insert_rowid()) # 10.
|
|
36
|
+
|
|
37
|
+
curs = await db.execute('select * from data')
|
|
38
|
+
async for row in curs:
|
|
39
|
+
print(row) # e.g., ('k00', 'v00')
|
|
40
|
+
|
|
41
|
+
# We can use named parameters with a dict as well.
|
|
42
|
+
row = await db.execute_one('select * from data where k = :key and v = :val',
|
|
43
|
+
{'key': 'k05', 'val': 'v05'})
|
|
44
|
+
print(row) # ('k05', 'v05')
|
|
45
|
+
|
|
46
|
+
await db.close()
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
import anyio
|
|
50
|
+
anyio.run(main)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Updated functionality is better
|
|
54
|
+
When it comes to database execution updated functionality normally results in better performance such as providing wrappers for `atomic`, `transaction`, and `savepoint` functions which the standard sqlite3 library doesn't currently have. Instead of having a completely seperate thread for the database all operations are done using the anyio's `CapacityLimiter` and `run_sync` making the database less resource heavy.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Batteries included
|
|
58
|
+
Unlike cysqlite (As of currently), this library retains access to typehints right out of the box making the library less of a function and attribute guessing game.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/Vizonex/anyio-cysqlite/main/anyio-cysqlite-logo.PNG"/>
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
# anyio-cysqlite
|
|
6
|
+
A Bidning for cysqlite that makes the database asynchronous
|
|
7
|
+
with any asynchronous library that can be binded to anyio
|
|
8
|
+
meaning that it will work with many server implementations and applications including FastAPI, Starlette and Litestar to name a few server libraries.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from anyio_cysqlite import connect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main():
|
|
15
|
+
async with await connect("cysqlite.db") as db:
|
|
16
|
+
await db.execute('create table IF NOT EXISTS data (k, v)')
|
|
17
|
+
|
|
18
|
+
async with db.atomic():
|
|
19
|
+
await db.executemany('insert into data (k, v) values (?, ?)',
|
|
20
|
+
[(f'k{i:02d}', f'v{i:02d}') for i in range(10)])
|
|
21
|
+
print(await db.last_insert_rowid()) # 10.
|
|
22
|
+
|
|
23
|
+
curs = await db.execute('select * from data')
|
|
24
|
+
async for row in curs:
|
|
25
|
+
print(row) # e.g., ('k00', 'v00')
|
|
26
|
+
|
|
27
|
+
# We can use named parameters with a dict as well.
|
|
28
|
+
row = await db.execute_one('select * from data where k = :key and v = :val',
|
|
29
|
+
{'key': 'k05', 'val': 'v05'})
|
|
30
|
+
print(row) # ('k05', 'v05')
|
|
31
|
+
|
|
32
|
+
await db.close()
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
import anyio
|
|
36
|
+
anyio.run(main)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Updated functionality is better
|
|
40
|
+
When it comes to database execution updated functionality normally results in better performance such as providing wrappers for `atomic`, `transaction`, and `savepoint` functions which the standard sqlite3 library doesn't currently have. Instead of having a completely seperate thread for the database all operations are done using the anyio's `CapacityLimiter` and `run_sync` making the database less resource heavy.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Batteries included
|
|
44
|
+
Unlike cysqlite (As of currently), this library retains access to typehints right out of the box making the library less of a function and attribute guessing game.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anyio-cysqlite"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "a anyio and cysqlite library"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Vizonex", email = "VizonexBusiness@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"anyio>=4.13.0",
|
|
12
|
+
"cysqlite>=0.3.2",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
test = ["pytest"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.dynamic]
|
|
20
|
+
version = {attr = "anyio_cysqlite.__version__"}
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
line-length = 79
|
|
27
|
+
indent-width = 4
|
|
28
|
+
target-version = "py310"
|
|
29
|
+
fix = true
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = [
|
|
33
|
+
"I",
|
|
34
|
+
"E"
|
|
35
|
+
]
|
|
36
|
+
ignore = [
|
|
37
|
+
"F405",
|
|
38
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .db import (
|
|
2
|
+
Atomic,
|
|
3
|
+
Connection,
|
|
4
|
+
Cursor,
|
|
5
|
+
Savepoint,
|
|
6
|
+
Transaction,
|
|
7
|
+
connect,
|
|
8
|
+
exception_logger,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__author__ = "Vizonex"
|
|
13
|
+
__license__ = "MIT"
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"Atomic",
|
|
17
|
+
"Connection",
|
|
18
|
+
"Cursor",
|
|
19
|
+
"Savepoint",
|
|
20
|
+
"Transaction",
|
|
21
|
+
"connect",
|
|
22
|
+
"exception_logger",
|
|
23
|
+
)
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections import deque
|
|
3
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
4
|
+
from functools import partial, wraps
|
|
5
|
+
from logging import Logger, getLogger
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import cysqlite
|
|
11
|
+
from anyio import CapacityLimiter
|
|
12
|
+
from anyio.to_thread import run_sync
|
|
13
|
+
from cysqlite._cysqlite import Cursor as _Cursor
|
|
14
|
+
|
|
15
|
+
from .typedefs import (
|
|
16
|
+
P,
|
|
17
|
+
T,
|
|
18
|
+
_Atomic,
|
|
19
|
+
_IsolationLevel,
|
|
20
|
+
_Parameters,
|
|
21
|
+
_Savepoint,
|
|
22
|
+
_Transaction,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if sys.version_info >= (3, 11):
|
|
26
|
+
from typing import Self
|
|
27
|
+
else:
|
|
28
|
+
from typing_extensions import Self # pragma: nocover
|
|
29
|
+
|
|
30
|
+
if sys.version_info >= (3, 13):
|
|
31
|
+
from typing import TypeVarTuple, Unpack
|
|
32
|
+
else:
|
|
33
|
+
from typing_extensions import TypeVarTuple, Unpack
|
|
34
|
+
|
|
35
|
+
Ts = TypeVarTuple("Ts", default=())
|
|
36
|
+
|
|
37
|
+
SENTINEL = object()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AsyncAction:
|
|
41
|
+
"""helper for handling other wrappers like atomic,
|
|
42
|
+
savepoint and transaction"""
|
|
43
|
+
|
|
44
|
+
__slots__ = ("_real", "_limiter")
|
|
45
|
+
|
|
46
|
+
async def __aenter__(self):
|
|
47
|
+
await run_sync(self._real.__enter__, limiter=self._limiter)
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
async def __aexit__(
|
|
51
|
+
self,
|
|
52
|
+
exc_type: type[BaseException] | None,
|
|
53
|
+
exc: BaseException | None,
|
|
54
|
+
tb: TracebackType | None,
|
|
55
|
+
):
|
|
56
|
+
return await run_sync(
|
|
57
|
+
self._real.__exit__, exc_type, exc, tb, limiter=self._limiter
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __call__(
|
|
61
|
+
self, func: Callable[P, Awaitable[T]]
|
|
62
|
+
) -> Callable[P, Awaitable[T]]:
|
|
63
|
+
"""wraps function to the database note:
|
|
64
|
+
it's limited to non-async generators"""
|
|
65
|
+
|
|
66
|
+
@wraps(func)
|
|
67
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
68
|
+
async with self:
|
|
69
|
+
return await func(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
return wrapper
|
|
72
|
+
|
|
73
|
+
async def run(
|
|
74
|
+
self, func: Callable[[Unpack[Ts]], Awaitable[T]], *args: Unpack[Ts]
|
|
75
|
+
) -> T:
|
|
76
|
+
"""
|
|
77
|
+
Custom function that Runs context manager inside a function without
|
|
78
|
+
having to setup a wrapper when database is not outside of a main
|
|
79
|
+
funciton
|
|
80
|
+
"""
|
|
81
|
+
async with self:
|
|
82
|
+
return await func(*args)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Transaction(AsyncAction):
|
|
86
|
+
def __init__(self, real: _Transaction, limiter: CapacityLimiter):
|
|
87
|
+
self._real = real
|
|
88
|
+
self._limiter = limiter
|
|
89
|
+
|
|
90
|
+
async def commit(self, begin: bool = True) -> None:
|
|
91
|
+
await run_sync(self._real.commit, begin, limiter=self._limiter)
|
|
92
|
+
|
|
93
|
+
async def rollback(self, begin: bool = True) -> None:
|
|
94
|
+
await run_sync(self._real.rollback, begin, limiter=self._limiter)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Savepoint(AsyncAction):
|
|
98
|
+
def __init__(self, real: _Savepoint, limiter: CapacityLimiter):
|
|
99
|
+
self._real = real
|
|
100
|
+
self._limiter = limiter
|
|
101
|
+
|
|
102
|
+
async def commit(self, begin: bool = True):
|
|
103
|
+
await run_sync(self._real.commit, begin, limiter=self._limiter)
|
|
104
|
+
|
|
105
|
+
async def rollback(self):
|
|
106
|
+
await run_sync(self._real.rollback, limiter=self._limiter)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Atomic(AsyncAction):
|
|
110
|
+
def __init__(self, real: _Atomic, limiter: CapacityLimiter):
|
|
111
|
+
self._real = real
|
|
112
|
+
self._limiter = limiter
|
|
113
|
+
|
|
114
|
+
async def commit(self, begin: bool = True):
|
|
115
|
+
await run_sync(self._real.commit, begin, limiter=self._limiter)
|
|
116
|
+
|
|
117
|
+
# it varies. hence *args and not begin: bool = True
|
|
118
|
+
async def rollback(self, *args):
|
|
119
|
+
await run_sync(self._real.rollback, *args, limiter=self._limiter)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# inspired by sqlite-anyio with a few of my own tweaks
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Cursor:
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
real_cursor: _Cursor,
|
|
129
|
+
limiter: CapacityLimiter,
|
|
130
|
+
exception_handler: Callable[
|
|
131
|
+
[type[BaseException], BaseException, TracebackType, Logger], bool
|
|
132
|
+
]
|
|
133
|
+
| None,
|
|
134
|
+
log: Logger,
|
|
135
|
+
) -> None:
|
|
136
|
+
self._real_cursor = real_cursor
|
|
137
|
+
self._limiter = limiter
|
|
138
|
+
self._exception_handler = exception_handler
|
|
139
|
+
self._log = log
|
|
140
|
+
|
|
141
|
+
# Deques can maker iterating a bit faster over the
|
|
142
|
+
# standard list object as linked lists are known to speed
|
|
143
|
+
# things up.
|
|
144
|
+
self._buffer: deque[Any | cysqlite.Row] = deque()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def description(self) -> tuple[tuple[str, ...], ...] | None:
|
|
148
|
+
return self._real_cursor.description
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def lastrowid(self) -> int | None:
|
|
152
|
+
return self._real_cursor.lastrowid
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def rowcount(self) -> int:
|
|
156
|
+
return self._real_cursor.rowcount
|
|
157
|
+
|
|
158
|
+
async def close(self) -> None:
|
|
159
|
+
await run_sync(self._real_cursor.close, limiter=self._limiter)
|
|
160
|
+
|
|
161
|
+
async def execute(
|
|
162
|
+
self, sql: str, parameters: Sequence[_Parameters] = (), /
|
|
163
|
+
) -> Self:
|
|
164
|
+
await run_sync(
|
|
165
|
+
self._real_cursor.execute, sql, parameters, limiter=self._limiter
|
|
166
|
+
)
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
async def executemany(
|
|
170
|
+
self, sql: str, parameters: Sequence[_Parameters], /
|
|
171
|
+
) -> Self:
|
|
172
|
+
await run_sync(
|
|
173
|
+
self._real_cursor.executemany,
|
|
174
|
+
sql,
|
|
175
|
+
parameters,
|
|
176
|
+
limiter=self._limiter,
|
|
177
|
+
)
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
async def fetchall(self) -> list[tuple[Any, ...]]:
|
|
181
|
+
return await run_sync(
|
|
182
|
+
self._real_cursor.fetchall, limiter=self._limiter
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def fetchone(self) -> cysqlite.Row | None:
|
|
186
|
+
return await run_sync(
|
|
187
|
+
self._real_cursor.fetchone, limiter=self._limiter
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def scalar(self) -> Any:
|
|
191
|
+
return await run_sync(self._real_cursor.scalar, limiter=self._limiter)
|
|
192
|
+
|
|
193
|
+
async def __aenter__(self) -> Self:
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
async def __aexit__(
|
|
197
|
+
self,
|
|
198
|
+
exc_type: type[BaseException] | None,
|
|
199
|
+
exc_val: BaseException | None,
|
|
200
|
+
exc_tb: TracebackType | None,
|
|
201
|
+
) -> bool | None:
|
|
202
|
+
await self.close()
|
|
203
|
+
if exc_val is None:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
assert exc_type is not None
|
|
207
|
+
assert exc_val is not None
|
|
208
|
+
assert exc_tb is not None
|
|
209
|
+
exception_handled = False
|
|
210
|
+
if self._exception_handler is not None:
|
|
211
|
+
exception_handled = self._exception_handler(
|
|
212
|
+
exc_type, exc_val, exc_tb, self._log
|
|
213
|
+
)
|
|
214
|
+
return exception_handled
|
|
215
|
+
|
|
216
|
+
async def fetchmany(self, size: int = 100) -> list[cysqlite.Row | Any]:
|
|
217
|
+
# next part comes form cysqlite/aio.py
|
|
218
|
+
def _fetch():
|
|
219
|
+
rows = []
|
|
220
|
+
for _ in range(size):
|
|
221
|
+
try:
|
|
222
|
+
rows.append(self._real_cursor.__next__())
|
|
223
|
+
except StopIteration:
|
|
224
|
+
break
|
|
225
|
+
return rows
|
|
226
|
+
|
|
227
|
+
return await run_sync(_fetch)
|
|
228
|
+
|
|
229
|
+
def __aiter__(self) -> Self:
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
async def __anext__(self) -> Any | cysqlite.Row:
|
|
233
|
+
if not self._buffer:
|
|
234
|
+
self._buffer.extend(await self.fetchmany())
|
|
235
|
+
if not self._buffer:
|
|
236
|
+
raise StopAsyncIteration
|
|
237
|
+
return self._buffer.popleft()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Connection:
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
real_connection: cysqlite.Connection,
|
|
244
|
+
exception_handler: Callable[
|
|
245
|
+
[type[BaseException], BaseException, TracebackType, Logger], bool
|
|
246
|
+
]
|
|
247
|
+
| None = None,
|
|
248
|
+
log: Logger | None = None,
|
|
249
|
+
) -> None:
|
|
250
|
+
self._conn = real_connection
|
|
251
|
+
self._exception_handler = exception_handler
|
|
252
|
+
self._log = log or getLogger(__name__)
|
|
253
|
+
self._limiter = CapacityLimiter(1)
|
|
254
|
+
|
|
255
|
+
def _cursor_factory(self, cursor: _Cursor) -> "Cursor":
|
|
256
|
+
return Cursor(
|
|
257
|
+
cursor,
|
|
258
|
+
self._limiter,
|
|
259
|
+
exception_handler=self._exception_handler,
|
|
260
|
+
log=self._log,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def __aenter__(self) -> Self:
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
async def __aexit__(
|
|
267
|
+
self,
|
|
268
|
+
exc_type: type[BaseException] | None,
|
|
269
|
+
exc_val: BaseException | None,
|
|
270
|
+
exc_tb: TracebackType | None,
|
|
271
|
+
) -> bool | None:
|
|
272
|
+
await self.close()
|
|
273
|
+
|
|
274
|
+
if exc_val is None:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
assert exc_type is not None
|
|
278
|
+
assert exc_val is not None
|
|
279
|
+
assert exc_tb is not None
|
|
280
|
+
|
|
281
|
+
exception_handled = False
|
|
282
|
+
if self._exception_handler is not None:
|
|
283
|
+
exception_handled = self._exception_handler(
|
|
284
|
+
exc_type, exc_val, exc_tb, self._log
|
|
285
|
+
)
|
|
286
|
+
return exception_handled
|
|
287
|
+
|
|
288
|
+
async def backup(
|
|
289
|
+
self,
|
|
290
|
+
dest: "Connection",
|
|
291
|
+
pages: int | None = None,
|
|
292
|
+
name: str | None = None,
|
|
293
|
+
progress: Callable[[int, int, bool], None] | None = None,
|
|
294
|
+
src_name: str | None = None,
|
|
295
|
+
) -> None:
|
|
296
|
+
return await run_sync(
|
|
297
|
+
self._conn.backup,
|
|
298
|
+
dest._conn,
|
|
299
|
+
pages,
|
|
300
|
+
name,
|
|
301
|
+
progress,
|
|
302
|
+
src_name,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def backup_to_file(
|
|
306
|
+
self,
|
|
307
|
+
filename: str,
|
|
308
|
+
pages: int | None = None,
|
|
309
|
+
name: str | None = None,
|
|
310
|
+
progress: Callable[[int, int, bool], None] | None = None,
|
|
311
|
+
src_name: str | None = None,
|
|
312
|
+
) -> None:
|
|
313
|
+
await run_sync(
|
|
314
|
+
self._conn.backup_to_file,
|
|
315
|
+
filename,
|
|
316
|
+
pages,
|
|
317
|
+
name,
|
|
318
|
+
progress,
|
|
319
|
+
src_name,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def begin(self, lock: _IsolationLevel = None) -> None:
|
|
323
|
+
await run_sync(self._conn.begin, lock, limiter=self._limiter)
|
|
324
|
+
|
|
325
|
+
async def execute(
|
|
326
|
+
self, sql: str, parameters: _Parameters | None = None, /
|
|
327
|
+
) -> "Cursor":
|
|
328
|
+
cursor = await run_sync(
|
|
329
|
+
self._conn.execute,
|
|
330
|
+
sql,
|
|
331
|
+
parameters,
|
|
332
|
+
limiter=self._limiter,
|
|
333
|
+
)
|
|
334
|
+
return self._cursor_factory(cursor)
|
|
335
|
+
|
|
336
|
+
async def executemany(
|
|
337
|
+
self, sql: str, seq_of_params: Sequence[_Parameters] | None = None
|
|
338
|
+
) -> "Cursor":
|
|
339
|
+
cursor = await run_sync(self._conn.executemany, sql, seq_of_params)
|
|
340
|
+
return self._cursor_factory(cursor)
|
|
341
|
+
|
|
342
|
+
async def executescript(self, sql: str) -> "Cursor":
|
|
343
|
+
cursor = await run_sync(self._conn.executescript, sql)
|
|
344
|
+
return self._cursor_factory(cursor)
|
|
345
|
+
|
|
346
|
+
async def close(self) -> None:
|
|
347
|
+
await run_sync(self._conn.close, limiter=self._limiter)
|
|
348
|
+
|
|
349
|
+
async def checkpoint(
|
|
350
|
+
self,
|
|
351
|
+
full: bool = False,
|
|
352
|
+
truncate: bool = False,
|
|
353
|
+
restart: bool = False,
|
|
354
|
+
name: str | None = None,
|
|
355
|
+
) -> tuple[int, int]:
|
|
356
|
+
return await run_sync(
|
|
357
|
+
self._conn.checkpoint, full, truncate, restart, name
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def commit(self) -> None:
|
|
361
|
+
await run_sync(self._conn.commit, limiter=self._limiter)
|
|
362
|
+
|
|
363
|
+
async def rollback(self) -> None:
|
|
364
|
+
await run_sync(self._conn.rollback, limiter=self._limiter)
|
|
365
|
+
|
|
366
|
+
async def cursor(self) -> "Cursor":
|
|
367
|
+
return self._cursor_factory(
|
|
368
|
+
await run_sync(self._conn.cursor, limiter=self._limiter)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def execute_one(self, sql: str, params: _Parameters | None = None):
|
|
372
|
+
return await run_sync(self._conn.execute_one, sql, params)
|
|
373
|
+
|
|
374
|
+
async def execute_scalar(
|
|
375
|
+
self, sql: str, params: _Parameters | None = None
|
|
376
|
+
):
|
|
377
|
+
return await run_sync(self._conn.execute_scalar, sql, params)
|
|
378
|
+
|
|
379
|
+
async def autocommit(self) -> int:
|
|
380
|
+
return await run_sync(self._conn.autocommit)
|
|
381
|
+
|
|
382
|
+
def atomic(self) -> Atomic:
|
|
383
|
+
"""Opens a new atomic function, this can be used
|
|
384
|
+
as both an async wrapper or wrappable function"""
|
|
385
|
+
return Atomic(self._conn.atomic(), self._limiter)
|
|
386
|
+
|
|
387
|
+
def savepoint(self, sid: str | None = None) -> Savepoint:
|
|
388
|
+
"""Opens a new savepoint, this can be used
|
|
389
|
+
as both an async wrapper or wrappable function"""
|
|
390
|
+
return Savepoint(self._conn.savepoint(sid), self._limiter)
|
|
391
|
+
|
|
392
|
+
def transaction(self, lock: _IsolationLevel = None):
|
|
393
|
+
"""Opens a new transaction, this can be used
|
|
394
|
+
as both an async wrapper or wrappable function"""
|
|
395
|
+
return Transaction(self._conn.transaction(lock), self._limiter)
|
|
396
|
+
|
|
397
|
+
async def last_insert_rowid(self) -> int:
|
|
398
|
+
return await run_sync(self._conn.last_insert_rowid)
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def in_transaction(self):
|
|
402
|
+
return self._conn.in_transaction
|
|
403
|
+
|
|
404
|
+
async def status(self, flag: int) -> tuple[int, int]:
|
|
405
|
+
return await run_sync(self._conn.status, flag)
|
|
406
|
+
|
|
407
|
+
async def pragma(
|
|
408
|
+
self,
|
|
409
|
+
key: str,
|
|
410
|
+
value: Any = SENTINEL,
|
|
411
|
+
database: str | None = None,
|
|
412
|
+
multi: bool = False,
|
|
413
|
+
permanent: bool = False,
|
|
414
|
+
) -> Any:
|
|
415
|
+
if value != SENTINEL:
|
|
416
|
+
return await run_sync(
|
|
417
|
+
self._conn.pragma, key, value, database, multi, permanent
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
# just make value empty we have a different SENTINEL anyways...
|
|
421
|
+
return await run_sync(
|
|
422
|
+
partial(
|
|
423
|
+
self._conn.pragma,
|
|
424
|
+
key=key,
|
|
425
|
+
database=database,
|
|
426
|
+
multi=multi,
|
|
427
|
+
permanent=permanent,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
async def connect(
|
|
433
|
+
database: str | Path,
|
|
434
|
+
flags: int | None = None,
|
|
435
|
+
timeout: float = 5.0,
|
|
436
|
+
vfs: str | None = None,
|
|
437
|
+
uri: bool = False,
|
|
438
|
+
cached_statements: int = 100,
|
|
439
|
+
extensions: bool = True,
|
|
440
|
+
row_factory: Callable[..., "cysqlite.Row"] | None = None,
|
|
441
|
+
autoconnect: bool = True,
|
|
442
|
+
log: Logger | None = None,
|
|
443
|
+
exception_handler: Callable[
|
|
444
|
+
[type[BaseException], BaseException, TracebackType, Logger], bool
|
|
445
|
+
]
|
|
446
|
+
| None = None,
|
|
447
|
+
) -> Connection:
|
|
448
|
+
"""
|
|
449
|
+
Open a Connection to the provided database.
|
|
450
|
+
|
|
451
|
+
:param database: database filename or ':memory:'
|
|
452
|
+
for an in-memory database.
|
|
453
|
+
:type database: str | pathlib.Path
|
|
454
|
+
:param flags: control how database is opened. See Sqlite Connection Flags.
|
|
455
|
+
:type flags: int | None
|
|
456
|
+
:param timeout: seconds to retry acquiring write lock before raising a
|
|
457
|
+
OperationalError when table is locked.
|
|
458
|
+
:type timeout: float
|
|
459
|
+
:param vfs: VFS to use, optional.
|
|
460
|
+
:type vfs: str | None
|
|
461
|
+
:param uri: Allow connecting using a URI.
|
|
462
|
+
:type uri: bool
|
|
463
|
+
:param cached_statements: Size of statement cache.
|
|
464
|
+
:type cached_statements: int
|
|
465
|
+
:param extensions: Support run-time loadable extensions.
|
|
466
|
+
:type extensions: bool
|
|
467
|
+
:param row_factory: Factory implementation for constructing rows, e.g. Row
|
|
468
|
+
:type row_factory: Callable[..., _T] | None
|
|
469
|
+
:param autoconnect: Open connection when initiated
|
|
470
|
+
:type autoconnect: bool
|
|
471
|
+
:rtype: Connection
|
|
472
|
+
:returns: Connection to database under an anyio asynchronous wrapper
|
|
473
|
+
"""
|
|
474
|
+
conn = await run_sync(
|
|
475
|
+
partial(
|
|
476
|
+
cysqlite.connect,
|
|
477
|
+
database=str(database),
|
|
478
|
+
flags=flags,
|
|
479
|
+
timeout=timeout,
|
|
480
|
+
vfs=vfs,
|
|
481
|
+
uri=uri,
|
|
482
|
+
cached_statements=cached_statements,
|
|
483
|
+
extensions=extensions,
|
|
484
|
+
row_factory=row_factory or cysqlite.Row,
|
|
485
|
+
autoconnect=autoconnect,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
return Connection(conn, exception_handler, log)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def exception_logger(
|
|
492
|
+
exc_type: type[BaseException] | None,
|
|
493
|
+
exc_val: BaseException | None,
|
|
494
|
+
exc_tb: TracebackType | None,
|
|
495
|
+
log: Logger,
|
|
496
|
+
) -> bool:
|
|
497
|
+
"""An exception handler that logs the exception and discards it."""
|
|
498
|
+
log.error("SQLite exception", exc_info=exc_val)
|
|
499
|
+
return True # the exception was handled
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import types
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal, ParamSpec, TypeAlias, TypeVar
|
|
7
|
+
|
|
8
|
+
import cysqlite
|
|
9
|
+
from cysqlite.metadata import *
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 12):
|
|
12
|
+
from collections.abc import Buffer
|
|
13
|
+
else:
|
|
14
|
+
from typing_extensions import Buffer
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 11):
|
|
17
|
+
from typing import Self
|
|
18
|
+
else:
|
|
19
|
+
from typing_extensions import Self
|
|
20
|
+
|
|
21
|
+
# cysqlite maintainer currently shows Zero intrest in the idea
|
|
22
|
+
# of utilizing typehints so that users don't have to second guess
|
|
23
|
+
# and validate code so we must do everything ourselves
|
|
24
|
+
# luckily, sqlite3 has a typeshed stubfile with pleantly of helpful information
|
|
25
|
+
# that we can safely utilize.
|
|
26
|
+
|
|
27
|
+
_SqliteData: TypeAlias = str | Buffer | int | float | None
|
|
28
|
+
_AdaptedInputData: TypeAlias = _SqliteData | Any
|
|
29
|
+
_Parameters: TypeAlias = (
|
|
30
|
+
Sequence[_AdaptedInputData] | Mapping[str, _AdaptedInputData]
|
|
31
|
+
)
|
|
32
|
+
_IsolationLevel: TypeAlias = (
|
|
33
|
+
Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
P = ParamSpec("P")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Cysqlite doesn't have good typehints avalible yet so we have to do a bit
|
|
41
|
+
# Of hacking ourselves, The most optimal solution was to make
|
|
42
|
+
# mimics of the function signatures as abstract classes to get around the issue
|
|
43
|
+
# SEE: https://github.com/coleifer/cysqlite/issues/1
|
|
44
|
+
|
|
45
|
+
# NOTE: in 0.3.1+ coleifer and cysqlite maintainers takes my advice seriously.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Know that these ABCs are not perfect but they try their best to typehint as a
|
|
49
|
+
# workaround
|
|
50
|
+
# this very bad issue.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _callable_context_manager(ABC):
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def __call__(self, fn: Callable[P, T]) -> Callable[P, T]: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Atomic(_callable_context_manager):
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def __init__(
|
|
61
|
+
self, conn: "cysqlite.Connection", lock: _IsolationLevel | str = None
|
|
62
|
+
) -> None: ...
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def __enter__(self) -> Self: ...
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def __exit__(
|
|
67
|
+
self,
|
|
68
|
+
type: type[BaseException] | None,
|
|
69
|
+
value: BaseException | None,
|
|
70
|
+
traceback: types.TracebackType | None,
|
|
71
|
+
) -> bool | None: ...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def commit(self, begin: bool = True) -> None: ...
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def rollback(self, begin: bool = True) -> None: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _Savepoint(_callable_context_manager):
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def __init__(
|
|
82
|
+
self, conn: "cysqlite.Connection", sid: str | None = None
|
|
83
|
+
) -> None: ...
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def commit(self, begin: bool = True) -> None: ...
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def rollback(self) -> None: ...
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def __enter__(self) -> Self: ...
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def __exit__(
|
|
92
|
+
self,
|
|
93
|
+
type: type[BaseException] | None,
|
|
94
|
+
value: BaseException | None,
|
|
95
|
+
traceback: types.TracebackType | None,
|
|
96
|
+
) -> bool | None: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _Transaction(_callable_context_manager):
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def __init__(
|
|
102
|
+
self, conn: "cysqlite.Connection", lock: _IsolationLevel = None
|
|
103
|
+
) -> None: ...
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def commit(self, begin: bool = True) -> None: ...
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def rollback(self, begin: bool = True) -> None: ...
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def __enter__(self) -> Self: ...
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def __exit__(
|
|
112
|
+
self,
|
|
113
|
+
type: type[BaseException] | None,
|
|
114
|
+
value: BaseException | None,
|
|
115
|
+
traceback: types.TracebackType | None,
|
|
116
|
+
) -> bool | None: ...
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anyio-cysqlite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: a anyio and cysqlite library
|
|
5
|
+
Author-email: Vizonex <VizonexBusiness@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: anyio>=4.13.0
|
|
10
|
+
Requires-Dist: cysqlite>=0.3.2
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest; extra == "test"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
<img src="https://raw.githubusercontent.com/Vizonex/anyio-cysqlite/main/anyio-cysqlite-logo.PNG"/>
|
|
16
|
+
|
|
17
|
+
[](https://opensource.org/licenses/MIT)
|
|
18
|
+
|
|
19
|
+
# anyio-cysqlite
|
|
20
|
+
A Bidning for cysqlite that makes the database asynchronous
|
|
21
|
+
with any asynchronous library that can be binded to anyio
|
|
22
|
+
meaning that it will work with many server implementations and applications including FastAPI, Starlette and Litestar to name a few server libraries.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from anyio_cysqlite import connect
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
async with await connect("cysqlite.db") as db:
|
|
30
|
+
await db.execute('create table IF NOT EXISTS data (k, v)')
|
|
31
|
+
|
|
32
|
+
async with db.atomic():
|
|
33
|
+
await db.executemany('insert into data (k, v) values (?, ?)',
|
|
34
|
+
[(f'k{i:02d}', f'v{i:02d}') for i in range(10)])
|
|
35
|
+
print(await db.last_insert_rowid()) # 10.
|
|
36
|
+
|
|
37
|
+
curs = await db.execute('select * from data')
|
|
38
|
+
async for row in curs:
|
|
39
|
+
print(row) # e.g., ('k00', 'v00')
|
|
40
|
+
|
|
41
|
+
# We can use named parameters with a dict as well.
|
|
42
|
+
row = await db.execute_one('select * from data where k = :key and v = :val',
|
|
43
|
+
{'key': 'k05', 'val': 'v05'})
|
|
44
|
+
print(row) # ('k05', 'v05')
|
|
45
|
+
|
|
46
|
+
await db.close()
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
import anyio
|
|
50
|
+
anyio.run(main)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Updated functionality is better
|
|
54
|
+
When it comes to database execution updated functionality normally results in better performance such as providing wrappers for `atomic`, `transaction`, and `savepoint` functions which the standard sqlite3 library doesn't currently have. Instead of having a completely seperate thread for the database all operations are done using the anyio's `CapacityLimiter` and `run_sync` making the database less resource heavy.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Batteries included
|
|
58
|
+
Unlike cysqlite (As of currently), this library retains access to typehints right out of the box making the library less of a function and attribute guessing game.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/anyio_cysqlite/__init__.py
|
|
5
|
+
src/anyio_cysqlite/db.py
|
|
6
|
+
src/anyio_cysqlite/py.typed
|
|
7
|
+
src/anyio_cysqlite/typedefs.py
|
|
8
|
+
src/anyio_cysqlite.egg-info/PKG-INFO
|
|
9
|
+
src/anyio_cysqlite.egg-info/SOURCES.txt
|
|
10
|
+
src/anyio_cysqlite.egg-info/dependency_links.txt
|
|
11
|
+
src/anyio_cysqlite.egg-info/requires.txt
|
|
12
|
+
src/anyio_cysqlite.egg-info/top_level.txt
|
|
13
|
+
tests/test_context_manager.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
anyio_cysqlite
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import cysqlite
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import anyio_cysqlite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def test_context_manager_commit(anyio_backend):
|
|
10
|
+
mem_uri = f"file:{anyio_backend}_mem0?mode=memory&cache=shared"
|
|
11
|
+
async with await anyio_cysqlite.connect(mem_uri, uri=True) as acon0:
|
|
12
|
+
async with acon0.atomic():
|
|
13
|
+
await acon0.execute(
|
|
14
|
+
"CREATE TABLE IF NOT EXISTS lang(id INTEGER PRIMARY KEY,"
|
|
15
|
+
" name VARCHAR UNIQUE)"
|
|
16
|
+
)
|
|
17
|
+
await acon0.execute(
|
|
18
|
+
"INSERT INTO lang(name) VALUES(?)", ("Python",)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Reason we don't open a new connection is due to how cysqlite
|
|
22
|
+
# handles memory. Otherwise the test with sqlite-anyio would be
|
|
23
|
+
# 1 to 1
|
|
24
|
+
acur1 = await acon0.cursor()
|
|
25
|
+
await acur1.execute("SELECT name FROM lang")
|
|
26
|
+
assert await acur1.fetchone() == ("Python",)
|
|
27
|
+
await acur1.execute("DROP TABLE IF EXISTS lang;")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def test_context_manager_execute(anyio_backend):
|
|
31
|
+
mem_uri = f"file:{anyio_backend}_mem0?mode=memory&cache=shared"
|
|
32
|
+
async with await anyio_cysqlite.connect(mem_uri, uri=True) as acon0:
|
|
33
|
+
await acon0.execute(
|
|
34
|
+
"CREATE TABLE lang(id INTEGER PRIMARY KEY, name VARCHAR UNIQUE)"
|
|
35
|
+
)
|
|
36
|
+
await acon0.execute("INSERT INTO lang(name) VALUES(?)", ("Python",))
|
|
37
|
+
|
|
38
|
+
acur1 = await acon0.cursor()
|
|
39
|
+
await acur1.execute("SELECT name FROM lang")
|
|
40
|
+
assert await acur1.fetchone() == ("Python",)
|
|
41
|
+
await acur1.execute("DROP TABLE IF EXISTS lang;")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def test_context_manager_rollback(anyio_backend):
|
|
45
|
+
mem_uri = f"file:{anyio_backend}_mem1?mode=memory&cache=shared"
|
|
46
|
+
with pytest.raises(RuntimeError):
|
|
47
|
+
async with await anyio_cysqlite.connect(mem_uri, uri=True) as acon0:
|
|
48
|
+
acur0 = await acon0.cursor()
|
|
49
|
+
await acur0.execute(
|
|
50
|
+
"CREATE TABLE lang(id INTEGER PRIMARY KEY,"
|
|
51
|
+
" name VARCHAR UNIQUE)"
|
|
52
|
+
)
|
|
53
|
+
await acur0.execute(
|
|
54
|
+
"INSERT INTO lang(name) VALUES(?)", ("Python",)
|
|
55
|
+
)
|
|
56
|
+
raise RuntimeError("foo")
|
|
57
|
+
|
|
58
|
+
async with await anyio_cysqlite.connect(mem_uri, uri=True) as db:
|
|
59
|
+
await db.execute("DROP TABLE IF EXISTS lang;")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def test_cursor_context_manager(anyio_backend, caplog):
|
|
63
|
+
caplog.set_level(logging.INFO)
|
|
64
|
+
mem_uri = f"file:{anyio_backend}_mem2?mode=memory&cache=shared"
|
|
65
|
+
log = logging.getLogger("logger")
|
|
66
|
+
async with await anyio_cysqlite.connect(
|
|
67
|
+
mem_uri,
|
|
68
|
+
uri=True,
|
|
69
|
+
exception_handler=anyio_cysqlite.exception_logger,
|
|
70
|
+
log=log,
|
|
71
|
+
) as acon0:
|
|
72
|
+
async with await acon0.cursor() as acur0:
|
|
73
|
+
await acur0.execute(
|
|
74
|
+
"CREATE TABLE lang(id INTEGER PRIMARY KEY,"
|
|
75
|
+
" name VARCHAR UNIQUE)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with pytest.raises(cysqlite.ProgrammingError):
|
|
79
|
+
await acur0.execute(
|
|
80
|
+
"INSERT INTO lang(name) VALUES(?)", ("Python",)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async with await acon0.cursor() as acur1:
|
|
84
|
+
await acur1.execute("SELECT name FROM lang")
|
|
85
|
+
assert await acur1.fetchone() is None
|
|
86
|
+
await acur1.execute("INSERT INTO foo(name) VALUES(?)", ("Python",))
|
|
87
|
+
|
|
88
|
+
assert "SQLite exception" in caplog.text
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def test_exception_logger(anyio_backend, caplog):
|
|
92
|
+
caplog.set_level(logging.INFO)
|
|
93
|
+
mem_uri = f"file:{anyio_backend}_mem3?mode=memory&cache=shared"
|
|
94
|
+
log = logging.getLogger("logger")
|
|
95
|
+
async with await anyio_cysqlite.connect(
|
|
96
|
+
mem_uri,
|
|
97
|
+
uri=True,
|
|
98
|
+
exception_handler=anyio_cysqlite.exception_logger,
|
|
99
|
+
log=log,
|
|
100
|
+
) as acon0:
|
|
101
|
+
acur0 = await acon0.cursor()
|
|
102
|
+
await acur0.execute(
|
|
103
|
+
"CREATE TABLE lang(id INTEGER PRIMARY KEY, name VARCHAR UNIQUE)"
|
|
104
|
+
)
|
|
105
|
+
await acur0.execute("INSERT INTO lang(name) VALUES(?)", ("Python",))
|
|
106
|
+
raise RuntimeError("foo")
|
|
107
|
+
|
|
108
|
+
assert "SQLite exception" in caplog.text
|