This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Python 3.14 async REST API. FastAPI + SQLAlchemy 2 (async) + PostgreSQL + Alembic. Served by Granian (uvloop). Dependencies managed by uv. Linted with ruff, type-checked with ty (not mypy). Observability via lite-bootstrap (OpenTelemetry, Sentry). DI via modern-di. Repositories via advanced-alchemy.
The development workflow runs inside Docker via just. The application service mounts the repo and depends on a db Postgres service.
just # default: install + lint + build + test
just run # alembic upgrade head + start app on :8000
just sh # shell inside the application container
just test [args] # downgrade to base, upgrade, then pytest <args>; brings stack down after
just migration -m "msg" # autogenerate alembic revision against current head
just lint # eof-fixer + ruff format + ruff check --fix + ty check
just build # docker compose build application
just down # docker compose down --remove-orphans
just install # uv lock --upgrade && uv sync --all-extras --all-groups --frozenRunning a single test (inside container or with DB available):
uv run pytest tests/test_decks.py::test_get_decks -xCI (.github/workflows/main.yml) runs ruff format --check, ruff check --no-fix, ty check, then alembic upgrade head and pytest against a Postgres service. Match this locally before pushing.
app/__main__.py boots Granian pointing at app.application:build_app (factory). build_app() in app/application.py:
- Creates a
modern_di.Containerwith theDependenciesgroup fromapp/ioc.py. - Builds a
FastAPIBootstrapperfromsettings.api_bootstrapper_config, injecting SQLAlchemy + asyncpg OpenTelemetry instrumentors. modern_di_fastapi.setup_di(app, container)wires DI scopes onto the FastAPI app.- Includes
app.api.decks.ROUTERunder/api. - Registers
DuplicateKeyError→ 422 handler fromapp/exceptions.py.
app/ioc.py defines providers:
database_engine— singleton-ishAsyncEnginewithcreate_sa_engine/close_sa_enginefinalizer.session—Scope.REQUEST, finalized byclose_session.decks_repository,cards_repository—Scope.REQUEST, depend onsession, configured withauto_commit=True(commit happens at session close, not per call).
Endpoints inject repositories with FromDI(Repository) from modern_di_fastapi. Add new providers to Dependencies rather than constructing services manually in routes.
app/models.py—BigIntAuditBasefromadvanced_alchemy(autoid,created_at,updated_at). The module aliasesorm_registry.metadataontoorm.DeclarativeBase.metadataso Alembic autogenerate sees both. New models go here.app/repositories.py— SubclassSQLAlchemyAsyncRepositoryService[Model]with a nestedBaseRepository(SQLAlchemyAsyncRepository[Model]). Routes use the service methods (list,get_one_or_none,create,update,create_many,upsert_many).app/resources/db.py—CustomAsyncSession.close()doesexpunge_all()instead of closing when bound to anAsyncConnection. This is what enables the test rollback pattern below — do not "fix" it.migrations/env.pyswaps the asyncpg driver for the syncpostgresqldriver and usesapp.models.METADATAastarget_metadata.
app/settings.py — pydantic_settings.BaseSettings. Env vars are unprefixed (DB_DSN, SERVICE_DEBUG, SERVICE_ENVIRONMENT, LOG_LEVEL, APP_HOST, APP_PORT, OPENTELEMETRY_ENDPOINT, SENTRY_DSN, CORS_ALLOWED_ORIGINS, ...). api_bootstrapper_config produces a FastAPIConfig for lite-bootstrap.
tests/conftest.py provides the test isolation pattern — read it before adding fixtures:
appfixture builds a fresh app viaLifespanManager.db_sessionopens a connection, begins a transaction, begins a nested savepoint, and overridesDependencies.database_enginewith the connection itself. The nested savepoint is rolled back at teardown so each test starts clean. This is whyCustomAsyncSession.closemustexpunge_allrather than close — closing would commit the outer transaction.set_async_session_in_base_sqlalchemy_factorywiresdb_sessionintoSQLAlchemyFactory.__async_session__sopolyfactoryfactories intests/factories.py(DeckModelFactory,CardModelFactory) persist via the rolled-back session. Test modules that use these factories opt in withpytestmark = [pytest.mark.usefixtures("set_async_session_in_base_sqlalchemy_factory")].
pytest.ini_options sets asyncio_mode = "auto" — async tests do not need @pytest.mark.asyncio. Coverage runs by default (--cov=. --cov-report term-missing).
- Type-ignore syntax is
# ty: ignore[error-code](this project usesty, not mypy). Seeapp/application.py:39for an example. - Ruff is configured with
select = ["ALL"]and a curated ignore list inpyproject.toml. Don't sprinkle# noqa; prefer fixing or extending the project ignore list if a rule is genuinely wrong for the codebase. - Routes return
typing.cast("schemas.X", obj)over ORM/dict objects rather than constructing Pydantic models — the schemas usefrom_attributes=True. - Line length is 120.