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:
@@ -88,11 +88,15 @@ class BlogRSSState:
|
||||
)
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Sauvegarde l'état dans le fichier JSON.
|
||||
"""Sauvegarde l'état dans le fichier JSON de manière atomique.
|
||||
|
||||
La sortie est déterministe : ``known_guids`` est trié
|
||||
alphabétiquement et le champ ``version`` vaut 1. En cas d'erreur
|
||||
d'écriture, une erreur est journalisée sans être propagée.
|
||||
alphabétiquement et le champ ``version`` vaut 1. Le JSON est
|
||||
d'abord écrit dans un fichier temporaire du même répertoire, puis
|
||||
remplacé atomiquement par :meth:`~pathlib.Path.replace` afin de ne
|
||||
jamais laisser un fichier partiel en cas d'interruption. En cas
|
||||
d'erreur d'écriture, une erreur est journalisée sans être
|
||||
propagée et le fichier temporaire est supprimé.
|
||||
"""
|
||||
payload = {
|
||||
"version": _STATE_VERSION,
|
||||
@@ -100,15 +104,24 @@ class BlogRSSState:
|
||||
"etag": self._etag,
|
||||
"last_modified": self._last_modified,
|
||||
}
|
||||
tmp_file = self._state_file.with_suffix(".tmp")
|
||||
try:
|
||||
with self._state_file.open("w", encoding="utf-8") as handle:
|
||||
with open(tmp_file, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2)
|
||||
tmp_file.replace(self._state_file)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Impossible d'écrire le fichier d'état blog RSS %s : %s.",
|
||||
redact_secrets(str(self._state_file)),
|
||||
redact_exception(exc),
|
||||
)
|
||||
try:
|
||||
tmp_file.unlink(missing_ok=True)
|
||||
except Exception as cleanup_exc:
|
||||
logger.debug(
|
||||
"Nettoyage du fichier temporaire échoué : %s",
|
||||
redact_exception(cleanup_exc),
|
||||
)
|
||||
|
||||
def get_known_guids(self) -> frozenset[str]:
|
||||
"""Renvoie une copie immuable des GUID d'articles déjà connus.
|
||||
|
||||
Reference in New Issue
Block a user