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:
@@ -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
|
||||
@@ -4622,73 +4646,81 @@ class PipelineRunner:
|
||||
logger.warning(f"Étape 'normalize' échouée: {e.message}")
|
||||
return None, self._errors + self._warnings
|
||||
|
||||
# Étape 2 bis: Récupération du blog (RSS)
|
||||
if self.blog_rss_client and self.blog_state:
|
||||
try:
|
||||
blog_articles = fetch_blog_step(
|
||||
self.blog_rss_client,
|
||||
self.blog_state,
|
||||
enabled=True,
|
||||
)
|
||||
except PipelineError as e:
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Récupération du blog échouée: {e.message}",
|
||||
step="fetch_blog",
|
||||
))
|
||||
logger.warning(f"Étape 'fetch_blog' échouée (non bloquante): {e.message}")
|
||||
blog_articles = []
|
||||
# Étape 2 bis: Récupération du blog (RSS)
|
||||
if self.blog_rss_client and self.blog_state:
|
||||
try:
|
||||
blog_articles = fetch_blog_step(
|
||||
self.blog_rss_client,
|
||||
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}",
|
||||
step="fetch_blog",
|
||||
))
|
||||
logger.warning(f"Étape 'fetch_blog' échouée (non bloquante): {e.message}")
|
||||
blog_articles = []
|
||||
|
||||
# Étape 3: Comparaison avec l'agenda théorique
|
||||
try:
|
||||
agenda_diff = compare_step(
|
||||
self.agenda_comparator,
|
||||
pronote_data.lessons,
|
||||
pronote_data.target_date,
|
||||
)
|
||||
except PipelineError as e:
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Comparaison échouée: {e.message}",
|
||||
step="compare",
|
||||
))
|
||||
logger.warning(f"Étape 'compare' échouée (non bloquante): {e.message}")
|
||||
# Étape 3: Comparaison avec l'agenda théorique
|
||||
try:
|
||||
agenda_diff = compare_step(
|
||||
self.agenda_comparator,
|
||||
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}",
|
||||
step="compare",
|
||||
))
|
||||
logger.warning(f"Étape 'compare' échouée (non bloquante): {e.message}")
|
||||
|
||||
# Étape 4: Synchronisation CalDAV
|
||||
try:
|
||||
sync_result = caldav_sync_step(
|
||||
self.caldav_client,
|
||||
pronote_data.lessons,
|
||||
pronote_data.homeworks,
|
||||
pronote_data.school_events,
|
||||
)
|
||||
if sync_result and sync_result.status.value == "failed":
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Synchronisation CalDAV échouée: {sync_result.errors}",
|
||||
step="sync",
|
||||
))
|
||||
logger.warning("Synchronisation CalDAV échouée (non bloquante)")
|
||||
except PipelineError as e:
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Synchronisation CalDAV échouée: {e.message}",
|
||||
step="sync",
|
||||
))
|
||||
logger.warning(f"Étape 'sync' échouée (non bloquante): {e.message}")
|
||||
# Étape 4: Synchronisation CalDAV
|
||||
try:
|
||||
sync_result = caldav_sync_step(
|
||||
self.caldav_client,
|
||||
pronote_data.lessons,
|
||||
pronote_data.homeworks,
|
||||
pronote_data.school_events,
|
||||
)
|
||||
if sync_result and sync_result.status.value == "failed":
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Synchronisation CalDAV échouée: {sync_result.errors}",
|
||||
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}",
|
||||
step="sync",
|
||||
))
|
||||
logger.warning(f"Étape 'sync' échouée (non bloquante): {e.message}")
|
||||
|
||||
# Étape 5: Synthèse IA (optionnelle)
|
||||
if self.synthesis_provider and agenda_diff:
|
||||
try:
|
||||
synthesis_input = SynthesisInput(
|
||||
agenda_diff=agenda_diff,
|
||||
messages=pronote_data.messages,
|
||||
school_events=pronote_data.school_events,
|
||||
target_date=pronote_data.target_date,
|
||||
)
|
||||
synthesis_result = synthesis_step(self.synthesis_provider, synthesis_input)
|
||||
except PipelineError as e:
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Synthèse IA échouée: {e.message}",
|
||||
step="synthesis",
|
||||
))
|
||||
logger.warning(f"Étape 'synthesis' échouée (non bloquante): {e.message}")
|
||||
# Étape 5: Synthèse IA (optionnelle)
|
||||
if self.synthesis_provider and agenda_diff:
|
||||
try:
|
||||
synthesis_input = SynthesisInput(
|
||||
agenda_diff=agenda_diff,
|
||||
messages=pronote_data.messages,
|
||||
school_events=pronote_data.school_events,
|
||||
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}",
|
||||
step="synthesis",
|
||||
))
|
||||
logger.warning(f"Étape 'synthesis' échouée (non bloquante): {e.message}")
|
||||
|
||||
# Étape 6: Construction du message XMPP
|
||||
xmpp_message = XmppMessage(
|
||||
@@ -4702,29 +4734,30 @@ class PipelineRunner:
|
||||
) if blog_articles else None,
|
||||
)
|
||||
|
||||
# Étape 7: Envoi XMPP
|
||||
try:
|
||||
send_step(self.channel, xmpp_message)
|
||||
except PipelineError as e:
|
||||
self._warnings.append(PipelineWarning(
|
||||
message=f"Envoi XMPP échoué: {e.message}",
|
||||
step="send",
|
||||
))
|
||||
logger.warning(f"Étape 'send' échouée (non bloquante): {e.message}")
|
||||
# É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}",
|
||||
step="send",
|
||||
))
|
||||
logger.warning(f"Étape 'send' échouée (non bloquante): {e.message}")
|
||||
|
||||
return pronote_data, self._errors + self._warnings
|
||||
|
||||
except PipelineCriticalError as e:
|
||||
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))
|
||||
logger.error(f"Erreur inattendue dans le pipeline: {safe_error}")
|
||||
return None, [PipelineCriticalError(
|
||||
message=safe_error,
|
||||
step="unknown",
|
||||
)]
|
||||
except Exception as e:
|
||||
safe_error = self._redact(e)
|
||||
logger.error(f"Erreur inattendue dans le pipeline: {safe_error}")
|
||||
return None, [PipelineCriticalError(
|
||||
message=safe_error,
|
||||
step="unknown",
|
||||
)]
|
||||
|
||||
def get_errors(self) -> List[PipelineError]:
|
||||
"""Récupère la liste des erreurs."""
|
||||
@@ -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/`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user