Add background IMS refresh scheduler (every 6h)
Browse files- backend/api/main.py +43 -1
backend/api/main.py
CHANGED
|
@@ -74,6 +74,43 @@ def _check_data_paths():
|
|
| 74 |
log.warning("Missing %s — %s", rel_path, msg)
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
@asynccontextmanager
|
| 78 |
async def lifespan(app: FastAPI):
|
| 79 |
global _start_time
|
|
@@ -87,7 +124,12 @@ async def lifespan(app: FastAPI):
|
|
| 87 |
if not os.environ.get("ADMIN_PASSWORD"):
|
| 88 |
log.warning("ADMIN_PASSWORD not set — login endpoint disabled")
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
yield
|
|
|
|
| 91 |
log.info("SolarWine API shutting down (uptime=%.0fs)", get_uptime())
|
| 92 |
|
| 93 |
|
|
@@ -138,7 +180,7 @@ _default_origins = ",".join([
|
|
| 138 |
"http://localhost:3000",
|
| 139 |
"http://localhost:5173",
|
| 140 |
"https://solarwine-api.hf.space",
|
| 141 |
-
"https://
|
| 142 |
])
|
| 143 |
allowed_origins = os.environ.get("ALLOWED_ORIGINS", _default_origins).split(",")
|
| 144 |
app.add_middleware(
|
|
|
|
| 74 |
log.warning("Missing %s — %s", rel_path, msg)
|
| 75 |
|
| 76 |
|
| 77 |
+
async def _ims_refresh_loop(interval_sec: int = 6 * 3600):
|
| 78 |
+
"""Background loop: refresh the IMS weather cache every `interval_sec` seconds.
|
| 79 |
+
|
| 80 |
+
Fetches the last 14 days of IMS data, overwrites the local CSV cache,
|
| 81 |
+
and invalidates the in-memory WeatherService cache so the next request
|
| 82 |
+
picks up fresh data.
|
| 83 |
+
"""
|
| 84 |
+
import asyncio
|
| 85 |
+
await asyncio.sleep(10) # let the server finish starting
|
| 86 |
+
while True:
|
| 87 |
+
try:
|
| 88 |
+
from datetime import date, timedelta
|
| 89 |
+
from src.data.ims_client import IMSClient
|
| 90 |
+
end = date.today()
|
| 91 |
+
start = end - timedelta(days=14)
|
| 92 |
+
client = IMSClient()
|
| 93 |
+
loop = asyncio.get_event_loop()
|
| 94 |
+
df = await loop.run_in_executor(
|
| 95 |
+
None,
|
| 96 |
+
lambda: client.fetch_and_cache(
|
| 97 |
+
str(start), str(end), chunk_days=14
|
| 98 |
+
),
|
| 99 |
+
)
|
| 100 |
+
rows = len(df) if df is not None and not df.empty else 0
|
| 101 |
+
log.info("IMS cache refreshed: %d rows (%s → %s)", rows, start, end)
|
| 102 |
+
# Invalidate WeatherService in-memory cache so next request uses fresh data
|
| 103 |
+
try:
|
| 104 |
+
from backend.api.deps import get_datahub
|
| 105 |
+
hub = get_datahub()
|
| 106 |
+
hub.weather._df_cache._store.clear()
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
except Exception as exc:
|
| 110 |
+
log.error("IMS refresh failed: %s", exc)
|
| 111 |
+
await asyncio.sleep(interval_sec)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
@asynccontextmanager
|
| 115 |
async def lifespan(app: FastAPI):
|
| 116 |
global _start_time
|
|
|
|
| 124 |
if not os.environ.get("ADMIN_PASSWORD"):
|
| 125 |
log.warning("ADMIN_PASSWORD not set — login endpoint disabled")
|
| 126 |
|
| 127 |
+
# Start background IMS data refresh (every 6 hours)
|
| 128 |
+
import asyncio
|
| 129 |
+
ims_task = asyncio.create_task(_ims_refresh_loop())
|
| 130 |
+
|
| 131 |
yield
|
| 132 |
+
ims_task.cancel()
|
| 133 |
log.info("SolarWine API shutting down (uptime=%.0fs)", get_uptime())
|
| 134 |
|
| 135 |
|
|
|
|
| 180 |
"http://localhost:3000",
|
| 181 |
"http://localhost:5173",
|
| 182 |
"https://solarwine-api.hf.space",
|
| 183 |
+
"https://solarwine.pages.dev",
|
| 184 |
])
|
| 185 |
allowed_origins = os.environ.get("ALLOWED_ORIGINS", _default_origins).split(",")
|
| 186 |
app.add_middleware(
|