Files
college-infos/pronote_sync/utils/logging.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.5 KiB
Python

"""Configuration de la journalisation avec masquage automatique des secrets.
Ce module fournit un formateur de logs qui rédige les secrets (tokens, mots
de passe, URLs sensibles) ainsi que l'initialisation du logging global du
pipeline ``pronote-sync``.
"""
from __future__ import annotations
import logging
import sys
from pronote_sync.utils.redaction import redact_secrets
_LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
class RedactingFormatter(logging.Formatter):
"""Formateur de logs qui masque les secrets des messages et des arguments."""
def format(self, record: logging.LogRecord) -> str:
"""Formate un enregistrement de log en masquant les secrets.
Le message et chaque argument textuel de l'enregistrement sont rédigés
avant le formatage final effectué par :class:`logging.Formatter`.
:param record: Enregistrement de log à formater.
:return: Message formaté, avec les secrets remplacés par ``REDACTED``.
:rtype: str
"""
record.msg = redact_secrets(str(record.msg))
args = record.args
if args:
if isinstance(args, tuple):
record.args = tuple(
redact_secrets(arg) if isinstance(arg, str) else arg for arg in args
)
else:
record.args = {
key: redact_secrets(value) if isinstance(value, str) else value
for key, value in args.items()
}
return super().format(record)
def setup_logging(level: str = "INFO") -> None:
"""Configure la journalisation globale avec masquage des secrets.
Les gestionnaires existants du logger racine sont supprimés, puis un
gestionnaire unique écrivant sur ``sys.stdout`` est installé. Les loggers
tiers ``urllib3`` et ``slixmpp`` sont ramenés au niveau ``WARNING``.
:param level: Nom du niveau de log (ex: ``"DEBUG"``, ``"INFO"``) ;
les noms inconnus sont ignorés au profit de ``INFO``.
:rtype: None
"""
numeric_level: int = logging.getLevelNamesMapping().get(level.upper(), logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(RedactingFormatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT))
root = logging.getLogger()
for existing in list(root.handlers):
root.removeHandler(existing)
root.setLevel(numeric_level)
root.addHandler(handler)
for noisy_name in ("urllib3", "slixmpp"):
logging.getLogger(noisy_name).setLevel(logging.WARNING)