IT-Stock/app/scheduler.py
maxsoch a788bf4392 Résumé de stock dans les alertes + email récapitulatif planifié
L'alerte immédiate de stock bas inclut maintenant un résumé de tous les
articles actuellement sous leur seuil, pas seulement celui qui l'a
déclenchée. Ajout d'un second email, planifié via une expression cron
(RESUME_STOCK_CRON, APScheduler intégré à l'app — pas de cron système à
configurer) : stock bas en tête, puis le reste du stock groupé par
catégorie. scripts/envoyer_resume_stock.py permet de le déclencher
manuellement, ou de brancher l'envoi sur un cron système à la place.
2026-07-14 19:28:39 +02:00

51 lines
2 KiB
Python

"""
Planification de l'email récapitulatif de stock (voir
email_alerts.envoyer_resume_stock).
Choix : un planificateur qui tourne dans le process FastAPI lui-même
(APScheduler), configuré par une expression cron classique (RESUME_STOCK_CRON
dans .env, ex: "0 8 * * *" = tous les jours à 8h). Rien à configurer côté
système d'exploitation du serveur — tout reste dans ce dossier, comme le
reste du projet.
Limite à connaître : la tâche ne se déclenche que si le process est en
cours d'exécution au moment prévu, sans rattrapage si le serveur était
arrêté à ce moment-là. Pour un outil interne redémarré rarement, c'est un
compromis raisonnable face à la complexité d'un vrai cron système. Si ça
devient un problème, scripts/envoyer_resume_stock.py peut être branché
sur un cron/Planificateur de tâches Windows à la place.
"""
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from sqlmodel import Session
from app.config import settings
from app.database import engine
from app.email_alerts import envoyer_resume_stock
_scheduler = BackgroundScheduler()
def _tache_resume_stock() -> None:
"""Ouvre sa propre session DB : cette fonction est appelée par le
thread du planificateur, en dehors du cycle requête/réponse FastAPI où
la session habituelle (get_session) n'existe pas."""
with Session(engine) as session:
envoyer_resume_stock(session)
def demarrer_planification() -> None:
"""Démarre la tâche planifiée. Appelé une fois au démarrage de l'app
(voir lifespan dans main.py)."""
trigger = CronTrigger.from_crontab(settings.resume_stock_cron)
_scheduler.add_job(_tache_resume_stock, trigger, id="resume_stock", replace_existing=True)
if not _scheduler.running:
_scheduler.start()
def arreter_planification() -> None:
"""Arrête proprement le planificateur (appelé à l'extinction de l'app)."""
if _scheduler.running:
_scheduler.shutdown(wait=False)