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%)
This commit is contained in:
2026-09-10 12:10:42 +02:00
parent 7dc48f6f43
commit d26cef8d3d
4 changed files with 240 additions and 14 deletions

View File

@@ -234,6 +234,24 @@ class PronoteClient:
self._client = self._connect_password()
return self._client
def _persist_credentials(self) -> None:
"""Persiste les credentials d'authentification après une opération réussie.
Le token pronotepy peut être rafraîchi (rotaté) par le serveur lors d'un
appel de données (agenda, devoirs, messages). Cette méthode persiste
systématiquement les credentials courantes pour garantir la disponibilité
du token valide au prochain run.
Ne fait rien si aucun :class:`PronoteAuthState` n'est configuré (mode
``password``) ou si le client n'est pas connecté.
"""
if self._auth_state is None or self._client is None:
return
try:
self._auth_state.save(self._client.export_credentials())
except Exception as exc:
logger.debug("Échec de la persistance des credentials : %s", redact_exception(exc))
def _connect_password(self) -> pronotepy.Client:
"""Connecte le client ``pronotepy`` en mode ``password``.
@@ -298,7 +316,8 @@ class PronoteClient:
try:
client = client_class.token_login(**creds)
if client.logged_in:
self._auth_state.save(client.export_credentials())
self._client = client
self._persist_credentials()
return client
# logged_in est False — le token est invalide
raise PronoteAuthRotationError(
@@ -324,8 +343,8 @@ class PronoteClient:
# Enrôlement : premier login via QR code (aucun credential persisté)
client = self._enroll_qr_code(client_class)
# Persister le token rotaté immédiatement
if self._auth_state is not None:
self._auth_state.save(client.export_credentials())
self._client = client
self._persist_credentials()
return client
def _enroll_qr_code(self, client_class: type[pronotepy.Client]) -> pronotepy.Client:
@@ -423,6 +442,7 @@ class PronoteClient:
read=message.seen,
)
)
self._persist_credentials()
return messages
except (
pronotepy.PronoteAPIError,
@@ -461,6 +481,7 @@ class PronoteClient:
read=info.read,
)
)
self._persist_credentials()
return messages
except (
pronotepy.PronoteAPIError,
@@ -529,6 +550,7 @@ class PronoteClient:
content=content.description if content is not None else None,
)
)
self._persist_credentials()
return lessons
def get_homeworks(self, start: date, end: date) -> list[Homework]:
@@ -566,4 +588,5 @@ class PronoteClient:
html=hw.description,
)
)
self._persist_credentials()
return homeworks