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.
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ anyio>=4.13.0
2
+ cysqlite>=0.3.2
3
+
4
+ [test]
5
+ pytest
@@ -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