"""Service de calendrier des vacances scolaires. Ce module fournit les modèles de données :class:`HolidayPeriod` et :class:`SchoolHolidayFile`, ainsi que le service :class:`SchoolHolidayCalendar` qui charge un fichier JSON de périodes de vacances scolaires et permet de déterminer si une date donnée tombe pendant ces vacances. """ from __future__ import annotations import json import logging from datetime import date from pathlib import Path from typing import Any, Self from pydantic import BaseModel, ConfigDict, Field, model_validator from pronote_sync.errors import PronoteSyncError from pronote_sync.utils.redaction import redact_exception, redact_secrets logger = logging.getLogger(__name__) class HolidayPeriod(BaseModel): """Période de vacances scolaires, bornes incluses. :ivar start_date: Date de début de la période (incluse). :ivar end_date: Date de fin de la période (incluse). :ivar label: Nom de la période (ex. « Toussaint »). """ model_config = ConfigDict(frozen=True) start_date: date end_date: date label: str @model_validator(mode="after") def _validate_date_order(self) -> Self: """Vérifie que la date de fin n'est pas antérieure à la date de début. :return: L'instance de période après validation. :rtype: Self :raises ValueError: Si ``end_date`` est strictement antérieure à ``start_date``. """ if self.end_date < self.start_date: raise ValueError("end_date doit être supérieure ou égale à start_date.") return self class SchoolHolidayFile(BaseModel): """Modèle de parsing d'un fichier JSON de vacances scolaires. :ivar zone: Zone académique (ex. « A »). :ivar school_year: Année scolaire (ex. « 2026-2027 »). :ivar periods: Périodes de vacances scolaires du fichier. """ zone: str school_year: str periods: tuple[HolidayPeriod, ...] = Field(default=()) class SchoolHolidayCalendar: """Calendrier des vacances scolaires chargé depuis un fichier JSON.""" def __init__(self, file_path: Path | str) -> None: """Charge les périodes de vacances scolaires depuis un fichier JSON. :param file_path: Chemin vers le fichier JSON. :raises PronoteSyncError: Si le fichier ne peut être lu ou analysé. """ path = Path(file_path) self._periods: tuple[HolidayPeriod, ...] if not path.is_file(): raise PronoteSyncError( f"Le fichier de vacances scolaires est introuvable : {redact_secrets(str(path))}" ) from None try: data: Any = json.loads(path.read_text(encoding="utf-8")) file_model: SchoolHolidayFile = SchoolHolidayFile.model_validate(data) except Exception as exc: logger.error( "Fichier de vacances scolaires invalide %s : %s.", redact_secrets(str(path)), redact_exception(exc), ) raise PronoteSyncError( f"Le fichier de vacances scolaires est invalide : {redact_secrets(str(path))}" ) from None self._periods = file_model.periods def is_holiday(self, target_date: date) -> bool: """Vérifie si la date donnée tombe pendant une période de vacances. La date est considérée comme étant en vacances si elle appartient à l'intervalle d'au moins une période, bornes incluses (``start_date <= target_date <= end_date``). :param target_date: Date à vérifier. :return: ``True`` si la date tombe pendant les vacances scolaires, ``False`` sinon. :rtype: bool """ return any(period.start_date <= target_date <= period.end_date for period in self._periods)