Immuabilité : - Collections des modèles frozen : list → tuple (Lesson, TheoreticalLesson, SchoolEvent, AgendaDiff, Homework, XmppMessage, ExternalInfo) - Gel de HomeworkBlock et ExternalInfo (sous-modèles imbriqués dans contrats) - Valeurs par défaut immuables : Field(default=(), ...) Validation de domaine : - TheoreticalLesson.day_of_week : Field(ge=0, le=6) - Lesson/TheoreticalLesson : start < end (validateur de modèle) - SchoolEvent : from_date <= to_date - AgendaChange : cohérence type/payload (ADDED/MODIFIED → lesson, REMOVED → theoretical_lesson) - CalDAVSyncResult : compteurs Field(ge=0) sur added/updated/removed Tests : 66 tests (construction/sérialisation, immuabilité, invariants) couvrant le gel, la mutabilité légitime, et les cas invalides par invariant. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Modèles de données pour l'agenda Pronote (cours, événements, agenda théorique)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date as _date
|
|
from datetime import datetime
|
|
from datetime import time as _time
|
|
from enum import StrEnum
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
|
|
class Status(StrEnum):
|
|
"""Statut générique pour les événements."""
|
|
|
|
NORMAL = "normal"
|
|
CANCELLED = "cancelled"
|
|
MOVED = "moved"
|
|
|
|
|
|
class LessonStatus(StrEnum):
|
|
"""Statut d'un cours."""
|
|
|
|
NORMAL = "normal"
|
|
CANCELLED = "cancelled"
|
|
MOVED = "moved"
|
|
|
|
|
|
class HomeworkBlock(BaseModel):
|
|
"""Représente un bloc de devoir extrait de la description d'un cours."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
kind: Literal["due", "assigned"] = Field(
|
|
..., description="Type de bloc (échéance ou attribution)"
|
|
)
|
|
date: _date = Field(..., description="Date associée au bloc")
|
|
text: str = Field(..., description="Texte du devoir (brut)")
|
|
html: str = Field(default="", description="Texte du devoir (HTML)")
|
|
|
|
|
|
class Lesson(BaseModel):
|
|
"""Représente un cours dans l'agenda Pronote."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
id: str = Field(..., description="UID stable du cours (normalisé)")
|
|
start: datetime = Field(..., description="Date/heure de début")
|
|
end: datetime = Field(..., description="Date/heure de fin")
|
|
subject: str = Field(..., description="Matière")
|
|
teachers: tuple[str, ...] = Field(default=(), description="Liste des professeurs")
|
|
rooms: tuple[str, ...] = Field(default=(), description="Liste des salles")
|
|
group: str | None = Field(None, description="Groupe (ex: Classe entière)")
|
|
status: LessonStatus = Field(default=LessonStatus.NORMAL, description="Statut du cours")
|
|
content: str | None = Field(None, description="Contenu pédagogique")
|
|
homework_blocks: tuple[HomeworkBlock, ...] = Field(
|
|
default=(),
|
|
description="Blocs de devoirs extraits de la description",
|
|
)
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_time_order(self) -> Lesson:
|
|
"""Valide que l'heure de fin est postérieure à l'heure de début.
|
|
|
|
:return: L'instance validée.
|
|
:rtype: Lesson
|
|
:raises ValueError: Si ``end`` n'est pas postérieur à ``start``.
|
|
"""
|
|
if self.end <= self.start:
|
|
raise ValueError("end doit être postérieur à start")
|
|
return self
|
|
|
|
|
|
class SchoolEventKind(StrEnum):
|
|
"""Type d'événement scolaire."""
|
|
|
|
HOLIDAY = "holiday"
|
|
PUBLIC_HOLIDAY = "public_holiday"
|
|
|
|
|
|
class SchoolEvent(BaseModel):
|
|
"""Représente un événement scolaire (vacances, jours fériés)."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
kind: SchoolEventKind = Field(..., description="Type d'événement")
|
|
label: str = Field(..., description="Libellé (ex: Vacances de Noël)")
|
|
from_date: _date = Field(..., description="Date de début (inclusive)")
|
|
to_date: _date = Field(..., description="Date de fin (exclusive)")
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_date_order(self) -> SchoolEvent:
|
|
"""Valide que la date de fin est postérieure ou égale à la date de début.
|
|
|
|
:return: L'instance validée.
|
|
:rtype: SchoolEvent
|
|
:raises ValueError: Si ``to_date`` est antérieur à ``from_date``.
|
|
"""
|
|
if self.to_date < self.from_date:
|
|
raise ValueError("to_date doit être postérieur ou égal à from_date")
|
|
return self
|
|
|
|
|
|
class TheoreticalLesson(BaseModel):
|
|
"""Représente un cours dans l'agenda théorique."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
id: str = Field(..., description="Identifiant unique")
|
|
day_of_week: int = Field(
|
|
..., ge=0, le=6, description="Jour de la semaine (0=lundi, 6=dimanche)"
|
|
)
|
|
start_time: _time = Field(..., description="Heure de début")
|
|
end_time: _time = Field(..., description="Heure de fin")
|
|
subject: str = Field(..., description="Matière")
|
|
teachers: tuple[str, ...] = Field(default=(), description="Liste des professeurs")
|
|
rooms: tuple[str, ...] = Field(default=(), description="Liste des salles")
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_time_order(self) -> TheoreticalLesson:
|
|
"""Valide que l'heure de fin est postérieure à l'heure de début.
|
|
|
|
:return: L'instance validée.
|
|
:rtype: TheoreticalLesson
|
|
:raises ValueError: Si ``end_time`` n'est pas postérieur à ``start_time``.
|
|
"""
|
|
if self.end_time <= self.start_time:
|
|
raise ValueError("end_time doit être postérieur à start_time")
|
|
return self
|