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>
71 lines
2.9 KiB
Python
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")
|