Skip to content

Commit d1c4625

Browse files
committed
update packages
1 parent 5561d0d commit d1c4625

3 files changed

Lines changed: 112 additions & 29 deletions

File tree

CLAUDE.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Stack
6+
7+
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`.
8+
9+
## Common commands
10+
11+
The development workflow runs inside Docker via `just`. The `application` service mounts the repo and depends on a `db` Postgres service.
12+
13+
```bash
14+
just # default: install + lint + build + test
15+
just run # alembic upgrade head + start app on :8000
16+
just sh # shell inside the application container
17+
just test [args] # downgrade to base, upgrade, then pytest <args>; brings stack down after
18+
just migration -m "msg" # autogenerate alembic revision against current head
19+
just lint # eof-fixer + ruff format + ruff check --fix + ty check
20+
just build # docker compose build application
21+
just down # docker compose down --remove-orphans
22+
just install # uv lock --upgrade && uv sync --all-extras --all-groups --frozen
23+
```
24+
25+
Running a single test (inside container or with DB available):
26+
27+
```bash
28+
uv run pytest tests/test_decks.py::test_get_decks -x
29+
```
30+
31+
CI (`.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.
32+
33+
## Architecture
34+
35+
### Request lifecycle
36+
37+
`app/__main__.py` boots Granian pointing at `app.application:build_app` (factory). `build_app()` in `app/application.py`:
38+
39+
1. Creates a `modern_di.Container` with the `Dependencies` group from `app/ioc.py`.
40+
2. Builds a `FastAPIBootstrapper` from `settings.api_bootstrapper_config`, injecting SQLAlchemy + asyncpg OpenTelemetry instrumentors.
41+
3. `modern_di_fastapi.setup_di(app, container)` wires DI scopes onto the FastAPI app.
42+
4. Includes `app.api.decks.ROUTER` under `/api`.
43+
5. Registers `DuplicateKeyError` → 422 handler from `app/exceptions.py`.
44+
45+
### DI scopes (modern-di)
46+
47+
`app/ioc.py` defines providers:
48+
- `database_engine` — singleton-ish `AsyncEngine` with `create_sa_engine` / `close_sa_engine` finalizer.
49+
- `session``Scope.REQUEST`, finalized by `close_session`.
50+
- `decks_repository`, `cards_repository``Scope.REQUEST`, depend on `session`, configured with `auto_commit=True` (commit happens at session close, not per call).
51+
52+
Endpoints inject repositories with `FromDI(Repository)` from `modern_di_fastapi`. Add new providers to `Dependencies` rather than constructing services manually in routes.
53+
54+
### Database layer
55+
56+
- `app/models.py``BigIntAuditBase` from `advanced_alchemy` (auto `id`, `created_at`, `updated_at`). The module aliases `orm_registry.metadata` onto `orm.DeclarativeBase.metadata` so Alembic autogenerate sees both. New models go here.
57+
- `app/repositories.py` — Subclass `SQLAlchemyAsyncRepositoryService[Model]` with a nested `BaseRepository(SQLAlchemyAsyncRepository[Model])`. Routes use the service methods (`list`, `get_one_or_none`, `create`, `update`, `create_many`, `upsert_many`).
58+
- `app/resources/db.py``CustomAsyncSession.close()` does `expunge_all()` instead of closing when bound to an `AsyncConnection`. This is what enables the test rollback pattern below — do not "fix" it.
59+
- `migrations/env.py` swaps the asyncpg driver for the sync `postgresql` driver and uses `app.models.METADATA` as `target_metadata`.
60+
61+
### Settings
62+
63+
`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`.
64+
65+
### Tests
66+
67+
`tests/conftest.py` provides the test isolation pattern — read it before adding fixtures:
68+
69+
- `app` fixture builds a fresh app via `LifespanManager`.
70+
- `db_session` opens a connection, begins a transaction, begins a nested savepoint, and **overrides `Dependencies.database_engine`** with the connection itself. The nested savepoint is rolled back at teardown so each test starts clean. This is why `CustomAsyncSession.close` must `expunge_all` rather than close — closing would commit the outer transaction.
71+
- `set_async_session_in_base_sqlalchemy_factory` wires `db_session` into `SQLAlchemyFactory.__async_session__` so `polyfactory` factories in `tests/factories.py` (`DeckModelFactory`, `CardModelFactory`) persist via the rolled-back session. Test modules that use these factories opt in with `pytestmark = [pytest.mark.usefixtures("set_async_session_in_base_sqlalchemy_factory")]`.
72+
73+
`pytest.ini_options` sets `asyncio_mode = "auto"` — async tests do not need `@pytest.mark.asyncio`. Coverage runs by default (`--cov=. --cov-report term-missing`).
74+
75+
## Conventions
76+
77+
- Type-ignore syntax is `# ty: ignore[error-code]` (this project uses `ty`, not mypy). See `app/application.py:39` for an example.
78+
- Ruff is configured with `select = ["ALL"]` and a curated ignore list in `pyproject.toml`. Don't sprinkle `# noqa`; prefer fixing or extending the project ignore list if a rule is genuinely wrong for the codebase.
79+
- Routes return `typing.cast("schemas.X", obj)` over ORM/dict objects rather than constructing Pydantic models — the schemas use `from_attributes=True`.
80+
- Line length is 120.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ isort.no-lines-before = ["standard-library", "local-folder"]
7474
addopts = "--cov=. --cov-report term-missing"
7575
asyncio_mode = "auto"
7676
asyncio_default_fixture_loop_scope = "function"
77+
filterwarnings = [
78+
"ignore::lite_bootstrap.InstrumentNotReadyWarning",
79+
]
7780

7881
[tool.coverage.report]
7982
exclude_also = [

0 commit comments

Comments
 (0)