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>
35 lines
1.5 KiB
Python
35 lines
1.5 KiB
Python
"""Étape de récupération non bloquante des articles RSS du collège."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pronote_sync.models.blog import BlogArticle
|
|
from pronote_sync.sources.blog.rss import BlogRSSClient
|
|
from pronote_sync.sources.blog.state import BlogRSSState
|
|
from pronote_sync.utils.redaction import redact_exception
|
|
|
|
|
|
def fetch_blog_step(client: BlogRSSClient | None, state: BlogRSSState | None) -> list[BlogArticle]:
|
|
"""Récupère les articles RSS nouveaux en conservant l'état du client.
|
|
|
|
:param client: Client RSS configuré, ou ``None`` lorsque le blog est désactivé.
|
|
:param state: État de déduplication et de cache HTTP associé au run.
|
|
:return: Nouveaux articles du blog.
|
|
:rtype: list[BlogArticle]
|
|
:raises RuntimeError: Si la récupération RSS injectée échoue.
|
|
"""
|
|
if client is None or state is None:
|
|
return []
|
|
try:
|
|
etag, last_modified = state.get_cache_headers()
|
|
result = client.fetch_and_parse(
|
|
known_guids=state.get_known_guids(), etag=etag, last_modified=last_modified
|
|
)
|
|
if result.error is not None:
|
|
raise RuntimeError(result.error) from None
|
|
if not result.not_modified:
|
|
state.add_guids(article.id for article in result.articles)
|
|
state.update_cache_headers(result.etag, result.last_modified)
|
|
return list(result.articles)
|
|
except Exception as exc:
|
|
raise RuntimeError(f"Récupération du blog échouée : {redact_exception(exc)}") from None
|