Skip to content

feat(api): optional FastAPI service layer (apis/)#111

Open
Sammyjoseph999 wants to merge 17 commits into
mainfrom
feat/api-service
Open

feat(api): optional FastAPI service layer (apis/)#111
Sammyjoseph999 wants to merge 17 commits into
mainfrom
feat/api-service

Conversation

@Sammyjoseph999

Copy link
Copy Markdown
Collaborator

Adds an optional FastAPI service layer (apis/) that exposes the toolkit over HTTP — a JSON API under /api/v1/* plus simple HTML pages — reconciled to the current codebase.

What's included

  • Service layer apis/: FastAPI app, 7 routers (data, statistics, hazards, compare, seasons, climatology, compare-datasets), Pydantic schemas, Jinja templates, static assets.
  • Optional dependency extra api (fastapi/uvicorn/jinja2/python-multipart) so the base CLI/library stays lean:
    uv sync --extra api
    uv run uvicorn apis.main:app --reload
  • README section documenting install + run.

Reconciliation to current code

  • Imports use fully-qualified climate_tookit.* paths (removed fragile sys.path hacks and from sources / from source_data shortcuts).
  • Ensemble call sites updated to the current model_workers kwarg (was max_workers).
  • Source options corrected per module via a single catalog (apis/catalog.py): analysis modules default to auto (CHIRPS v3 + AgERA5 with fallbacks) plus standalone daily temp+precip sources and NEX-GDDP; removed invalid keys (chirps, hirts, chirps+chirts) and monthly TerraClimate from the daily modules.
  • NEX-GDDP ensemble UI on climatology, statistics, hazards and compare-periods: selecting NEX-GDDP reveals scenario + model selectors and routes to the matching ensemble function.

Verification

  • App boots; all 8 UI pages and /docs / /openapi.json return 200; ensemble controls render on all four modules; ruff clean.
  • Caveat: the GEE-backed compute endpoints (single and ensemble) require a live Earth Engine session to exercise end to end — imports, routing, signatures and template rendering are verified, but real data responses have not been run here. Ensemble output is currently shown as a guarded summary + collapsible JSON (the ensemble return schemas are heterogeneous); a formatted view can follow once we validate against real payloads.

Port the HTTP service into this repo, reconciled to the current package:
- imports use fully-qualified climate_tookit.* paths (dropped the fragile
  sys.path hacks and 'from sources' / 'from source_data' shortcuts)
- ensemble call sites updated to the current 'model_workers' kwarg
  (was 'max_workers') across hazards/statistics/compare/climatology
- fastapi/uvicorn/jinja2/python-multipart added as an optional 'api' extra
  so the base CLI/library stays lean
- README documents install + run (uvicorn apis.main:app)

Verified the app boots and all routers register (/, /docs, /openapi.json,
/api/v1/data/sources return 200); ruff clean. GEE-backed endpoints require
live Earth Engine auth to exercise end to end.
Fixes reported UI issues:
- source dropdowns were hardcoded per-template with stale/invalid keys
  ('chirps', 'hirts', 'chirps+chirts') and incomplete lists. Centralized a
  single valid source catalog (apis/catalog.py) exposed to all templates via
  Jinja globals; every module now offers the correct current source keys.
- climatology defaulted to the invalid 'chirps' source, so it always failed;
  it now defaults to agera_5 with a valid list.
- ensembles were only reachable via the JSON API. Added NEX-GDDP ensemble
  controls (scenario + model multi-select) to the climatology page, wired to
  calculate_climatology_ensemble when source=nex_gddp.

Verified all UI pages render 200 and the ensemble controls appear; ruff clean.
…/periods

Source catalog now reflects what each module actually supports:
- analysis modules (statistics/seasons/hazards/compare_periods) default to
  'auto' (recommended: CHIRPS v3 + AgERA5 with fallbacks) plus standalone
  daily temp+precip sources (agera_5/era_5/nasa_power) and nex_gddp; dropped
  monthly TerraClimate from these daily modules. 'paired' is deferred until
  the forms collect precip/temp sources.
- climatology/compare-datasets keep concrete single-source lists.

Ensemble UI (previously only on climatology) now also on statistics, hazards
and compare-periods via shared _ensemble_controls / _ensemble_result partials:
choosing NEX-GDDP reveals scenario + model selectors and routes to the
matching ensemble function (analyze_ensemble_nex_gddp / calculate_ensemble /
ensemble_compare).

All 8 UI pages render 200; ensemble controls on all 4 modules; ruff clean.
… JSON)

Surface scenario / models-used / failed-model counts above the payload when
present (all guarded, so it degrades to just the JSON for any shape), and move
the full result into a collapsible <details>. Climatology now uses the shared
_ensemble_result partial too. All UI pages render 200; ruff clean.
@Sammyjoseph999 Sammyjoseph999 requested a review from bjyberg July 3, 2026 00:43
Sammyjoseph999 and others added 6 commits July 3, 2026 04:45
Adds a shared client-side download button (_download_button.html) to the
result view of every module (fetch, statistics, seasons, climatology, hazards,
compare-periods, compare-datasets). Serializes the already-computed result to
a JSON file with no server round-trip or recompute. Verified the button and
embedded payload render for a mock success result; all pages 200, ruff clean.
calculate_climatology already writes annual/monthly PNGs when given an
output_dir; the climatology handler now points it at a served artifacts
directory (outputs/api, git-ignored) mounted at /artifacts, forces the Agg
matplotlib backend for server-thread safety, and passes the resulting plot
URLs to the template, which renders them as images. Verified: artifacts route
serves files (200), path->URL mapping correct, template renders <img>. Actual
plot content requires a live GEE session. Downloads (JSON) already cover all
modules; visualizations start with climatology as the exemplar.
Point compare_sources/print_report at a unique served artifacts subdir per
run (outputs/api/compare_datasets/<id>, git-ignored), glob the generated PNGs
and render them as images on the results page. Verified pages render 200 and
the <img> block renders for mock plot URLs; ruff clean. Plot content needs a
live GEE session.
Handlers parsed form fields (float/int) outside their try/except, so a missing
or invalid field raised an uncaught exception and the browser showed a bare
'Internal Server Error' on every module. Add an app-level exception handler
that renders the actual exception type/message (+ traceback) as a readable
page for HTML routes and JSON for /api routes, so failures are diagnosable
rather than opaque. Verified a previously-uncaught error now renders a 'Server
error' page with the real exception; ruff clean.
Two robustness fixes for the reported 'Internal Server Error on every module':
- Startup guard: if python-multipart is not installed (the most likely cause
  of a universal form-POST 500, since request.form() fails before any handler
  runs), refuse to start with a clear 'uv sync --extra api' message instead of
  an opaque per-request 500.
- Moved form parsing (await request.form() + payload float/int conversions)
  inside each page handler's try/except, so a missing/invalid field now renders
  a clean inline error on that module's page instead of escaping as a 500.

Verified: all pages render 200; a missing-field POST now shows an inline module
error (not the global error page); ruff clean.
@bjyberg

bjyberg commented Jul 3, 2026

Copy link
Copy Markdown
Member

I haven't had the chance to do a full review of this yet, but initial thought is that it might be helpful to have the api dockerized with an example of how to deploy it via docker. Awesome work on this though, I'm excited to test it and see how it goes!

The download partial now offers 'Download (CSV)' next to '(JSON)'. Client-side
and generic: when the result has a season_statistics list it exports a proper
per-season table; otherwise it flattens the nested result to a field,value CSV.
Works for every module with no server round-trip. Verified the partial renders
both buttons/handlers and all pages compile 200.
Rename climate_tookit -> climate_toolkit across apis/ to match the merged
package rename (#116); resolve uv.lock and regenerate with the api extra.
Verified: app imports, all pages render 200, ruff clean, 22 packaging/cli
tests pass.
@Sammyjoseph999

Copy link
Copy Markdown
Collaborator Author

Thanks @bjyberg! Added Docker support for the service:

  • Dockerfile.api — reproducible image (base pinned by digest, uv + uv.lock pinned) that installs the api extra and serves the app with uvicorn in production mode (no --reload). Runs as non-root, exposes 8000, and has a HEALTHCHECK against /health.
  • README → "Deploy the API with Docker" — build/run commands, including the Earth Engine credential mount + GCP_PROJECT_ID for GEE-backed routes, plus notes on reverse proxy/TLS, tightening CORS, and --workers for production.
  • tests/test_api_smoke.py — import/route smoke test; skips when the api extra is absent (so base CI stays green), runs for real with .[api].

Verified locally: image builds with no compiler, container boots and returns 200 on /health, /, /docs, and /api/v1/data/sources.

docker build -f Dockerfile.api -t climate-toolkit-api .
docker run --rm -p 8000:8000 climate-toolkit-api   # then open http://127.0.0.1:8000/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants