Co-authored-by: codex/gpt-5.6-sol <codex-gpt-5.6-sol@agents.invalid>
18 KiB
TODO — Développement de pronote-sync (Pronote → CalDAV + XMPP)
Plan de développement dérivé du
GUIDE_DEV_PYTHON.md. Package Python :pronote_sync. Pipeline : récupération Pronote (iCal / pronotepy) → blog RSS → comparaison agenda théorique → sync CalDAV → synthèse IA → message XMPP. Contraintes transverses : Python ≥ 3.13.5, Pydantic v2 + pydantic-settings, injection partyping.Protocol, tests sans réseau, idempotence, mode dégradé, masquage systématique des secrets, coverage ≥ 90 %.
M1. Échafaudage et outillage — Priorité : Haute
Mettre en place le dépôt, l'environnement, l'arborescence du package et la chaîne d'outils (lint/type/test/pré-commit).
- Créer l'environnement virtuel Python (≥ 3.13.5) et l'activer (
python -m venv venv). - Créer
pyproject.tomld'après l'Annexe C (projetpronote-sync,requires-python = ">=3.13.5", dépendances, extrasdevetai-litellm, script consolepronote-sync). - Configurer ruff (
line-length = 100,target-version = "py313", règles E/W/F/I/B/C4/UP), mypy (strict), bandit et coverage (fail_under = 90) danspyproject.toml. - Créer
.gitignore(venv,__pycache__,.env,.blog_rss_state.json,.coverage, artefacts.icstemporaires). - Créer l'arborescence
pronote_sync/avec__init__.pydans chaque package (config,models,sources/pronote,sources/blog,sources/theoretical,sync,synthesis,channels,pipeline/steps,cli,utils,tests). - Ajouter la configuration pre-commit (ruff, mypy, bandit, detect-secrets,
trailing-whitespace,end-of-file). - Installer le projet en mode éditable :
pip install -e ".[dev]". - Créer le commit initial (scaffold +
.gitignore, sans aucun secret).
Critères d'acceptation
- Le package
pronote_syncest importable sans erreur. ruff check .,mypy .etpytests'exécutent sans erreur d'import.pre-commit run --all-filesréussit.- Le dépôt ne contient aucun secret (vérifié par detect-secrets).
M2. Configuration et gestion des secrets — Priorité : Haute
Implémenter la configuration Pydantic Settings, le masquage des secrets et la journalisation sûre.
- Créer
config/settings.py:PronoteSettings,CalDAVSettings,XmppSettings,AISettings,AppSettings,Settings(§3.2) avecSecretStret prefixes d'env. - Créer
config/env.pypour le chargement du.env(SettingsConfigDict(env_file=".env")). - Créer
.env.examplecomplet (toutes variables obligatoires §3.1.1 + optionnelles §3.1.2). - Créer
utils/redaction.py:redact_url,redact_secrets,redact_exception(§4.2.1). - Créer
utils/logging.py:setup_logging+RedactingFormattermasquant les secrets dans messages et args (§4.2.2). - Créer
utils/uid.py:normalize_uid(suppression des suffixes temporels des UID Pronote). - Vérifier qu'aucun
SecretStrn'est affiché en clair viastr()/print.
Critères d'acceptation
from pronote_sync.config.settings import settingsfonctionne et charge.env.redact_secrets("...icalsecurise=TOKEN...")masque le token et l'URL.- Les logs ne contiennent jamais de token, mot de passe ou clé API (même en DEBUG).
M3. Modèles de données Pydantic — Priorité : Haute
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égrantexternal_info. - Créer
models/__init__.pyré-exportant tous les modèles. - 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.
Lesson,Homework,Message,AgendaChange,AgendaDiff,XmppMessage,BlogArticlesontfrozen=True.PronoteDataetCalDAVSyncResultsont mutables ; tous les modèles importables viamodels/__init__.py.
M4. Sources Pronote (iCal + pronotepy + repli) — Priorité : Haute
Récupérer et normaliser l'agenda, les devoirs et les messages Pronote, avec repli entre iCal et pronotepy.
- Créer
sources/pronote/ical.py:fetch_ical(url)(HTTP viarequests, erreurs redactées) et parsing iCal →Lesson/Homework/SchoolEvent(icalendar). - Extraire les blocs de devoirs (
HomeworkBlock) depuisDESCRIPTIONdans une séquence qui préserve plusieurs blocs à la même date ; dédupliquer ensuite viacollect_homeworks(lessons, target_date). - Détecter les statuts (
CANCELLED/MOVED) viaCATEGORIESetSTATUS:CANCELLED. - Ajouter
PRONOTE_URLà la configuration et créersources/pronote/client.pyautour depronotepy.ParentClient(pronote_url, username, password, ent=ent_function); résoudre le slug ENT par liste fermée. - Exposer séparément les cours, devoirs, messages et informations dans le client
pronotepy; filtrer les devoirs surdue_on == target_date. - Créer
sources/pronote/fallback.py: sélection de source selonPRONOTE_*_SOURCE(auto/ical/pronotepy) etPronoteFetcherunifiantfetch_agenda/fetch_homework/fetch_messages. - Implémenter le contrat de source : modes
ical/pronotepystricts ; modeauto= iCal puis replipronotepyuniquement sur exception ; deux échecs enauto→PipelineCriticalError. - Distinguer un succès vide d'un échec : les récupérations critiques agenda/devoirs propagent une erreur expurgée ; seuls les messages/informations non critiques peuvent se dégrader en liste vide avec warning.
- Normaliser les UID via
utils/uid.normalize_pronote_uidpour la stabilité des événements.
Critères d'acceptation
parse_icalparsetests/fixtures/pronote-4e.icsen leçons/événements corrects, conserve les blocs bruts et retourne une liste deHomeworkvide ;collect_homeworksretourne ensuite le devoir attendu pour la date cible.- Les cours annulés sont détectés aussi bien par catégorie que par
STATUS:CANCELLED; plusieurs blocs de devoirs partageant une date sont tous conservés avant déduplication. - Le constructeur
ParentClientest testé avec l'ordre réel de ses paramètres, l'URL Pronote et une fonction ENT autorisée. - Le client
pronotepyrécupère messages/cours/devoirs (mocké) et ne retourne que les devoirs de la date cible. - Le mode
autobascule uniquement après une exception et lève une erreur critique si les deux sources échouent ; un résultat vide reste un succès. - Aucun secret n'apparaît dans le message, les logs, la cause, le contexte ou le traceback complet d'une erreur de source.
M5. Source blog (RSS) — Priorité : Moyenne
Récupérer le flux RSS du blog du collège, parser et dédupliquer les articles.
- Créer
sources/blog/rss.py:BlogRSSClient.fetch_and_parse(known_guids)avecfeedparser(§5 bis.7.1). - Parser les dates (RFC 822 / ISO 8601) et convertir le HTML en texte brut (
BeautifulSoup+html.unescape). - Créer
sources/blog/state.py(ousync/blog_state.py) :BlogRSSState(JSON :known_guids,etag,last_modified). - Implémenter la déduplication par GUID et le cache HTTP (
If-Modified-Since/etag). - Gérer un flux invalide (
bozo) et les exceptions sans fuite de secret (retour[]/warning).
Critères d'acceptation
fetch_and_parserenvoie les nouveaux articles triés par date décroissante, sans doublons.- L'état persiste les GUID connus entre deux appels.
- Un flux invalide ne plante pas le pipeline (warning non bloquant).
M6. Source agenda théorique — Priorité : Moyenne
Lire l'agenda théorique (iCal ou CSV) via une interface de provider extensible.
- Créer
sources/theoretical/provider.py: protocoleTheoreticalAgendaProvider(§8.2). - Créer
sources/theoretical/file.py: lecture fichier iCal/CSV → liste deTheoreticalLesson(§8.3). - Normaliser les matières et créneaux pour le matching déterministe.
- Supporter les deux formats (iCal et CSV) derrière la même interface.
Critères d'acceptation
file.pylittests/fixtures/theoretical.icsettheoretical.csvenTheoreticalLesson.- Le provider renvoie une liste stable et déterministe (tri par identifiant).
M7. Synchronisation CalDAV — Priorité : Haute
Synchroniser différentiellement les événements Pronote vers le calendrier CalDAV, de façon idempotente.
- Créer
sync/caldav.py:CalDAVClient(connexion, liste/ajout/MAJ/suppression, marqueurX-PRONOTE-SYNC-MANAGED: v1). - Créer
sync/state.py: état local de sync (SQLite ou JSON) assurant l'idempotence (UID connus). - Calculer le
CalDAVSyncPlan(to_add / to_update / to_remove) par UID stable. - Avant de figer le plan de sync, vérifier sur fixture anonymisée que le même cours provenant d'iCal et de
pronotepypossède le même identifiant canonique ; corriger la normalisation à la frontière des sources si nécessaire. - Implémenter la sync différentielle : conserver les cours annulés (
STATUS:CANCELLED), ne pas supprimer. - Garantir l'idempotence (2 exécutions identiques → même
CalDAVSyncResult). - Réutiliser
BlogRSSStatepour l'état blog si pertinent (sinonsync/blog_state.py).
Critères d'acceptation
- Le plan de sync est correctement calculé (PronoteData vs état local).
- Un changement de source iCal ↔
pronotepyne crée ni doublon ni suppression/ajout artificiel pour un cours équivalent. - Un run dry-run n'écrit rien ; deux runs identiques donnent un résultat identique.
- Les événements annulés restent (
STATUS:CANCELLED) et sont marquésMANAGED.
M8. Comparaison avec l'agenda théorique (diff) — Priorité : Haute
Comparer l'agenda réel et l'agenda théorique pour générer les ajouts/suppressions/modifications.
- Créer
sync/diff.py:AgendaComparatoravec matching déterministe (jour + créneau avec tolérance + matière normalisée). - Générer
AgendaDiff/AgendaChange(added / removed / modified). - Appliquer la politique de départage : tri par UID stable puis comparaison exacte ; première correspondance en cas de multi-match (§8.4).
- Gérer l'absence de fichier théorique (diff vide, non bloquant).
Critères d'acceptation
- La comparaison produit les bons
added/removed/modified. - Le matching est déterministe (même entrée → même résultat).
- Sans
THEORETICAL_AGENDA_PATH, retourne un diff vide sans erreur.
M9. Synthèse IA — Priorité : Moyenne
Générer une synthèse optionnelle via un fournisseur IA, avec mode dégradé strict.
- Créer
synthesis/provider.py: protocoleSynthesisProvider.generate → Optional[SynthesisResult](ne lève jamais d'exception). - Créer
synthesis/openai.py:OpenAISynthesisProvider(httpx, prompt système FR, max 800 car., timeout 30 s, temp 0.3). - Créer
synthesis/litellm.py:LiteLLMSynthesisProvider(optionnel, extraai-litellm). - Créer
synthesis/__init__.py: factoryget_synthesis_provider(settings)(OpenAI par défaut, litellm siAI_PROVIDER=litellm). - Mode dégradé : clé absente / timeout / exception → retour
None(le pipeline continue sans synthèse). - Respecter les contraintes (3-5 phrases, ton sobre, pas d'emoji dans le texte IA).
Critères d'acceptation
generateretourne une synthèse ≤ 800 car. conforme au prompt système.- Clé absente ou erreur réseau →
None(aucune exception propagée). - La factory renvoie le bon provider ; litellm derrière l'extra optionnel.
M10. Canal XMPP — Priorité : Haute
Construire et envoyer le message XMPP structuré via un compte bot dédié (message direct, pas de PubSub).
- Créer
channels/protocol.py: protocoleChannel(méthode d'envoi). - Créer
channels/xmpp.py:XmppChannel(slixmpp, message direct, compte bot dédié). - Implémenter
_format_message(XmppMessage): synthèse + liste brute des devoirs + changements + messages + infos blog (emojis 📌📅📚💬 autorisés). - Gérer les erreurs XMPP (reconnexion, timeout) avec masquage des secrets, non bloquant (
PipelineWarning). - Créer
channels/__init__.py: factory de canaux.
Critères d'acceptation
XmppChannel.sendenvoie un message direct formaté (slixmpp mocké en test).- Erreur XMPP →
PipelineWarning, jamais d'exception non gérée. - Aucun secret dans les logs XMPP.
M11. Orchestration du pipeline — Priorité : Haute
Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et mode dry-run.
- Compléter si nécessaire la hiérarchie canonique dans
pronote_sync/errors.py(ErrorSeverity,PipelineError,PipelineWarning,PipelineCriticalError) ; ne pas créer de doublon danspipeline/steps/errors.py. - Créer les étapes
pipeline/steps/:fetch.py,normalize.py,compare.py,caldav_sync.py,synthesis.py,send.py,fetch_blog.py. - Créer
pipeline/run.py:PipelineRunner(composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send. - Gérer les erreurs dégradées (continuer sauf critique) et renvoyer
(PronoteData, erreurs + warns). - Implémenter le mode
dry_run(aucune écriture CalDAV/XMPP). - Câbler l'injection des dépendances (Protocol + composition root), sans singleton global.
- Réutiliser, dans une même exécution, un unique téléchargement/parsing iCal pour l'agenda et les devoirs lorsque les sources sélectionnées le permettent ; rester sur un cache local au run, sans cache global ni persistant.
Critères d'acceptation
- Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre.
- Une sélection iCal commune à l'agenda et aux devoirs ne déclenche qu'un téléchargement/parsing du flux par run.
- Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP.
dry_run=Truen'effectue aucune écriture ; aucune source disponible → erreur critique explicite.
M12. Point d'entrée CLI — Priorité : Haute
Exposer le lancement du pipeline via une interface en ligne de commande.
- Créer
cli/main.py:main()(point d'entréepronote-sync), args--dry-run,--log-level. - Initialiser les logs (
setup_logging) et chargersettingsau démarrage. - Construire la composition root et lancer
PipelineRunner.run(). - Gérer le code de retour et l'affichage des erreurs (redactées).
Critères d'acceptation
pronote-sync --dry-run --log-level DEBUGs'exécute sans effet de bord.- Le script console est installable (
[project.scripts]danspyproject.toml). - Les erreurs affichées ne contiennent aucun secret, y compris avec l'affichage d'un traceback complet en mode debug.
M13. Tests et couverture — Priorité : Haute
Couvrir l'ensemble du code par des tests sans réseau, avec fixtures anonymisées, jusqu'à ≥ 90 %.
- Créer
tests/fixtures/:pronote-4e.ics,pronote-6e.ics,theoretical.ics,theoretical.csv,blog_rss.xml(anonymisés, sansicalsecurise). - Créer
tests/conftest.py: fixtures partagées (sample_lesson, sample_cancelled_lesson, sample_homework, sample_school_event, sample_message, sample_pronote_data…). - Écrire
tests/unit/:test_models,test_parsing(iCal),test_uid,test_redaction,test_diff,test_sync. - Couvrir les régressions M4 : signature réelle de
ParentClient, ENT autorisé/inconnu, erreur vs résultat vide,STATUS:CANCELLEDsans catégorie, plusieurs devoirs à la même date, filtragepronotepysur la date cible et stabilité d'identité entre sources. - Écrire
tests/integration/:test_pipeline,test_caldav(mocké),test_xmpp(mocké). - Écrire
tests/e2e/test_cli.py: exécution CLI en dry-run. - Tests sans réseau (mocks
responses/aioresponses/pytest-mock) ; couverture ≥ 90 %. - Ajouter un test négatif : les messages, logs, causes, contextes et tracebacks complets ne fuient pas de secrets (
icalsecurise, clés API, mots de passe).
Critères d'acceptation
pytestpasse etpytest --covatteint ≥ 90 % (fail_under = 90).- Aucun test ne fait de requête réseau réelle.
- Le test de non-fuite de secrets passe.
M14. Déploiement — Priorité : Moyenne
Mettre en production de façon supervisée (planification, rotation des logs, vérification des secrets).
- Créer une unité systemd (
pronote-sync.service+ timer) ou une ligne cron (exécution quotidienne). - Créer
logrotate.d/pronote_sync(daily, rotate 7, compress, delaycompress). - Ajouter un script de vérification des secrets (§13.6) exécuté avant chaque déploiement.
- Documenter la supervision (logs, alertes en cas d'échec) et la maintenance (maj dépendances, dry-run avant MAJ).
- Vérifier
pip checket tester le dry-run avant mise en production.
Critères d'acceptation
- Le service/timer systemd (ou cron) lance le pipeline quotidiennement.
logrotateest configuré et valide.- Le dry-run passe en pré-production ; le script de secrets ne donne pas de faux négatif.
M15. Documentation et revue finale — Priorité : Moyenne
Rédiger la documentation utilisateur et finaliser le projet.
- Créer
README.md(installation, configuration.env, usage CLI, systemd/docker, limites, RGPD). - Documenter l'architecture (pipeline, modules) en résumé.
- Ajouter
CHANGELOGinitial et la licence (MIT). - Revue finale : cohérence avec le guide, aucun secret documenté en clair.
- (Optionnel) Configurer GitHub Actions CI/CD (pytest + bandit + ruff + mypy) d'après §Prochaines étapes.
Critères d'acceptation
README.mdpermet d'installer et de lancer le projet sans le guide.- La CI exécute tests + lint + sécurité.
- Aucun secret dans la documentation.