3.4 KiB
backend/ — agent instructions
The Thermograph API service: a FastAPI app serving the graded-climate API
(/api/v2/...), user accounts, notifications, and the SSR-content JSON API the
frontend renders pages from. It owns the climate data pipeline and the
derived-payload cache. It serves no HTML/CSS/JS — that is frontend/.
Read the root CLAUDE.md first for the branch model, deploy contract and image
names.
Build & verify
make test— builds a py3.12 venv and runs the hermetic pytest suite (scripts/test.sh; passARGS=...). Tests are hermetic pertests/conftest.py: no real Open-Meteo/Nominatim/GeoNames calls, throwaway SQLite, notifier thread disabled.make smoke— boots the built image plus a throwaway db and asserts/healthz+/api/version.- CI runs this suite inside the built image (
build.yml), so the exact interpreter and deps that ship are what gets tested.
app.py at the root is a one-line re-export shim for web/app.py, kept stable
for systemd/CI callers. Entry target is app:app.
Contracts that break silently if you change them
GET /api/version→{backend_version, min_frontend, payload_ver}, driven byAPI_CONTRACT_VERSIONandMIN_SUPPORTED_FRONTENDinweb/app.py. A breaking change ships as a newv3router mounted alongsidev2, re-registering only changed handlers, with the constant bumped in the same PR./apiand/api/v1stay mounted as aliases.PAYLOAD_VER(api/payloads.py) is separate from URL versioning — it is the cache/ETag invalidation token. Bump it whenever a response payload's shape changes, so one bump atomically orphans every pre-upgrade cached row.- ETag /
If-None-Match— every graded payload is cached against a validity token that doubles as a weak ETag.expose_headers=["ETag"]in the CORS middleware matters as much asallow_origins: without it the frontend'scache.jsreadsnullcross-origin and never revalidates. Don't change the ETag derivation or that list without checkingfrontend/static/cache.js. data/grading.py::pct_ordinal()is mirrored byfrontend'sshared.js::pctOrd()— floor a percentile into1..99, never 0 or 100.TEMP_BANDS/RAIN_BANDSare the source of truth for tier names and thresholds; changing them without the frontend produces tiers drawn in colours that disagree with the labels the API returns.- Fahrenheit country set —
api/content_payloads.py'sF_COUNTRIESmust stay identical tofrontend'sformat.py::F_COUNTRIESandstatic/units.js'sF_REGIONS. There is a test asserting this. /healthzand/api/versionare deliberately I/O-free so they stay cheap under tight healthcheck intervals.
Layout
accounts/ (fastapi-users + alembic), api/ (payload builders + SSR content
routes), core/ (metrics, audit, singleton helper), data/ (grid, climate
fetch/cache, grading/scoring, places/cities, derived-payload store),
notifications/ (push, email, Discord bot, digest, scheduler), web/app.py
(the real app), daemon/.
paths.py resolves every filesystem location from the repo root — don't
reintroduce __file__-relative paths in a module; it breaks the moment a module
moves. deploy/entrypoint.sh is the container entrypoint (alembic migrate, then
serve, with a Swarm-secrets-as-files shim).
Commits & PRs
Concise and technical. Never mention AI, assistants or automated authorship.