Skip to content

Commit 1fe6707

Browse files
Initial commit
0 parents  commit 1fe6707

16 files changed

Lines changed: 884 additions & 0 deletions

.gitignore

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/

DEVELOPMENT.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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),
12+
[dqlite-client](https://github.com/letsdiscodev/python-dqlite-client)
13+
and [dqlite-dbapi](https://github.com/letsdiscodev/python-dqlite-dbapi).
14+
15+
```bash
16+
# Install uv (if not already installed)
17+
curl -LsSf https://astral.sh/uv/install.sh | sh
18+
19+
# Create virtual environment and install dependencies
20+
uv venv --python 3.13
21+
uv pip install -e "../python-dqlite-wire" -e "../python-dqlite-client" -e "../python-dqlite-dbapi" -e ".[dev]"
22+
```
23+
24+
## Development Tools
25+
26+
| Tool | Purpose | Command |
27+
|------|---------|---------|
28+
| **pytest** | Testing framework | `pytest` |
29+
| **ruff** | Linter (replaces flake8, isort, etc.) | `ruff check` |
30+
| **ruff format** | Code formatter (replaces black) | `ruff format` |
31+
| **mypy** | Static type checker | `mypy src` |
32+
33+
## Running Tests
34+
35+
```bash
36+
# Run unit tests only
37+
.venv/bin/pytest tests/ --ignore=tests/integration
38+
39+
# Run all tests (requires Docker cluster)
40+
cd ../dqlite-test-cluster && docker compose up -d
41+
.venv/bin/pytest tests/
42+
```
43+
44+
## Linting & Formatting
45+
46+
```bash
47+
# Lint
48+
.venv/bin/ruff check src tests
49+
50+
# Auto-fix lint issues
51+
.venv/bin/ruff check --fix src tests
52+
53+
# Format
54+
.venv/bin/ruff format src tests
55+
```
56+
57+
## Type Checking
58+
59+
```bash
60+
.venv/bin/mypy src
61+
```
62+
63+
## Pre-commit Workflow
64+
65+
```bash
66+
.venv/bin/ruff format src tests
67+
.venv/bin/ruff check --fix src tests
68+
.venv/bin/mypy src
69+
.venv/bin/pytest tests/ --ignore=tests/integration
70+
```
71+
72+
## SQLAlchemy URL Format
73+
74+
```
75+
# Sync
76+
dqlite://host:port/database
77+
78+
# Async
79+
dqlite+aio://host:port/database
80+
```
81+
82+
## Dialect Registration
83+
84+
The dialects are registered via entry points in `pyproject.toml`:
85+
86+
```toml
87+
[project.entry-points."sqlalchemy.dialects"]
88+
dqlite = "sqlalchemydqlite:DqliteDialect"
89+
"dqlite.aio" = "sqlalchemydqlite.aio:DqliteDialect_aio"
90+
```

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# sqlalchemy-dqlite
2+
3+
SQLAlchemy 2.0 dialect for [dqlite](https://dqlite.io/).
4+
5+
## Installation
6+
7+
```bash
8+
pip install sqlalchemy-dqlite
9+
```
10+
11+
## Usage
12+
13+
```python
14+
from sqlalchemy import create_engine, text
15+
16+
# Sync
17+
engine = create_engine("dqlite://localhost:9001/mydb")
18+
with engine.connect() as conn:
19+
result = conn.execute(text("SELECT 1"))
20+
print(result.fetchone())
21+
22+
# Async
23+
from sqlalchemy.ext.asyncio import create_async_engine
24+
25+
async_engine = create_async_engine("dqlite+aio://localhost:9001/mydb")
26+
async with async_engine.connect() as conn:
27+
result = await conn.execute(text("SELECT 1"))
28+
print(result.fetchone())
29+
```
30+
31+
## URL Format
32+
33+
```
34+
dqlite://host:port/database
35+
dqlite+aio://host:port/database
36+
```
37+
38+
## Development
39+
40+
See [DEVELOPMENT.md](DEVELOPMENT.md) for setup and contribution guidelines.
41+
42+
## License
43+
44+
MIT

pyproject.toml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "sqlalchemy-dqlite"
7+
version = "0.1.0"
8+
description = "SQLAlchemy 2.0 dialect for dqlite distributed SQLite"
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", "sqlalchemy", "orm"]
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-dbapi>=0.1.0", "sqlalchemy>=2.0"]
27+
28+
[project.urls]
29+
Homepage = "https://github.com/antoineleclair/sqlalchemy-dqlite"
30+
Repository = "https://github.com/antoineleclair/sqlalchemy-dqlite"
31+
Issues = "https://github.com/antoineleclair/sqlalchemy-dqlite/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+
[project.entry-points."sqlalchemy.dialects"]
37+
dqlite = "sqlalchemydqlite:DqliteDialect"
38+
"dqlite.aio" = "sqlalchemydqlite.aio:DqliteDialect_aio"
39+
40+
[tool.hatch.build.targets.wheel]
41+
packages = ["src/sqlalchemydqlite"]
42+
43+
[tool.pytest.ini_options]
44+
testpaths = ["tests"]
45+
pythonpath = ["src"]
46+
asyncio_mode = "auto"
47+
asyncio_default_fixture_loop_scope = "function"
48+
49+
[tool.mypy]
50+
strict = true
51+
python_version = "3.13"
52+
53+
[tool.ruff]
54+
target-version = "py313"
55+
line-length = 100
56+
src = ["src", "tests"]
57+
58+
[tool.ruff.lint]
59+
select = ["E", "F", "I", "UP", "B", "SIM"]
60+
61+
[tool.ruff.lint.isort]
62+
known-first-party = ["sqlalchemydqlite", "dqlitedbapi", "dqliteclient", "dqlitewire"]
63+
64+
[tool.ruff.format]
65+
quote-style = "double"
66+
indent-style = "space"
67+
docstring-code-format = true

src/sqlalchemydqlite/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""SQLAlchemy 2.0 dialect for dqlite."""
2+
3+
from sqlalchemydqlite.base import DqliteDialect
4+
5+
__all__ = ["DqliteDialect"]
6+
7+
__version__ = "0.1.0"

src/sqlalchemydqlite/aio.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Async dqlite dialect for SQLAlchemy."""
2+
3+
from typing import Any
4+
5+
from sqlalchemy import pool
6+
from sqlalchemy.dialects.sqlite.base import SQLiteDialect
7+
from sqlalchemy.engine import URL
8+
from sqlalchemy.pool import AsyncAdaptedQueuePool
9+
10+
11+
class DqliteDialect_aio(SQLiteDialect): # noqa: N801
12+
"""Async SQLAlchemy dialect for dqlite.
13+
14+
Use with SQLAlchemy's async engine:
15+
create_async_engine("dqlite+aio://host:port/database")
16+
"""
17+
18+
name = "dqlite"
19+
driver = "dqlitedbapi_aio"
20+
is_async = True
21+
22+
# dqlite uses qmark parameter style
23+
paramstyle = "qmark"
24+
25+
@classmethod
26+
def get_pool_class(cls, url: URL) -> type[pool.Pool]:
27+
return AsyncAdaptedQueuePool
28+
29+
@classmethod
30+
def import_dbapi(cls) -> Any:
31+
from dqlitedbapi import aio
32+
33+
return aio
34+
35+
def create_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
36+
"""Create connection arguments from URL.
37+
38+
URL format: dqlite+aio://host:port/database
39+
"""
40+
host = url.host or "localhost"
41+
port = url.port or 9001
42+
database = url.database or "default"
43+
44+
address = f"{host}:{port}"
45+
46+
return [], {
47+
"address": address,
48+
"database": database,
49+
}
50+
51+
def get_driver_connection(self, connection: Any) -> Any:
52+
"""Return the driver-level connection."""
53+
return connection
54+
55+
56+
# Register the dialect
57+
dialect = DqliteDialect_aio

0 commit comments

Comments
 (0)