Compare commits

..
Author SHA1 Message Date
Codex 211dd1af1d fix(caldav): ignorer les événements gérés sans UID 2026-09-13 00:13:05 +02:00
2 changed files with 9 additions and 63 deletions
+9 -26
View File
@@ -97,10 +97,7 @@ class BlogRSSState:
redact_exception(exc), redact_exception(exc),
) )
def _save( def _save(self) -> None:
self,
state: tuple[set[str], str | None, str | None] | None = None,
) -> bool:
"""Sauvegarde l'état dans le fichier JSON de manière atomique. """Sauvegarde l'état dans le fichier JSON de manière atomique.
La sortie est déterministe : ``known_guids`` est trié La sortie est déterministe : ``known_guids`` est trié
@@ -110,30 +107,20 @@ class BlogRSSState:
jamais laisser un fichier partiel en cas d'interruption. En cas jamais laisser un fichier partiel en cas d'interruption. En cas
d'erreur d'écriture, une erreur est journalisée sans être d'erreur d'écriture, une erreur est journalisée sans être
propagée et le fichier temporaire est supprimé. propagée et le fichier temporaire est supprimé.
:param state: État à sauvegarder ; l'état courant est utilisé par défaut.
:return: ``True`` si l'état a été sauvegardé ou si la persistance est désactivée.
:rtype: bool
""" """
if not self._persistence_enabled: if not self._persistence_enabled:
return True return
known_guids, etag, last_modified = state or (
self._known_guids,
self._etag,
self._last_modified,
)
payload = { payload = {
"version": _STATE_VERSION, "version": _STATE_VERSION,
"known_guids": sorted(known_guids), "known_guids": sorted(self._known_guids),
"etag": etag, "etag": self._etag,
"last_modified": last_modified, "last_modified": self._last_modified,
} }
tmp_file = self._state_file.with_suffix(".tmp") tmp_file = self._state_file.with_suffix(".tmp")
try: try:
with open(tmp_file, "w", encoding="utf-8") as handle: with open(tmp_file, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2) json.dump(payload, handle, indent=2)
tmp_file.replace(self._state_file) tmp_file.replace(self._state_file)
return True
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"Impossible d'écrire le fichier d'état blog RSS %s : %s.", "Impossible d'écrire le fichier d'état blog RSS %s : %s.",
@@ -147,7 +134,6 @@ class BlogRSSState:
"Nettoyage du fichier temporaire échoué : %s", "Nettoyage du fichier temporaire échoué : %s",
redact_exception(cleanup_exc), redact_exception(cleanup_exc),
) )
return False
def get_known_guids(self) -> frozenset[str]: def get_known_guids(self) -> frozenset[str]:
"""Renvoie une copie immuable des GUID d'articles déjà connus. """Renvoie une copie immuable des GUID d'articles déjà connus.
@@ -182,13 +168,10 @@ class BlogRSSState:
""" """
if result.not_modified: if result.not_modified:
return return
new_state = ( self._known_guids.update(article.id for article in result.articles)
self._known_guids | {article.id for article in result.articles}, self._etag = result.etag
result.etag, self._last_modified = result.last_modified
result.last_modified, self._save()
)
if self._save(new_state):
self._known_guids, self._etag, self._last_modified = new_state
def get_cache_headers(self) -> tuple[str | None, str | None]: def get_cache_headers(self) -> tuple[str | None, str | None]:
"""Renvoie les en-têtes de cache HTTP mémorisés. """Renvoie les en-têtes de cache HTTP mémorisés.
-37
View File
@@ -425,41 +425,4 @@ def test_atomic_save_preserves_on_error(tmp_path: Path) -> None:
assert state.get_known_guids() == frozenset({"original-guid-1", "original-guid-2", "new-guid"}) assert state.get_known_guids() == frozenset({"original-guid-1", "original-guid-2", "new-guid"})
def test_acknowledge_does_not_advance_memory_when_save_fails(
tmp_path: Path,
) -> None:
"""Conserve l'état précédent en mémoire si l'acquittement ne peut pas être sauvegardé.
:param tmp_path: Fixture pytest pour un répertoire temporaire.
:return: None
"""
state_file = tmp_path / "state.json"
state = BlogRSSState(state_file)
state.add_guids(["existing-guid"])
state.update_cache_headers("old-etag", "old-last-modified")
article = BlogArticle(
id="new-guid",
title="Article",
url="https://example.com/article",
published_at=datetime(2026, 9, 12, 8, 0, tzinfo=UTC),
updated_at=None,
category=None,
author=None,
content_html="<p>Contenu</p>",
content_text="Contenu",
)
with patch.object(Path, "replace", side_effect=OSError("replace failed")):
state.acknowledge(
BlogRSSFetchResult(
articles=(article,),
etag="new-etag",
last_modified="new-last-modified",
)
)
assert state.get_known_guids() == frozenset({"existing-guid"})
assert state.get_cache_headers() == ("old-etag", "old-last-modified")
# Ensure trailing newline # Ensure trailing newline