feat(web): add weather API endpoints (locations, series, stress, alerts)

Adds 4 REST endpoints under /api/v1/weather/:
- GET /weather/locations — 12 locations with latest stress, sorted by severity
- GET /weather/locations/<id> — daily series for one location (?metrics, ?days)
- GET /weather/stress — global daily stress trend (?days)
- GET /weather/alerts — locations with active crop stress flags

All endpoints use @api_key_required(scopes=["read"]) and return {"data": ...}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-26 02:39:24 +01:00
parent a8cfd68eda
commit 89c9f89c8e

View File

@@ -298,6 +298,59 @@ async def commodity_ice_stocks_by_port(code: str):
return jsonify({"commodity": code, "data": data}) return jsonify({"commodity": code, "data": data})
@bp.route("/weather/locations")
@api_key_required(scopes=["read"])
async def weather_locations():
"""12 coffee-growing locations with latest-day stress index, sorted by severity."""
data = await analytics.get_weather_locations()
return jsonify({"data": data})
@bp.route("/weather/locations/<location_id>")
@api_key_required(scopes=["read"])
async def weather_location_series(location_id: str):
"""Daily weather time series for one location.
Query params:
metrics — repeated param, e.g. ?metrics=crop_stress_index&metrics=precipitation_mm
days — trailing days (default 365, max 3650)
"""
raw_metrics = request.args.getlist("metrics") or None
if raw_metrics:
metrics = [m for m in raw_metrics if m in analytics.ALLOWED_WEATHER_METRICS]
if not metrics:
return jsonify({"error": f"No valid metrics. Allowed: {sorted(analytics.ALLOWED_WEATHER_METRICS)}"}), 400
else:
metrics = None
days = min(int(request.args.get("days", 365)), 3650)
data = await analytics.get_weather_location_series(location_id, metrics=metrics, days=days)
if not data:
return jsonify({"error": f"No data found for location_id {location_id!r}"}), 404
return jsonify({"location_id": location_id, "data": data})
@bp.route("/weather/stress")
@api_key_required(scopes=["read"])
async def weather_stress():
"""Daily global crop stress trend (avg + max across 12 origins).
Query params:
days — trailing days (default 365, max 3650)
"""
days = min(int(request.args.get("days", 365)), 3650)
data = await analytics.get_weather_stress_trend(days=days)
return jsonify({"data": data})
@bp.route("/weather/alerts")
@api_key_required(scopes=["read"])
async def weather_alerts():
"""Locations with active crop stress flags on the latest observation date."""
data = await analytics.get_weather_active_alerts()
return jsonify({"data": data})
@bp.route("/commodities/<int:code>/metrics.csv") @bp.route("/commodities/<int:code>/metrics.csv")
@api_key_required(scopes=["read"]) @api_key_required(scopes=["read"])
async def commodity_metrics_csv(code: int): async def commodity_metrics_csv(code: int):