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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.
@@ -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")