"""Scheduler APScheduler — 3 jobs de collecte météo.""" import logging from datetime import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler logger = logging.getLogger(__name__) scheduler = AsyncIOScheduler(timezone="Europe/Paris") def _store_station_current() -> None: """Collecte et stocke les données actuelles de la station.""" from app.services.station import fetch_current from app.models.meteo import MeteoStation from app.database import engine from sqlmodel import Session data = fetch_current() if not data: logger.warning("Station current: aucune donnée collectée") return now_str = datetime.now().strftime("%Y-%m-%dT%H:00") entry = MeteoStation(date_heure=now_str, type="current", **data) with Session(engine) as session: existing = session.get(MeteoStation, now_str) if existing: for k, v in data.items(): setattr(existing, k, v) session.add(existing) else: session.add(entry) session.commit() logger.info(f"Station current stockée : {now_str}") def _store_station_veille() -> None: """Collecte et stocke le résumé de la veille (NOAA).""" from datetime import timedelta from app.services.station import fetch_yesterday_summary from app.models.meteo import MeteoStation from app.database import engine from sqlmodel import Session data = fetch_yesterday_summary() if not data: logger.warning("Station veille: aucune donnée collectée") return yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%dT00:00") entry = MeteoStation(date_heure=yesterday, type="veille", **data) with Session(engine) as session: existing = session.get(MeteoStation, yesterday) if existing: for k, v in data.items(): setattr(existing, k, v) session.add(existing) else: session.add(entry) session.commit() logger.info(f"Station veille stockée : {yesterday}") def _store_open_meteo() -> None: """Collecte et stocke les prévisions Open-Meteo.""" from app.services.meteo import fetch_and_store_forecast from app.models.meteo import MeteoOpenMeteo from app.database import engine from sqlmodel import Session rows = fetch_and_store_forecast() if not rows: logger.warning("Open-Meteo: aucune donnée collectée") return with Session(engine) as session: for row in rows: existing = session.get(MeteoOpenMeteo, row["date"]) if existing: for k, v in row.items(): if k != "date": setattr(existing, k, v) session.add(existing) else: session.add(MeteoOpenMeteo(**row)) session.commit() logger.info(f"Open-Meteo stocké : {len(rows)} jours") def backfill_station_missing_dates(max_days_back: int = 365) -> None: """Remplit les dates manquantes de la station météo au démarrage. Cherche toutes les dates sans entrée « veille » dans meteostation depuis max_days_back jours en arrière jusqu'à hier (excl. aujourd'hui), puis télécharge les fichiers NOAA mois par mois pour remplir les trous. Un seul appel HTTP par mois manquant. """ from datetime import date, timedelta from itertools import groupby from app.services.station import fetch_month_summaries from app.models.meteo import MeteoStation from app.database import engine from sqlmodel import Session, select today = date.today() start_date = today - timedelta(days=max_days_back) # 1. Dates « veille » déjà présentes en BDD with Session(engine) as session: rows = session.exec( select(MeteoStation.date_heure).where(MeteoStation.type == "veille") ).all() existing_dates: set[str] = {dh[:10] for dh in rows} # 2. Dates manquantes entre start_date et hier (aujourd'hui exclu) missing: list[date] = [] cursor = start_date while cursor < today: if cursor.isoformat() not in existing_dates: missing.append(cursor) cursor += timedelta(days=1) if not missing: logger.info("Backfill station : aucune date manquante") return logger.info(f"Backfill station : {len(missing)} date(s) manquante(s) à récupérer") # 3. Grouper par (année, mois) → 1 requête HTTP par mois def month_key(d: date) -> tuple[int, int]: return (d.year, d.month) filled = 0 for (year, month), group_iter in groupby(sorted(missing), key=month_key): month_data = fetch_month_summaries(year, month) if not month_data: logger.debug(f"Backfill station : pas de données NOAA pour {year}-{month:02d}") continue with Session(engine) as session: for d in group_iter: data = month_data.get(d.day) if not data: continue date_heure = f"{d.isoformat()}T00:00" if not session.get(MeteoStation, date_heure): session.add(MeteoStation(date_heure=date_heure, type="veille", **data)) filled += 1 session.commit() logger.info(f"Backfill station terminé : {filled} date(s) insérée(s)") def setup_scheduler() -> None: """Configure et démarre le scheduler.""" scheduler.add_job( _store_station_current, "interval", hours=1, next_run_time=datetime.now(), id="station_current", replace_existing=True, ) scheduler.add_job( _store_station_veille, "cron", hour=6, minute=0, next_run_time=datetime.now(), id="station_veille", replace_existing=True, ) scheduler.add_job( _store_open_meteo, "interval", hours=1, next_run_time=datetime.now(), id="open_meteo", replace_existing=True, ) scheduler.start() logger.info("Scheduler météo démarré (3 jobs)")