fix(M7): corrections d'audit FIXME_M7 — sécurité, fenêtre, UID, timezone
Corrige les 5 constats de l'audit FIXME_M7 : #1 (Bloquant) — Protection des événements non marqués : - upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant modification ; lève PronoteSyncError en cas de collision avec un événement non géré (aucune écriture) - delete_event() vérifie le marqueur ; no-op avec warning si non géré - Méthode privée _is_managed_event() factorisant le contrôle #2 (Bloquant) — Fenêtre de synchronisation : - Calcul en journées entières (minuit à minuit exclusif) - Filtrage des données locales (lessons, homeworks, school_events) avant passage au planner - Paramètre now injectable pour les tests #3 (Bloquant) — UID canonique vs brut : - list_managed_events() retourne (raw_uid, canonical_uid, vevent) - compute_plan() matche par UID canonique, route les raw UID vers *_to_remove, retourne le mapping remote_raw_by_canonical - executor.execute() utilise le raw UID pour les mises à jour (pas de doublon) - Pas de migration destructive des UID distants existants #4 (Correction) — Normalisation temporelle UTC : - normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris → UTC ; consciente → UTC - Utilisée par generate_deterministic_uid() et component_to_signature() - Deux représentations du même instant → même UID et même signature #5 (Compatibilité) — date_search déprécié : - Remplacement par calendar.search(start, end, event=True, expand=True) Documentation : - GUIDE_DEV_PYTHON.md : suppression des références obsolètes à sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV (search au lieu de date_search, upsert par UID) - TODO.md : M7 décoché (corrections en cours de validation) Tests : 390 passés, couverture 95.61% Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid> Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
This commit is contained in:
@@ -13,6 +13,7 @@ mais le résultat reflète les opérations qui auraient été effectuées.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pronote_sync.errors import PronoteSyncError
|
||||
from pronote_sync.models.agenda import Lesson, SchoolEvent
|
||||
@@ -67,7 +68,12 @@ class CalDAVSyncExecutor:
|
||||
self._gateway = gateway
|
||||
self._dry_run = dry_run
|
||||
|
||||
def execute(self, plan: CalDAVSyncPlan) -> CalDAVSyncResult:
|
||||
def execute(
|
||||
self,
|
||||
plan: CalDAVSyncPlan,
|
||||
*,
|
||||
remote_raw_by_canonical: Mapping[str, str] | None = None,
|
||||
) -> CalDAVSyncResult:
|
||||
"""Exécute le plan de synchronisation et retourne le résultat.
|
||||
|
||||
Les cours, devoirs et événements scolaires sont traités dans l'ordre
|
||||
@@ -80,6 +86,10 @@ class CalDAVSyncExecutor:
|
||||
(reprise idempotente), sinon ``SUCCESS``.
|
||||
|
||||
:param plan: Plan de synchronisation à appliquer.
|
||||
:param remote_raw_by_canonical: Mapping canonical_uid -> raw_uid des
|
||||
événements distants gérés, requis pour cibler l'UID brut lors des
|
||||
mises à jour. ``None`` ou une clé absente entraîne une erreur
|
||||
consignée dans ``result.errors`` pour chaque mise à jour concernée.
|
||||
:return: Résultat de la synchronisation (statut, compteurs, erreurs).
|
||||
:rtype: CalDAVSyncResult
|
||||
"""
|
||||
@@ -89,7 +99,12 @@ class CalDAVSyncExecutor:
|
||||
for lesson in plan.lessons_to_add:
|
||||
self._do_save(lesson, result, is_update=False)
|
||||
for lesson in plan.lessons_to_update:
|
||||
self._do_save(lesson, result, is_update=True)
|
||||
self._do_save(
|
||||
lesson,
|
||||
result,
|
||||
is_update=True,
|
||||
remote_raw_by_canonical=remote_raw_by_canonical,
|
||||
)
|
||||
for uid in plan.lessons_to_remove:
|
||||
self._do_delete(uid, result)
|
||||
|
||||
@@ -97,7 +112,12 @@ class CalDAVSyncExecutor:
|
||||
for homework in plan.homeworks_to_add:
|
||||
self._do_save(homework, result, is_update=False)
|
||||
for homework in plan.homeworks_to_update:
|
||||
self._do_save(homework, result, is_update=True)
|
||||
self._do_save(
|
||||
homework,
|
||||
result,
|
||||
is_update=True,
|
||||
remote_raw_by_canonical=remote_raw_by_canonical,
|
||||
)
|
||||
for uid in plan.homeworks_to_remove:
|
||||
self._do_delete(uid, result)
|
||||
|
||||
@@ -105,7 +125,12 @@ class CalDAVSyncExecutor:
|
||||
for school_event in plan.school_events_to_add:
|
||||
self._do_save(school_event, result, is_update=False)
|
||||
for school_event in plan.school_events_to_update:
|
||||
self._do_save(school_event, result, is_update=True)
|
||||
self._do_save(
|
||||
school_event,
|
||||
result,
|
||||
is_update=True,
|
||||
remote_raw_by_canonical=remote_raw_by_canonical,
|
||||
)
|
||||
for uid in plan.school_events_to_remove:
|
||||
self._do_delete(uid, result)
|
||||
|
||||
@@ -123,6 +148,7 @@ class CalDAVSyncExecutor:
|
||||
model: Lesson | Homework | SchoolEvent,
|
||||
result: CalDAVSyncResult,
|
||||
is_update: bool,
|
||||
remote_raw_by_canonical: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Écrit un événement sur la passerelle, ou simule l'écriture.
|
||||
|
||||
@@ -133,12 +159,18 @@ class CalDAVSyncExecutor:
|
||||
l'UID dérivé via :func:`_model_uid` (l'upsert par UID permet la
|
||||
création ou la mise à jour de l'événement) ; en cas d'erreur
|
||||
``PronoteSyncError``, le message expurgé est ajouté à
|
||||
``result.errors``.
|
||||
``result.errors``. Pour une mise à jour, l'UID cible est l'UID brut
|
||||
distant (via ``remote_raw_by_canonical``) afin de mettre à jour le
|
||||
vrai événement distant au lieu d'en créer un doublon ; si le mapping
|
||||
est absent, l'erreur est consignée dans ``result.errors`` sans
|
||||
interrompre le lot.
|
||||
|
||||
:param model: Modèle Pronote à écrire (Lesson, Homework ou SchoolEvent).
|
||||
:param result: Résultat à mettre à jour (compteurs et erreurs).
|
||||
:param is_update: Si ``True``, l'opération est une mise à jour,
|
||||
sinon un ajout.
|
||||
:param remote_raw_by_canonical: Mapping canonical_uid -> raw_uid des
|
||||
événements distants gérés, utilisé uniquement pour les mises à jour.
|
||||
"""
|
||||
action = "mise à jour" if is_update else "ajout"
|
||||
uid = _model_uid(model)
|
||||
@@ -151,7 +183,15 @@ class CalDAVSyncExecutor:
|
||||
return
|
||||
try:
|
||||
vcalendar_text = model_to_vcalendar_text(model)
|
||||
self._gateway.upsert_event(vcalendar_text, uid)
|
||||
target_uid = uid
|
||||
if is_update:
|
||||
if remote_raw_by_canonical is None or uid not in remote_raw_by_canonical:
|
||||
result.errors.append(
|
||||
f"UID canonique sans correspondant distant : {redact_secrets(uid)}"
|
||||
)
|
||||
return
|
||||
target_uid = remote_raw_by_canonical[uid]
|
||||
self._gateway.upsert_event(vcalendar_text, target_uid)
|
||||
except PronoteSyncError as exc:
|
||||
result.errors.append(redact_secrets(str(exc)))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user