# AGENTS.md

This is an async HTTP API built with FastAPI, SQLAlchemy 2.0, and PostgreSQL.

## Project

Stack: Python 3.12, FastAPI, SQLAlchemy 2.0 (async), Alembic, Pydantic v2, PostgreSQL, uv, ruff, pytest.

Layout:

- `app/main.py` application factory, middleware, router registration
- `app/api/v1/` routers, one module per resource
- `app/schemas/` Pydantic request and response models
- `app/models/` SQLAlchemy ORM models
- `app/services/` business logic, the only layer that composes repositories
- `app/repositories/` database access, one per aggregate
- `app/core/config.py` Settings loaded from the environment
- `app/core/deps.py` dependency providers (session, current user)
- `alembic/versions/` generated migrations
- `tests/` mirrors the `app/` tree

Entry points: `app/main.py`, `app/core/config.py`, `app/core/deps.py`.

## Commands

Use uv. Do not use pip, poetry, or a bare virtualenv.

```bash
uv sync                                  # install dependencies from uv.lock
uv run uvicorn app.main:app --reload     # dev server on :8000
uv run ruff check .                      # lint
uv run ruff format .                     # format
uv run mypy app                          # type check
uv run pytest                            # full test suite
uv run pytest tests/api/test_users.py -k create   # one test
uv run alembic revision --autogenerate -m "add user table"
uv run alembic upgrade head              # apply migrations
uv run alembic downgrade -1              # roll back one revision
```

Add a dependency with `uv add <package>`, never by editing pyproject.toml by hand.

Before returning work run `uv run ruff check .`, `uv run mypy app`, and `uv run pytest`.

## Code style

- Every route handler is `async def`. Never call a blocking library inside one, wrap it in `anyio.to_thread.run_sync`.
- Never use the sync SQLAlchemy session. Sessions come from `app.core.deps.get_session` via `Depends`.
- Routers contain no business logic. They validate input, call a service, and shape the response.
- Pydantic schemas are the only types crossing the HTTP boundary. ORM models never leave the service layer.
- Full type hints on every function, including return types. `Any` requires a comment explaining why.
- Raise `HTTPException` with an explicit status code, never return a bare error dict.
- Configuration is read from `Settings` only. No `os.environ` access outside `app/core/config.py`.
- Line length 100, enforced by ruff. Double quotes.
- No wildcard imports, no mutable default arguments.

## Boundaries

Do not touch without explicit instruction:

- Existing files in `alembic/versions/`. Migrations are generated and append only. Never edit or delete a revision that has been applied.
- `uv.lock`. Use `uv add` and `uv sync`.
- `.env` and anything holding a credential. Document new settings in `.env.example`.
- `Dockerfile`, `docker-compose.yml`, `.github/workflows/`.

Needs human review: authentication and permission dependencies in `app/core/deps.py`, rate limiting, any migration that drops or renames a column, and changes to a public response schema.

If a model change implies a migration, generate it, print the SQL, and stop for review before applying it anywhere but the local database.

## Testing

- `uv run pytest` runs everything against a disposable test database created per session.
- Every endpoint has at least a success test and a failure test covering validation and authorization.
- Service layer functions with branching logic require unit tests.
- Repositories are exercised through service tests, not mocked.
- Use the factories in `tests/factories.py` rather than constructing models inline.
- A bug fix includes a regression test that fails without the fix.

## Git workflow

- Branch from `main`: `feat/short-description`, `fix/short-description`.
- Conventional commits: `feat(users): add email verification endpoint`.
- A migration and the code that depends on it ship in the same commit.
- Never commit to `main` directly, never force push a shared branch.
- PR description lists new environment variables, migrations to run, and any breaking API change.
