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.
Metadata
Release files for dbapi-helper 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dbapi_helper-0.1.0.tar.gz | 15.9 kB | Without provenance | Details for dbapi_helper-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dbapi_helper-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for dbapi_helper-0.1.0-py3-none-any.whl |
Total release size: 32.4 kB
Release files / dbapi_helper-0.1.0.tar.gz
| Download URL | dbapi_helper-0.1.0.tar.gz |
|---|---|
| Size | 15.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eea4b052bf49a8b289047c0d4b261b3c6a90c44d86eef125dfb20b9d9c2fd89b
|
|
BLAKE2b-256 checksum How to use checksums |
8a5847d516e6d6ba55cd57c648621f65bf73882701074c2fbdb2fabd81c8b953
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / dbapi_helper-0.1.0-py3-none-any.whl
| Download URL | dbapi_helper-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e51bb00b31bd846ff584ac6cd5019e8553c99424736e2bced0e5a8df8c95348
|
|
BLAKE2b-256 checksum How to use checksums |
4866c981a7944ca3c7090cf7288add6c0e04120b14469063a3e148b411e4225c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Security for dbapi-helper 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.