From 89c9f89c8edc02479bb2dcceaf93351d96d182e6 Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 02:39:24 +0100 Subject: [PATCH] feat(web): add weather API endpoints (locations, series, stress, alerts) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 4 REST endpoints under /api/v1/weather/: - GET /weather/locations — 12 locations with latest stress, sorted by severity - GET /weather/locations/ — 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 --- web/src/beanflows/api/routes.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/web/src/beanflows/api/routes.py b/web/src/beanflows/api/routes.py index a5ab769..6a6b0dd 100644 --- a/web/src/beanflows/api/routes.py +++ b/web/src/beanflows/api/routes.py @@ -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/") +@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//metrics.csv") @api_key_required(scopes=["read"]) async def commodity_metrics_csv(code: int):