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})
|
||||
|
||||
|
||||
@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")
|
||||
@api_key_required(scopes=["read"])
|
||||
async def commodity_metrics_csv(code: int):
|
||||
|
||||
Reference in New Issue
Block a user