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",
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
"is_verified": true,
"line_number": 5084,
"line_number": 5085,
"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_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` |
| `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` |
> ⚠️ **Décision d'implémentation** :
@@ -3241,7 +3241,8 @@ class CalDAVClient:
### 7.4 Points clés
- **Différentielle** : La synchronisation compare les UID existants avec ceux à synchroniser.
- **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.
- **Cours annulés** : Conservés avec `STATUS:CANCELLED` (ne pas supprimer).
- **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 | |
| 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 | |
| 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 | |
@@ -5960,7 +5961,7 @@ Exemple de ligne cron (exécution tous les jours à 18h) :
| **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. |
| **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). |
| **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. |
| **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. |
| **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). |
| **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. |
| **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
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
```
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

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).
- 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 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 `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] 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] 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] 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] `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] 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).
### 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`).
- 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
commit, utilisez `scripts/check_secrets.py --staged`.
Le dry-run vérifie le pipeline sans appliquer les écritures de synchronisation ;
il ne remplace pas une vérification des paramètres réellement chargés.
Le dry-run autorise les lectures nécessaires à la simulation, mais n'applique aucune sortie
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

View File

@@ -32,7 +32,10 @@ def _parse_arguments(arguments: Sequence[str] | None = None) -> argparse.Namespa
"--dry-run",
action="store_true",
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(
"--log-level",

View File

@@ -11,6 +11,23 @@ from pronote_sync.errors import PipelineCriticalError, PipelineWarning
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(
mocker: MockerFixture,
) -> 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
blog_persistence: list[bool] = []
auth_persistence: list[bool] = []
class RecordingBlogState:
"""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)
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:
"""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
monkeypatch.setattr(run_module, "BlogRSSState", RecordingBlogState)
monkeypatch.setattr(run_module, "PronoteAuthState", RecordingAuthState)
monkeypatch.setattr(run_module, "PronoteClient", RecordingClient)
PipelineRunner.from_settings(
Settings(
app=AppSettings(dry_run=dry_run),
blog=BlogSettings(enabled=True),
pronote=PronoteSettings(auth_mode="qr_token"),
)
)
assert blog_persistence == [not dry_run]
assert auth_persistence == [not dry_run]
def test_runner_reuses_ical_download_and_parse_within_one_run(