""" utils/api_client.py -------------------- Centralized, cached API wrappers. HF Spaces compatible: reads API_URL from environment variable so the same code works locally (localhost:8000) and on HuggingFace (localhost:8000 since both services run in the same container). """ import os import requests import pandas as pd import streamlit as st # HF Spaces: backend always on localhost:8000 inside the container API = os.environ.get("API_URL", "http://localhost:8000") TIMEOUT = 15 @st.cache_data(ttl=300) def _get(endpoint: str, params: dict | None = None): """Raw cached GET — returns JSON or None on any error.""" try: r = requests.get(f"{API}{endpoint}", params=params or {}, timeout=TIMEOUT) r.raise_for_status() return r.json() except requests.exceptions.ConnectionError: return None except requests.exceptions.Timeout: return None except Exception: return None def _df(data) -> pd.DataFrame: if not data: return pd.DataFrame() if isinstance(data, list): return pd.DataFrame(data) if isinstance(data, dict): return pd.DataFrame([data]) return pd.DataFrame() # ── Health ───────────────────────────────────────────────────────────────────── def is_online() -> bool: try: requests.get(f"{API}/health", timeout=5) return True except Exception: return False # ── /districts/* ─────────────────────────────────────────────────────────────── def fetch_stats() -> dict: return _get("/districts/stats") or {} def fetch_states() -> list[str]: return _get("/districts/states") or [] def fetch_districts(state: str) -> list[str]: return _get("/districts/list", {"state": state}) or [] def fetch_district_history(state: str, district: str) -> pd.DataFrame: return _df(_get("/districts/history", {"state": state, "district": district})) def fetch_top_districts( state: str | None = None, metric: str = "person_days_lakhs", n: int = 12, ) -> pd.DataFrame: params = {"metric": metric, "n": n} if state: params["state"] = state return _df(_get("/districts/top", params)) def fetch_yearly_trend(state: str | None = None) -> pd.DataFrame: params = {"state": state} if state else {} return _df(_get("/districts/trend", params)) # ── /predictions/* ───────────────────────────────────────────────────────────── def fetch_predictions( state: str | None = None, district: str | None = None, year: int | None = None, ) -> pd.DataFrame: params = {} if state: params["state"] = state if district: params["district"] = district if year: params["year"] = year return _df(_get("/predictions/", params)) # ── /optimizer/* ─────────────────────────────────────────────────────────────── def fetch_optimizer_results(state: str | None = None) -> pd.DataFrame: params = {"state": state} if state else {} return _df(_get("/optimizer/results", params)) def run_optimizer_live( state: str | None = None, budget_scale: float = 1.0, min_fraction: float = 0.40, max_fraction: float = 2.50, ) -> dict | None: payload = { "state": state, "budget_scale": budget_scale, "min_fraction": min_fraction, "max_fraction": max_fraction, } try: r = requests.post(f"{API}/optimizer/run", json=payload, timeout=60) r.raise_for_status() return r.json() except requests.exceptions.ConnectionError: st.error("Cannot reach API — backend may still be starting up, refresh in a moment.") return None except Exception as e: st.error(f"Optimizer error: {e}") return None