dqlite-dbapi 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,50 @@
1
+ name: Publish to PyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v6
11
+ with:
12
+ persist-credentials: false
13
+ - name: Set up Python
14
+ uses: actions/setup-python@v6
15
+ with:
16
+ python-version: "3.x"
17
+ - name: Install pypa/build
18
+ run: >-
19
+ python3 -m
20
+ pip install
21
+ build
22
+ --user
23
+ - name: Build a binary wheel and a source tarball
24
+ run: python3 -m build
25
+ - name: Store the distribution packages
26
+ uses: actions/upload-artifact@v5
27
+ with:
28
+ name: python-package-distributions
29
+ path: dist/
30
+
31
+ publish-to-pypi:
32
+ name: >-
33
+ Publish to PyPI
34
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
35
+ needs:
36
+ - build
37
+ runs-on: ubuntu-latest
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/dqlite-dbapi
41
+ permissions:
42
+ id-token: write
43
+ steps:
44
+ - name: Download all the dists
45
+ uses: actions/download-artifact@v6
46
+ with:
47
+ name: python-package-distributions
48
+ path: dist/
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Testing
35
+ .pytest_cache/
36
+ .coverage
37
+ htmlcov/
38
+ .tox/
39
+ .nox/
40
+
41
+ # mypy
42
+ .mypy_cache/
43
+
44
+ # Distribution
45
+ dist/
46
+ build/
@@ -0,0 +1,76 @@
1
+ # Development Guide
2
+
3
+ ## Prerequisites
4
+
5
+ - Python 3.13+
6
+ - [uv](https://github.com/astral-sh/uv) - Fast Python package manager
7
+ - Docker (for integration tests)
8
+
9
+ ## Setup
10
+
11
+ Start by also cloning [dqlite-wire](https://github.com/letsdiscodev/python-dqlite-wire) and [dqlite-client](https://github.com/letsdiscodev/python-dqlite-client).
12
+
13
+ ```bash
14
+ # Install uv (if not already installed)
15
+ curl -LsSf https://astral.sh/uv/install.sh | sh
16
+
17
+ # Create virtual environment and install dependencies
18
+ uv venv --python 3.13
19
+ uv pip install -e "../python-dqlite-wire" -e "../python-dqlite-client" -e ".[dev]"
20
+ ```
21
+
22
+ ## Development Tools
23
+
24
+ | Tool | Purpose | Command |
25
+ |------|---------|---------|
26
+ | **pytest** | Testing framework | `pytest` |
27
+ | **ruff** | Linter (replaces flake8, isort, etc.) | `ruff check` |
28
+ | **ruff format** | Code formatter (replaces black) | `ruff format` |
29
+ | **mypy** | Static type checker | `mypy src` |
30
+
31
+ ## Running Tests
32
+
33
+ ```bash
34
+ # Run unit tests only
35
+ .venv/bin/pytest tests/ --ignore=tests/integration
36
+
37
+ # Run all tests (requires Docker cluster)
38
+ cd ../dqlite-test-cluster && docker compose up -d
39
+ .venv/bin/pytest tests/
40
+ ```
41
+
42
+ ## Linting & Formatting
43
+
44
+ ```bash
45
+ # Lint
46
+ .venv/bin/ruff check src tests
47
+
48
+ # Auto-fix lint issues
49
+ .venv/bin/ruff check --fix src tests
50
+
51
+ # Format
52
+ .venv/bin/ruff format src tests
53
+ ```
54
+
55
+ ## Type Checking
56
+
57
+ ```bash
58
+ .venv/bin/mypy src
59
+ ```
60
+
61
+ ## Pre-commit Workflow
62
+
63
+ ```bash
64
+ .venv/bin/ruff format src tests
65
+ .venv/bin/ruff check --fix src tests
66
+ .venv/bin/mypy src
67
+ .venv/bin/pytest tests/ --ignore=tests/integration
68
+ ```
69
+
70
+ ## PEP 249 Compliance
71
+
72
+ This package implements the DB-API 2.0 specification (PEP 249):
73
+
74
+ - `apilevel = "2.0"`
75
+ - `threadsafety = 1` (threads may share module, not connections)
76
+ - `paramstyle = "qmark"` (question mark placeholders)
@@ -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,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,51 @@
1
+ # dqlite-dbapi
2
+
3
+ PEP 249 compliant interface for [dqlite](https://dqlite.io/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install dqlite-dbapi
9
+ ```
10
+
11
+ ## Sync Usage
12
+
13
+ ```python
14
+ import dqlitedbapi
15
+
16
+ conn = dqlitedbapi.connect("localhost:9001")
17
+ cursor = conn.cursor()
18
+ cursor.execute("SELECT 1")
19
+ print(cursor.fetchone())
20
+ conn.close()
21
+ ```
22
+
23
+ ## Async Usage
24
+
25
+ ```python
26
+ import asyncio
27
+ from dqlitedbapi.aio import aconnect
28
+
29
+ async def main():
30
+ conn = await aconnect("localhost:9001")
31
+ cursor = await conn.cursor()
32
+ await cursor.execute("SELECT 1")
33
+ print(await cursor.fetchone())
34
+ await conn.close()
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ ## PEP 249 Compliance
40
+
41
+ - `apilevel = "2.0"`
42
+ - `threadsafety = 1`
43
+ - `paramstyle = "qmark"`
44
+
45
+ ## Development
46
+
47
+ See [DEVELOPMENT.md](DEVELOPMENT.md) for setup and contribution guidelines.
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dqlite-dbapi"
7
+ version = "0.1.0"
8
+ description = "PEP 249 (DB-API 2.0) compliant interface for dqlite"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "MIT"
12
+ authors = [{ name = "Antoine Leclair", email = "antoineleclair@gmail.com" }]
13
+ keywords = ["dqlite", "sqlite", "distributed", "database", "dbapi", "pep249"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Database",
22
+ "Topic :: Database :: Database Engines/Servers",
23
+ "Topic :: Database :: Front-Ends",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = ["dqlite-client>=0.1.0"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/letsdiscodev/python-dqlite-dbapi"
30
+ Repository = "https://github.com/letsdiscodev/python-dqlite-dbapi"
31
+ Issues = "https://github.com/letsdiscodev/python-dqlite-dbapi/issues"
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=8.0", "pytest-cov>=4.0", "pytest-asyncio>=0.23", "mypy>=1.0", "ruff>=0.4"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/dqlitedbapi"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = ["src"]
42
+ asyncio_mode = "auto"
43
+ asyncio_default_fixture_loop_scope = "function"
44
+
45
+ [tool.mypy]
46
+ strict = true
47
+ python_version = "3.13"
48
+
49
+ [tool.ruff]
50
+ target-version = "py313"
51
+ line-length = 100
52
+ src = ["src", "tests"]
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B", "SIM"]
56
+
57
+ [tool.ruff.lint.isort]
58
+ known-first-party = ["dqlitedbapi", "dqliteclient", "dqlitewire"]
59
+
60
+ [tool.ruff.format]
61
+ quote-style = "double"
62
+ indent-style = "space"
63
+ docstring-code-format = true
@@ -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()