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>
Crée data/school_holidays.json avec les dates officielles de l'académie de Bordeaux (zone A) pour 2026-2027. Corrige l'erreur au démarrage quand SCHOOL_HOLIDAYS_PATH pointe vers un fichier inexistant. Documentation wiki : page GuideAgendaVacancesScolaires créée et liée dans le sidebar.
Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Complete the M15 documentation and final review milestone:
README.md (French, brief):
- Project description, quick start, usage, deployment link, acknowledgments
- Acknowledgments: pronotepy, icalendar, caldav, slixmpp, pydantic, openai,
feedparser, beautifulsoup4, and inspiration from pronote-digest (Antoine Coulon)
- Author: Antoine Van Elstraete
README.LLM.md (English, AI agent guide):
- Prerequisites, installation on LXC/VPS (Debian/CentOS)
- Non-secret configuration preparation (all env vars listed)
- Secret variables clearly identified as operator-only
- Pre-deployment checks (check_secrets.py, pip check, dry-run)
- systemd/timer/logrotate installation steps
- Notes for AI agent (do not commit secrets, do not modify existing files)
LICENSE:
- Standard MIT License, copyright Antoine Van Elstraete (2026)
CHANGELOG.md:
- Initial changelog following Keep a Changelog format
- Covers M1 through M14 with milestone summaries
- Gitea Actions noted as planned/optional, not delivered
.gitignore:
- Added FEAT_* pattern for temporary work files
TODO.md:
- M15 items 1-4 checked (README, architecture docs, CHANGELOG+LICENSE, final review)
- M15 item 5 (Gitea Actions) left unchecked (optional, not done)
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
Correct five findings from independent review and security audit of the
M14 deployment secret scanner:
scripts/check_secrets.py:
- Regex: \b[a-z0-9_]* prefix before sensitive keywords to detect
PRONOTE_PASSWORD, CALDAV_PASSWORD, AI_API_KEY and similar prefixed
variable names (was: \b which doesn't match before underscore)
- Regex: minimum secret value length reduced from {8,} to {3,} for
literal, unquoted, and URL parameter patterns
- Regex: unquoted pattern {3,} -> {2,} for 3-char total minimum
- --staged: reads Git index blobs via `git show :<path>` instead of
working-tree files (ContentProvider type alias, _staged_content_provider)
- Extensionless deployment files: _EXTRA_NAMES allowlist for pronote_sync
- ContentProvider type alias documented with #: Sphinx comment
tests/unit/test_check_secrets.py (4 new tests, 9 total):
- test_main_detects_prefixed_secret_assignment: PRONOTE_PASSWORD detected
- test_main_detects_short_secret_assignment: 6-char secret detected
- test_staged_mode_reads_index_content_not_working_tree: working-tree
content set to non-matching value to distinguish index from worktree
- test_main_scans_extensionless_deployment_file: pronote_sync scanned
TODO.md: all 5 M14 checklist items checked
Validation: 636 tests, coverage 95.67%, ruff/mypy/bandit/pre-commit green.
Co-authored-by: opencode/coder <coder@agents.invalid>
Finalize M13 test and coverage milestone:
tests/fixtures/pronote-6e.ics (new):
- Anonymized iCal fixture for Classe de 6e (3 VEVENTs: SVT lesson with
homework block, Histoire-Géo modified lesson, all-day school outing)
- Same structure as pronote-4e.ics, no secrets or real data
tests/conftest.py:
- Added sample_message fixture (Message with MessageType.INFORMATION)
- Integrated sample_message into pronote_data fixture (messages=[sample_message])
- Sphinx/reST docstring with :return: and :rtype:
.gitignore:
- Fixed typo: .worktress/ -> .worktrees/ (line 56)
pyproject.toml:
- Added ".worktrees" to ruff extend-exclude to prevent ruff format --check .
from scanning worktree files
TODO.md:
- Checked M13 items: tests/fixtures/ and tests/conftest.py
Validation: 627 tests pass, coverage 95.67% (threshold 90%), ruff/mypy/
bandit/pre-commit all green.
Co-authored-by: opencode/coder <coder@agents.invalid>
GUIDE_DEV_PYTHON.md:
- §11.3: add except PipelineCriticalError: raise before each non-critical
except in the illustrative PipelineRunner.run() code
- §11.3: replace redact_exception(exc) with self._redact(exc) in all except
blocks, add explanatory paragraph about _redaction_secrets and _redact()
- §11.3: fix Google-style Returns: to Sphinx/reST :return: and :rtype:
- §11.3: fix malformed Markdown code fence (get_errors/get_warnings orphaned)
- §4.2.1: fix redact_exception() example to pass extra_secrets to
redact_secrets() in the return statement
TODO.md M11:
- Add and check criterion: PipelineCriticalError from non-blocking step
stops the pipeline
.secrets.baseline:
- Line numbers updated for documentation shifts
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
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>
Update GUIDE_DEV_PYTHON.md, TODO.md, and AGENTS.md to reflect the
decisions and work done in the FEAT_M9 and FIXME_M9 sessions.
GUIDE_DEV_PYTHON.md:
- Header: add entry in recent updates
- 3.1.2: AI_PROVIDER now documents openai-compatible with
Literal type; add AI_ALLOW_INSECURE_HTTP row; move decision
block after table to fix rendering
- 3.2: AISettings code block updated with openai-compatible and
allow_insecure_http field; decision note extended
- 3.1.3: .env.example adds OpenRouter (HTTPS) and Ollama (HTTP)
examples, both commented
TODO.md:
- M9 factory line now mentions openai-compatible with URL validation
- Add FEAT_M9 and FIXME_M9 notes after M9 acceptance criteria
AGENTS.md:
- Section 5: new subsection for openai-compatible provider contract
documenting validation rules, degraded mode, and security constraints
Co-authored-by: opencode/tech-writer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@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>
M8 livré : AgendaComparator dans sync/diff.py avec matching déterministe,
tolérance ±15 min, normalisation NFKC des matières, REMOVED par existence.
Le critère d'acceptation 3 (absence de THEORETICAL_AGENDA_PATH) est couvert
par design et reporté à M11 (composition root).
Co-authored-by: opencode/coder <coder@agents.invalid>
Couvre : agendas vides, ADDED/REMOVED/MODIFIED, tolérance ±15 min
(bord inclusif), normalisation NFKC des matières, matching multi-candidats
par plus petit id, comparaison ordre-insensible des enseignants/salles,
statut != NORMAL, REMOVED par existence (pas par sélection), ordre
déterministe et idempotence. Couverture de sync/diff.py : 92%.
Co-authored-by: opencode/test-engineer <test-engineer@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>