From c05fc432eb4734c3b898ec4b70d987a8c647f568 Mon Sep 17 00:00:00 2001 From: Laurent Chedanne Date: Thu, 15 Oct 2026 15:19:23 +0200 Subject: [PATCH] Export caldav with logs --- .gitignore | 1 + AGENTS.md | 28 +++ crealead-caldav.py | 257 +++++++++++++++++++----- docs/crealead-caldav.md | 235 ++++++++++++++++++++++ tests/test_crealead_caldav.py | 364 ++++++++++++++++++++++++++++++++++ 5 files changed, 836 insertions(+), 49 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/crealead-caldav.md create mode 100644 tests/test_crealead_caldav.py diff --git a/.gitignore b/.gitignore index ad6dd79..3d361b6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ env/* config.json config.json.* csv/* +logs/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d052a2a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# Consignes de maintenance + +- Mettre à jour `docs/crealead-caldav.md` lors de toute évolution impactant + `crealead-caldav.py`, notamment ses pré-requis, sa configuration, son + lancement, le traitement des événements, ses sorties CSV, sa journalisation + ou ses tests dans `tests/`. +- Garder les exemples de configuration, les commandes d'exécution et les cas + de test documentés cohérents avec le comportement du script. +- Chaque nouvelle ligne écrite dans `logs/crealead-caldav.log` doit commencer + par l'heure d'écriture en UTC au format ISO 8601 avec suffixe `Z`, entre + crochets. Le format par défaut est : + `[2026-10-15T12:34:56Z]: [info] - message`. + Cela s'applique aux sorties standard et d'erreur ainsi qu'aux lignes des + traces d'exception. +- Tout événement non exporté doit laisser une trace au format + `[2026-10-15T12:34:56Z]: [warning][event] - message`, sans préfixe `[info]` + supplémentaire. Préciser le calendrier concerné, le titre, le moment de + l'événement (début et fin disponibles) et la raison de son exclusion. + Ajouter le fichier CSV et l'UID pour faciliter son identification. +- Ne jamais ignorer silencieusement un événement, notamment en cas de + `SUMMARY` absent, de dates absentes ou invalides, de document iCalendar + illisible ou d'export CSV interrompu. Récupérer les métadonnées disponibles + dans les données brutes si nécessaire et indiquer `indisponible` pour celles + qui ne peuvent pas être retrouvées. Échapper les retours à la ligne des + métadonnées et du motif pour conserver un avertissement sur une seule ligne. +- Lors d'une évolution du traitement des événements ou de la journalisation, + vérifier dans les tests que les événements écartés sont bien tracés avec les + informations et les préfixes ci-dessus. diff --git a/crealead-caldav.py b/crealead-caldav.py index 764060e..4e75067 100644 --- a/crealead-caldav.py +++ b/crealead-caldav.py @@ -17,11 +17,144 @@ import sys import caldav import json import csv -import calendar -from datetime import date, datetime, timezone +from datetime import date, datetime, time, timezone +from icalendar import Calendar import pytz from urllib.parse import unquote import re +import traceback +from contextlib import contextmanager, redirect_stdout, redirect_stderr +from pathlib import Path + + +def utc_timestamp(): + return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + + +class TimestampedLog: + """Prefix each log line, including lines written in several fragments.""" + + def __init__(self, log_file): + self.log_file = log_file + self.line_start = True + + def write(self, value): + # Keep an event warning separate from an unfinished console message. + if value.startswith('[warning][event] - ') and not self.line_start: + self.log_file.write('\n') + self.line_start = True + for fragment in value.splitlines(keepends=True): + if self.line_start: + self.log_file.write('[{}]: '.format(utc_timestamp())) + if not fragment.startswith('[warning][event] - '): + self.log_file.write('[info] - ') + self.log_file.write(fragment) + self.line_start = fragment.endswith(('\n', '\r')) + return len(value) + + def flush(self): + self.log_file.flush() + + +class TeeOutput: + """Write to the original console stream and to the shared log file.""" + + def __init__(self, stream, log_file): + self.stream = stream + self.log_file = log_file + + def write(self, value): + written = self.stream.write(value) + self.log_file.write(value) + self.log_file.flush() + return written + + def flush(self): + self.stream.flush() + self.log_file.flush() + + def __getattr__(self, name): + return getattr(self.stream, name) + + +@contextmanager +def log_output(): + """Append console output to logs/