Files
jardin/backend/app/services/scheduler.py
2026-03-01 07:21:46 +01:00

173 lines
5.9 KiB
Python

"""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)")