Files
college-infos/pronote_sync/utils/uid.py
Antoine Van Elstraete c3f76ab91b feat(M2): configuration, gestion des secrets et logging
- config/settings.py : modèles Pydantic Settings (Pronote, CalDAV, XMPP,
  AI, Blog, App) avec SecretStr pour les mots de passe et clés API
- config/env.py : fonction load_settings() pour le chargement du .env
- utils/redaction.py : redact_url, redact_secrets, redact_exception
  (masquage des tokens icalsecurise, mots de passe et URLs sensibles)
- utils/logging.py : setup_logging + RedactingFormatter
  (masquage automatique des secrets dans les logs)
- utils/uid.py : normalize_pronote_uid (suppression suffixes temporels)
  et generate_deterministic_uid (hash SHA-1 usedforsecurity=False)
- .env.example : aligné sur les modèles finaux (XMPP_TO, XMPP_HOST,
  XMPP_ENABLED, BLOG_ENABLED, etc.)
- .pre-commit-config.yaml : ajout pydantic + pydantic-settings aux
  additional_dependencies du hook mypy
- TODO.md : items M2 cochés

Décisions d'architecture (@architect) :
- XmppSettings : modèle complet §10.2.3, tous champs optionnels
- sync_past_days/future_days déplacés vers AppSettings (sans préfixe)
- AISettings.enabled = False par défaut
- BlogSettings inclus dès M2
- redact_exception comme fonction module (pas méthode)
- normalize_pronote_uid (nom du guide et des tests)

Validations :
- ruff check : PASS
- ruff format --check : PASS
- mypy strict : PASS (7 fichiers)
- bandit : PASS (0 issue)
- import settings : OK (toutes valeurs par défaut)
- redact_secrets/icalsecurise : masqué en REDACTED
- logging : secret masqué dans la sortie
- uid normalize : idempotent, deterministic OK
- SecretStr : pas de fuite dans repr
- pytest : 0 test (infrastructure OK)

Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
2026-09-05 22:52:07 +02:00

68 lines
2.4 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
_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 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 = [
start.isoformat(),
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]