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>
This commit is contained in:
@@ -850,6 +850,7 @@ from datetime import UTC, datetime
|
||||
from html import unescape
|
||||
|
||||
import feedparser
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from ..models.blog import BlogArticle
|
||||
@@ -891,16 +892,17 @@ class BlogRSSClient:
|
||||
indicateur ``304 Not Modified``.
|
||||
"""
|
||||
try:
|
||||
# Récupération du flux avec requête conditionnelle (ETag / Last-Modified)
|
||||
feed = feedparser.parse(
|
||||
self.rss_url,
|
||||
etag=etag,
|
||||
modified=last_modified,
|
||||
request_timeout=self.timeout,
|
||||
)
|
||||
# Transport HTTP séparé du parsing
|
||||
headers: dict[str, str] = {"user-agent": "pronote-sync"}
|
||||
if etag is not None:
|
||||
headers["If-None-Match"] = etag
|
||||
if last_modified is not None:
|
||||
headers["If-Modified-Since"] = last_modified
|
||||
|
||||
# Réponse 304 Not Modified : rien n'a changé, on restitue les en-têtes mémorisés
|
||||
if getattr(feed, "status", None) == 304:
|
||||
response = requests.get(self.rss_url, headers=headers, timeout=self.timeout)
|
||||
|
||||
# 304 Not Modified : pas de nouveaux articles
|
||||
if response.status_code == 304:
|
||||
return BlogRSSFetchResult(
|
||||
articles=(),
|
||||
etag=etag,
|
||||
@@ -908,10 +910,18 @@ class BlogRSSClient:
|
||||
not_modified=True,
|
||||
)
|
||||
|
||||
response_etag: str | None = getattr(feed, "etag", None)
|
||||
response_last_modified: str | None = getattr(feed, "modified", None)
|
||||
# Rejeter les statuts d'erreur (4xx/5xx)
|
||||
response.raise_for_status()
|
||||
|
||||
# Flux invalide (erreur HTTP, XML malformé, etc.) : résultat vide, sans erreur
|
||||
# Extraire les en-têtes de cache de la réponse (ETag / Last-Modified)
|
||||
response_etag: str | None = response.headers.get("ETag")
|
||||
response_last_modified: str | None = response.headers.get("Last-Modified")
|
||||
|
||||
# Parsing du contenu reçu (pas de l'URL)
|
||||
feed = feedparser.parse(response.content)
|
||||
|
||||
# Flux invalide (XML malformé, etc.) : résultat vide, sans erreur ;
|
||||
# les en-têtes de cache d'entrée sont conservés tels quels
|
||||
if getattr(feed, "bozo", None):
|
||||
logger.warning(
|
||||
"Flux RSS du blog invalide, ignoré : %s",
|
||||
@@ -919,8 +929,8 @@ class BlogRSSClient:
|
||||
)
|
||||
return BlogRSSFetchResult(
|
||||
articles=(),
|
||||
etag=response_etag,
|
||||
last_modified=response_last_modified,
|
||||
etag=etag,
|
||||
last_modified=last_modified,
|
||||
not_modified=False,
|
||||
)
|
||||
|
||||
@@ -988,7 +998,12 @@ class BlogRSSClient:
|
||||
except Exception as e:
|
||||
safe_url = redact_url(self.rss_url)
|
||||
logger.error(f"Échec de la récupération du flux RSS {safe_url}: {e}")
|
||||
return BlogRSSFetchResult(articles=(), not_modified=False)
|
||||
return BlogRSSFetchResult(
|
||||
articles=(),
|
||||
etag=etag,
|
||||
last_modified=last_modified,
|
||||
not_modified=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_date(date_tuple: tuple[int, ...] | None) -> datetime | None:
|
||||
|
||||
Reference in New Issue
Block a user