Install all dependencies (including dev tools):
uv sync --group devAll common tasks are available via make. Run make help to see the full list.
Runtime and dev dependencies are declared in pyproject.toml and pinned in uv.lock. Always commit both files together.
Add a runtime dependency:
uv add <package>Add a dev-only dependency (tools, linters, test libraries):
uv add --group dev <package>Remove a dependency:
uv remove <package>Upgrade all dependencies to the latest allowed versions:
uv lock --upgradeUpgrade a single package:
uv lock --upgrade-package <package>After any lock file change, run uv sync --group dev to update your local environment, then run make check to confirm nothing is broken.
make lint # ruff — linting
make format # black — formatting check
make typecheck # mypy — strict type checking
make check # all three, plus tests (use before pushing)To automatically fix violations:
make fix # ruff --fix + black reformat- Formatting: black, 88-char line length
- Linting: ruff with a broad community ruleset — pycodestyle, pyflakes, isort, pep8-naming, pyupgrade, bugbear, comprehensions, simplify, type-checking, annotations, pathlib, perflint
- Types: mypy in strict mode — all public functions and methods must be fully annotated
This project enforces a strict quality baseline. All checks must pass before code is merged.
Every function and method — including tests — must carry complete type annotations. This is enforced by two layers:
- ruff (
ANNrules) rejects missing annotations at lint time. - mypy in strict mode performs full static type checking, including inferred return types,
Anyusage, and untyped third-party imports.
There are no per-file or per-directory exemptions. Annotate everything.
Code must be formatted with black and pass all ruff rules without suppression. If a rule produces a false positive, discuss disabling it project-wide in pyproject.toml rather than adding inline # noqa comments.
All new code must be covered by tests. Branch coverage is measured — ensure conditional paths are exercised, not just the happy path. make check will fail if coverage drops.
make test # pytest with branch coverageTests live in tests/. Coverage is reported to the terminal and written to coverage.xml. All new code should be covered.
This project uses Conventional Commits. Every commit message must follow the format:
<type>(<scope>): <description>
The scope is optional. Use the types below:
| Type | When to use |
|---|---|
feat |
A new feature or behaviour |
fix |
A bug fix |
docs |
Documentation only |
chore |
Maintenance — dependencies, config, tooling |
build |
Build system or CI changes |
test |
Adding or updating tests |
refactor |
Code change that neither fixes a bug nor adds a feature |
perf |
Performance improvement |
Keep the description short (under 72 characters), in the imperative mood, and in lower case. For example:
feat(auth): add JWT validation middleware
fix: handle empty response from upstream API
docs: document src layout rationale
make build # build the image
make run # run the containerThe image uses a multi-stage build: dependencies are installed in a builder stage and only the compiled venv is copied to the final python:3.14-slim runtime image. The container runs as a non-root user.
Terraform configuration lives in terraform/. AWS credentials must be available in the environment (e.g. via AWS_PROFILE or aws sso login).
make tf-init # initialise working directory and download providers
make tf-plan # preview changes
make tf-apply # apply changes
make tf-destroy # tear down all managed resourcesThe state is stored locally by default. Before working in a team or running in CI, configure the S3 backend in terraform/versions.tf.