# Export CalDAV vers CSV Le script [`crealead-caldav.py`](../crealead-caldav.py) récupère des événements d'agendas accessibles en CalDAV, notamment Nextcloud, et produit un fichier CSV par calendrier configuré. Ces fichiers peuvent ensuite être utilisés par `csv-to-timemanager.py`. ## Pré-requis - Python 3, avec `pip` et le module `venv`. L'environnement Docker du dépôt utilise Python 3.9.2. - Les dépendances de [`requirements.txt`](../requirements.txt), notamment `caldav`, `icalendar` et `pytz`. - Un serveur CalDAV accessible depuis la machine d'exécution et un compte autorisé à lire les calendriers à exporter. - Des droits de lecture sur `config.json` et d'écriture sur les fichiers CSV ainsi que sur le dossier `logs/`, situé à côté du script. - Bash pour utiliser le lanceur `exec-export-caldav.sh`. Depuis la racine du dépôt, préparer l'environnement : ```bash python3 -m venv env source env/bin/activate python -m pip install -r requirements.txt ``` L'environnement Docker fourni constitue une autre façon d'exécuter le script et les tests. Sa construction est décrite dans [`support/docker-dev/README.md`](../support/docker-dev/README.md). ## Usage ### Configuration minimale Créer un fichier `config.json` à la racine du dépôt. Exemple pour exporter les événements d'octobre 2026 : ```json { "caldav_url": "https://cloud.example.com/remote.php/dav/", "username": "utilisateur", "password": "mot-de-passe-application", "year_start": 2026, "month_start": 10, "day_start": 1, "year_end": 2026, "month_end": 11, "day_end": 1, "calendars": [ { "calendar_name": "Temps de travail", "csv_name": "csv/utilisateur.csv" } ] } ``` | Champ | Rôle | | --- | --- | | `caldav_url` | URL du serveur CalDAV. | | `username`, `password` | Identifiants du compte qui peut lire les calendriers. | | `year_start`, `month_start`, `day_start` | Date de début de la recherche, à minuit. Les valeurs sont des nombres entiers. | | `year_end`, `month_end`, `day_end` | Date de fin de la recherche, à minuit. Pour couvrir un mois, utiliser le premier jour du mois suivant. | | `calendars` | Liste des calendriers à exporter. | | `calendars[].calendar_name` | Texte contenu dans le nom du calendrier distant, avec respect de la casse. Il doit correspondre à un seul calendrier. | | `calendars[].csv_name` | Chemin du fichier CSV à écrire. Un chemin relatif est résolu depuis le répertoire de lancement. | Ajouter une entrée dans `calendars` pour chaque calendrier à exporter. Les champs `db_*`, `user_name` et `api_password`, utilisés par d'autres scripts du dépôt, ne sont pas nécessaires à cet export. `config.json` est ignoré par Git. Remplacer les valeurs d'exemple par les informations du serveur et du compte utilisés. ### Lancement Lancer les commandes depuis la racine du dépôt : le script lit `./config.json`, et le lanceur recherche `env/bin/activate` dans le répertoire courant. Créer au préalable les dossiers de destination des CSV ; le script ne les crée pas. ```bash mkdir -p csv bash ./exec-export-caldav.sh ``` Le lanceur active l'environnement `env/`, puis exécute le script. Il est aussi possible de lancer directement Python : ```bash env/bin/python crealead-caldav.py ``` Le script ne propose pas d'arguments en ligne de commande. La période et les calendriers sont définis dans `config.json`. ### Fichiers produits et traitement des événements Chaque CSV existant est remplacé lors de l'export. Les lignes sont triées par date et heure de début. Le séparateur est `;` et les colonnes sont : ```text Titre;Date debut;Heure debut;Date fin;Heure fin;Categories;Emplacement;Description ``` Les événements récurrents sont demandés au serveur avec `expand=True`. Le lecteur iCalendar prend en charge les paramètres des propriétés, comme `TZID` et `VALUE=DATE`, ainsi que les textes répartis sur plusieurs lignes. Les heures sont converties vers `Europe/Paris`. Une heure sans fuseau est interprétée dans ce fuseau ; une date seule est exportée à minuit. Si `DTEND` est absent, `DURATION` peut fournir la date de fin. Les événements sans propriété `SUMMARY` sont ignorés. Ceux dont les dates de début ou de fin sont absentes ou invalides sont également ignorés. Chaque exclusion laisse un avertissement détaillé dans le journal, y compris en l'absence de `SUMMARY`. Un document iCalendar illisible ou une ressource sans composant `VEVENT` est signalé puis ignoré. Les autres événements restent exportés. Si un calendrier configuré est introuvable ou correspond à plusieurs calendriers distants, le script affiche un message et s'arrête avant l'écriture des CSV. En cas d'erreur d'écriture d'un CSV, le script s'arrête avec une exception. Il laisse un avertissement pour chaque événement du calendrier en cours, dont le fichier peut être incomplet, et des calendriers restant à écrire. Les fichiers déjà écrits avec succès sont conservés. ### Journalisation Les sorties standard et d'erreur sont affichées dans le terminal et copiées dans `logs/crealead-caldav.log`, à côté du script. Le dossier est créé automatiquement. Le journal utilise UTF-8, conserve les exécutions précédentes et enregistre les traces d'exception en cas de plantage pendant le traitement. Il n'y a pas de rotation automatique du journal. `logs/` est ignoré par Git. Chaque nouvelle ligne du journal est préfixée par l'heure d'écriture en UTC, au format ISO 8601 `YYYY-MM-DDTHH:MM:SSZ`, puis par `[info]` : ```text [2026-10-15T12:34:56Z]: [info] - csv/utilisateur.csv ... ``` Cela s'applique aussi aux messages écrits en plusieurs fragments et à chaque ligne d'une trace d'exception. L'horodatage est ajouté dans le fichier de journal, sans modifier les messages ordinaires du terminal. Tout événement non exporté est signalé avec `[warning][event]`, à la place de `[info]`. Le message indique le calendrier distant (ou son nom configuré), le fichier CSV, le titre, le début, la fin, l'UID et la raison de l'exclusion : ```text [2026-10-15T12:34:56Z]: [warning][event] - Calendrier='Temps de travail' ; CSV='csv/utilisateur.csv' ; titre='Réunion' ; début='2026-10-15T08:00:00Z' ; fin='indisponible' ; UID='reunion-1' ; événement non exporté : dates de début/fin absentes ou invalides (date absente ou invalide) ``` L'heure entre crochets est celle du message ; les champs `début` et `fin` décrivent le moment de l'événement. Les dates et heures décodées sont affichées en UTC, les dates de journée entière restent des dates seules. En cas de document illisible ou de date invalide, le script récupère les champs bruts disponibles. Une métadonnée absente ou irrécupérable vaut `indisponible`. Les retours à la ligne dans les métadonnées et le motif sont échappés pour garder un avertissement sur une seule ligne. Ces avertissements sont aussi visibles dans le terminal. ## Tests Les tests se trouvent dans [`tests/test_crealead_caldav.py`](../tests/test_crealead_caldav.py) et utilisent `unittest`, fourni avec Python. Aucune dépendance de test supplémentaire n'est nécessaire. ### Cas couverts Les 13 tests de `ExportEventsTests` vérifient : - La lecture de `DTSTART` et `DTEND` avec `TZID`, leur conversion vers Paris et le tri chronologique des lignes. - La conversion des dates UTC en été et en hiver, ainsi que celle d'un autre fuseau horaire. - Les événements sur une journée entière (`VALUE=DATE`), les heures sans fuseau et le calcul de la fin à partir de `DURATION`. - L'exclusion des événements sans propriété `SUMMARY`, des dates manquantes ou invalides et des documents iCalendar illisibles, sans bloquer les événements valides, ainsi que la présence d'un avertissement contextualisé dans le journal pour chaque événement écarté. - La récupération des métadonnées de chaque événement dans un document illisible, la conservation d'une date brute invalide et le signalement d'une ressource sans composant `VEVENT`. - La conservation des deux-points dans les textes, des paramètres de texte, des catégories et des lignes repliées, ainsi que le nettoyage de la description HTML encodée et le contenu du CSV écrit. Les 9 tests de `LogOutputTests` vérifient : - La création de `logs/` à côté du script, la copie des deux sorties et l'écriture immédiate des messages, y compris les accents. - L'ajout au journal existant et le choix du nom du journal d'après celui du script, y compris lorsqu'une exécution se termine sans retour à la ligne. - L'horodatage en UTC, le préfixe `[info]` de chaque ligne, les écritures en plusieurs fragments, les messages sur plusieurs lignes et les lignes vides. - Le préfixe `[warning][event]` sans `[info]`, les métadonnées de l'événement, l'échappement des retours à la ligne et la séparation d'un avertissement après un message ordinaire inachevé. - La sauvegarde des traces d'exception et la restauration des sorties après une erreur. - La conservation du code de sortie d'un `SystemExit`, sans ajouter de trace d'exception, et la sauvegarde de son éventuel message. Le test de `ExportFailureTests` provoque une erreur d'écriture CSV. Il vérifie qu'un avertissement est enregistré pour les événements du calendrier en échec et des calendriers restant à écrire, et que le premier CSV déjà écrit est conservé. Ces tests utilisent le vrai lecteur `icalendar`, des événements synthétiques et des dossiers temporaires pour les CSV et les journaux. Le module CalDAV et les réponses du serveur sont simulés : aucun accès réseau, compte, `config.json` ou calendrier réel n'est nécessaire. Ils vérifient que la recherche demande `expand=True`, mais ne valident pas l'expansion des récurrences par un serveur réel ni l'import dans TimeManager. ### Exécution Depuis la racine du dépôt, avec les dépendances installées dans `env/` : ```bash env/bin/python -B -m unittest discover -s tests -v ``` L'option `-B` évite de créer des fichiers de cache Python. La suite contient actuellement 23 tests ; une exécution réussie se termine par `OK`. Avec l'image Docker du dépôt déjà construite : ```bash docker run --rm -v "$(pwd)":/app -w /app cal-to-time-manager:dev -B -m unittest discover -s tests -v ```