dqlite-dbapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dqlite_dbapi-0.1.0.dist-info/METADATA +82 -0
- dqlite_dbapi-0.1.0.dist-info/RECORD +13 -0
- dqlite_dbapi-0.1.0.dist-info/WHEEL +4 -0
- dqlite_dbapi-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- dqlitedbapi/__init__.py +98 -0
- dqlitedbapi/aio/__init__.py +31 -0
- dqlitedbapi/aio/connection.py +86 -0
- dqlitedbapi/aio/cursor.py +163 -0
- dqlitedbapi/connection.py +110 -0
- dqlitedbapi/cursor.py +181 -0
- dqlitedbapi/exceptions.py +61 -0
- dqlitedbapi/py.typed +0 -0
- dqlitedbapi/types.py +64 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dqlite-dbapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PEP 249 (DB-API 2.0) compliant interface for dqlite
|
|
5
|
+
Project-URL: Homepage, https://github.com/letsdiscodev/python-dqlite-dbapi
|
|
6
|
+
Project-URL: Repository, https://github.com/letsdiscodev/python-dqlite-dbapi
|
|
7
|
+
Project-URL: Issues, https://github.com/letsdiscodev/python-dqlite-dbapi/issues
|
|
8
|
+
Author-email: Antoine Leclair <antoineleclair@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Keywords: database,dbapi,distributed,dqlite,pep249,sqlite
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
20
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: dqlite-client>=0.1.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# dqlite-dbapi
|
|
33
|
+
|
|
34
|
+
PEP 249 compliant interface for [dqlite](https://dqlite.io/).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install dqlite-dbapi
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Sync Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import dqlitedbapi
|
|
46
|
+
|
|
47
|
+
conn = dqlitedbapi.connect("localhost:9001")
|
|
48
|
+
cursor = conn.cursor()
|
|
49
|
+
cursor.execute("SELECT 1")
|
|
50
|
+
print(cursor.fetchone())
|
|
51
|
+
conn.close()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Async Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import asyncio
|
|
58
|
+
from dqlitedbapi.aio import aconnect
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
conn = await aconnect("localhost:9001")
|
|
62
|
+
cursor = await conn.cursor()
|
|
63
|
+
await cursor.execute("SELECT 1")
|
|
64
|
+
print(await cursor.fetchone())
|
|
65
|
+
await conn.close()
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## PEP 249 Compliance
|
|
71
|
+
|
|
72
|
+
- `apilevel = "2.0"`
|
|
73
|
+
- `threadsafety = 1`
|
|
74
|
+
- `paramstyle = "qmark"`
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for setup and contribution guidelines.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
dqlitedbapi/__init__.py,sha256=mCP0ru8Xlor99Bxo7q_QUPWXuzy896Awx4ih0rKzB10,1955
|
|
2
|
+
dqlitedbapi/connection.py,sha256=I_zTYxiuem-dX5DQhC0Fl1rQ9Hu9kcFef9DniKeAOi4,3515
|
|
3
|
+
dqlitedbapi/cursor.py,sha256=AZALE886OfthknwsiG8agCaAWw8tPvd7wz6OELYimrw,5913
|
|
4
|
+
dqlitedbapi/exceptions.py,sha256=u5y1xcDJudLKUdCFr-ZHBFk5QsJasRBZ8E6lBGTubTg,1016
|
|
5
|
+
dqlitedbapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dqlitedbapi/types.py,sha256=_8ZUOl0rWWxRGepOt7X7kfXgXxtrEA_fCPNIohY_dIY,2008
|
|
7
|
+
dqlitedbapi/aio/__init__.py,sha256=UKkGUzN4ALSYEyHyNF7htxu_WPLcI12_NoLMpoUaadM,722
|
|
8
|
+
dqlitedbapi/aio/connection.py,sha256=bJ3Nrk3ejk7bTZmqF_LkoS3b4EyO8myXpsYJW4sb3m8,2565
|
|
9
|
+
dqlitedbapi/aio/cursor.py,sha256=N-8gsgrXAYoiEKLGurZJ1GpEO5fuMx7AJ7_BLPMYdoE,5131
|
|
10
|
+
dqlite_dbapi-0.1.0.dist-info/METADATA,sha256=2PI3TjJriA2Obo8519fYKdnGLwTn-LONstwKFKUtPwU,2115
|
|
11
|
+
dqlite_dbapi-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
dqlite_dbapi-0.1.0.dist-info/licenses/LICENSE.md,sha256=25ur0qy0m17OgHzc1Ve3NeLumyjGGgVBrN-JL3znfDg,1090
|
|
13
|
+
dqlite_dbapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine Leclair and Greg Sadetsky
|
|
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.
|
dqlitedbapi/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""PEP 249 compliant interface for dqlite."""
|
|
2
|
+
|
|
3
|
+
from dqlitedbapi.connection import Connection
|
|
4
|
+
from dqlitedbapi.cursor import Cursor
|
|
5
|
+
from dqlitedbapi.exceptions import (
|
|
6
|
+
DatabaseError,
|
|
7
|
+
DataError,
|
|
8
|
+
Error,
|
|
9
|
+
IntegrityError,
|
|
10
|
+
InterfaceError,
|
|
11
|
+
InternalError,
|
|
12
|
+
NotSupportedError,
|
|
13
|
+
OperationalError,
|
|
14
|
+
ProgrammingError,
|
|
15
|
+
Warning,
|
|
16
|
+
)
|
|
17
|
+
from dqlitedbapi.types import (
|
|
18
|
+
BINARY,
|
|
19
|
+
DATETIME,
|
|
20
|
+
NUMBER,
|
|
21
|
+
ROWID,
|
|
22
|
+
STRING,
|
|
23
|
+
Binary,
|
|
24
|
+
Date,
|
|
25
|
+
DateFromTicks,
|
|
26
|
+
Time,
|
|
27
|
+
TimeFromTicks,
|
|
28
|
+
Timestamp,
|
|
29
|
+
TimestampFromTicks,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# PEP 249 module-level attributes
|
|
33
|
+
apilevel = "2.0"
|
|
34
|
+
threadsafety = 1 # Threads may share the module, but not connections
|
|
35
|
+
paramstyle = "qmark" # Question mark style: WHERE name=?
|
|
36
|
+
|
|
37
|
+
# SQLite compatibility attributes (for SQLAlchemy)
|
|
38
|
+
# dqlite uses SQLite 3.x internally
|
|
39
|
+
sqlite_version_info = (3, 35, 0)
|
|
40
|
+
sqlite_version = "3.35.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Module attributes
|
|
44
|
+
"apilevel",
|
|
45
|
+
"threadsafety",
|
|
46
|
+
"paramstyle",
|
|
47
|
+
# Functions
|
|
48
|
+
"connect",
|
|
49
|
+
# Classes
|
|
50
|
+
"Connection",
|
|
51
|
+
"Cursor",
|
|
52
|
+
# Exceptions
|
|
53
|
+
"Warning",
|
|
54
|
+
"Error",
|
|
55
|
+
"InterfaceError",
|
|
56
|
+
"DatabaseError",
|
|
57
|
+
"DataError",
|
|
58
|
+
"OperationalError",
|
|
59
|
+
"IntegrityError",
|
|
60
|
+
"InternalError",
|
|
61
|
+
"ProgrammingError",
|
|
62
|
+
"NotSupportedError",
|
|
63
|
+
# Type constructors
|
|
64
|
+
"Date",
|
|
65
|
+
"Time",
|
|
66
|
+
"Timestamp",
|
|
67
|
+
"DateFromTicks",
|
|
68
|
+
"TimeFromTicks",
|
|
69
|
+
"TimestampFromTicks",
|
|
70
|
+
"Binary",
|
|
71
|
+
# Type objects
|
|
72
|
+
"STRING",
|
|
73
|
+
"BINARY",
|
|
74
|
+
"NUMBER",
|
|
75
|
+
"DATETIME",
|
|
76
|
+
"ROWID",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
__version__ = "0.1.0"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def connect(
|
|
83
|
+
address: str,
|
|
84
|
+
*,
|
|
85
|
+
database: str = "default",
|
|
86
|
+
timeout: float = 10.0,
|
|
87
|
+
) -> Connection:
|
|
88
|
+
"""Connect to a dqlite database.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
address: Node address in "host:port" format
|
|
92
|
+
database: Database name to open
|
|
93
|
+
timeout: Connection timeout in seconds
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A Connection object
|
|
97
|
+
"""
|
|
98
|
+
return Connection(address, database=database, timeout=timeout)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Async PEP 249-style interface for dqlite."""
|
|
2
|
+
|
|
3
|
+
from dqlitedbapi.aio.connection import AsyncConnection
|
|
4
|
+
from dqlitedbapi.aio.cursor import AsyncCursor
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"aconnect",
|
|
8
|
+
"AsyncConnection",
|
|
9
|
+
"AsyncCursor",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def aconnect(
|
|
14
|
+
address: str,
|
|
15
|
+
*,
|
|
16
|
+
database: str = "default",
|
|
17
|
+
timeout: float = 10.0,
|
|
18
|
+
) -> AsyncConnection:
|
|
19
|
+
"""Connect to a dqlite database asynchronously.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
address: Node address in "host:port" format
|
|
23
|
+
database: Database name to open
|
|
24
|
+
timeout: Connection timeout in seconds
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
An AsyncConnection object
|
|
28
|
+
"""
|
|
29
|
+
conn = AsyncConnection(address, database=database, timeout=timeout)
|
|
30
|
+
await conn.connect()
|
|
31
|
+
return conn
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Async connection implementation for dqlite."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from dqliteclient import DqliteConnection
|
|
6
|
+
from dqlitedbapi.aio.cursor import AsyncCursor
|
|
7
|
+
from dqlitedbapi.exceptions import InterfaceError, OperationalError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncConnection:
|
|
11
|
+
"""Async database connection."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
address: str,
|
|
16
|
+
*,
|
|
17
|
+
database: str = "default",
|
|
18
|
+
timeout: float = 10.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Initialize connection (does not connect yet).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
address: Node address in "host:port" format
|
|
24
|
+
database: Database name to open
|
|
25
|
+
timeout: Connection timeout in seconds
|
|
26
|
+
"""
|
|
27
|
+
self._address = address
|
|
28
|
+
self._database = database
|
|
29
|
+
self._timeout = timeout
|
|
30
|
+
self._async_conn: DqliteConnection | None = None
|
|
31
|
+
self._closed = False
|
|
32
|
+
|
|
33
|
+
async def connect(self) -> None:
|
|
34
|
+
"""Establish the connection."""
|
|
35
|
+
if self._closed:
|
|
36
|
+
raise InterfaceError("Connection is closed")
|
|
37
|
+
|
|
38
|
+
if self._async_conn is not None:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
self._async_conn = DqliteConnection(
|
|
42
|
+
self._address,
|
|
43
|
+
database=self._database,
|
|
44
|
+
timeout=self._timeout,
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
await self._async_conn.connect()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self._async_conn = None
|
|
50
|
+
raise OperationalError(f"Failed to connect: {e}") from e
|
|
51
|
+
|
|
52
|
+
async def close(self) -> None:
|
|
53
|
+
"""Close the connection."""
|
|
54
|
+
if self._async_conn is not None:
|
|
55
|
+
await self._async_conn.close()
|
|
56
|
+
self._async_conn = None
|
|
57
|
+
self._closed = True
|
|
58
|
+
|
|
59
|
+
async def commit(self) -> None:
|
|
60
|
+
"""Commit any pending transaction."""
|
|
61
|
+
if self._closed:
|
|
62
|
+
raise InterfaceError("Connection is closed")
|
|
63
|
+
|
|
64
|
+
if self._async_conn is not None:
|
|
65
|
+
await self._async_conn.execute("COMMIT")
|
|
66
|
+
|
|
67
|
+
async def rollback(self) -> None:
|
|
68
|
+
"""Roll back any pending transaction."""
|
|
69
|
+
if self._closed:
|
|
70
|
+
raise InterfaceError("Connection is closed")
|
|
71
|
+
|
|
72
|
+
if self._async_conn is not None:
|
|
73
|
+
await self._async_conn.execute("ROLLBACK")
|
|
74
|
+
|
|
75
|
+
async def cursor(self) -> AsyncCursor:
|
|
76
|
+
"""Return a new AsyncCursor object."""
|
|
77
|
+
if self._closed:
|
|
78
|
+
raise InterfaceError("Connection is closed")
|
|
79
|
+
return AsyncCursor(self)
|
|
80
|
+
|
|
81
|
+
async def __aenter__(self) -> "AsyncConnection":
|
|
82
|
+
await self.connect()
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
86
|
+
await self.close()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Async cursor implementation for dqlite."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from dqlitedbapi.exceptions import InterfaceError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from dqlitedbapi.aio.connection import AsyncConnection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncCursor:
|
|
13
|
+
"""Async database cursor."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, connection: "AsyncConnection") -> None:
|
|
16
|
+
self._connection = connection
|
|
17
|
+
self._description: list[tuple[str, None, None, None, None, None, None]] | None = None
|
|
18
|
+
self._rowcount = -1
|
|
19
|
+
self._arraysize = 1
|
|
20
|
+
self._rows: list[tuple[Any, ...]] = []
|
|
21
|
+
self._row_index = 0
|
|
22
|
+
self._closed = False
|
|
23
|
+
self._lastrowid: int | None = None
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def description(
|
|
27
|
+
self,
|
|
28
|
+
) -> list[tuple[str, None, None, None, None, None, None]] | None:
|
|
29
|
+
"""Column descriptions for the last query."""
|
|
30
|
+
return self._description
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def rowcount(self) -> int:
|
|
34
|
+
"""Number of rows affected by the last execute."""
|
|
35
|
+
return self._rowcount
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def lastrowid(self) -> int | None:
|
|
39
|
+
"""Row ID of the last inserted row."""
|
|
40
|
+
return self._lastrowid
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def arraysize(self) -> int:
|
|
44
|
+
"""Number of rows to fetch at a time with fetchmany()."""
|
|
45
|
+
return self._arraysize
|
|
46
|
+
|
|
47
|
+
@arraysize.setter
|
|
48
|
+
def arraysize(self, value: int) -> None:
|
|
49
|
+
self._arraysize = value
|
|
50
|
+
|
|
51
|
+
def _check_closed(self) -> None:
|
|
52
|
+
if self._closed:
|
|
53
|
+
raise InterfaceError("Cursor is closed")
|
|
54
|
+
|
|
55
|
+
async def execute(
|
|
56
|
+
self, operation: str, parameters: Sequence[Any] | None = None
|
|
57
|
+
) -> "AsyncCursor":
|
|
58
|
+
"""Execute a database operation (query or command)."""
|
|
59
|
+
self._check_closed()
|
|
60
|
+
|
|
61
|
+
conn = self._connection._async_conn
|
|
62
|
+
if conn is None:
|
|
63
|
+
raise InterfaceError("Connection is not open")
|
|
64
|
+
|
|
65
|
+
params = list(parameters) if parameters else None
|
|
66
|
+
|
|
67
|
+
# Determine if this is a SELECT query
|
|
68
|
+
is_query = operation.strip().upper().startswith(("SELECT", "PRAGMA", "EXPLAIN"))
|
|
69
|
+
|
|
70
|
+
if is_query:
|
|
71
|
+
assert conn._protocol is not None and conn._db_id is not None
|
|
72
|
+
columns, rows = await conn._protocol.query_sql(conn._db_id, operation, params)
|
|
73
|
+
self._description = [(name, None, None, None, None, None, None) for name in columns]
|
|
74
|
+
self._rows = [tuple(row) for row in rows]
|
|
75
|
+
self._row_index = 0
|
|
76
|
+
self._rowcount = len(rows)
|
|
77
|
+
else:
|
|
78
|
+
assert conn._protocol is not None and conn._db_id is not None
|
|
79
|
+
last_id, affected = await conn._protocol.exec_sql(conn._db_id, operation, params)
|
|
80
|
+
self._lastrowid = last_id
|
|
81
|
+
self._rowcount = affected
|
|
82
|
+
self._description = None
|
|
83
|
+
self._rows = []
|
|
84
|
+
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
async def executemany(
|
|
88
|
+
self, operation: str, seq_of_parameters: Sequence[Sequence[Any]]
|
|
89
|
+
) -> "AsyncCursor":
|
|
90
|
+
"""Execute a database operation multiple times."""
|
|
91
|
+
self._check_closed()
|
|
92
|
+
|
|
93
|
+
total_affected = 0
|
|
94
|
+
for params in seq_of_parameters:
|
|
95
|
+
await self.execute(operation, params)
|
|
96
|
+
if self._rowcount >= 0:
|
|
97
|
+
total_affected += self._rowcount
|
|
98
|
+
self._rowcount = total_affected
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
async def fetchone(self) -> tuple[Any, ...] | None:
|
|
102
|
+
"""Fetch the next row of a query result set."""
|
|
103
|
+
self._check_closed()
|
|
104
|
+
|
|
105
|
+
if not self._rows or self._row_index >= len(self._rows):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
row = self._rows[self._row_index]
|
|
109
|
+
self._row_index += 1
|
|
110
|
+
return row
|
|
111
|
+
|
|
112
|
+
async def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
|
|
113
|
+
"""Fetch the next set of rows of a query result."""
|
|
114
|
+
self._check_closed()
|
|
115
|
+
|
|
116
|
+
if size is None:
|
|
117
|
+
size = self._arraysize
|
|
118
|
+
|
|
119
|
+
result: list[tuple[Any, ...]] = []
|
|
120
|
+
for _ in range(size):
|
|
121
|
+
row = await self.fetchone()
|
|
122
|
+
if row is None:
|
|
123
|
+
break
|
|
124
|
+
result.append(row)
|
|
125
|
+
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
async def fetchall(self) -> list[tuple[Any, ...]]:
|
|
129
|
+
"""Fetch all remaining rows of a query result."""
|
|
130
|
+
self._check_closed()
|
|
131
|
+
|
|
132
|
+
result = self._rows[self._row_index :]
|
|
133
|
+
self._row_index = len(self._rows)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
async def close(self) -> None:
|
|
137
|
+
"""Close the cursor."""
|
|
138
|
+
self._closed = True
|
|
139
|
+
self._rows = []
|
|
140
|
+
self._description = None
|
|
141
|
+
|
|
142
|
+
def setinputsizes(self, sizes: Sequence[int | None]) -> None:
|
|
143
|
+
"""Set input sizes (no-op for dqlite)."""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def setoutputsize(self, size: int, column: int | None = None) -> None:
|
|
147
|
+
"""Set output size (no-op for dqlite)."""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def __aiter__(self) -> "AsyncCursor":
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __anext__(self) -> tuple[Any, ...]:
|
|
154
|
+
row = await self.fetchone()
|
|
155
|
+
if row is None:
|
|
156
|
+
raise StopAsyncIteration
|
|
157
|
+
return row
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self) -> "AsyncCursor":
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
163
|
+
await self.close()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""PEP 249 Connection implementation for dqlite."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from dqliteclient import DqliteConnection
|
|
7
|
+
from dqlitedbapi.cursor import Cursor
|
|
8
|
+
from dqlitedbapi.exceptions import InterfaceError, OperationalError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Connection:
|
|
12
|
+
"""PEP 249 compliant database connection."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
address: str,
|
|
17
|
+
*,
|
|
18
|
+
database: str = "default",
|
|
19
|
+
timeout: float = 10.0,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize connection (does not connect yet).
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
address: Node address in "host:port" format
|
|
25
|
+
database: Database name to open
|
|
26
|
+
timeout: Connection timeout in seconds
|
|
27
|
+
"""
|
|
28
|
+
self._address = address
|
|
29
|
+
self._database = database
|
|
30
|
+
self._timeout = timeout
|
|
31
|
+
self._async_conn: DqliteConnection | None = None
|
|
32
|
+
self._closed = False
|
|
33
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
34
|
+
|
|
35
|
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
|
36
|
+
if self._loop is None:
|
|
37
|
+
try:
|
|
38
|
+
self._loop = asyncio.get_running_loop()
|
|
39
|
+
except RuntimeError:
|
|
40
|
+
# No running loop, create a new one
|
|
41
|
+
self._loop = asyncio.new_event_loop()
|
|
42
|
+
asyncio.set_event_loop(self._loop)
|
|
43
|
+
return self._loop
|
|
44
|
+
|
|
45
|
+
async def _get_async_connection(self) -> DqliteConnection:
|
|
46
|
+
"""Get or create the underlying async connection."""
|
|
47
|
+
if self._closed:
|
|
48
|
+
raise InterfaceError("Connection is closed")
|
|
49
|
+
|
|
50
|
+
if self._async_conn is None:
|
|
51
|
+
self._async_conn = DqliteConnection(
|
|
52
|
+
self._address,
|
|
53
|
+
database=self._database,
|
|
54
|
+
timeout=self._timeout,
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
await self._async_conn.connect()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self._async_conn = None
|
|
60
|
+
raise OperationalError(f"Failed to connect: {e}") from e
|
|
61
|
+
|
|
62
|
+
return self._async_conn
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
"""Close the connection."""
|
|
66
|
+
if self._async_conn is not None:
|
|
67
|
+
loop = self._get_loop()
|
|
68
|
+
loop.run_until_complete(self._async_conn.close())
|
|
69
|
+
self._async_conn = None
|
|
70
|
+
self._closed = True
|
|
71
|
+
|
|
72
|
+
def commit(self) -> None:
|
|
73
|
+
"""Commit any pending transaction."""
|
|
74
|
+
if self._closed:
|
|
75
|
+
raise InterfaceError("Connection is closed")
|
|
76
|
+
|
|
77
|
+
if self._async_conn is not None:
|
|
78
|
+
loop = self._get_loop()
|
|
79
|
+
loop.run_until_complete(self._commit_async())
|
|
80
|
+
|
|
81
|
+
async def _commit_async(self) -> None:
|
|
82
|
+
"""Async implementation of commit."""
|
|
83
|
+
conn = await self._get_async_connection()
|
|
84
|
+
await conn.execute("COMMIT")
|
|
85
|
+
|
|
86
|
+
def rollback(self) -> None:
|
|
87
|
+
"""Roll back any pending transaction."""
|
|
88
|
+
if self._closed:
|
|
89
|
+
raise InterfaceError("Connection is closed")
|
|
90
|
+
|
|
91
|
+
if self._async_conn is not None:
|
|
92
|
+
loop = self._get_loop()
|
|
93
|
+
loop.run_until_complete(self._rollback_async())
|
|
94
|
+
|
|
95
|
+
async def _rollback_async(self) -> None:
|
|
96
|
+
"""Async implementation of rollback."""
|
|
97
|
+
conn = await self._get_async_connection()
|
|
98
|
+
await conn.execute("ROLLBACK")
|
|
99
|
+
|
|
100
|
+
def cursor(self) -> Cursor:
|
|
101
|
+
"""Return a new Cursor object."""
|
|
102
|
+
if self._closed:
|
|
103
|
+
raise InterfaceError("Connection is closed")
|
|
104
|
+
return Cursor(self)
|
|
105
|
+
|
|
106
|
+
def __enter__(self) -> "Connection":
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *args: Any) -> None:
|
|
110
|
+
self.close()
|
dqlitedbapi/cursor.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""PEP 249 Cursor implementation for dqlite."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from dqlitedbapi.exceptions import InterfaceError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dqlitedbapi.connection import Connection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Cursor:
|
|
14
|
+
"""PEP 249 compliant database cursor."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, connection: "Connection") -> None:
|
|
17
|
+
self._connection = connection
|
|
18
|
+
self._description: list[tuple[str, None, None, None, None, None, None]] | None = None
|
|
19
|
+
self._rowcount = -1
|
|
20
|
+
self._arraysize = 1
|
|
21
|
+
self._rows: list[tuple[Any, ...]] = []
|
|
22
|
+
self._row_index = 0
|
|
23
|
+
self._closed = False
|
|
24
|
+
self._lastrowid: int | None = None
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(
|
|
28
|
+
self,
|
|
29
|
+
) -> list[tuple[str, None, None, None, None, None, None]] | None:
|
|
30
|
+
"""Column descriptions for the last query.
|
|
31
|
+
|
|
32
|
+
Returns a list of 7-tuples:
|
|
33
|
+
(name, type_code, display_size, internal_size, precision, scale, null_ok)
|
|
34
|
+
|
|
35
|
+
Only name is populated; others are None for compatibility.
|
|
36
|
+
"""
|
|
37
|
+
return self._description
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def rowcount(self) -> int:
|
|
41
|
+
"""Number of rows affected by the last execute.
|
|
42
|
+
|
|
43
|
+
Returns -1 if not applicable or unknown.
|
|
44
|
+
"""
|
|
45
|
+
return self._rowcount
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def lastrowid(self) -> int | None:
|
|
49
|
+
"""Row ID of the last inserted row."""
|
|
50
|
+
return self._lastrowid
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def arraysize(self) -> int:
|
|
54
|
+
"""Number of rows to fetch at a time with fetchmany()."""
|
|
55
|
+
return self._arraysize
|
|
56
|
+
|
|
57
|
+
@arraysize.setter
|
|
58
|
+
def arraysize(self, value: int) -> None:
|
|
59
|
+
self._arraysize = value
|
|
60
|
+
|
|
61
|
+
def _check_closed(self) -> None:
|
|
62
|
+
if self._closed:
|
|
63
|
+
raise InterfaceError("Cursor is closed")
|
|
64
|
+
|
|
65
|
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
|
66
|
+
# Use the connection's event loop for consistency
|
|
67
|
+
return self._connection._get_loop()
|
|
68
|
+
|
|
69
|
+
def execute(self, operation: str, parameters: Sequence[Any] | None = None) -> "Cursor":
|
|
70
|
+
"""Execute a database operation (query or command)."""
|
|
71
|
+
self._check_closed()
|
|
72
|
+
|
|
73
|
+
loop = self._get_loop()
|
|
74
|
+
loop.run_until_complete(self._execute_async(operation, parameters))
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def _execute_async(self, operation: str, parameters: Sequence[Any] | None = None) -> None:
|
|
78
|
+
"""Async implementation of execute."""
|
|
79
|
+
conn = await self._connection._get_async_connection()
|
|
80
|
+
params = list(parameters) if parameters else None
|
|
81
|
+
|
|
82
|
+
# Determine if this is a SELECT query
|
|
83
|
+
is_query = operation.strip().upper().startswith(("SELECT", "PRAGMA", "EXPLAIN"))
|
|
84
|
+
|
|
85
|
+
if is_query:
|
|
86
|
+
assert conn._protocol is not None and conn._db_id is not None
|
|
87
|
+
columns, rows = await conn._protocol.query_sql(conn._db_id, operation, params)
|
|
88
|
+
self._description = [(name, None, None, None, None, None, None) for name in columns]
|
|
89
|
+
self._rows = [tuple(row) for row in rows]
|
|
90
|
+
self._row_index = 0
|
|
91
|
+
self._rowcount = len(rows)
|
|
92
|
+
else:
|
|
93
|
+
assert conn._protocol is not None and conn._db_id is not None
|
|
94
|
+
last_id, affected = await conn._protocol.exec_sql(conn._db_id, operation, params)
|
|
95
|
+
self._lastrowid = last_id
|
|
96
|
+
self._rowcount = affected
|
|
97
|
+
self._description = None
|
|
98
|
+
self._rows = []
|
|
99
|
+
|
|
100
|
+
def executemany(self, operation: str, seq_of_parameters: Sequence[Sequence[Any]]) -> "Cursor":
|
|
101
|
+
"""Execute a database operation multiple times."""
|
|
102
|
+
self._check_closed()
|
|
103
|
+
|
|
104
|
+
loop = self._get_loop()
|
|
105
|
+
loop.run_until_complete(self._executemany_async(operation, seq_of_parameters))
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
async def _executemany_async(
|
|
109
|
+
self, operation: str, seq_of_parameters: Sequence[Sequence[Any]]
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Async implementation of executemany."""
|
|
112
|
+
total_affected = 0
|
|
113
|
+
for params in seq_of_parameters:
|
|
114
|
+
await self._execute_async(operation, params)
|
|
115
|
+
if self._rowcount >= 0:
|
|
116
|
+
total_affected += self._rowcount
|
|
117
|
+
self._rowcount = total_affected
|
|
118
|
+
|
|
119
|
+
def fetchone(self) -> tuple[Any, ...] | None:
|
|
120
|
+
"""Fetch the next row of a query result set."""
|
|
121
|
+
self._check_closed()
|
|
122
|
+
|
|
123
|
+
if not self._rows or self._row_index >= len(self._rows):
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
row = self._rows[self._row_index]
|
|
127
|
+
self._row_index += 1
|
|
128
|
+
return row
|
|
129
|
+
|
|
130
|
+
def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
|
|
131
|
+
"""Fetch the next set of rows of a query result."""
|
|
132
|
+
self._check_closed()
|
|
133
|
+
|
|
134
|
+
if size is None:
|
|
135
|
+
size = self._arraysize
|
|
136
|
+
|
|
137
|
+
result: list[tuple[Any, ...]] = []
|
|
138
|
+
for _ in range(size):
|
|
139
|
+
row = self.fetchone()
|
|
140
|
+
if row is None:
|
|
141
|
+
break
|
|
142
|
+
result.append(row)
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def fetchall(self) -> list[tuple[Any, ...]]:
|
|
147
|
+
"""Fetch all remaining rows of a query result."""
|
|
148
|
+
self._check_closed()
|
|
149
|
+
|
|
150
|
+
result = self._rows[self._row_index :]
|
|
151
|
+
self._row_index = len(self._rows)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def close(self) -> None:
|
|
155
|
+
"""Close the cursor."""
|
|
156
|
+
self._closed = True
|
|
157
|
+
self._rows = []
|
|
158
|
+
self._description = None
|
|
159
|
+
|
|
160
|
+
def setinputsizes(self, sizes: Sequence[int | None]) -> None:
|
|
161
|
+
"""Set input sizes (no-op for dqlite)."""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
def setoutputsize(self, size: int, column: int | None = None) -> None:
|
|
165
|
+
"""Set output size (no-op for dqlite)."""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def __iter__(self) -> "Cursor":
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def __next__(self) -> tuple[Any, ...]:
|
|
172
|
+
row = self.fetchone()
|
|
173
|
+
if row is None:
|
|
174
|
+
raise StopIteration
|
|
175
|
+
return row
|
|
176
|
+
|
|
177
|
+
def __enter__(self) -> "Cursor":
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def __exit__(self, *args: Any) -> None:
|
|
181
|
+
self.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""PEP 249 exception hierarchy for dqlite."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Warning(Exception): # noqa: A001
|
|
5
|
+
"""Exception raised for important warnings."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Error(Exception):
|
|
11
|
+
"""Base class for all database errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InterfaceError(Error):
|
|
17
|
+
"""Error related to the database interface."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DatabaseError(Error):
|
|
23
|
+
"""Error related to the database."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DataError(DatabaseError):
|
|
29
|
+
"""Error due to problems with the processed data."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OperationalError(DatabaseError):
|
|
35
|
+
"""Error related to database operation."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IntegrityError(DatabaseError):
|
|
41
|
+
"""Error related to database integrity."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InternalError(DatabaseError):
|
|
47
|
+
"""Internal database error."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProgrammingError(DatabaseError):
|
|
53
|
+
"""Programming error (e.g., table not found, SQL syntax error)."""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NotSupportedError(DatabaseError):
|
|
59
|
+
"""Method or database API not supported by database."""
|
|
60
|
+
|
|
61
|
+
pass
|
dqlitedbapi/py.typed
ADDED
|
File without changes
|
dqlitedbapi/types.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""PEP 249 type objects and constructors for dqlite."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Type constructors
|
|
7
|
+
def Date(year: int, month: int, day: int) -> datetime.date: # noqa: N802
|
|
8
|
+
"""Construct a date value."""
|
|
9
|
+
return datetime.date(year, month, day)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def Time(hour: int, minute: int, second: int) -> datetime.time: # noqa: N802
|
|
13
|
+
"""Construct a time value."""
|
|
14
|
+
return datetime.time(hour, minute, second)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def Timestamp( # noqa: N802
|
|
18
|
+
year: int, month: int, day: int, hour: int, minute: int, second: int
|
|
19
|
+
) -> datetime.datetime:
|
|
20
|
+
"""Construct a timestamp value."""
|
|
21
|
+
return datetime.datetime(year, month, day, hour, minute, second)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def DateFromTicks(ticks: float) -> datetime.date: # noqa: N802
|
|
25
|
+
"""Construct a date from a Unix timestamp."""
|
|
26
|
+
return datetime.date.fromtimestamp(ticks)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def TimeFromTicks(ticks: float) -> datetime.time: # noqa: N802
|
|
30
|
+
"""Construct a time from a Unix timestamp."""
|
|
31
|
+
return datetime.datetime.fromtimestamp(ticks).time()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def TimestampFromTicks(ticks: float) -> datetime.datetime: # noqa: N802
|
|
35
|
+
"""Construct a timestamp from a Unix timestamp."""
|
|
36
|
+
return datetime.datetime.fromtimestamp(ticks)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def Binary(data: bytes) -> bytes: # noqa: N802
|
|
40
|
+
"""Construct a binary value."""
|
|
41
|
+
return bytes(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Type objects for column type checking
|
|
45
|
+
class _DBAPIType:
|
|
46
|
+
"""Base type for DB-API type objects."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *values: str) -> None:
|
|
49
|
+
self.values = set(values)
|
|
50
|
+
|
|
51
|
+
def __eq__(self, other: object) -> bool:
|
|
52
|
+
if isinstance(other, str):
|
|
53
|
+
return other.upper() in self.values
|
|
54
|
+
return NotImplemented
|
|
55
|
+
|
|
56
|
+
def __hash__(self) -> int:
|
|
57
|
+
return hash(frozenset(self.values))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
STRING = _DBAPIType("TEXT", "VARCHAR", "CHAR", "CLOB")
|
|
61
|
+
BINARY = _DBAPIType("BLOB", "BINARY", "VARBINARY")
|
|
62
|
+
NUMBER = _DBAPIType("INTEGER", "INT", "SMALLINT", "BIGINT", "REAL", "FLOAT", "DOUBLE", "NUMERIC")
|
|
63
|
+
DATETIME = _DBAPIType("DATE", "TIME", "TIMESTAMP", "DATETIME")
|
|
64
|
+
ROWID = _DBAPIType("ROWID", "INTEGER PRIMARY KEY")
|