Cause racine : pronotepy peut rafraîchir (rotater) le token en mémoire pendant
l'exécution via refresh() automatique après une PronoteAPIError. L'ancien code
ne persistait les credentials qu'après le login initial, pas après les
opérations de données. Le token roté en mémoire était perdu → au run suivant,
token_login échouait avec le token périmé (KeyError 'dataSec').
Correction :
- PronoteClient._persist_credentials() : méthode centralisée qui persiste
export_credentials() après chaque opération réussie (get_lessons,
get_homeworks, get_messages, get_informations)
- Le token rafraîchi par le serveur pendant l'exécution est maintenant
toujours persisté, même si le pipeline échoue ensuite
Documentation :
- .env.example : variables QR plus visibles (exemple qr_token décommentable)
- AGENTS.md : QR code depuis le site web Pronote (pas l'app mobile),
persistance après chaque opération de données
- Wiki GuidePronote : procédure corrigée (site web, pas app Android/iOS),
mention de la persistance après chaque opération
Tests : 5 nouveaux tests de persistance (691 passés, couverture 94.92%)
Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative
au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect
où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML).
Nouveaux éléments :
- PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr)
- PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json
(écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW)
- PronoteClient._connect_qr_token() : token_login avec creds persistés,
qrcode_login pour l'enrôlement initial, export_credentials persisté après
chaque login réussi
- PronoteAuthRotationError : levée en cas d'échec de rotation du token,
propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à
PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif)
- _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL
- _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR)
dans tous les logs du chemin d'authentification
Documentation :
- .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN
- AGENTS.md : contrat d'authentification QR code / token
- Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement
Tests (686 passés, couverture 94.87%) :
- 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation,
4 tests intégration rotation end-to-end, 4 tests fallback mode-aware
- Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR
Co-authored-by: coder/litellm/coder <coder@agents.invalid>
Le champ pronote_url dans PronoteSettings avec env_prefix=PRONOTE_ produisait PRONOTE_PRONOTE_URL au lieu de PRONOTE_URL. Renomme le champ en url pour que le mécanisme standard produise PRONOTE_URL. Toutes les références mises à jour dans le code de production et les tests. Décision d'architecture : renommage préféré à un contournement par alias (mypy + dette technique).
Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
PRONOTE_ENT était incorrectement traité comme obligatoire pour pronotepy. Rend ENT optionnel pour les connexions directes, conformément à la spécification. Corrige le mode pronotepy explicite et le mode auto sans iCal. 10 tests de régression ajoutés.
Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Correct 4 findings from the independent M11 review:
#1 (Critical) — PipelineCriticalError was downgraded to PipelineWarning:
- Add except PipelineCriticalError: raise before each except Exception
in all 5 non-blocking steps (fetch_blog, compare, caldav_sync, synthesis, send)
- Critical errors now propagate to the outer handler and stop the pipeline
#2 (Critical) — redact_exception() did not use configured secrets:
- Extend redact_exception() with extra_secrets parameter (upward compatible)
- Harden redact_secrets(): sort extra_secrets by length descending
- Add Settings.redaction_secrets() collecting all 6 SecretStr fields
- Add PipelineRunner._redact(exc) using self._redaction_secrets
- All except blocks in run() now use self._redact(exc)
- CalDAV FAILED-status path uses full redaction_secrets collection
#3 (Medium) — BlogRSSClient silently swallowed failures:
- Add error field to BlogRSSFetchResult
- rss.py sets error on failure paths (except Exception, bozo/invalid feed)
- fetch_blog_step raises RuntimeError when result.error is set
- PipelineRunner now produces PipelineWarning for blog failures
#4 (Medium) — Test coverage at 80%, now 91%:
- 11 new integration tests covering blog failure/success, compare failure,
CalDAV failure (exception + FAILED status), send False/exception,
PipelineCriticalError propagation, secret redaction with sentinel,
empty agenda/homework, iCal cache cleanup
- Secret redaction test uses mock (no network) and proves configured-secret
propagation via non-URL sentinel in RuntimeError
Validation: 619 tests pass, ruff/mypy/bandit/pre-commit green, coverage 91%.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Fix all 8 findings from the independent review (FIXME_M10.md):
#1 Transport compatible with slixmpp 1.17.0 (D5):
- Use real ClientXMPP type (remove Any), JID with resource
- connect(host, port) explicit, no use_tls kwarg
- enable_direct_tls/enable_starttls configured before connect
- Single timeout via asyncio.Future for session_start/failed_auth/disconnected
- Remove premature 'starttls' in features check, remove auto_reconnect
- try/finally guarantees disconnect on all paths (#4)
#2 Factory dry_run no longer bypassed (D6):
- Single send() entry point in SyncXmppChannel
- dry_run check before any ClientXMPP creation
- Remove XmppChannel.send() dual implementation
#3 Thread daemon removed — single asyncio.run(), documented limitation
#5 Richer message format:
- Target date header, change type [Ajouté/Supprimé/Modifié]
- Lesson times, homework due date, message author
- No pronote_messages duplication (external_info = blog + other_info only)
#6 Error contract unified (D6):
- Channel.send() -> bool never raises PipelineWarning
- Errors logged with redaction, returns False
- PipelineWarning(step='xmpp') will be created by pipeline M11
#7 Tests faithful to slixmpp 1.17.0 API:
- FakeClientXMPP with real connect(host,port)/disconnect() signatures
- Assertions on host, port, resource, mtype='chat'
- No RuntimeWarning from unawaited coroutines
#8 .secrets.baseline restored from main
Coverage: 96.44% on channels/, 600 tests pass, pre-commit all-files green.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Add PipelineWarning(PronoteSyncError) to the canonical error hierarchy.
This non-blocking warning type is used by the XMPP channel (and future
channels) to signal recoverable failures without breaking the pipeline.
- PipelineWarning inherits from PronoteSyncError, not Warning builtin
- Constructor: (message, step=None) with recoverable=True
- 8 unit tests covering inheritance, raising, catching, attributes
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
Quatre corrections bloquantes/majeures de l'audit FIXME_M8 :
- Appariement un-à-un déterministe (consommation du candidat sélectionné) ;
1 réel / 2 théoriques → 1 REMOVED, 2 réels / 1 théorique → 1 ADDED.
- Filtrage strict par date : les cours réels hors target_date sont exclus
du matching avec un warning logé (décision architecte : pas d'exception).
- Déterminisme des détails : formatage via sorted(set(...)) au lieu de
set(...) brut, indépendant de PYTHONHASHSEED.
- Validateur AgendaChange strict : ADDED = lesson seule, REMOVED =
theoretical_lesson seule, MODIFIED = les deux requis.
- Comparaison à la minute près dans _is_modified (cohérent avec _matches).
- Documentation §8.4/§8.5 alignée avec l'implémentation (tolérance 15 min,
API compare(), normalize_subject référencé, appariement consommé).
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
Comparaison déterministe entre l'agenda réel (Lesson) et l'agenda
théorique (TheoreticalLesson) produisant un AgendaDiff (ADDED/REMOVED/
MODIFIED). Matching par jour + tolérance ±15 min symétrique + matière
normalisée ; tri des candidats par id stable. REMOVED par existence
(non-appariement), pas par sélection. Comparaison ordre-insensible des
enseignants et salles via set(). Détection MODIFIED incluant les horaires,
la matière, les enseignants, les salles et le statut.
Co-authored-by: opencode/coder <coder@agents.invalid>
La normalisation des matières (NFKC + espaces + ponctuation + minuscules)
est désormais dans pronote_sync/utils/text.py pour permettre son partage
entre sources/theoretical/file.py et sync/diff.py (M8) sans couplage de
couche. L'import depuis file.py est préservé par ré-export explicite.
Co-authored-by: opencode/coder <coder@agents.invalid>
Corrige les 5 constats de l'audit FIXME_M7 :
#1 (Bloquant) — Protection des événements non marqués :
- upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant
modification ; lève PronoteSyncError en cas de collision avec un
événement non géré (aucune écriture)
- delete_event() vérifie le marqueur ; no-op avec warning si non géré
- Méthode privée _is_managed_event() factorisant le contrôle
#2 (Bloquant) — Fenêtre de synchronisation :
- Calcul en journées entières (minuit à minuit exclusif)
- Filtrage des données locales (lessons, homeworks, school_events) avant
passage au planner
- Paramètre now injectable pour les tests
#3 (Bloquant) — UID canonique vs brut :
- list_managed_events() retourne (raw_uid, canonical_uid, vevent)
- compute_plan() matche par UID canonique, route les raw UID vers
*_to_remove, retourne le mapping remote_raw_by_canonical
- executor.execute() utilise le raw UID pour les mises à jour (pas de
doublon)
- Pas de migration destructive des UID distants existants
#4 (Correction) — Normalisation temporelle UTC :
- normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris →
UTC ; consciente → UTC
- Utilisée par generate_deterministic_uid() et component_to_signature()
- Deux représentations du même instant → même UID et même signature
#5 (Compatibilité) — date_search déprécié :
- Remplacement par calendar.search(start, end, event=True, expand=True)
Documentation :
- GUIDE_DEV_PYTHON.md : suppression des références obsolètes à
sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV
(search au lieu de date_search, upsert par UID)
- TODO.md : M7 décoché (corrections en cours de validation)
Tests : 390 passés, couverture 95.61%
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
Corrige les 5 points de l'audit FIXME_M6 :
1. Normalisation des matières : fonction normalize_subject (NFKC +
unification des espaces + suppression ponctuation + minuscule)
partagée par la génération d'ID et le futur comparateur M8.
2. Expurgation du secret dans l'erreur de collision d'IDs :
redact_secrets enveloppe l'identifiant dans le message.
3. Test even/odd avec même matière pour isoler la parité comme seul
différenciateur d'ID ; tests de normalisation (casse, espaces,
Unicode) ; test de non-fuite de secret.
4. TODO.md M6 : 8 items cochés après validation.
5. GUIDE_DEV §8.4 : bloc de code corrigé (clôture, types Lesson/
TheoreticalLesson, comparaison des horaires en minutes, début ET
fin, référence à normalize_subject).
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Implémentation complète de la source d'agenda théorique :
- model.py : modèles Pydantic de parsing JSON (TheoreticalLessonEntry,
TheoreticalAgendaFile) avec validation des formats d'heure et de l'ordre
début/fin.
- parity.py : WeekParityService déterministe calculant la parité d'une
semaine (paire/impaire) à partir d'une date de référence.
- holidays.py : SchoolHolidayCalendar lisant un fichier JSON de vacances
scolaires (zone A) avec bornes inclusives.
- provider.py : protocole TheoreticalAgendaProvider (get_lessons,
get_lessons_for_range).
- file.py : JsonTheoreticalAgendaProvider implémentant le protocole :
filtrage par parité et vacances, génération d'IDs déterministes
incluant le type de semaine, validation de l'unicité des IDs,
tri stable par identifiant.
- __init__.py : factory get_theoretical_provider câblant la
configuration (None si désactivé, erreur si config de parité partielle).
- Fixtures : theoretical.json (9 leçons all/even/odd) et
school_holidays.json (zone A, 4 périodes).
- 57 tests unitaires couvrant parsing, parité, vacances, provider,
factory, déduplication de range, collisions d'IDs.
- Guide : §8 et §12 alignés avec le format JSON.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Met à jour la documentation et la configuration pour le jalon M6 selon
les décisions d'architecture :
- GUIDE_DEV_PYTHON.md §8 : remplace iCal/CSV par JSON avec parité de
semaine (paire/impaire) et calendrier de vacances scolaires séparé
- TODO.md M6 : nouveaux items (WeekParityService, SchoolHolidayCalendar,
configuration, fixtures JSON)
- .env.example : THEORETICAL_AGENDA_PATH passe en .json, ajout de
SCHOOL_HOLIDAYS_PATH, THEORETICAL_WEEK_ANCHOR_DATE et
THEORETICAL_WEEK_ANCHOR_TYPE
- AppSettings : 3 nouveaux champs (school_holidays_path,
theoretical_week_anchor_date, theoretical_week_anchor_type)
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
Corrige les 5 points de l'audit FIXME_M5 :
1. (Bloquant) Sépare transport HTTP et parsing : utilise requests.get()
avec timeout explicite et en-têtes conditionnels, puis transmet le
contenu à feedparser.parse() — supprime le paramètre inexistant
request_timeout qui faisait échouer toute récupération réelle.
2. Rejette les statuts HTTP 4xx/5xx via raise_for_status() avant le
parsing.
3. Préserve les validateurs de cache (etag, last_modified) d'entrée sur
les chemins d'échec (exception, bozo) au lieu de les écraser à None.
4. Sauvegarde atomique de BlogRSSState : écrit dans un .tmp puis
Path.replace() pour éviter la corruption sur interruption.
5. Déduplication normale silencieuse : les GUID déjà connus sont
ignorés sans warning ; seuls les doublons intra-flux génèrent un
avertissement.
Tests : 49 tests (32 client + 17 state) dont 11 nouveaux couvrant
transport HTTP réel, statuts 401/404/500, préservation des validateurs,
en-têtes conditionnels, doublons intra-flux et sauvegarde atomique.
Guide : §5 bis.7.1 aligné avec le nouveau pattern transport/parsing.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Les modes explicites (ical, pronotepy) ne basculent plus silencieusement vers
l'autre source en cas d'échec. Seul le mode auto conserve le repli iCal →
pronotepy sur exception. Conforme au contrat de TODO.md M4 et AGENTS.md.
- fallback.py : _agenda_sources() et _homework_sources() retournent
(source, None) en mode explicite ; docstrings mises à jour.
- test_fallback.py : 4 tests remplacés, 4 tests ajoutés (non-repli +
empty-list-is-success), 4 tests ajustés ; :rtype: None ajouté.
Co-authored-by: coder/coder coder@agents.invalid
Co-authored-by: test-engineer/test-engineer test-engineer@agents.invalid
Configuration (D1+D3) :
- PronoteSettings : ajout pronote_url (str | None) et account_type
(Literal student/parent, défaut parent)
Parsing iCal (Fix 4+7) :
- parse_ical : détection STATUS:CANCELLED en plus de CATEGORIES
- parse_body : list[tuple[date, str]] au lieu de dict[date, str]
pour préserver les blocs multiples à la même date
- parse_homework_blocks : adapté aux listes
Co-authored-by: opencode/coder <coder@agents.invalid>
- AgendaSource : enum StrEnum (AUTO/ICAL/PRONOTEPY)
- PronoteFetcherProtocol : Protocol consommé par le pipeline
- PronoteFetcher : unifie iCal et pronotepy avec repli automatique
- AUTO : iCal d'abord, repli pronotepy, PipelineCriticalError si
les deux échouent
- ICAL/PRONOTEPY : source explicite, erreur redactée puis
PipelineCriticalError
- Messages et informations : toujours via pronotepy
- Avertissement quand le repli pronotepy retourne un résultat vide
après échec iCal (limite du mode dégradé)
Co-authored-by: opencode/coder <coder@agents.invalid>
- PronoteClientProtocol : Protocol (get_messages, get_informations,
get_agenda_fallback) pour la logique de repli
- PronoteClient : encapsule pronotepy.Client (connexion paresseuse)
avec API réelle 2.15.7 (lessons/homework/discussions/information_and_surveys)
- Erreurs PronoteAPIError et ValueError catchées, loggées avec
redact_exception, retournent des résultats vides
- Aucune fuite de secret (get_secret_value() uniquement à la connexion)
- pre-commit : ajout de pronotepy au hook mypy
Co-authored-by: opencode/coder <coder@agents.invalid>
- config/settings.py : modèles Pydantic Settings (Pronote, CalDAV, XMPP,
AI, Blog, App) avec SecretStr pour les mots de passe et clés API
- config/env.py : fonction load_settings() pour le chargement du .env
- utils/redaction.py : redact_url, redact_secrets, redact_exception
(masquage des tokens icalsecurise, mots de passe et URLs sensibles)
- utils/logging.py : setup_logging + RedactingFormatter
(masquage automatique des secrets dans les logs)
- utils/uid.py : normalize_pronote_uid (suppression suffixes temporels)
et generate_deterministic_uid (hash SHA-1 usedforsecurity=False)
- .env.example : aligné sur les modèles finaux (XMPP_TO, XMPP_HOST,
XMPP_ENABLED, BLOG_ENABLED, etc.)
- .pre-commit-config.yaml : ajout pydantic + pydantic-settings aux
additional_dependencies du hook mypy
- TODO.md : items M2 cochés
Décisions d'architecture (@architect) :
- XmppSettings : modèle complet §10.2.3, tous champs optionnels
- sync_past_days/future_days déplacés vers AppSettings (sans préfixe)
- AISettings.enabled = False par défaut
- BlogSettings inclus dès M2
- redact_exception comme fonction module (pas méthode)
- normalize_pronote_uid (nom du guide et des tests)
Validations :
- ruff check : PASS
- ruff format --check : PASS
- mypy strict : PASS (7 fichiers)
- bandit : PASS (0 issue)
- import settings : OK (toutes valeurs par défaut)
- redact_secrets/icalsecurise : masqué en REDACTED
- logging : secret masqué dans la sortie
- uid normalize : idempotent, deterministic OK
- SecretStr : pas de fuite dans repr
- pytest : 0 test (infrastructure OK)
Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
- Guide de développement (GUIDE_DEV_PYTHON.md) : spécification complète
- TODO.md : 15 jalons de développement (M1-M15) avec étapes et critères d'acceptation
- AGENTS.md : guide de contribution pour agents et développeurs
- pyproject.toml : configuration projet (dépendances, ruff, mypy strict, pytest)
- .gitignore : exclusion venv, bytecode, .env, caches, state files
- .env.example : template des variables d'environnement (placeholders)
- Structure du package pronote_sync/ (14 sous-packages avec __init__.py)
- tests/ avec conftest.py et fixtures/
- Environnement virtuel .venv/ (Python 3.14)