- 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>
68 lines
2.4 KiB
Python
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]
|