Commit Graph

40 Commits

Author SHA1 Message Date
4df930bfe6 fix: persister le token même dans le chemin d'erreur de get_informations/get_messages
Cause racine : quand pronotepy reçoit une PronoteAPIError (ex. code 20 « page
expirée »), il appelle refresh() en interne, qui peut roter le token en mémoire
(client.password mis à jour avec un nouveau jetonConnexionAppliMobile). Le retry
peut aussi échouer — l'exception atteint get_informations()/get_messages() qui
l'attrapent et retournent [] (mode dégradé). Mais _persist_credentials()
n'était appelé que dans le chemin de SUCCÈS — le token rafraîchi en mémoire
n'était jamais persisté. Au run suivant, token_login utilisait le token
périmé → échec KeyError 'dataSec' → PronoteAuthRotationError.

Correction : appeler _persist_credentials() aussi dans le chemin d'erreur de
get_informations() et get_messages(), avant le return []. L'ancien token est
déjà invalidé côté serveur lors du refresh — ne pas persister le nouveau token
garantit la perte du seul token valide.

Tests : 3 nouveaux tests (persistance dans le chemin d'erreur, sauvegarde du
token rafraîchi sur erreur). 694 passés, couverture 94.93%.
2026-09-10 13:34:51 +02:00
d26cef8d3d fix: persister le token après chaque opération de données + corriger doc QR code
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%)
2026-09-10 12:10:42 +02:00
0363898669 feat: authentification QR code / token pour Pronote
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>
2026-09-08 23:15:06 +02:00
3b38253575 fix: PRONOTE_URL ignoré à cause du double préfixe env_prefix
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>
2026-09-08 21:24:10 +02:00
82b9877aad fix: PRONOTE_ENT optionnel pour les connexions pronotepy directes
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>
2026-09-08 20:54:03 +02:00
fd9b604849 feat(M12): CLI entry point with dry-run, log-level, redacted error display
Implement the CLI entry point for pronote-sync:

cli/main.py:
- main() entry point with --dry-run (tri-state: None defers to settings,
  True overrides) and --log-level (choices: DEBUG/INFO/WARNING/ERROR/CRITICAL)
- setup_logging called before settings load (to capture config errors),
  then reconfigured with settings.app.log_level
- PipelineRunner.from_settings() as composition root, runner.run()
- Return codes: 0 success, 1 failure, 2 argparse rejection
- _safe_traceback: strips exception messages, replaces with "erreur expurgée",
  walks __cause__/__context__ with cycle protection
- _settings_secrets: collects redaction_secrets() + usernames + JID/recipient
- All error messages redacted via redact_secrets() with configured secrets
- DEBUG-level traceback only shown when DEBUG is enabled

cli/__init__.py:
- Module docstring added (French, Sphinx/reST)

tests/e2e/test_cli.py (8 tests):
- Dry-run and log-level propagation to composition root
- Configured dry-run preserved (tri-state None)
- Success with warnings returns 0
- Pipeline error redaction at DEBUG (sentinel secret)
- Configuration failure redacted traceback at DEBUG
- Pronote username non-disclosure
- Unexpected pipeline exception: redacted traceback at DEBUG, no traceback at INFO
- Argparse rejection of unknown log level (exit code 2)

Coverage: cli/ 94.74%, 627 total tests pass.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-08 15:57:28 +02:00
28c695795a fix(M11): propagate PipelineCriticalError, redact configured secrets, signal blog failures
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>
2026-09-08 12:20:29 +02:00
d7d31e14ff feat: orchestrer le pipeline M11
Co-authored-by: Codex/gpt-5.6-terra <codex-gpt-5-6-terra@agents.invalid>
2026-09-08 11:28:02 +02:00
b2106e75ac fix(M10): apply FIXME_M10 corrections (transport, dry_run, format, security)
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>
2026-09-08 02:16:28 +02:00
e07a6d709d feat: implement get_channel factory for XMPP channel (M10-U6)
get_channel(settings, dry_run=False) -> Channel | None with:
- enabled=False → None (no warning, no exception)
- enabled=True + missing jid/password/to/host → redacted warning log, None
- enabled=True + complete config → SyncXmppChannel instance
- Factory never raises exceptions (D2 non-blocking degradation)
- redact_secrets with extra_secrets=[password, jid, to] on warning logs

Re-exports Channel, XmppChannel, SyncXmppChannel from channels package.

19 unit tests covering disabled, misconfigured, complete, dry-run, and
secret-safe warning log scenarios.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:29:25 +02:00
1962e13eba feat: implement XmppChannel and SyncXmppChannel (M10-U4+U5)
XmppChannel sends direct messages via slixmpp ClientXMPP with:
- _format_message: 5 emoji sections (synthèse, agenda, devoirs, messages, infos)
  with sanitize_plaintext on all content (SEC-XMPP-06)
- send_async: public async method with connect, STARTTLS verification,
  send_message, disconnect lifecycle (D1, SEC-XMPP-04)
- send: sync wrapper via asyncio.run() for direct callers

SyncXmppChannel adapts async XmppChannel for synchronous pipeline use (D4):
- asyncio.run() when no event loop running (nominal pipeline)
- daemon thread with timeout when event loop already running
- Returns False on any error, never raises (non-blocking)

Security:
- auto_reconnect=False, failed_auth → disconnect + PipelineWarning (SEC-XMPP-04)
- __cause__ and __context__ cleared on all PipelineWarning raises (SEC-XMPP-05)
- redact_secrets with extra_secrets=[jid, password, to] on all logs (SEC-XMPP-02)
- STARTTLS features check post-connection, disconnect on failure (D1)
- dry-run mode logs redacted message without connecting

28 unit tests (20 channel + 8 adapter) covering success, errors, security.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:28:18 +02:00
dcf7f69c5a feat: add sanitize_plaintext for XMPP text sanitization (M10-U3)
Add sanitize_plaintext(text: str) -> str to utils/text.py for preparing
XMPP plain-text message bodies from untrusted Pronote/AI content.

- Strips HTML tags via BeautifulSoup (html.parser)
- Strips C0, DEL, and C1 control characters (preserves \t, \n, \r)
- Preserves Unicode including emojis (📌📅📚💬📢)
- Idempotent: f(f(x)) == f(x)
- Addresses SEC-XMPP-06: XMPP injection hardening

37 unit tests covering HTML, entities, control chars, emojis, idempotence.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:13 +02:00
7d765476de feat: define Channel Protocol for output channels (M10-U2)
Add @runtime_checkable Channel Protocol with send(XmppMessage) -> bool
as the structural contract for all output channels (XMPP, future CalDAV, etc).

6 unit tests covering protocol structure, conforming/non-conforming classes,
method signature introspection, and bool return type.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:01 +02:00
68a5d96c2a feat: add TLS policy and field constraints to XmppSettings (M10-U1)
Enforce TLS on non-loopback hosts via @field_validator on use_tls,
and add unconditional Field constraints on port (1-65535) and timeout (>0).

Security:
- use_tls=False rejected outside {localhost, 127.0.0.1, ::1} regardless of enabled
- field_validator on use_tls (not model_validator) prevents raw config leakage
- hide_input_in_errors=True as defense-in-depth
- Validation error messages contain no secrets (jid, password, recipient)

16 unit tests covering port/timeout bounds, TLS policy, loopback, secret safety.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:01:48 +02:00
a5a8183663 feat: add PipelineWarning for non-blocking pipeline errors (M10-U0)
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>
2026-09-07 21:01:36 +02:00
13e058f22c feat: add openai-compatible provider for custom AI endpoints
Add AI_PROVIDER=openai-compatible mode that reuses OpenAISynthesisProvider
with a validated custom base_url, allowing any OpenAI-compatible API
(OpenRouter, Ollama, LiteLLM proxy, etc.) without new code.

Configuration:
- AISettings.provider now accepts openai-compatible
- New AISettings.allow_insecure_http: bool = False (HTTP opt-in)
- .env.example: commented examples for OpenRouter (HTTPS) and Ollama (HTTP)

Factory validation (_validate_openai_compatible_config):
- base_url and model required, api_key required (MVP)
- HTTPS enforced unless allow_insecure_http=true
- Credentials in URL rejected, sensitive query params rejected
  (including valueless params via keep_blank_values=True)
- Malformed URLs and missing hostname rejected (ValueError caught)
- No /v1 manipulation; degraded to None + warning on invalid config
- redact_url() used for all URL warnings

Tests: 13 new factory tests in test_synthesis.py covering routing,
URL validation, HTTP policy, credentials, sentinel non-leak, no-network.
Coverage: 91.57% (synthesis module).

Docs: GUIDE_DEV_PYTHON.md §9.5 updated with 3-provider table, validation
rules, and synchronized code example.

mypy override for openai.* (follow_imports=skip) to work around
mypy 2.3.1 internal error in pre-commit's isolated environment.

Co-authored-by: opencode/coder anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/test-engineer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/tech-writer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
2026-09-07 19:44:40 +02:00
19cbf8f13f fix(M9): corrections d'audit FIXME_M9 — secrets, messages, validation, tests
Cinq corrections de l'audit FIXME_M9 :
- redact_secrets() étendue avec extra_secrets pour masquer les clés brutes ;
  providers stockent SecretStr jusqu'à l'appel SDK.
- _build_prompt() inclut le contenu des messages (tronqué à 500 car.) ;
  prompt système renforcé contre l'injection.
- _validate_output() supprime les emojis et rejette titre/liste/HTML → None.
- Tests litellm utilisent importorskip + LITELLM_LOCAL_MODEL_COST_MAP=true.
- .env.example désactive l'IA par défaut (AI_ENABLED=false).
- Documentation §9.2-§9.5 alignée avec l'implémentation (SDK openai, SecretStr,
  factory réelle, validation sortie, politique hors réseau).

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>
2026-09-07 19:01:25 +02:00
92833060e2 feat(M9): synthèse IA — protocole, providers OpenAI/litellm, factory, tests
Synthèse optionnelle via SDK openai (client injectable, prompt système
FR, max 800 car., timeout 30 s, temp 0.3). Mode dégradé strict :
generate() ne lève jamais, retourne None si clé absente/timeout/erreur.
Provider litellm optionnel (extra ai-litellm) réutilisant le prompt
OpenAI. Factory get_synthesis_provider() selon AISettings. 23 tests
sans réseau, couverture synthesis/ 93%.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 17:07:05 +02:00
5907c9aeaf fix(M8): corrections d'audit FIXME_M8 — appariement, date, déterminisme, validateur
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>
2026-09-07 15:59:17 +02:00
10e5f22501 feat(M8): comparateur d'agenda (AgendaComparator) dans sync/diff.py
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>
2026-09-07 13:39:22 +02:00
557555c65b refactor: déplacer normalize_subject vers utils/text.py avec ré-export
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>
2026-09-07 13:30:15 +02:00
a1bae41be8 fix(M7): corrections d'audit FIXME_M7 — sécurité, fenêtre, UID, timezone
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>
2026-09-07 12:24:22 +02:00
b4b0247919 feat(M7): synchronisation différentielle CalDAV
Implémente la synchronisation des événements Pronote vers un calendrier
CalDAV (Nextcloud) de façon idempotente et sécurisée.

Production :
- sync/serialization.py : sérialisation Lesson/Homework/SchoolEvent vers
  VEVENT, signature sémantique (exclut DTSTAMP/CREATED/LAST-MODIFIED),
  enveloppe VCALENDAR complète avec VERSION:2.0 et PRODID
- sync/caldav.py : passerelle CalDAV isolant caldav>=1.3.0, résolution du
  calendrier via principal().calendars() avec boundary matching, upsert par
  UID (fetch-then-save), exceptions expurgées et __context__ propre, mot de
  passe non stocké en clair, context manager
- sync/planner.py : calcul explicite du CalDAVSyncPlan (add/update/remove
  par comparaison de signatures sémantiques, routage par préfixe d'UID)
- sync/executor.py : exécution du plan avec dry-run (aucune écriture),
  isolation des erreurs par événement, statut FAILED/SKIPPED/SUCCESS
- sync/synchronizer.py : orchestration en trois phases (scan, plan,
  exécution), SKIPPED si CalDAV non configuré
- sync/__init__.py : export synchronize()
- sources/pronote/client.py : normalisation UID via normalize_pronote_uid/
  generate_deterministic_uid (parité avec ical.py)
- config/settings.py : CalDAVSettings durci (url SecretStr, validation
  HTTPS, allow_insecure_http pour localhost, serializer redact_url)

Tests (381 passés, couverture 95.58%) :
- tests/unit/test_sync_serialization.py (21 tests)
- tests/unit/test_caldav_planner.py (16 tests)
- tests/unit/test_caldav_executor.py (18 tests)
- tests/unit/test_caldav_gateway.py (24 tests)
- tests/unit/test_caldav_security.py (18 tests)
- tests/unit/test_uid_equivalence.py (8 tests)
- tests/integration/test_caldav_sync.py (11 tests, faux serveur en mémoire)
- tests/conftest.py : fixtures partagées

Documentation :
- GUIDE_DEV_PYTHON.md §7 : API réelle caldav>=1.3.0, principal().calendars(),
  VCALENDAR complet, upsert par UID, pas d'état local, événements non gérés
  protégés, CalDAVSettings durci (SecretStr, HTTPS, allow_insecure_http)
- TODO.md : M7 coché
- .env.example : CALDAV_ALLOW_INSECURE_HTTP=false

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>
2026-09-07 09:24:18 +02:00
1d26d49e74 fix(M6): corrections d'audit FIXME_M6 — normalisation, secret, tests, doc
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>
2026-09-06 23:30:47 +02:00
f9a1a5aa43 feat(M6): agenda théorique JSON avec parité des semaines et vacances scolaires
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>
2026-09-06 23:06:29 +02:00
4ec827a945 docs(M6): aligner guide, TODO et configuration pour l'agenda théorique JSON
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>
2026-09-06 22:30:50 +02:00
bfae1ca87f fix(M5): corrections d'audit — transport HTTP, statuts d'erreur, cache atomique
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>
2026-09-06 20:58:04 +02:00
6d1a7a649f feat(M5): source blog RSS — fetch, parsing, déduplication et état persistant
Implémentation complète de la source blog RSS du collège :
- BlogRSSClient (sources/blog/rss.py) : client sans état récupérant et
  parsant le flux via feedparser, avec déduplication par ensemble de
  GUIDs connus, cache HTTP conditionnel (ETag/Last-Modified), conversion
  HTML→texte (BeautifulSoup), tri déterministe (date desc puis id asc),
  et mode dégradé (flux invalide/erreur → warning expurgé + liste vide).
- BlogRSSFetchResult (sources/blog/result.py) : résultat immuable
  contenant articles, en-têtes de cache et indicateur not_modified.
- BlogRSSState (sources/blog/state.py) : persistance JSON tolérante
  (GUIDs triés, version, ETag, Last-Modified) avec redaction des chemins
  dans les logs.
- Fixture tests/fixtures/blog_rss.xml : flux RSS 2.0 anonymisé, 3
  articles, dates fixes, ordre non chronologique.
- 38 tests unitaires (22 client + 16 state) couvrant parsing nominal,
  déduplication intra-flux, 304, bozo, erreurs réseau, non-fuite de
  secrets, tri secondaire, persistance d'état et tolérance aux fichiers
  corrompus.
- Documentation : TODO.md M5 coché, GUIDE_DEV_PYTHON.md §5 bis aligné
  avec l'API livrée (known_guids, BlogRSSFetchResult, BlogRSSState).
- Configuration : feedparser ajouté aux additional_dependencies du hook
  mypy pre-commit pour aligner l'environnement isolé avec le .venv.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 20:23:49 +02:00
16f9b57dc2 fix(M4): modes explicites ical/pronotepy stricts — suppression du repli réciproque
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
2026-09-06 17:42:04 +02:00
1b550aa818 fix(M4): pronote_url + account_type et parsing iCal (STATUS:CANCELLED + blocs multi-dates)
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>
2026-09-06 15:23:34 +02:00
f2be495f55 feat(M4): sources/pronote/fallback.py — repli iCal/pronotepy
- 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>
2026-09-06 13:45:08 +02:00
f3177e70f6 feat(M4): sources/pronote/client.py — client pronotepy avec Protocol
- 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>
2026-09-06 13:32:54 +02:00
cb621c15f4 feat(M4): sources/pronote/ical.py — fetch, parsing et collecte des devoirs
- fetch_ical : HTTP via requests, support file:// (URI décodé),
  validation BEGIN:VCALENDAR, exceptions redactées
- get_calendar_name : extraction X-WR-CALNAME (paramètres + lignes repliées)
- parse_ical : parsing VEVENT → Lesson/SchoolEvent, détection cours
  annulé/déplacé, UID déterministe si absent, homeworks toujours vide
- collect_homeworks : deux passes (due + assigned), déduplication
  par ID, tri par (subject, text), target_date injecté
- generate_homework_id : SHA-1 12 chars (usedforsecurity=False)
- normalize_homework_text : unification whitespace/HTML/lowercase
- pre-commit : ajout de types-requests et icalendar au hook mypy

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-06 13:32:11 +02:00
f82e79360b feat(M4): hiérarchie d'erreurs, fixture iCal et correction TODO
- pronote_sync/errors.py : PronoteSyncError (base) + PipelineCriticalError
- tests/fixtures/pronote-4e.ics : fixture anonymisée (3 VEVENTs : cours
  normal avec devoirs, cours annulé, événement scolaire)
- TODO.md ligne 82 : normalize_uid → normalize_pronote_uid (alignement
  sur le code existant dans utils/uid.py)

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 13:20:24 +02:00
292aef4f5b fix(M3): immuabilité et validation des modèles — correction des écarts FIXME_M3
Immuabilité :
- Collections des modèles frozen : list → tuple (Lesson, TheoreticalLesson,
  SchoolEvent, AgendaDiff, Homework, XmppMessage, ExternalInfo)
- Gel de HomeworkBlock et ExternalInfo (sous-modèles imbriqués dans contrats)
- Valeurs par défaut immuables : Field(default=(), ...)

Validation de domaine :
- TheoreticalLesson.day_of_week : Field(ge=0, le=6)
- Lesson/TheoreticalLesson : start < end (validateur de modèle)
- SchoolEvent : from_date <= to_date
- AgendaChange : cohérence type/payload (ADDED/MODIFIED → lesson,
  REMOVED → theoretical_lesson)
- CalDAVSyncResult : compteurs Field(ge=0) sur added/updated/removed

Tests : 66 tests (construction/sérialisation, immuabilité, invariants)
couvrant le gel, la mutabilité légitime, et les cas invalides par invariant.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 12:36:27 +02:00
2f6184df0b fix(M2): configuration, secrets et rédaction — correction des écarts FIXME_M2
Sécurité :
- ical_url : str → SecretStr | None (masquage dans str/repr/model_dump_json)
- En-têtes Authorization/Proxy-Authorization : masquage complet de la valeur
- _URL_PATTERN : insensible à la casse (HTTPS:// reconnu)
- redact_url() : masquage du nom d'utilisateur (userinfo complet)

Configuration :
- Sous-configs : Field(default_factory=...) pour rechargement à chaque appel
- Suppression du singleton settings (injection de dépendances)
- .env.example : ajout AI_PROVIDER et CALDAV_CALENDAR_PATH
- Guide : BLOG_RSS_ENABLED → BLOG_ENABLED, AI_MODEL=None par défaut
- .secrets.baseline : ligne décalée 5112 → 5117 (faux positif audité)
- pre-commit : ajout de pytest aux additional_dependencies du hook mypy

Tests : 15 tests (config + redaction) couvrant rechargement, masquage
SecretStr, en-têtes Authorization, URL auth intégrée et casse variable.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 12:35:55 +02:00
aaca78c55d docs+fix: renforcement de l'architecture agentique et corrections de sécurité
AGENTS.md :
- Rôles d'agents renforcés : @coder ne valide pas, @debugger ne code pas,
  @verifier ne modifie pas, etc.
- Table « Séparation des rôles » : tâche → agent responsable → ne pas confier à
- Workflow étape 5 : délégation explicite à @verifier pour la validation

GUIDE_DEV_PYTHON.md :
- 7 notes « Décision d'implémentation » ajoutées aux sections concernées :
  3.1.1 (XMPP_RECIPIENT→XMPP_TO, vars ajoutées), 3.1.2 (SYNC_PAST_DAYS→AppSettings),
  3.2 (Pydantic v2 style, defaults corrigés), 4.2.1 (redact_exception module function),
  4.2.2 (getLevelNamesMapping), 5.1.5 (normalize_pronote_uid, usedforsecurity=False),
  6 (ConfigDict, StrEnum, alias _date, external_info Optional)

Sécurité (audit @security-auditor, corrections @coder, validation @verifier) :
- RedactingFormatter : redaction APRÈS formatage (corrige TypeError %s + fuite traceback)
- redact_url : masquage des credentials dans userinfo URL (HTTP Basic Auth)
- redact_secrets : patterns étendus (api_key, access_token, authorization, auth)
- redact_secrets : support JSON-style « key: value » avec guillemets

Validations (@verifier) :
- ruff check : PASS | mypy strict : PASS | bandit : PASS (0 issue)
- %s formatting : OK (password=REDACTED, pas de TypeError)
- Traceback redaction : OK (icalsecurise=REDACTED)
- URL userinfo : OK (user:REDACTED@host)
- JSON-style redaction : OK ({"token": "REDACTED"})
- Régression red-to-green : OK (historical HEAD reproduction)

Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
2026-09-05 23:52:02 +02:00
2e7dfe6e46 feat(M3): modèles de données Pydantic (22 modèles, 10 modules)
Modèles créés :
- models/agenda.py : Status, LessonStatus, HomeworkBlock, Lesson (frozen),
  SchoolEventKind, SchoolEvent (frozen), TheoreticalLesson (frozen)
- models/homework.py : Homework (frozen)
- models/message.py : MessageType, Message (frozen)
- models/blog.py : BlogArticle (frozen), ExternalInfo (mutable)
- models/diff.py : AgendaChangeType, AgendaChange (frozen), AgendaDiff (frozen)
- models/pronote.py : PronoteData (mutable)
- models/sync.py : CalDAVSyncStatus, CalDAVSyncPlan, CalDAVSyncResult (mutable)
- models/synthesis.py : SynthesisInput, SynthesisResult (mutable)
- models/xmpp.py : XmppMessage (frozen, external_info: ExternalInfo | None)
- models/__init__.py : ré-export des 22 modèles via __all__

Conventions appliquées :
- Pydantic v2 : model_config = ConfigDict(frozen=True), pas de json_encoders
- StrEnum au lieu de (str, Enum) (ruff UP042)
- Alias _date/_time pour éviter collision champ date/type date
- Sphinx/reST docstrings sur toutes les classes
- TODO.md : 11 items M3 cochés

Validations (@verifier) :
- ruff check : PASS (0 error)
- ruff format --check : PASS (10 fichiers)
- mypy strict : PASS (10 fichiers)
- bandit : PASS (0 issue)
- import 22 modèles : OK
- sérialisation JSON (frozen + mutable) : OK
- frozen immuable, PronoteData/CalDAVSyncResult mutable : OK
- StrEnum isinstance str : OK
- pytest : 0 test (infrastructure OK)

Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
2026-09-05 23:21:39 +02:00
c3f76ab91b feat(M2): configuration, gestion des secrets et logging
- 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>
2026-09-05 22:52:07 +02:00
a2efd61c4e Scaffold: préparation du projet pronote-sync
- 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)
2026-09-05 18:45:43 +02:00