api / ui /bootstrap.py
safraeli's picture
Deploy: 2026 sensor migration + redesign + bucket B endpoints
13fc29d verified
Raw
History Blame Contribute Delete
4.6 kB
"""
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)