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:
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user