Files
college-infos/pronote_sync/models/diff.py
Antoine Van Elstraete 5907c9aeaf fix(M8): corrections d'audit FIXME_M8 — appariement, date, déterminisme, validateur
Quatre corrections bloquantes/majeures de l'audit FIXME_M8 :
- Appariement un-à-un déterministe (consommation du candidat sélectionné) ;
  1 réel / 2 théoriques → 1 REMOVED, 2 réels / 1 théorique → 1 ADDED.
- Filtrage strict par date : les cours réels hors target_date sont exclus
  du matching avec un warning logé (décision architecte : pas d'exception).
- Déterminisme des détails : formatage via sorted(set(...)) au lieu de
  set(...) brut, indépendant de PYTHONHASHSEED.
- Validateur AgendaChange strict : ADDED = lesson seule, REMOVED =
  theoretical_lesson seule, MODIFIED = les deux requis.
- Comparaison à la minute près dans _is_modified (cohérent avec _matches).
- Documentation §8.4/§8.5 alignée avec l'implémentation (tolérance 15 min,
  API compare(), normalize_subject référencé, appariement consommé).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 15:59:17 +02:00

71 lines
2.9 KiB
Python

"""Modèles de données pour la comparaison d'agenda (diff)."""
from __future__ import annotations
from datetime import date
from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pronote_sync.models.agenda import Lesson, TheoreticalLesson
class AgendaChangeType(StrEnum):
"""Type de changement dans l'agenda."""
ADDED = "added"
REMOVED = "removed"
MODIFIED = "modified"
class AgendaChange(BaseModel):
"""Représente un changement entre l'agenda réel et l'agenda théorique."""
model_config = ConfigDict(frozen=True)
type: AgendaChangeType = Field(..., description="Type de changement")
lesson: Lesson | None = Field(None, description="Cours concerné (pour ADDED/MODIFIED)")
theoretical_lesson: TheoreticalLesson | None = Field(
None, description="Cours théorique concerné (pour REMOVED/MODIFIED)"
)
details: str = Field(default="", description="Détails du changement")
@model_validator(mode="after")
def _validate_payload_consistency(self) -> AgendaChange:
"""Valide la cohérence entre le type de changement et le payload.
Applique la matrice stricte de payload :
- ``ADDED`` : ``lesson`` requis et ``theoretical_lesson`` doit être ``None``.
- ``REMOVED`` : ``theoretical_lesson`` requis et ``lesson`` doit être ``None``.
- ``MODIFIED`` : ``lesson`` et ``theoretical_lesson`` tous deux requis.
:return: L'instance validée.
:rtype: AgendaChange
:raises ValueError: Si le payload ne correspond pas au type de changement.
"""
if self.type == AgendaChangeType.ADDED:
if self.lesson is None:
raise ValueError(f"lesson est requis pour le type {self.type!r}")
if self.theoretical_lesson is not None:
raise ValueError(f"theoretical_lesson doit être None pour le type {self.type!r}")
elif self.type == AgendaChangeType.REMOVED:
if self.theoretical_lesson is None:
raise ValueError(f"theoretical_lesson est requis pour le type {self.type!r}")
if self.lesson is not None:
raise ValueError(f"lesson doit être None pour le type {self.type!r}")
elif self.type == AgendaChangeType.MODIFIED:
if self.lesson is None:
raise ValueError(f"lesson est requis pour le type {self.type!r}")
if self.theoretical_lesson is None:
raise ValueError(f"theoretical_lesson est requis pour le type {self.type!r}")
return self
class AgendaDiff(BaseModel):
"""Représente les différences entre l'agenda réel et l'agenda théorique."""
model_config = ConfigDict(frozen=True)
target_date: date = Field(..., description="Date cible de la comparaison")
changes: tuple[AgendaChange, ...] = Field(default=(), description="Liste des changements")