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:
@@ -28,20 +28,32 @@ from pronote_sync.sync.serialization import (
|
||||
|
||||
def compute_plan(
|
||||
pronote_data: PronoteData,
|
||||
remote_managed: list[tuple[str, Any]],
|
||||
) -> CalDAVSyncPlan:
|
||||
"""Calcule le plan de synchronisation CalDAV.
|
||||
remote_managed: list[tuple[str, str, Any]],
|
||||
) -> tuple[CalDAVSyncPlan, dict[str, str]]:
|
||||
"""Calcule le plan de synchronisation CalDAV et le mapping des UID distants.
|
||||
|
||||
L'appariement entre les événements locaux et distants se fait sur l'UID
|
||||
canonique (forme normalisée, identique pour une même source Pronote,
|
||||
suffixe temporel retiré) tandis que les mutations (suppressions, mises à
|
||||
jour) ciblent l'UID brut tel que stocké sur le serveur. Le mapping
|
||||
``canonical_uid -> raw_uid`` retourné permet à l'exécuteur de cibler le
|
||||
bon objet distant lors des mises à jour.
|
||||
|
||||
:param pronote_data: Données Pronote normalisées (cours, devoirs, événements).
|
||||
:param remote_managed: Liste de couples (uid, vevent) pour les événements
|
||||
distants marqués comme gérés par pronote-sync.
|
||||
:return: Plan de synchronisation avec les listes d'ajouts, mises à jour et
|
||||
suppressions pour chaque type d'événement.
|
||||
:rtype: CalDAVSyncPlan
|
||||
:param remote_managed: Liste de tuples (raw_uid, canonical_uid, vevent)
|
||||
pour les événements distants marqués comme gérés par pronote-sync.
|
||||
:return: Tuple (plan de synchronisation, mapping canonical_uid -> raw_uid).
|
||||
Les listes ``*_to_remove`` contiennent l'UID brut distant, les autres
|
||||
listes contiennent les modèles Pronote locaux.
|
||||
:rtype: tuple[CalDAVSyncPlan, dict[str, str]]
|
||||
"""
|
||||
remote_signatures: dict[str, str] = {}
|
||||
for uid, vevent in remote_managed:
|
||||
remote_signatures[uid] = component_to_signature(vevent)
|
||||
#: canonical_uid -> signature sémantique du VEVENT distant (pour l'appariement).
|
||||
remote_signatures_by_canonical: dict[str, str] = {}
|
||||
#: canonical_uid -> UID brut distant (pour cibler le bon objet lors des mutations).
|
||||
remote_raw_by_canonical: dict[str, str] = {}
|
||||
for raw_uid, canonical_uid, vevent in remote_managed:
|
||||
remote_signatures_by_canonical[canonical_uid] = component_to_signature(vevent)
|
||||
remote_raw_by_canonical[canonical_uid] = raw_uid
|
||||
|
||||
lessons_to_add: list[Lesson] = []
|
||||
lessons_to_update: list[Lesson] = []
|
||||
@@ -49,18 +61,12 @@ def compute_plan(
|
||||
|
||||
local_lessons_by_uid: dict[str, Lesson] = {lesson.id: lesson for lesson in pronote_data.lessons}
|
||||
for lesson in pronote_data.lessons:
|
||||
local_canonical = lesson.id
|
||||
local_sig = component_to_signature(lesson_to_vevent(lesson))
|
||||
if lesson.id not in remote_signatures:
|
||||
if local_canonical not in remote_signatures_by_canonical:
|
||||
lessons_to_add.append(lesson)
|
||||
elif remote_signatures[lesson.id] != local_sig:
|
||||
elif remote_signatures_by_canonical[local_canonical] != local_sig:
|
||||
lessons_to_update.append(lesson)
|
||||
for uid in remote_signatures:
|
||||
if (
|
||||
uid not in local_lessons_by_uid
|
||||
and not uid.startswith("homework-")
|
||||
and not uid.startswith("school-event-")
|
||||
):
|
||||
lessons_to_remove.append(uid)
|
||||
|
||||
homeworks_to_add: list[Homework] = []
|
||||
homeworks_to_update: list[Homework] = []
|
||||
@@ -70,15 +76,12 @@ def compute_plan(
|
||||
f"homework-{homework.id}": homework for homework in pronote_data.homeworks
|
||||
}
|
||||
for homework in pronote_data.homeworks:
|
||||
uid = f"homework-{homework.id}"
|
||||
local_canonical = f"homework-{homework.id}"
|
||||
local_sig = component_to_signature(homework_to_vevent(homework))
|
||||
if uid not in remote_signatures:
|
||||
if local_canonical not in remote_signatures_by_canonical:
|
||||
homeworks_to_add.append(homework)
|
||||
elif remote_signatures[uid] != local_sig:
|
||||
elif remote_signatures_by_canonical[local_canonical] != local_sig:
|
||||
homeworks_to_update.append(homework)
|
||||
for uid in remote_signatures:
|
||||
if uid.startswith("homework-") and uid not in local_homeworks_by_uid:
|
||||
homeworks_to_remove.append(uid)
|
||||
|
||||
school_events_to_add: list[SchoolEvent] = []
|
||||
school_events_to_update: list[SchoolEvent] = []
|
||||
@@ -89,24 +92,39 @@ def compute_plan(
|
||||
for event in pronote_data.school_events
|
||||
}
|
||||
for school_event in pronote_data.school_events:
|
||||
uid = f"school-event-{school_event.label}-{school_event.from_date.isoformat()}"
|
||||
local_canonical = f"school-event-{school_event.label}-{school_event.from_date.isoformat()}"
|
||||
local_sig = component_to_signature(school_event_to_vevent(school_event))
|
||||
if uid not in remote_signatures:
|
||||
if local_canonical not in remote_signatures_by_canonical:
|
||||
school_events_to_add.append(school_event)
|
||||
elif remote_signatures[uid] != local_sig:
|
||||
elif remote_signatures_by_canonical[local_canonical] != local_sig:
|
||||
school_events_to_update.append(school_event)
|
||||
for uid in remote_signatures:
|
||||
if uid.startswith("school-event-") and uid not in local_school_events_by_uid:
|
||||
school_events_to_remove.append(uid)
|
||||
|
||||
return CalDAVSyncPlan(
|
||||
lessons_to_add=lessons_to_add,
|
||||
lessons_to_update=lessons_to_update,
|
||||
lessons_to_remove=lessons_to_remove,
|
||||
homeworks_to_add=homeworks_to_add,
|
||||
homeworks_to_update=homeworks_to_update,
|
||||
homeworks_to_remove=homeworks_to_remove,
|
||||
school_events_to_add=school_events_to_add,
|
||||
school_events_to_update=school_events_to_update,
|
||||
school_events_to_remove=school_events_to_remove,
|
||||
# Détection des événements distants orphelins : un UID canonique distant
|
||||
# absent des données locales est supprimé en ciblant l'UID brut stocké sur
|
||||
# le serveur. L'acheminement vers la bonne liste de suppression se fait sur
|
||||
# le préfixe de l'UID canonique.
|
||||
for canonical_uid in remote_signatures_by_canonical:
|
||||
raw_uid = remote_raw_by_canonical[canonical_uid]
|
||||
if canonical_uid.startswith("homework-"):
|
||||
if canonical_uid not in local_homeworks_by_uid:
|
||||
homeworks_to_remove.append(raw_uid)
|
||||
elif canonical_uid.startswith("school-event-"):
|
||||
if canonical_uid not in local_school_events_by_uid:
|
||||
school_events_to_remove.append(raw_uid)
|
||||
elif canonical_uid not in local_lessons_by_uid:
|
||||
lessons_to_remove.append(raw_uid)
|
||||
|
||||
return (
|
||||
CalDAVSyncPlan(
|
||||
lessons_to_add=lessons_to_add,
|
||||
lessons_to_update=lessons_to_update,
|
||||
lessons_to_remove=lessons_to_remove,
|
||||
homeworks_to_add=homeworks_to_add,
|
||||
homeworks_to_update=homeworks_to_update,
|
||||
homeworks_to_remove=homeworks_to_remove,
|
||||
school_events_to_add=school_events_to_add,
|
||||
school_events_to_update=school_events_to_update,
|
||||
school_events_to_remove=school_events_to_remove,
|
||||
),
|
||||
remote_raw_by_canonical,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user