docs(M11): align GUIDE sections 4.2.1 and 11.3 with FIXME_M11 corrections

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>
This commit is contained in:
2026-09-08 12:47:42 +02:00
parent 28c695795a
commit 2232e4fb7d
3 changed files with 121 additions and 85 deletions

View File

@@ -140,7 +140,7 @@
"filename": "GUIDE_DEV_PYTHON.md",
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
"is_verified": true,
"line_number": 5029,
"line_number": 5064,
"is_secret": false
}
],
@@ -177,5 +177,5 @@
}
]
},
"generated_at": "2026-09-08T00:29:02Z"
"generated_at": "2026-09-08T10:45:46Z"
}

View File

@@ -492,9 +492,12 @@ le contexte et le traceback complet.
> `redact_exception` est implémenté comme une **fonction au niveau du module** dans `utils/redaction.py`, et non comme une méthode de `RedactingFormatter` (contrairement à §4.2.2 où elle apparaît comme une méthode).
> `redact_url` utilise `urlsplit`/`urlunsplit`/`parse_qsl` au lieu de `urlparse`/`urlunparse`/`parse_qs`.
> La correspondance des clés sensibles est insensible à la casse.
> `redact_secrets()` trie les `extra_secrets` par longueur décroissante pour éviter les masquages partiels.
> `Settings.redaction_secrets()` retourne un tuple des secrets configurés (mots de passe Pronote, CalDAV, XMPP et clé API IA) à passer à `redact_exception`.
```python
import re
from typing import Iterable, SecretStr
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
@@ -543,6 +546,20 @@ def redact_secrets(text: str) -> str:
)
return text
def redact_exception(
exc: Exception, extra_secrets: Iterable[SecretStr | str] = ()
) -> str:
"""
Masque les secrets dans une exception.
:param exc: Exception à masquer.
:param extra_secrets: Secrets configurés à masquer dans le message.
:return: Message de l'exception avec les secrets masqués.
:rtype: str
"""
return redact_secrets(str(exc), extra_secrets)
```
#### 4.2.2 Configuration des logs (`logging.py`)
@@ -4576,6 +4593,7 @@ class PipelineRunner:
blog_rss_client: Optional["BlogRSSClient"] = None,
blog_state: Optional["## (section obsolète supprimée)"] = None,
dry_run: bool = False,
settings: "Settings" | None = None,
):
self.pronote_fetcher = pronote_fetcher
self.caldav_client = caldav_client
@@ -4587,13 +4605,19 @@ class PipelineRunner:
self.dry_run = dry_run
self._errors: List[PipelineError] = []
self._warnings: List[PipelineWarning] = []
self._redaction_secrets = settings.redaction_secrets() if settings else ()
def _redact(self, exc: Exception) -> str:
"""Masque les secrets configurés dans une exception."""
from ..utils.redaction import redact_exception
return redact_exception(exc, self._redaction_secrets)
def run(self) -> Tuple[Optional[PronoteData], List[PipelineError]]:
"""
Exécute le pipeline complet.
Returns:
Tuple (PronoteData final, liste des erreurs).
:return: Tuple (PronoteData final, liste des erreurs).
:rtype: tuple[PronoteData | None, list[PipelineError]]
"""
pronote_data: Optional[PronoteData] = None
agenda_diff = None
@@ -4630,6 +4654,8 @@ class PipelineRunner:
self.blog_state,
enabled=True,
)
except PipelineCriticalError:
raise
except PipelineError as e:
self._warnings.append(PipelineWarning(
message=f"Récupération du blog échouée: {e.message}",
@@ -4645,6 +4671,8 @@ class PipelineRunner:
pronote_data.lessons,
pronote_data.target_date,
)
except PipelineCriticalError:
raise
except PipelineError as e:
self._warnings.append(PipelineWarning(
message=f"Comparaison échouée: {e.message}",
@@ -4666,6 +4694,8 @@ class PipelineRunner:
step="sync",
))
logger.warning("Synchronisation CalDAV échouée (non bloquante)")
except PipelineCriticalError:
raise
except PipelineError as e:
self._warnings.append(PipelineWarning(
message=f"Synchronisation CalDAV échouée: {e.message}",
@@ -4683,6 +4713,8 @@ class PipelineRunner:
target_date=pronote_data.target_date,
)
synthesis_result = synthesis_step(self.synthesis_provider, synthesis_input)
except PipelineCriticalError:
raise
except PipelineError as e:
self._warnings.append(PipelineWarning(
message=f"Synthèse IA échouée: {e.message}",
@@ -4705,6 +4737,8 @@ class PipelineRunner:
# Étape 7: Envoi XMPP
try:
send_step(self.channel, xmpp_message)
except PipelineCriticalError:
raise
except PipelineError as e:
self._warnings.append(PipelineWarning(
message=f"Envoi XMPP échoué: {e.message}",
@@ -4718,8 +4752,7 @@ class PipelineRunner:
logger.error(f"Erreur critique dans le pipeline: {e.message}")
return None, [e]
except Exception as e:
from ..utils.redaction import redact_secrets
safe_error = redact_secrets(str(e))
safe_error = self._redact(e)
logger.error(f"Erreur inattendue dans le pipeline: {safe_error}")
return None, [PipelineCriticalError(
message=safe_error,
@@ -4735,6 +4768,8 @@ class PipelineRunner:
return self._warnings
```
Le ``PipelineRunner`` calcule ``self._redaction_secrets = settings.redaction_secrets()`` dans son constructeur. La méthode privée ``_redact(exc)`` délègue à ``redact_exception(exc, self._redaction_secrets)`` pour masquer les secrets configurés (mots de passe Pronote, CalDAV, XMPP et clé API IA). Chaque bloc ``except Exception`` utilise ``self._redact(exc)`` au lieu de ``redact_exception(exc)`` directement.
### 11.4 Étapes du pipeline (`pipeline/steps/`)

View File

@@ -233,6 +233,7 @@ Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et m
- [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] 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.
---