Files
college-infos/TODO.md

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 par typing.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.toml d'après l'Annexe C (projet pronote-sync, requires-python = ">=3.13.5", dépendances, extras dev et ai-litellm, script console pronote-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) dans pyproject.toml.
  • Créer .gitignore (venv, __pycache__, .env, .blog_rss_state.json, .coverage, artefacts .ics temporaires).
  • Créer l'arborescence pronote_sync/ avec __init__.py dans 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_sync est importable sans erreur.
  • ruff check ., mypy . et pytest s'exécutent sans erreur d'import.
  • pre-commit run --all-files ré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) avec SecretStr et prefixes d'env.
  • Créer config/env.py pour le chargement du .env (SettingsConfigDict(env_file=".env")).
  • Créer .env.example complet (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 + RedactingFormatter masquant 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 SecretStr n'est affiché en clair via str()/print.

Critères d'acceptation

  • from pronote_sync.config.settings import settings fonctionne 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é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.

Critères d'acceptation

  • Chaque modèle s'instancie et se sérialise en JSON valide.
  • Lesson, Homework, Message, AgendaChange, AgendaDiff, XmppMessage, BlogArticle sont frozen=True.
  • PronoteData et CalDAVSyncResult sont mutables ; tous les modèles importables via models/__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 via requests, erreurs redactées) et parsing iCal → Lesson/Homework/SchoolEvent (icalendar).
  • Extraire les blocs de devoirs (HomeworkBlock) depuis DESCRIPTION dans une séquence qui préserve plusieurs blocs à la même date ; dédupliquer ensuite via collect_homeworks(lessons, target_date).
  • Détecter les statuts (CANCELLED/MOVED) via CATEGORIES et STATUS:CANCELLED.
  • Ajouter PRONOTE_URL à la configuration et créer sources/pronote/client.py autour de pronotepy.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 sur due_on == target_date.
  • Créer sources/pronote/fallback.py : sélection de source selon PRONOTE_*_SOURCE (auto/ical/pronotepy) et PronoteFetcher unifiant fetch_agenda/fetch_homework/fetch_messages.
  • Implémenter le contrat de source : modes ical/pronotepy stricts ; mode auto = iCal puis repli pronotepy uniquement sur exception ; deux échecs en autoPipelineCriticalError.
  • 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_uid pour la stabilité des événements.

Critères d'acceptation

  • parse_ical parse tests/fixtures/pronote-4e.ics en leçons/événements corrects, conserve les blocs bruts et retourne une liste de Homework vide ; collect_homeworks retourne 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 ParentClient est testé avec l'ordre réel de ses paramètres, l'URL Pronote et une fonction ENT autorisée.
  • Le client pronotepy récupère messages/cours/devoirs (mocké) et ne retourne que les devoirs de la date cible.
  • Le mode auto bascule 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) avec feedparser (§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 (ou sync/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_parse renvoie 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 : protocole TheoreticalAgendaProvider (§8.2).
  • Créer sources/theoretical/file.py : lecture fichier iCal/CSV → liste de TheoreticalLesson (§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.py lit tests/fixtures/theoretical.ics et theoretical.csv en TheoreticalLesson.
  • 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, marqueur X-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 pronotepy possè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 BlogRSSState pour l'état blog si pertinent (sinon sync/blog_state.py).

Critères d'acceptation

  • Le plan de sync est correctement calculé (PronoteData vs état local).
  • Un changement de source iCal ↔ pronotepy ne 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és MANAGED.

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 : AgendaComparator avec 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 : protocole SynthesisProvider.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, extra ai-litellm).
  • Créer synthesis/__init__.py : factory get_synthesis_provider(settings) (OpenAI par défaut, litellm si AI_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

  • generate retourne 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 : protocole Channel (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.send envoie 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 dans pipeline/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=True n'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ée pronote-sync), args --dry-run, --log-level.
  • Initialiser les logs (setup_logging) et charger settings au 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 DEBUG s'exécute sans effet de bord.
  • Le script console est installable ([project.scripts] dans pyproject.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, sans icalsecurise).
  • 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:CANCELLED sans catégorie, plusieurs devoirs à la même date, filtrage pronotepy sur 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

  • pytest passe et pytest --cov atteint ≥ 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 check et tester le dry-run avant mise en production.

Critères d'acceptation

  • Le service/timer systemd (ou cron) lance le pipeline quotidiennement.
  • logrotate est 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 CHANGELOG initial 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.md permet d'installer et de lancer le projet sans le guide.
  • La CI exécute tests + lint + sécurité.
  • Aucun secret dans la documentation.