""" Shared bootstrap helpers for the SolarWine Streamlit app. Cached data loaders, AI/IMS health checks, and common utilities. """ from __future__ import annotations import base64 import threading import streamlit as st _BRAND_GREEN = "#00BD3E" _BRAND_DARK = "#1A1A1A" try: import plotly.express import plotly.graph_objects from plotly.subplots import make_subplots _HAS_PLOTLY = True except ImportError: _HAS_PLOTLY = False from pathlib import Path import pandas as pd # Threading state for IMS background refresh (shared with app.py via module-level vars) _IMS_REFRESH_LOCK = threading.Lock() _IMS_REFRESH_STARTED = False _IMS_REFRESH_STATUS: dict = {"ok": None, "error": None, "running": False} @st.cache_data def load_labels(path: str) -> pd.DataFrame: return pd.read_csv(path, index_col=0, parse_dates=True) @st.cache_data def load_metrics(path: str) -> pd.DataFrame: return pd.read_csv(path) def _check_ai_health_impl() -> dict: """Actual Gemini ping; cached so we only run once.""" from src.genai_utils import get_genai_client try: client = get_genai_client() except Exception as exc: # noqa: BLE001 return {"ok": False, "error": f"client init failed: {exc}"} try: resp = client.models.generate_content( model="gemini-2.5-flash", contents="ping", ) text = getattr(resp, "text", "") or "" return {"ok": bool(text.strip()), "error": None} except Exception as exc: # noqa: BLE001 return {"ok": False, "error": f"generate_content failed: {exc}"} @st.cache_resource(show_spinner=False) def _cached_ai_ping() -> dict: """Cached Gemini health check (used by check_ai_health with timeout).""" return _check_ai_health_impl() def check_ai_health(timeout_seconds: float = 1.5) -> dict: """ Lightweight startup validation that Gemini is reachable. Waits at most timeout_seconds so app load stays under ~5s. Returns a dict with: - ok: bool - error: str | None """ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError with ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(_cached_ai_ping) try: return future.result(timeout=timeout_seconds) except FuturesTimeoutError: return {"ok": False, "error": "Health check timed out. Try refreshing."} def _run_ims_refresh(days: int = 7) -> None: """Background worker to refresh IMS cache without blocking the UI.""" from datetime import date, timedelta from src.ims_client import IMSClient global _IMS_REFRESH_STATUS try: ims = IMSClient() except Exception as exc: # noqa: BLE001 _IMS_REFRESH_STATUS = { "ok": False, "error": f"IMS init failed: {exc}", "running": False, } return try: today = date.today() start = (today - timedelta(days=days)).strftime("%Y-%m-%d") end = today.strftime("%Y-%m-%d") df = ims.fetch_and_cache(start, end) if df.empty: _IMS_REFRESH_STATUS = { "ok": False, "error": "IMS returned no data for the requested range.", "running": False, } else: _IMS_REFRESH_STATUS = {"ok": True, "error": None, "running": False} except Exception as exc: # noqa: BLE001 _IMS_REFRESH_STATUS = { "ok": False, "error": f"IMS fetch_and_cache failed: {exc}", "running": False, } def ensure_ims_refresh_background(days: int = 7) -> dict: """ Kick off IMS cache refresh in a background thread if not already started. Returns the latest known status dict without blocking page load. """ global _IMS_REFRESH_STARTED, _IMS_REFRESH_STATUS with _IMS_REFRESH_LOCK: if not _IMS_REFRESH_STARTED: _IMS_REFRESH_STARTED = True _IMS_REFRESH_STATUS = {"ok": None, "error": None, "running": True} t = threading.Thread(target=_run_ims_refresh, args=(days,), daemon=True) t.start() return dict(_IMS_REFRESH_STATUS) def img_to_base64(path: Path) -> str: return base64.b64encode(path.read_bytes()).decode() def now_israel(): from datetime import datetime try: from zoneinfo import ZoneInfo tz = ZoneInfo("Asia/Jerusalem") except ImportError: import datetime as _dt tz = _dt.timezone(_dt.timedelta(hours=2)) # fallback UTC+2 return datetime.now(tz)