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>
84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
"""Utilitaires de gestion des identifiants uniques (UID) des événements.
|
|
|
|
Ce module fournit la normalisation des UIDs Pronote (suppression des
|
|
suffixes temporels) et la génération d'UIDs déterministes par hachage
|
|
des champs clés d'un événement, garantissant l'idempotence de la
|
|
synchronisation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
|
|
_TEMPORAL_SUFFIX_PATTERN = re.compile(r"-\d{8}T\d{6}Z-Index-Education$")
|
|
_EDUCATION_SUFFIX_PATTERN = re.compile(r"-Index-Education$")
|
|
|
|
|
|
def normalize_pronote_uid(uid: str) -> str:
|
|
"""Normalise un UID Pronote en supprimant ses suffixes temporels.
|
|
|
|
Les suffixes de type ``-AAAAMMJJTHHMMSSZ-Index-Education`` puis
|
|
``-Index-Education`` sont retirés. La fonction est idempotente :
|
|
appliquée à un UID déjà normalisé, elle retourne la même valeur.
|
|
|
|
:param uid: UID brut provenant de Pronote (ex: ``L-1234-20250901T080000Z-Index-Education``).
|
|
:return: UID normalisé, sans suffixe temporel ni marque ``Index-Education``.
|
|
:rtype: str
|
|
"""
|
|
normalized = _TEMPORAL_SUFFIX_PATTERN.sub("", uid)
|
|
return _EDUCATION_SUFFIX_PATTERN.sub("", normalized)
|
|
|
|
|
|
def normalize_datetime_to_utc(dt: datetime) -> datetime:
|
|
"""Normalise une datetime vers UTC pour les signatures et hachages.
|
|
|
|
Les datetimes naïves sont interprétées comme Europe/Paris puis converties
|
|
vers UTC. Les datetimes conscientes sont converties vers UTC.
|
|
|
|
:param dt: Datetime à normaliser (naïve ou consciente).
|
|
:return: Datetime en UTC.
|
|
:rtype: datetime
|
|
"""
|
|
if dt.tzinfo is None:
|
|
return dt.replace(tzinfo=ZoneInfo("Europe/Paris")).astimezone(ZoneInfo("UTC"))
|
|
return dt.astimezone(ZoneInfo("UTC"))
|
|
|
|
|
|
def generate_deterministic_uid(
|
|
start: datetime,
|
|
end: datetime,
|
|
subject: str,
|
|
teachers: list[str],
|
|
rooms: list[str],
|
|
group: str | None = None,
|
|
) -> str:
|
|
"""Génère un UID déterministe par hachage des champs clés d'un événement.
|
|
|
|
Utilisé lorsqu'aucun UID exploitable n'est disponible : deux appels avec
|
|
des champs identiques produisent le même identifiant, ce qui garantit
|
|
l'idempotence de la synchronisation.
|
|
|
|
:param start: Début de l'événement.
|
|
:param end: Fin de l'événement.
|
|
:param subject: Intitulé de la matière.
|
|
:param teachers: Liste des enseignants (triée avant hachage).
|
|
:param rooms: Liste des salles (triée avant hachage).
|
|
:param group: Groupe éventuel ; traité comme chaîne vide si absent.
|
|
:return: Identifiant déterministe : 12 premiers caractères hexadécimaux
|
|
du SHA-1 des champs clés joints par ``|``.
|
|
:rtype: str
|
|
"""
|
|
parts = [
|
|
normalize_datetime_to_utc(start).isoformat(),
|
|
normalize_datetime_to_utc(end).isoformat(),
|
|
subject,
|
|
",".join(sorted(teachers)),
|
|
",".join(sorted(rooms)),
|
|
group or "",
|
|
]
|
|
payload = "|".join(parts).encode("utf-8")
|
|
return hashlib.sha1(payload, usedforsecurity=False).hexdigest()[:12]
|