docs: define strict dry-run contract
# Conflicts: # .secrets.baseline
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. |
|
||||||
|
|
||||||
|
|||||||
@@ -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
14
TODO.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user