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:
2026-09-06 23:06:29 +02:00
parent 4ec827a945
commit f9a1a5aa43
15 changed files with 1992 additions and 19 deletions

View File

@@ -0,0 +1,116 @@
"""Tests unitaires pour le service de parité des semaines.
Ce module contient les tests pour la classe :class:`WeekParityService`
du module :mod:`pronote_sync.sources.theoretical.parity`.
"""
from __future__ import annotations
from datetime import date
from pronote_sync.sources.theoretical.parity import WeekParityService
class TestWeekParityService:
"""Tests pour la classe WeekParityService."""
def test_same_week_as_anchor_even(self) -> None:
"""Teste qu'une date dans la même semaine que l'ancrage pair retourne "even".
:assert: parity_for retourne "even" pour une date dans la même semaine.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# Même semaine (lundi 31 août 2026)
target_date = date(2026, 9, 1)
assert service.parity_for(target_date) == "even"
def test_one_week_after_anchor_even(self) -> None:
"""Teste qu'une date une semaine après l'ancrage pair retourne "odd".
:assert: parity_for retourne "odd" pour une date une semaine après.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# Une semaine après (mardi 8 septembre 2026)
target_date = date(2026, 9, 8)
assert service.parity_for(target_date) == "odd"
def test_two_weeks_after_anchor_even(self) -> None:
"""Teste qu'une date deux semaines après l'ancrage pair retourne "even".
:assert: parity_for retourne "even" pour une date deux semaines après.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# Deux semaines après (mardi 15 septembre 2026)
target_date = date(2026, 9, 15)
assert service.parity_for(target_date) == "even"
def test_same_week_as_anchor_odd(self) -> None:
"""Teste qu'une date dans la même semaine que l'ancrage impair retourne "odd".
:assert: parity_for retourne "odd" pour une date dans la même semaine.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "odd")
target_date = date(2026, 9, 1)
assert service.parity_for(target_date) == "odd"
def test_one_week_before_anchor(self) -> None:
"""Teste qu'une date une semaine avant l'ancrage retourne la parité opposée.
:assert: parity_for retourne la parité opposée pour une date une semaine avant.
"""
anchor_date = date(2026, 9, 8) # Mardi
service = WeekParityService(anchor_date, "even")
# Une semaine avant (mardi 1er septembre 2026)
target_date = date(2026, 9, 1)
assert service.parity_for(target_date) == "odd"
def test_many_weeks_later(self) -> None:
"""Teste qu'une date 10 semaines après l'ancrage pair retourne "even".
:assert: parity_for retourne "even" pour une date 10 semaines après.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# 10 semaines après (70 jours)
target_date = date(2026, 11, 10)
assert service.parity_for(target_date) == "even"
def test_negative_offset(self) -> None:
"""Teste qu'une date loin avant l'ancrage calcule correctement la parité.
:assert: parity_for retourne la parité correcte pour une date loin dans le passé.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# 10 semaines avant (70 jours)
target_date = date(2026, 6, 23)
assert service.parity_for(target_date) == "even"
def test_different_day_in_same_week(self) -> None:
"""Teste que lundi et vendredi de la même semaine ont la même parité.
:assert: parity_for retourne la même parité pour lundi et vendredi de la même semaine.
"""
anchor_date = date(2026, 9, 1) # Mardi
service = WeekParityService(anchor_date, "even")
# Lundi de la même semaine
monday = date(2026, 8, 31)
# Vendredi de la même semaine
friday = date(2026, 9, 4)
assert service.parity_for(monday) == "even"
assert service.parity_for(friday) == "even"
def test_crosses_year_boundary(self) -> None:
"""Teste que le calcul de parité fonctionne à cheval sur une année.
:assert: parity_for retourne la parité correcte à cheval sur une année.
"""
anchor_date = date(2026, 12, 29) # Mardi (semaine 53)
service = WeekParityService(anchor_date, "even")
# Date dans la semaine suivante (année 2027)
target_date = date(2027, 1, 5) # Mardi de la semaine suivante
assert service.parity_for(target_date) == "odd"