"""Orchestrateur de la synchronisation CalDAV. Ce module fournit :func:`synchronize`, le point d'entrée haut niveau qui enchaîne les trois phases de la synchronisation : connexion à la passerelle CalDAV, scan des événements distants gérés et calcul du plan, puis exécution du plan (ou simulation en mode ``dry_run``). Il s'appuie sur :class:`~pronote_sync.sync.caldav.CalDAVGateway`, :func:`~pronote_sync.sync.planner.compute_plan` et :class:`~pronote_sync.sync.executor.CalDAVSyncExecutor`. """ from __future__ import annotations import logging from collections.abc import Callable from datetime import datetime, timedelta from typing import Any from pronote_sync.config.settings import Settings from pronote_sync.errors import PronoteSyncError from pronote_sync.models.pronote import PronoteData from pronote_sync.models.sync import CalDAVSyncResult, CalDAVSyncStatus from pronote_sync.sync.caldav import CalDAVGateway from pronote_sync.sync.executor import CalDAVSyncExecutor from pronote_sync.sync.planner import compute_plan logger = logging.getLogger(__name__) def synchronize( pronote_data: PronoteData, settings: Settings, client_factory: Callable[..., Any] | None = None, ) -> CalDAVSyncResult: """Synchronise les données Pronote vers le calendrier CalDAV. Enchaîne les trois phases : connexion à la passerelle, scan distant et calcul du plan, puis exécution (ou simulation dry-run). :param pronote_data: Données Pronote normalisées à synchroniser. :param settings: Configuration racine du pipeline. :param client_factory: Fabrique optionnelle de client DAV (pour les tests). :return: Résultat de la synchronisation (statut, compteurs, erreurs). :rtype: CalDAVSyncResult :raises PronoteSyncError: Si la configuration CalDAV est incomplète ou si la connexion échoue. """ # Fenêtre temporelle fondée sur l'instant courant. Aucune abstraction # d'horloge n'existe encore dans le dépôt (les données Pronote sont par # convention naïves en heure locale) : ``datetime.now()`` est utilisé ici, # point d'entrée de l'orchestration, et pourrait être refactoré plus tard # vers une horloge injectable sans changer le contrat. now = datetime.now() start = now - timedelta(days=settings.app.sync_past_days) end = now + timedelta(days=settings.app.sync_future_days) if ( settings.caldav.url is None or settings.caldav.username is None or settings.caldav.password is None ): logger.info("CalDAV non configuré — synchronisation ignorée") return CalDAVSyncResult(status=CalDAVSyncStatus.SKIPPED, added=0, updated=0, removed=0) gateway = CalDAVGateway(settings.caldav, client_factory=client_factory) try: with gateway: remote_managed = gateway.list_managed_events(start=start, end=end) logger.info( "Synchronisation CalDAV : %d événements distants gérés trouvés", len(remote_managed), ) plan = compute_plan(pronote_data, remote_managed) n_add = ( len(plan.lessons_to_add) + len(plan.homeworks_to_add) + len(plan.school_events_to_add) ) n_update = ( len(plan.lessons_to_update) + len(plan.homeworks_to_update) + len(plan.school_events_to_update) ) n_remove = ( len(plan.lessons_to_remove) + len(plan.homeworks_to_remove) + len(plan.school_events_to_remove) ) logger.info( "Plan : %d ajouts, %d mises à jour, %d suppressions", n_add, n_update, n_remove, ) if settings.app.dry_run: logger.info("DRY-RUN : aucune écriture ne sera effectuée sur le calendrier") executor = CalDAVSyncExecutor(gateway, dry_run=settings.app.dry_run) return executor.execute(plan) except PronoteSyncError: logger.error("Échec de la synchronisation CalDAV") # Re-lève la même exception de domaine sans en créer de nouvelle. # ``PronoteSyncError`` a déjà été levée avec ``from None`` en amont # (passerelle CalDAV), donc ``__cause__`` et ``__context__`` restent # propres : un ``raise`` nu préserve cet état sans ajouter de chaînage. raise