Files
college-infos/pronote_sync/models/agenda.py
Codex d45d38d365 fix(ical): fiabiliser le parsing Pronote 2026
Revue Codex validée. Corrections de parsing iCal, conservation du contexte des devoirs et fixtures anonymisées.

Co-authored-by: Codex <codex@antoineve.me>
Co-committed-by: Codex <codex@antoineve.me>
2026-09-12 14:54:55 +02:00

132 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)")
class_part: str | None = None
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