feat(M6): agenda théorique JSON avec parité des semaines et vacances scolaires
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>
This commit is contained in:
55
pronote_sync/sources/theoretical/parity.py
Normal file
55
pronote_sync/sources/theoretical/parity.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Service déterministe de calcul de la parité des semaines pour l'agenda théorique.
|
||||
|
||||
Ce module fournit :class:`WeekParityService`, un service sans état qui détermine
|
||||
si la semaine contenant une date donnée est paire ou impaire, à partir d'une
|
||||
date d'ancrage dont la parité est connue. L'algorithme repose sur le décalage
|
||||
entre les lundis des deux semaines, et non sur les numéros de semaine ISO.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class WeekParityService:
|
||||
"""Service déterministe de calcul de la parité des semaines.
|
||||
|
||||
La parité d'une semaine est déduite d'une date d'ancrage fournie à la
|
||||
construction : la semaine contenant cette date a une parité connue
|
||||
(paire ou impaire). Le service est immuable après construction et ne
|
||||
dépend d'aucun état global ni de l'horloge système.
|
||||
"""
|
||||
|
||||
def __init__(self, anchor_date: date, anchor_type: Literal["even", "odd"]) -> None:
|
||||
"""Initialise le service avec la date d'ancrage et sa parité.
|
||||
|
||||
:param anchor_date: Date de référence dont la semaine a une parité connue.
|
||||
:param anchor_type: Parité de la semaine d'ancrage (``"even"`` ou ``"odd"``).
|
||||
"""
|
||||
self._anchor_monday = anchor_date - timedelta(days=anchor_date.weekday())
|
||||
self._anchor_type = anchor_type
|
||||
|
||||
def parity_for(self, target_date: date) -> Literal["even", "odd"]:
|
||||
"""Détermine la parité de la semaine contenant la date cible.
|
||||
|
||||
Algorithme :
|
||||
1. Calculer le lundi de la semaine de la date cible.
|
||||
2. Utiliser le lundi de la semaine de la date d'ancrage (stocké à
|
||||
l'initialisation).
|
||||
3. Calculer le nombre de semaines entre les deux lundis :
|
||||
``(target_monday - anchor_monday).days // 7``.
|
||||
4. Si le décalage de semaines est pair, la cible a la même parité que
|
||||
l'ancrage.
|
||||
5. Si le décalage de semaines est impair, la cible a la parité opposée.
|
||||
|
||||
:param target_date: Date dont il faut déterminer la parité.
|
||||
:return: ``"even"`` ou ``"odd"`` selon la parité de la semaine cible.
|
||||
:rtype: Literal["even", "odd"]
|
||||
"""
|
||||
target_monday = target_date - timedelta(days=target_date.weekday())
|
||||
week_offset = (target_monday - self._anchor_monday).days // 7
|
||||
if week_offset % 2 == 0:
|
||||
return self._anchor_type
|
||||
# Inverse la parité.
|
||||
return "odd" if self._anchor_type == "even" else "even"
|
||||
Reference in New Issue
Block a user