Implémentation complète de la source d'agenda théorique : - model.py : modèles Pydantic de parsing JSON (TheoreticalLessonEntry, TheoreticalAgendaFile) avec validation des formats d'heure et de l'ordre début/fin. - parity.py : WeekParityService déterministe calculant la parité d'une semaine (paire/impaire) à partir d'une date de référence. - holidays.py : SchoolHolidayCalendar lisant un fichier JSON de vacances scolaires (zone A) avec bornes inclusives. - provider.py : protocole TheoreticalAgendaProvider (get_lessons, get_lessons_for_range). - file.py : JsonTheoreticalAgendaProvider implémentant le protocole : filtrage par parité et vacances, génération d'IDs déterministes incluant le type de semaine, validation de l'unicité des IDs, tri stable par identifiant. - __init__.py : factory get_theoretical_provider câblant la configuration (None si désactivé, erreur si config de parité partielle). - Fixtures : theoretical.json (9 leçons all/even/odd) et school_holidays.json (zone A, 4 périodes). - 57 tests unitaires couvrant parsing, parité, vacances, provider, factory, déduplication de range, collisions d'IDs. - Guide : §8 et §12 alignés avec le format JSON. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
"""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)
|