Repository navigation
Development
Python 3.11. No build step, no code generation, no ORM.
git clone https://github.com/reportmate/reportmate-api.git
cd reportmate-api
python -m venv .venv
. .venv/bin/activate
pip install -r requirements-dev.txt
requirements-dev.txt includes the runtime requirements plus pytest and
httpx.
uvicorn main:app --reload --port 8000
DATABASE_URL defaults to
postgresql://reportmate:password@localhost:5432/reportmate, so a local
Postgres on the default port needs no configuration beyond DB_SSL=false.
To work without a credential:
export APP_ENV=development
export DISABLE_AUTH=true
APP_ENV=development is what makes this work. With APP_ENV set to
development, dev, local, test or testing the startup guard always
allows DISABLE_AUTH. With APP_ENV unset it refuses to boot as soon as
REPORTMATE_PASSPHRASE or API_INTERNAL_SECRET is configured, and with a
production or staging APP_ENV it always refuses.
pytest
Most tests are pure and run anywhere. The SQL-level tests — the usage upsert,
the migrations, the pagination and aggregate queries — skip themselves unless
TEST_DATABASE_URL points at a real PostgreSQL server:
export TEST_DATABASE_URL=postgresql://reportmate:reportmate@localhost:5432/reportmate
export DB_SSL=false
pytest -rs
-rs prints the skip reasons, which is how you tell "passed" from "did not
run". The reason those tests need a server rather than a mocked cursor is
concrete: a statement-level type error once shipped green through a full unit
suite and silently dropped fleet-wide utilization data. CI runs a
postgres:16 service container for exactly this.
tests/conftest.py sets deterministic non-secret values for
REPORTMATE_PASSPHRASE and API_INTERNAL_SECRET before dependencies is
imported, because those are read into module-level globals at import time. It
also resets the rate limiter between tests, since TestClient requests all
share one remote address.
Some tests assert structure rather than behaviour and are worth knowing about before you change things:
| Test | What it enforces |
|---|---|
test_route_auth.py |
Every route on every router depends on verify_authentication, except an explicit allowlist of /health, /health/live, /health/ready, and /auth/config. A new router module has to be added to the test's router list. |
test_dockerfile.py |
The Dockerfile copies what the app needs. Adding a new top-level module means adding a COPY line. |
test_migrations.py |
The Alembic chain applies against a real database. |
test_openapi_security.py |
The published schema advertises the auth schemes. |
- Put the handler in the router for its area under
routers/. Adddependencies=[Depends(verify_authentication)]—test_route_auth.pyfails without it. - Pick the path deliberately. The required scope is derived from the method and whether the path contains
/admin, not declared per route. - Non-trivial SQL goes in a file under
sql/and is loaded withload_sql("area/name"). Add it to thepreload_sql_queries()list independencies.py. - If the response is cacheable, add a namespace to
_CACHE_TTLand wrap the handler incache_get/cache_set. - If it is a list endpoint, take
limitandoffsetand calladd_pagination_headers.
alembic revision -m "what it does"
Revision ids in this repo are sequential four-digit strings (0001…0011),
not hashes; follow that. Migrations are raw SQL through op.execute — there
is no ORM metadata to autogenerate against, and target_metadata is None on
purpose.
If your statement touches a table the deployment schema owns rather than
Alembic — devices, events, or any collection-module table — wrap it in the
_tolerant() helper the existing migrations use. It runs the DDL inside a
PL/pgSQL block that swallows only undefined_table, so a fresh database
migrates cleanly before ingestion has created those tables. Any other error
still aborts.
Write migrations idempotently (IF NOT EXISTS, ADD COLUMN IF NOT EXISTS) —
they run on every startup of every replica.
Alembic reads DATABASE_URL and DB_SSL from the environment via
alembic/env.py and rewrites the scheme to postgresql+pg8000://. No
credentials live in alembic.ini.
python scripts/export_openapi.py
Writes docs/api/openapi.json and prints the path count and an endpoint count
per tag. The
security schemes are injected by a custom openapi() in main.py, because
authentication is enforced by a dependency rather than by FastAPI's security
utilities and so would not otherwise appear in the schema.
.github/workflows/build.yml has three jobs, run on every push to main,
every PR, and every v* tag:
| Job | What it does |
|---|---|
test |
pytest -rs against a postgres:16 service container, so the SQL-level tests run rather than skip. |
openapi |
Runs scripts/export_openapi.py, fails if the spec has fewer than 50 paths or no security schemes, and uploads docs/api/openapi.json as the openapi artifact for that commit. |
build |
After test passes, builds the linux/amd64 image and pushes it to GHCR. Pull requests build without pushing. |
The reportmate-website
repo exports the same file from main on its own schedule and publishes it at
/openapi.json, so integrators read the deployed contract.
Pull requests are welcome. First-time contributors are asked by the CLA Assistant bot to sign a one-page CLA, which is what makes the dual-licence model work; sign it once and it covers all future PRs.
AGPL-3.0 — a commercial license is available. reportmate.app