Database Dependencies

Setup with Settings and Environment

from typing import Annotated, AsyncGenerator
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from contextlib import asynccontextmanager
from typing_extensions import Self

# Load settings from .env file and/or from environment variables
class DatabaseSettings(BaseSettings):
    database_url: str = "sqlite+aiosqlite:///database.db"

    @classmethod
    def from_env(cls) -> Self:
        return cls()

def get_db_settings() -> DatabaseSettings:
    return DatabaseSettings()


## Create AsyncSessionMaker

def get_sessionmaker(
    settings: Annotated[DatabaseSettings, DatabaseSettings.from_env, "singleton"],
) -> async_sessionmaker[AsyncSession]:
    """
    Create an async sessionmaker from database settings.
    This is typically a singleton in your app.
    """
    engine = create_async_engine(
        settings.database_url,
        echo=False,  # Set to True for SQL debugging
        future=True,
    )

    return async_sessionmaker(
        bind=engine,
        class_=AsyncSession,
        autocommit=False,
        autoflush=False,
        expire_on_commit=False,
    )

# We can share sessionmaker between dependencies
type SessionMaker = Annotated[async_sessionmaker[AsyncSession], get_sessionmaker, "singleton"]

# Create a session for each request

@asynccontextmanager
async def get_session(sessionmaker: SessionMaker) -> AsyncGenerator[AsyncSession, None]:
    """
    Get a new database async session with automatic commit/rollback.
    Yields the session and handles cleanup.
    """
    async with sessionmaker() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

# For concurrenty safety we will create a new session for each dependency
GetSession = Annotated[AsyncSession, get_session, "transient"]

Usage in Handlers

from stario import Query
from sqlalchemy import select

@app.query("/users")
async def list_users(db: GetSession):
    """List all users from database."""
    stmt = select(User)
    result = await db.execute(stmt)
    users = result.scalars().all()
    return div([render_user(u) for u in users])

Key Points

  • Settings: Use BaseSettings to load database URLs from environment
  • SessionMaker: Create once (singleton), reuse for all requests
  • Sessions: Create transiently, auto-commit on success, auto-rollback on error
  • Dependency Injection: Use Annotated for clean, reusable dependency injection
  • Type Aliases: Define custom type aliases for frequently-used dependencies to reduce boilerplate
  • Error Handling: Sessions automatically handle commit/rollback in the context manager