A helper library for implementing Python DB API 2.0 database drivers
Project description
dbapi-helper
A modern Python library that provides the basics for implementing Python DB API 2.0 database drivers. This library handles all the boilerplate, leaving you to implement only the cursor.execute() method.
Perfect for creating database drivers for services with REST/HTTP endpoints that accept SQL queries.
Features
- ✅ Full DB API 2.0 compliance (PEP 249)
- ✅ Complete base implementations of
ConnectionandCursor - ✅ All standard exceptions and type objects included
- ✅ Modern Python (3.9+) with full type hints
- ✅ Generic cursor type support for better IDE experience
- ✅ Comprehensive documentation and examples
- ✅ Only requires implementing
cursor.execute()method
Installation
pip install dbapi-helper
Quick Start
Here's a minimal example of implementing a database driver:
import requests
from dbapi_helper import Connection, Cursor
class RESTCursor(Cursor):
def __init__(self, api_url: str):
super().__init__()
self.api_url = api_url
def execute(self, operation: str, parameters=None):
# Make REST API call
response = requests.post(
f"{self.api_url}/query",
json={"sql": operation, "params": parameters}
)
data = response.json()
# Set description (column metadata)
self.description = tuple(
(col["name"], col["type"], None, None, None, None, None)
for col in data["columns"]
)
# Set results iterator
self._results = iter(data["rows"])
# Store operation for debugging
self.operation = operation
return self
class RESTConnection(Connection[RESTCursor]):
def __init__(self, api_url: str):
super().__init__()
self.api_url = api_url
def cursor(self) -> RESTCursor:
cursor = RESTCursor(self.api_url)
self.cursors.append(cursor)
return cursor
# Usage
conn = RESTConnection("https://api.example.com")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE age > ?", (25,))
for row in cursor.fetchall():
print(row)
conn.close()
Detailed Usage
Implementing execute()
The execute() method is the only required method to implement. It must:
- Execute the operation with the given parameters
- Set
self.descriptionto column metadata (orNonefor non-SELECT queries) - Set
self._resultsto an iterator of result tuples - Set
self.operationto the operation string - Return
self
def execute(self, operation: str, parameters=None):
# 1. Execute the query (your custom logic here)
result = self.my_query_executor(operation, parameters)
# 2. Set column metadata
self.description = tuple(
(name, type_code, None, None, None, None, None)
for name, type_code in result.columns
)
# 3. Set results iterator
self._results = iter(result.rows)
# 4. Store the operation
self.operation = operation
# 5. Return self
return self
Important: When overriding execute(), apply the @check_closed decorator to ensure proper error handling:
from dbapi_helper import Cursor
from dbapi_helper.cursor import check_closed
class MyCursor(Cursor):
@check_closed # Important!
def execute(self, operation: str, parameters=None):
# Your implementation
...
return self
Implementing cursor()
The cursor() method creates and returns a new cursor instance:
from dbapi_helper import Connection
from dbapi_helper.connection import check_closed
class MyConnection(Connection[MyCursor]):
@check_closed # Recommended!
def cursor(self) -> MyCursor:
cursor = MyCursor(self) # Pass connection if needed
self.cursors.append(cursor) # Track cursor
return cursor
Column Description Format
The description attribute is a tuple of 7-item tuples, one per column:
(
name, # Column name (str)
type_code, # Type code (type object or None)
display_size, # Display size (int or None)
internal_size, # Internal size (int or None)
precision, # Precision (int or None)
scale, # Scale (int or None)
null_ok, # Nullable (bool or None)
)
Most implementations only set name and type_code, leaving others as None.
Using Context Managers
Connections support context managers for automatic cleanup:
with RESTConnection("https://api.example.com") as conn:
cursor = conn.execute("SELECT * FROM users")
print(cursor.fetchall())
# Connection is automatically committed and closed
Exception Handling
All DB API 2.0 exceptions are included:
from dbapi_helper import (
Error, # Base exception
InterfaceError, # Interface-related errors
DatabaseError, # Database-related errors (base for below)
DataError, # Data processing errors
OperationalError, # Database operation errors
IntegrityError, # Relational integrity errors
InternalError, # Database internal errors
ProgrammingError, # Programming errors
NotSupportedError, # Feature not supported
)
try:
cursor.execute("INVALID SQL")
except ProgrammingError as e:
print(f"SQL error: {e}")
Type Objects
Use type objects to describe column types:
from dbapi_helper import STRING, BINARY, NUMBER, DATETIME, ROWID
# In your execute() implementation:
self.description = (
("id", NUMBER, None, None, None, None, None),
("name", STRING, None, None, None, None, None),
("created_at", DATETIME, None, None, None, None, None),
)
Complete Example: REST API Driver
from typing import Any
import requests
from dbapi_helper import Connection, Cursor, OperationalError
class RestAPICursor(Cursor):
"""Cursor for a REST API database."""
def __init__(self, connection: "RestAPIConnection"):
super().__init__()
self.connection = connection
def execute(self, operation: str, parameters: tuple[Any, ...] | None = None):
"""Execute SQL via REST API."""
try:
response = requests.post(
f"{self.connection.base_url}/query",
json={
"sql": operation,
"params": list(parameters) if parameters else []
},
headers={"Authorization": f"Bearer {self.connection.token}"},
timeout=30,
)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
raise OperationalError(f"Query failed: {e}")
# Set column descriptions
if data.get("columns"):
self.description = tuple(
(
col["name"],
self._map_type(col.get("type")),
None,
None,
None,
None,
None,
)
for col in data["columns"]
)
else:
self.description = None
# Set results
self._results = iter(tuple(row) for row in data.get("rows", []))
self.operation = operation
return self
@staticmethod
def _map_type(type_name: str | None) -> type | None:
"""Map API type names to Python types."""
if not type_name:
return None
type_map = {
"string": str,
"integer": int,
"float": float,
"boolean": bool,
"timestamp": str, # or datetime
}
return type_map.get(type_name.lower())
class RestAPIConnection(Connection[RestAPICursor]):
"""Connection to a REST API database."""
def __init__(self, base_url: str, token: str):
super().__init__()
self.base_url = base_url.rstrip("/")
self.token = token
def cursor(self) -> RestAPICursor:
"""Create a new cursor."""
cursor = RestAPICursor(self)
self.cursors.append(cursor)
return cursor
# Usage
def main():
conn = RestAPIConnection(
base_url="https://api.example.com",
token="your-api-token"
)
try:
# Using cursor directly
cursor = conn.cursor()
cursor.execute("SELECT id, name, email FROM users WHERE active = ?", (True,))
print(f"Columns: {[desc[0] for desc in cursor.description]}")
for row in cursor:
print(row)
# Using connection.execute() shortcut
cursor2 = conn.execute("SELECT COUNT(*) FROM users")
count = cursor2.fetchone()[0]
print(f"Total users: {count}")
finally:
conn.close()
if __name__ == "__main__":
main()
API Reference
Connection
cursor()→ Cursor: Create a new cursor (must implement)close(): Close connection and all cursorscommit(): Commit transaction (no-op by default)rollback(): Rollback transaction (no-op by default)execute(operation, parameters): Convenience method
Cursor
execute(operation, parameters)→ Cursor: Execute query (must implement)fetchone()→ tuple | None: Fetch next rowfetchmany(size)→ list[tuple]: Fetch multiple rowsfetchall()→ list[tuple]: Fetch all remaining rowsclose(): Close cursorexecutemany(): Not supported (raises NotSupportedError)setinputsizes(): No-op (compatibility)setoutputsizes(): No-op (compatibility)
Attributes
cursor.description: Column metadata tuplecursor.rowcount: Number of rows affected/returnedcursor.arraysize: Rows to fetch with fetchmany() (default: 1)connection.cursors: List of created cursors
Module Attributes
import dbapi_helper
print(dbapi_helper.apilevel) # "2.0"
print(dbapi_helper.threadsafety) # 2
print(dbapi_helper.paramstyle) # "qmark"
Examples
The examples/ directory contains complete, working implementations:
REST API Driver (examples/rest_driver.py)
A simple REST API database driver that demonstrates the basic pattern for implementing a DB API 2.0 driver. This example shows how to:
- Make HTTP requests to query a REST API
- Map API responses to DB API 2.0 structures
- Handle column descriptions and type mapping
Run with: PYTHONPATH=src python examples/rest_driver.py
Apache Druid Driver (examples/druid_driver.py)
A production-ready implementation for Apache Druid's SQL API. This example demonstrates:
- Streaming large result sets from HTTP responses
- Incremental JSON parsing for memory efficiency
- Multiple authentication methods (Basic Auth, JWT)
- Parameter escaping with pyformat style (
%(name)s) - Type inference from result data
- Context managers and connection pooling
Run with: PYTHONPATH=src python examples/druid_driver.py
Both examples serve as templates you can adapt for your own database drivers.
Development
# Clone the repository
git clone https://github.com/yourusername/dbapi-helper.git
cd dbapi-helper
# Install in development mode
pip install -e .
# Run tests
pytest
# Type checking
mypy src/dbapi_helper
License
Apache License 2.0 - see LICENSE file for details.
Credits
Based on the generic DB API 2.0 implementation from Shillelagh by Beto Dealmeida.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dbapi_helper-0.1.0.tar.gz.
File metadata
- Download URL: dbapi_helper-0.1.0.tar.gz
- Upload date:
- Size: 15.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eea4b052bf49a8b289047c0d4b261b3c6a90c44d86eef125dfb20b9d9c2fd89b
|
|
| MD5 |
bad36c98fb9f3c93f544373778999d84
|
|
| BLAKE2b-256 |
8a5847d516e6d6ba55cd57c648621f65bf73882701074c2fbdb2fabd81c8b953
|
File details
Details for the file dbapi_helper-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dbapi_helper-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e51bb00b31bd846ff584ac6cd5019e8553c99424736e2bced0e5a8df8c95348
|
|
| MD5 |
1184f06f492e5d2708125e1b8645e652
|
|
| BLAKE2b-256 |
4866c981a7944ca3c7090cf7288add6c0e04120b14469063a3e148b411e4225c
|