Database Support in Panther#
Panther natively supports MongoDB, PantherDB, and PostgreSQL. You can also define custom database connections and queries.
The built-in panther.db.Model is a document model. It remains available for compatibility; panther.db.DocumentModel is its explicit name. Relational integrations should provide their own persistence models rather than inheriting from either class.
Configuration Structure#
Define your database configuration in the following format:
DATABASE = {
'engine': {
'class': 'address of engine',
'arg1': 'value1',
# ... additional arguments ...
},
'query': 'address of query class', # Optional
}
Notes#
- Built-in supported engines:
panther.db.connections.PantherDBConnectionpanther.db.connections.MongoDBConnectionpanther.db.connections.PostgreSQLConnection- All values in
engine(exceptclass) are passed to the__init__method of the specified class. - The
querykey is optional for the default supported engines, but you can customize it if needed.
Custom Backends#
Custom engines should subclass panther.db.connections.BaseDatabaseConnection and implement the session property. They can return their query implementation from get_query_engine(); alternatively, set the existing query configuration value explicitly.
Query implementations must subclass panther.db.queries.base_queries.BaseQuery.
Backends declare capabilities rather than requiring Panther to check their concrete class. The built-in document backends use uses_document_models; MongoDB additionally uses uses_object_ids and uses_mongo_query_syntax. Relational backends leave these capabilities disabled and do not use the document query API.
If a backend needs an active event loop to allocate or release resources, implement its async startup() and shutdown() hooks. Panther invokes them during ASGI lifespan startup and shutdown.
Backends can also override session_context() to provide a scoped unit of work. The built-in document backends yield their existing connection; relational backends can create, commit or roll back, and close a session there. Code that needs a scoped session can use db.session_context().
from contextlib import asynccontextmanager
from panther.db.connections import BaseDatabaseConnection, db
class SQLConnection(BaseDatabaseConnection):
def init(self, session_factory):
self._session_factory = session_factory
@property
def session(self):
return self._session_factory
@asynccontextmanager
async def session_context(self):
async with self._session_factory() as session:
yield session
async with db.session_context() as session:
# Use the backend's scoped session.
pass
PantherDB#
Example configuration for PantherDB:
DATABASE = {
'engine': {
'class': 'panther.db.connections.PantherDBConnection',
'path': BASE_DIR / 'database.pdb', # Optional
'encryption': True # Optional, default is False
}
}
Notes#
pathis optional; you can customize the directory and filename of your database.encryptionis optional and defaults toFalse.- The
cryptographypackage is required if you setencryptiontoTrue.
MongoDB#
Example configuration for MongoDB:
DATABASE = {
'engine': {
'class': 'panther.db.connections.MongoDBConnection',
'host': 'mongodb://127.0.0.1:27017/database_name'
}
}
Notes#
- The parameters for the engine are the same as those for
pymongo.MongoClient. See the PyMongo documentation for details.
PostgreSQL#
Install the optional PostgreSQL dependencies:
pip install 'panther[postgresql]'
Configure Panther with a SQLAlchemy async URL:
DATABASE = {
'engine': {
'class': 'panther.db.connections.PostgreSQLConnection',
'url': 'postgresql+asyncpg://postgres:password@127.0.0.1:5432/my_database',
'echo': False, # Optional SQLAlchemy engine setting
},
}
PostgreSQLConnection validates the engine configuration while Panther loads settings. During ASGI startup, Panther opens a connection and runs a health check; invalid credentials, hosts, or database names prevent the application from serving requests.
Define SQLAlchemy models#
PostgreSQL applications use ordinary SQLAlchemy declarative models. Do not inherit from panther.db.DocumentModel or use document query methods such as find().
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
Use sessions explicitly#
db.session is the SQLAlchemy session factory. Prefer db.session_context() so every unit of work receives a new session and failures are rolled back automatically. Successful writes require an explicit commit.
from sqlalchemy import select
from panther.db.connections import db
async def create_and_list_users(name: str) -> list[User]:
async with db.session_context() as session:
session.add(User(name=name))
await session.commit()
result = await session.execute(select(User).order_by(User.id))
return list(result.scalars())
Panther disposes the connection pool during ASGI shutdown.
Use sqlalchemy.text() for raw SQL and bind external values as parameters rather than formatting them into the query string:
from sqlalchemy import text
async with db.session_context() as session:
result = await session.execute(
text('SELECT id, name FROM users WHERE id = :user_id'),
{'user_id': user_id},
)
user = result.mappings().one_or_none()
For a complete CRUD, relationship, and cursor-pagination example, see the examples/postgresql directory in the Panther repository.
How Does It Work?#
- Panther creates a database connection based on the
DATABASEconfiguration you define in your configs. - You can access this connection through your models, or directly via:
from panther.db.connections import db