feat(M3): modèles de données Pydantic (22 modèles, 10 modules)
Modèles créés : - models/agenda.py : Status, LessonStatus, HomeworkBlock, Lesson (frozen), SchoolEventKind, SchoolEvent (frozen), TheoreticalLesson (frozen) - models/homework.py : Homework (frozen) - models/message.py : MessageType, Message (frozen) - models/blog.py : BlogArticle (frozen), ExternalInfo (mutable) - models/diff.py : AgendaChangeType, AgendaChange (frozen), AgendaDiff (frozen) - models/pronote.py : PronoteData (mutable) - models/sync.py : CalDAVSyncStatus, CalDAVSyncPlan, CalDAVSyncResult (mutable) - models/synthesis.py : SynthesisInput, SynthesisResult (mutable) - models/xmpp.py : XmppMessage (frozen, external_info: ExternalInfo | None) - models/__init__.py : ré-export des 22 modèles via __all__ Conventions appliquées : - Pydantic v2 : model_config = ConfigDict(frozen=True), pas de json_encoders - StrEnum au lieu de (str, Enum) (ruff UP042) - Alias _date/_time pour éviter collision champ date/type date - Sphinx/reST docstrings sur toutes les classes - TODO.md : 11 items M3 cochés Validations (@verifier) : - ruff check : PASS (0 error) - ruff format --check : PASS (10 fichiers) - mypy strict : PASS (10 fichiers) - bandit : PASS (0 issue) - import 22 modèles : OK - sérialisation JSON (frozen + mutable) : OK - frozen immuable, PronoteData/CalDAVSyncResult mutable : OK - StrEnum isinstance str : OK - pytest : 0 test (infrastructure OK) Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
This commit is contained in:
22
TODO.md
22
TODO.md
@@ -50,17 +50,17 @@ Implémenter la configuration Pydantic Settings, le masquage des secrets et la j
|
||||
|
||||
Définir tous les modèles de domaine, immuables pour les contrats, mutables pour les résultats de travail.
|
||||
|
||||
- [ ] Créer `models/agenda.py` : `Status`, `LessonStatus`, `HomeworkBlock`, `Lesson` (frozen), `SchoolEventKind`, `SchoolEvent`, `TheoreticalLesson`.
|
||||
- [ ] Créer `models/homework.py` : `Homework` (frozen, `id` = hachage stable).
|
||||
- [ ] Créer `models/message.py` : `MessageType`, `Message` (frozen).
|
||||
- [ ] Créer `models/diff.py` : `AgendaChangeType`, `AgendaChange` (frozen), `AgendaDiff` (frozen).
|
||||
- [ ] Créer `models/pronote.py` : `PronoteData` (mutable).
|
||||
- [ ] Créer `models/sync.py` : `CalDAVSyncStatus`, `CalDAVSyncPlan`, `CalDAVSyncResult` (mutable).
|
||||
- [ ] Créer `models/synthesis.py` : `SynthesisInput`, `SynthesisResult`.
|
||||
- [ ] Créer `models/blog.py` : `BlogArticle` (frozen), `ExternalInfo` (§5 bis.5/6).
|
||||
- [ ] Créer `models/xmpp.py` : `XmppMessage` (frozen) intégrant `external_info`.
|
||||
- [ ] Créer `models/__init__.py` ré-exportant tous les modèles.
|
||||
- [ ] Valider la sérialisation JSON (datetime/date en ISO) pour chaque modèle.
|
||||
- [x] Créer `models/agenda.py` : `Status`, `LessonStatus`, `HomeworkBlock`, `Lesson` (frozen), `SchoolEventKind`, `SchoolEvent`, `TheoreticalLesson`.
|
||||
- [x] Créer `models/homework.py` : `Homework` (frozen, `id` = hachage stable).
|
||||
- [x] Créer `models/message.py` : `MessageType`, `Message` (frozen).
|
||||
- [x] Créer `models/diff.py` : `AgendaChangeType`, `AgendaChange` (frozen), `AgendaDiff` (frozen).
|
||||
- [x] Créer `models/pronote.py` : `PronoteData` (mutable).
|
||||
- [x] Créer `models/sync.py` : `CalDAVSyncStatus`, `CalDAVSyncPlan`, `CalDAVSyncResult` (mutable).
|
||||
- [x] Créer `models/synthesis.py` : `SynthesisInput`, `SynthesisResult`.
|
||||
- [x] Créer `models/blog.py` : `BlogArticle` (frozen), `ExternalInfo` (§5 bis.5/6).
|
||||
- [x] Créer `models/xmpp.py` : `XmppMessage` (frozen) intégrant `external_info`.
|
||||
- [x] Créer `models/__init__.py` ré-exportant tous les modèles.
|
||||
- [x] Valider la sérialisation JSON (datetime/date en ISO) pour chaque modèle.
|
||||
|
||||
### Critères d'acceptation
|
||||
- Chaque modèle s'instancie et se sérialise en JSON valide.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Modèles de données Pydantic pour le pipeline pronote-sync."""
|
||||
|
||||
from pronote_sync.models.agenda import (
|
||||
HomeworkBlock,
|
||||
Lesson,
|
||||
LessonStatus,
|
||||
SchoolEvent,
|
||||
SchoolEventKind,
|
||||
Status,
|
||||
TheoreticalLesson,
|
||||
)
|
||||
from pronote_sync.models.blog import BlogArticle, ExternalInfo
|
||||
from pronote_sync.models.diff import AgendaChange, AgendaChangeType, AgendaDiff
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.message import Message, MessageType
|
||||
from pronote_sync.models.pronote import PronoteData
|
||||
from pronote_sync.models.sync import CalDAVSyncPlan, CalDAVSyncResult, CalDAVSyncStatus
|
||||
from pronote_sync.models.synthesis import SynthesisInput, SynthesisResult
|
||||
from pronote_sync.models.xmpp import XmppMessage
|
||||
|
||||
__all__ = [
|
||||
"AgendaChange",
|
||||
"AgendaChangeType",
|
||||
"AgendaDiff",
|
||||
"BlogArticle",
|
||||
"CalDAVSyncPlan",
|
||||
"CalDAVSyncResult",
|
||||
"CalDAVSyncStatus",
|
||||
"ExternalInfo",
|
||||
"Homework",
|
||||
"HomeworkBlock",
|
||||
"Lesson",
|
||||
"LessonStatus",
|
||||
"Message",
|
||||
"MessageType",
|
||||
"PronoteData",
|
||||
"SchoolEvent",
|
||||
"SchoolEventKind",
|
||||
"Status",
|
||||
"SynthesisInput",
|
||||
"SynthesisResult",
|
||||
"TheoreticalLesson",
|
||||
"XmppMessage",
|
||||
]
|
||||
|
||||
90
pronote_sync/models/agenda.py
Normal file
90
pronote_sync/models/agenda.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""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
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
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: list[str] = Field(default_factory=list, description="Liste des professeurs")
|
||||
rooms: list[str] = Field(default_factory=list, 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: list[HomeworkBlock] = Field(
|
||||
default_factory=list,
|
||||
description="Blocs de devoirs extraits de la description",
|
||||
)
|
||||
|
||||
|
||||
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)")
|
||||
|
||||
|
||||
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(..., 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: list[str] = Field(default_factory=list, description="Liste des professeurs")
|
||||
rooms: list[str] = Field(default_factory=list, description="Liste des salles")
|
||||
42
pronote_sync/models/blog.py
Normal file
42
pronote_sync/models/blog.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Modèles de données pour le blog du collège (RSS) et les informations externes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from pronote_sync.models.message import Message
|
||||
|
||||
|
||||
class BlogArticle(BaseModel):
|
||||
"""Représente un article du blog du collège."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(..., description="GUID de l'article (clé de déduplication)")
|
||||
title: str = Field(..., description="Titre de l'article")
|
||||
url: str = Field(..., description="URL canonique de l'article")
|
||||
published_at: datetime = Field(..., description="Date de publication (UTC)")
|
||||
updated_at: datetime | None = Field(None, description="Date de dernière mise à jour")
|
||||
category: str | None = Field(None, description="Catégorie de l'article")
|
||||
author: str | None = Field(None, description="Auteur (si disponible)")
|
||||
content_html: str = Field(..., description="Contenu HTML complet")
|
||||
content_text: str = Field(..., description="Contenu en texte brut (pour XMPP)")
|
||||
|
||||
|
||||
class ExternalInfo(BaseModel):
|
||||
"""Agrège les informations externes (blog, messages Pronote) pour le message XMPP."""
|
||||
|
||||
blog_articles: list[BlogArticle] = Field(
|
||||
default_factory=list,
|
||||
description="Liste des nouveaux articles du blog",
|
||||
)
|
||||
pronote_messages: list[Message] = Field(
|
||||
default_factory=list,
|
||||
description="Liste des messages Pronote",
|
||||
)
|
||||
other_info: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Autres informations (extensible)",
|
||||
)
|
||||
40
pronote_sync/models/diff.py
Normal file
40
pronote_sync/models/diff.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""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
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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: list[AgendaChange] = Field(default_factory=list, description="Liste des changements")
|
||||
21
pronote_sync/models/homework.py
Normal file
21
pronote_sync/models/homework.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Modèle de données pour les devoirs Pronote."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class Homework(BaseModel):
|
||||
"""Représente un devoir Pronote."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(..., description="ID stable (hachage)")
|
||||
subject: str = Field(..., description="Matière")
|
||||
teachers: list[str] = Field(default_factory=list, description="Liste des professeurs")
|
||||
assigned_on: date | None = Field(None, description="Date de distribution")
|
||||
due_on: date = Field(..., description="Date d'échéance")
|
||||
text: str = Field(..., description="Texte du devoir (brut)")
|
||||
html: str = Field(default="", description="Texte du devoir (HTML)")
|
||||
30
pronote_sync/models/message.py
Normal file
30
pronote_sync/models/message.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Modèle de données pour les messages Pronote."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class MessageType(StrEnum):
|
||||
"""Type de message Pronote."""
|
||||
|
||||
DISCUSSION = "discussion"
|
||||
INFORMATION = "information"
|
||||
SURVEY = "survey"
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""Représente un message ou une information Pronote."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(..., description="Identifiant unique")
|
||||
type: MessageType = Field(..., description="Type de message")
|
||||
title: str = Field(..., description="Titre")
|
||||
content: str = Field(..., description="Contenu")
|
||||
author: str = Field(..., description="Auteur")
|
||||
date: datetime = Field(..., description="Date de création")
|
||||
read: bool = Field(False, description="Lu ou non")
|
||||
26
pronote_sync/models/pronote.py
Normal file
26
pronote_sync/models/pronote.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Modèle de données normalisées récupérées depuis Pronote."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pronote_sync.models.agenda import Lesson, SchoolEvent
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.message import Message
|
||||
|
||||
|
||||
class PronoteData(BaseModel):
|
||||
"""Données normalisées récupérées depuis Pronote."""
|
||||
|
||||
lessons: list[Lesson] = Field(default_factory=list, description="Liste des cours")
|
||||
homeworks: list[Homework] = Field(default_factory=list, description="Liste des devoirs")
|
||||
school_events: list[SchoolEvent] = Field(
|
||||
default_factory=list, description="Liste des événements scolaires"
|
||||
)
|
||||
messages: list[Message] = Field(
|
||||
default_factory=list, description="Liste des messages/informations"
|
||||
)
|
||||
target_date: date = Field(..., description="Date cible (J+1 ou prochain jour scolaire)")
|
||||
generated_at: datetime = Field(..., description="Date/heure de génération")
|
||||
42
pronote_sync/models/sync.py
Normal file
42
pronote_sync/models/sync.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Modèles de données pour la synchronisation CalDAV."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pronote_sync.models.agenda import Lesson, SchoolEvent
|
||||
from pronote_sync.models.homework import Homework
|
||||
|
||||
|
||||
class CalDAVSyncStatus(StrEnum):
|
||||
"""Statut de synchronisation CalDAV."""
|
||||
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
class CalDAVSyncPlan(BaseModel):
|
||||
"""Plan de synchronisation CalDAV."""
|
||||
|
||||
lessons_to_add: list[Lesson] = Field(default_factory=list)
|
||||
lessons_to_update: list[Lesson] = Field(default_factory=list)
|
||||
lessons_to_remove: list[str] = Field(default_factory=list, description="Liste d'UID")
|
||||
homeworks_to_add: list[Homework] = Field(default_factory=list)
|
||||
homeworks_to_update: list[Homework] = Field(default_factory=list)
|
||||
homeworks_to_remove: list[str] = Field(default_factory=list, description="Liste d'UID")
|
||||
school_events_to_add: list[SchoolEvent] = Field(default_factory=list)
|
||||
school_events_to_update: list[SchoolEvent] = Field(default_factory=list)
|
||||
school_events_to_remove: list[str] = Field(default_factory=list, description="Liste d'UID")
|
||||
|
||||
|
||||
class CalDAVSyncResult(BaseModel):
|
||||
"""Résultat d'une synchronisation CalDAV (mutable)."""
|
||||
|
||||
status: CalDAVSyncStatus = Field(..., description="Statut global")
|
||||
added: int = Field(0, description="Nombre d'événements ajoutés")
|
||||
updated: int = Field(0, description="Nombre d'événements mis à jour")
|
||||
removed: int = Field(0, description="Nombre d'événements supprimés")
|
||||
errors: list[str] = Field(default_factory=list, description="Liste des erreurs")
|
||||
28
pronote_sync/models/synthesis.py
Normal file
28
pronote_sync/models/synthesis.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Modèles de données pour la synthèse IA."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pronote_sync.models.agenda import SchoolEvent
|
||||
from pronote_sync.models.diff import AgendaDiff
|
||||
from pronote_sync.models.message import Message
|
||||
|
||||
|
||||
class SynthesisInput(BaseModel):
|
||||
"""Entrée pour la synthèse IA."""
|
||||
|
||||
agenda_diff: AgendaDiff | None = Field(None, description="Différences d'agenda")
|
||||
messages: list[Message] = Field(default_factory=list, description="Messages importants")
|
||||
school_events: list[SchoolEvent] = Field(
|
||||
default_factory=list, description="Événements scolaires"
|
||||
)
|
||||
target_date: date = Field(..., description="Date cible")
|
||||
|
||||
|
||||
class SynthesisResult(BaseModel):
|
||||
"""Résultat de la synthèse IA."""
|
||||
|
||||
text: str | None = Field(None, description="Texte de la synthèse IA")
|
||||
31
pronote_sync/models/xmpp.py
Normal file
31
pronote_sync/models/xmpp.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Modèle de données pour le message XMPP final."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from pronote_sync.models.blog import ExternalInfo
|
||||
from pronote_sync.models.diff import AgendaChange
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.message import Message
|
||||
|
||||
|
||||
class XmppMessage(BaseModel):
|
||||
"""Représente le message final à envoyer par XMPP."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
target_date: date = Field(..., description="Date cible")
|
||||
synthesis: str | None = Field(None, description="Synthèse IA (optionnelle, 3-5 phrases)")
|
||||
homeworks: list[Homework] = Field(default_factory=list, description="Liste brute des devoirs")
|
||||
changes: list[AgendaChange] = Field(
|
||||
default_factory=list, description="Liste des changements d'agenda"
|
||||
)
|
||||
messages: list[Message] = Field(
|
||||
default_factory=list, description="Liste des messages/informations"
|
||||
)
|
||||
external_info: ExternalInfo | None = Field(
|
||||
None, description="Informations externes (blog, messages Pronote)"
|
||||
)
|
||||
Reference in New Issue
Block a user