docs: define strict dry-run contract

# Conflicts:
#	.secrets.baseline
This commit is contained in:
2026-09-11 11:57:21 +02:00
parent fde8fbe264
commit 0dd4ee68c1
8 changed files with 51 additions and 29 deletions

View File

@@ -140,7 +140,7 @@
"filename": "GUIDE_DEV_PYTHON.md", "filename": "GUIDE_DEV_PYTHON.md",
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
"is_verified": true, "is_verified": true,
"line_number": 5084, "line_number": 5085,
"is_secret": false "is_secret": false
} }
], ],
@@ -177,5 +177,5 @@
} }
] ]
}, },
"generated_at": "2026-09-10T19:26:08Z" "generated_at": "2026-09-11T09:57:08Z"
} }

View File

@@ -310,7 +310,7 @@ d'un besoin réel et testé.
| `AI_API_KEY` | Clé API pour l'API IA. | `None` | `SecretStr` | | `AI_API_KEY` | Clé API pour l'API IA. | `None` | `SecretStr` |
| `AI_MODEL` | Modèle IA à utiliser (exemple recommandé : `gpt-4o-mini`). | `None` | `str \| None`| | `AI_MODEL` | Modèle IA à utiliser (exemple recommandé : `gpt-4o-mini`). | `None` | `str \| None`|
| `AI_ALLOW_INSECURE_HTTP` | Autoriser HTTP (non sécurisé) pour `openai-compatible` uniquement. | `False` | `bool` | | `AI_ALLOW_INSECURE_HTTP` | Autoriser HTTP (non sécurisé) pour `openai-compatible` uniquement. | `False` | `bool` |
| `DRY_RUN` | Mode dry-run (pas de modifications CalDAV/XMPP). | `False` | `bool` | | `DRY_RUN` | Simulation sans sortie distante ni état local persistant ; incompatible avec `qr_token`. | `False` | `bool` |
| `LOG_LEVEL` | Niveau de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | `INFO` | `str` | | `LOG_LEVEL` | Niveau de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | `INFO` | `str` |
> ⚠️ **Décision d'implémentation** : > ⚠️ **Décision d'implémentation** :
@@ -3241,7 +3241,8 @@ class CalDAVClient:
### 7.4 Points clés ### 7.4 Points clés
- **Différentielle** : La synchronisation compare les UID existants avec ceux à synchroniser. - **Différentielle** : La synchronisation compare les UID existants avec ceux à synchroniser.
- **Idempotence** : Deux exécutions identiques ne modifient pas le calendrier. - **Idempotence** : Deux exécutions identiques ne modifient pas le calendrier.
- **Dry-run** : Mode obligatoire pour tester sans effet de bord. - **Dry-run** : Les lectures sont autorisées, sans sortie distante ni état local persistant ; le
mode `qr_token` est refusé avant connexion car son authentification implique une rotation distante.
- **Marquage** : Les événements gérés sont marqués avec `X-PRONOTE-SYNC-MANAGED: v1` pour éviter les conflits. - **Marquage** : Les événements gérés sont marqués avec `X-PRONOTE-SYNC-MANAGED: v1` pour éviter les conflits.
- **Cours annulés** : Conservés avec `STATUS:CANCELLED` (ne pas supprimer). - **Cours annulés** : Conservés avec `STATUS:CANCELLED` (ne pas supprimer).
- **Plan explicite** : Le `CalDAVSyncPlan` est calculé avant l'exécution. - **Plan explicite** : Le `CalDAVSyncPlan` est calculé avant l'exécution.
@@ -5780,7 +5781,7 @@ repos:
|----------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------|------------| |----------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------|------------|
| Configuration des variables d'environnement | Vérifier que toutes les variables obligatoires sont définies (voir [Section 3.1](#31-variables-denvironnement)). | ✅ Oui | | | Configuration des variables d'environnement | Vérifier que toutes les variables obligatoires sont définies (voir [Section 3.1](#31-variables-denvironnement)). | ✅ Oui | |
| Vérification des secrets | Exécuter le script de vérification de sécurité (voir [Section 13.6](#136-exemple-de-script-de-vérification-de-sécurité)). | ✅ Oui | | | Vérification des secrets | Exécuter le script de vérification de sécurité (voir [Section 13.6](#136-exemple-de-script-de-vérification-de-sécurité)). | ✅ Oui | |
| Test en mode dry-run | Exécuter le pipeline avec `DRY_RUN=true` pour vérifier que tout fonctionne sans effet de bord. | ✅ Oui | | | Test en mode dry-run | Exécuter le pipeline avec `DRY_RUN=true` sans sortie distante ni état local persistant (`qr_token` exclu). | ✅ Oui | |
| Configuration des logs | Vérifier que les logs sont configurés avec masquage des secrets (voir [Section 4.2](#42-implémentation)). | ✅ Oui | | | Configuration des logs | Vérifier que les logs sont configurés avec masquage des secrets (voir [Section 4.2](#42-implémentation)). | ✅ Oui | |
| Vérification des dépendances | Exécuter `pip check` pour vérifier que toutes les dépendances sont installées. | ✅ Oui | | | Vérification des dépendances | Exécuter `pip check` pour vérifier que toutes les dépendances sont installées. | ✅ Oui | |
| Configuration du cron (si planifié) | Configurer une tâche cron pour exécuter le script régulièrement (ex: tous les jours à 18h). | ⚠️ Non | | | Configuration du cron (si planifié) | Configurer une tâche cron pour exécuter le script régulièrement (ex: tous les jours à 18h). | ⚠️ Non | |
@@ -5960,7 +5961,7 @@ Exemple de ligne cron (exécution tous les jours à 18h) :
| **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. | | **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. |
| **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). | | **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). |
| **UID** | Identifiant unique pour un événement iCal/CalDAV. | | **UID** | Identifiant unique pour un événement iCal/CalDAV. |
| **Dry-run** | Mode de test où aucune modification n'est appliquée (lecture seule). | | **Dry-run** | Simulation avec lectures autorisées, sans sortie distante ni état local persistant ; incompatible avec `qr_token`. |
| **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. | | **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. |
| **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. | | **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. |
@@ -6127,7 +6128,7 @@ Ce guide fournit une **base architecturale et technique solide** pour développe
| **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. | | **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. |
| **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). | | **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). |
| **UID** | Identifiant unique pour un événement iCal/CalDAV. | | **UID** | Identifiant unique pour un événement iCal/CalDAV. |
| **Dry-run** | Mode de test où aucune modification n'est appliquée (lecture seule). | | **Dry-run** | Simulation avec lectures autorisées, sans sortie distante ni état local persistant ; incompatible avec `qr_token`. |
| **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. | | **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. |
| **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. | | **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. |

View File

@@ -36,10 +36,16 @@ pronote-sync --dry-run --log-level DEBUG
```bash ```bash
pronote-sync # Exécute la synchronisation pronote-sync # Exécute la synchronisation
pronote-sync --dry-run # Simulation sans écriture pronote-sync --dry-run # Simulation : lectures autorisées, aucune écriture persistante ni sortie distante
pronote-sync --log-level DEBUG # Verbosité des journaux pronote-sync --log-level DEBUG # Verbosité des journaux
``` ```
En `--dry-run`, les données peuvent être lues pour construire la simulation, mais aucun état local
de source n'est enregistré : l'état RSS reste en mémoire pendant l'exécution. Les écritures CalDAV
et l'envoi XMPP sont également désactivés. Le mode
`PRONOTE_AUTH_MODE=qr_token` est refusé avant toute connexion, car la rotation de son token ne peut
pas garantir un état persistant cohérent pendant une simulation.
--- ---
## 🛠️ Déploiement ## 🛠️ Déploiement

14
TODO.md
View File

@@ -147,7 +147,8 @@ Synchroniser différentiellement les événements Pronote vers le calendrier Cal
- Le plan de sync est correctement calculé (données Pronote vs événements distants gérés). - Le plan de sync est correctement calculé (données Pronote vs événements distants gérés).
- Un changement de source iCal ↔ `pronotepy` ne crée ni doublon ni suppression/ajout artificiel pour un cours équivalent. - Un changement de source iCal ↔ `pronotepy` ne crée ni doublon ni suppression/ajout artificiel pour un cours équivalent.
- Un run dry-run n'écrit rien ; deux runs identiques donnent un résultat identique. - Un run dry-run autorise les lectures, mais ne produit aucune sortie distante ni écriture locale
persistante ; `qr_token` est refusé avant connexion. Deux runs identiques donnent le même résultat.
- Les événements annulés restent (`STATUS:CANCELLED`) et sont marqués `MANAGED`. - Les événements annulés restent (`STATUS:CANCELLED`) et sont marqués `MANAGED`.
- Les événements non marqués ne sont jamais modifiés ni supprimés. - Les événements non marqués ne sont jamais modifiés ni supprimés.
@@ -223,7 +224,9 @@ Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et m
- [x] Créer les étapes `pipeline/steps/` : `fetch.py`, `normalize.py`, `compare.py`, `caldav_sync.py`, `synthesis.py`, `send.py`, `fetch_blog.py`. - [x] Créer les étapes `pipeline/steps/` : `fetch.py`, `normalize.py`, `compare.py`, `caldav_sync.py`, `synthesis.py`, `send.py`, `fetch_blog.py`.
- [x] Créer `pipeline/run.py` : `PipelineRunner` (composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send. - [x] Créer `pipeline/run.py` : `PipelineRunner` (composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send.
- [x] Gérer les erreurs dégradées (continuer sauf critique) et renvoyer `(PronoteData, erreurs + warns)`. - [x] Gérer les erreurs dégradées (continuer sauf critique) et renvoyer `(PronoteData, erreurs + warns)`.
- [x] Implémenter le mode `dry_run` (aucune écriture CalDAV/XMPP). - [x] Implémenter le mode `dry_run` : lectures autorisées, aucune écriture CalDAV/XMPP ni écriture
persistante locale ; l'état RSS reste en mémoire pendant l'exécution, et
`PRONOTE_AUTH_MODE=qr_token` est refusé avant toute connexion.
- [x] Câbler l'injection des dépendances (Protocol + composition root), sans singleton global. - [x] Câbler l'injection des dépendances (Protocol + composition root), sans singleton global.
- [x] Réutiliser, dans une même exécution, un unique téléchargement/parsing iCal pour l'agenda et les devoirs lorsque les sources sélectionnées le permettent ; rester sur un cache local au run, sans cache global ni persistant. - [x] Réutiliser, dans une même exécution, un unique téléchargement/parsing iCal pour l'agenda et les devoirs lorsque les sources sélectionnées le permettent ; rester sur un cache local au run, sans cache global ni persistant.
@@ -231,7 +234,9 @@ Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et m
- [x] Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre. - [x] Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre.
- [x] Une sélection iCal commune à l'agenda et aux devoirs ne déclenche qu'un téléchargement/parsing du flux par run. - [x] Une sélection iCal commune à l'agenda et aux devoirs ne déclenche qu'un téléchargement/parsing du flux par run.
- [x] Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP. - [x] Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP.
- [x] `dry_run=True` n'effectue aucune écriture ; aucune source disponible → erreur critique explicite. - [x] `dry_run=True` autorise les lectures mais n'effectue aucune écriture persistante locale ni
sortie distante ; aucune source disponible → erreur critique explicite. L'état RSS n'est pas
enregistré et `qr_token` est refusé avant toute connexion.
- [x] Si `THEORETICAL_AGENDA_PATH` est absent, le pipeline produit un diff vide sans erreur et n'instancie pas `AgendaComparator` ; si présent, il instancie le comparateur et effectue la comparaison. - [x] Si `THEORETICAL_AGENDA_PATH` est absent, le pipeline produit un diff vide sans erreur et n'instancie pas `AgendaComparator` ; si présent, il instancie le comparateur et effectue la comparaison.
- [x] Les erreurs critiques (`PipelineCriticalError`) propagées depuis une étape non-bloquante arrêtent le pipeline. - [x] Les erreurs critiques (`PipelineCriticalError`) propagées depuis une étape non-bloquante arrêtent le pipeline.
@@ -247,7 +252,8 @@ Exposer le lancement du pipeline via une interface en ligne de commande.
- [x] Gérer le code de retour et l'affichage des erreurs (redactées). - [x] Gérer le code de retour et l'affichage des erreurs (redactées).
### Critères d'acceptation ### Critères d'acceptation
- `pronote-sync --dry-run --log-level DEBUG` s'exécute sans effet de bord. - `pronote-sync --dry-run --log-level DEBUG` s'exécute sans sortie distante ni état local persistant ;
le mode `qr_token`, qui implique une rotation distante, est refusé avant connexion.
- Le script console est installable (`[project.scripts]` dans `pyproject.toml`). - Le script console est installable (`[project.scripts]` dans `pyproject.toml`).
- Les erreurs affichées ne contiennent aucun secret, y compris avec l'affichage d'un traceback complet en mode debug. - Les erreurs affichées ne contiennent aucun secret, y compris avec l'affichage d'un traceback complet en mode debug.

View File

@@ -42,8 +42,11 @@ bloquent donc pas le déploiement. Il ne valide ni les valeurs ni les permission
du fichier d'environnement. Pour analyser seulement le contenu indexé avant un du fichier d'environnement. Pour analyser seulement le contenu indexé avant un
commit, utilisez `scripts/check_secrets.py --staged`. commit, utilisez `scripts/check_secrets.py --staged`.
Le dry-run vérifie le pipeline sans appliquer les écritures de synchronisation ; Le dry-run autorise les lectures nécessaires à la simulation, mais n'applique aucune sortie
il ne remplace pas une vérification des paramètres réellement chargés. CalDAV/XMPP et ne modifie aucun état local persistant. L'état RSS reste limité à la mémoire du
processus. Le mode `PRONOTE_AUTH_MODE=qr_token` est incompatible avec cette garantie : la commande
le refuse avant toute connexion afin de ne pas désynchroniser le token local du token distant.
Le dry-run ne remplace pas une vérification des paramètres réellement chargés.
## Installation systemd ## Installation systemd

View File

@@ -32,7 +32,10 @@ def _parse_arguments(arguments: Sequence[str] | None = None) -> argparse.Namespa
"--dry-run", "--dry-run",
action="store_true", action="store_true",
default=None, default=None,
help="Simule la synchronisation sans écrire vers CalDAV ni XMPP.", help=(
"Simule la synchronisation sans sortie distante ni état local persistant "
"(incompatible avec PRONOTE_AUTH_MODE=qr_token)."
),
) )
parser.add_argument( parser.add_argument(
"--log-level", "--log-level",

View File

@@ -11,6 +11,23 @@ from pronote_sync.errors import PipelineCriticalError, PipelineWarning
from pronote_sync.models.pronote import PronoteData from pronote_sync.models.pronote import PronoteData
def test_help_documents_strict_dry_run_contract(capsys: pytest.CaptureFixture[str]) -> None:
"""L'aide CLI expose le contrat strict et l'incompatibilité QR/token.
:param capsys: Capture des sorties standard de pytest.
:return: None
"""
from pronote_sync.cli.main import main
with pytest.raises(SystemExit) as exc_info:
main(["--help"])
output = " ".join(capsys.readouterr().out.split())
assert exc_info.value.code == 0
assert "sans sortie distante ni état local persistant" in output
assert "PRONOTE_AUTH_MODE=qr_token" in output
def test_main_runs_composition_root_in_dry_run_with_requested_log_level( def test_main_runs_composition_root_in_dry_run_with_requested_log_level(
mocker: MockerFixture, mocker: MockerFixture,
) -> None: ) -> None:

View File

@@ -505,7 +505,6 @@ def test_from_settings_configures_source_state_persistence_for_dry_run(
import pronote_sync.pipeline.run as run_module import pronote_sync.pipeline.run as run_module
blog_persistence: list[bool] = [] blog_persistence: list[bool] = []
auth_persistence: list[bool] = []
class RecordingBlogState: class RecordingBlogState:
"""Blog state factory recording its persistence configuration.""" """Blog state factory recording its persistence configuration."""
@@ -517,16 +516,6 @@ def test_from_settings_configures_source_state_persistence_for_dry_run(
""" """
blog_persistence.append(persistence_enabled) blog_persistence.append(persistence_enabled)
class RecordingAuthState:
"""Authentication state factory recording its persistence configuration."""
def __init__(self, *, persistence_enabled: bool = True) -> None:
"""Record the requested persistence setting.
:param persistence_enabled: Whether disk writes are enabled.
"""
auth_persistence.append(persistence_enabled)
class RecordingClient: class RecordingClient:
"""Pronote client constructor accepting the injected auth state.""" """Pronote client constructor accepting the injected auth state."""
@@ -539,19 +528,16 @@ def test_from_settings_configures_source_state_persistence_for_dry_run(
del settings, auth_state del settings, auth_state
monkeypatch.setattr(run_module, "BlogRSSState", RecordingBlogState) monkeypatch.setattr(run_module, "BlogRSSState", RecordingBlogState)
monkeypatch.setattr(run_module, "PronoteAuthState", RecordingAuthState)
monkeypatch.setattr(run_module, "PronoteClient", RecordingClient) monkeypatch.setattr(run_module, "PronoteClient", RecordingClient)
PipelineRunner.from_settings( PipelineRunner.from_settings(
Settings( Settings(
app=AppSettings(dry_run=dry_run), app=AppSettings(dry_run=dry_run),
blog=BlogSettings(enabled=True), blog=BlogSettings(enabled=True),
pronote=PronoteSettings(auth_mode="qr_token"),
) )
) )
assert blog_persistence == [not dry_run] assert blog_persistence == [not dry_run]
assert auth_persistence == [not dry_run]
def test_runner_reuses_ical_download_and_parse_within_one_run( def test_runner_reuses_ical_download_and_parse_within_one_run(