Harden portable Redmine CLI runtime and cache writes

This commit is contained in:
Jason Thistlethwaite
2026-10-18 03:18:36 -04:00
parent f6915face2
commit 2e686215ed
19 changed files with 1353 additions and 224 deletions
+20 -25
View File
@@ -12,6 +12,7 @@ enough for sync + local search + show next to tickets.
from __future__ import annotations
import json
from redmine_cache_io import cache_lock, read_json, atomic_write_json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable
@@ -34,12 +35,7 @@ class ContactCache:
def load_manifest(self) -> dict[str, Any]:
if not self.manifest_path.is_file():
return {"version": 1, "contacts": {}, "updated_at": None}
try:
data = json.loads(self.manifest_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {"version": 1, "contacts": {}, "updated_at": None}
if not isinstance(data, dict):
return {"version": 1, "contacts": {}, "updated_at": None}
data = read_json(self.manifest_path)
data.setdefault("version", 1)
data.setdefault("contacts", {})
return data
@@ -48,9 +44,7 @@ class ContactCache:
data = dict(data)
data["version"] = 1
data["updated_at"] = now_iso()
tmp = self.manifest_path.with_suffix(".tmp")
tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
tmp.replace(self.manifest_path)
atomic_write_json(self.manifest_path, data)
def should_fetch(self, stub: dict[str, Any]) -> bool:
cid = stub.get("id")
@@ -64,22 +58,23 @@ class ContactCache:
return str(entry.get("updated_on") or "") != str(stub.get("updated_on") or "")
def write_contact(self, contact: dict[str, Any]) -> Path:
cid = contact.get("id")
if cid is None:
raise ValueError("contact missing id")
path = self.contact_path(int(cid))
path.write_text(json.dumps(contact, indent=2, sort_keys=True) + "\n", encoding="utf-8")
manifest = self.load_manifest()
contacts = manifest.setdefault("contacts", {})
contacts[str(int(cid))] = {
"id": int(cid),
"updated_on": contact.get("updated_on"),
"last_synced": now_iso(),
"email": primary_email(contact),
"name": contact_display_name(contact),
}
self.save_manifest(manifest)
return path
with cache_lock(self.manifest_path):
cid = contact.get("id")
if cid is None:
raise ValueError("contact missing id")
path = self.contact_path(int(cid))
atomic_write_json(path, contact)
manifest = self.load_manifest()
contacts = manifest.setdefault("contacts", {})
contacts[str(int(cid))] = {
"id": int(cid),
"updated_on": contact.get("updated_on"),
"last_synced": now_iso(),
"email": primary_email(contact),
"name": contact_display_name(contact),
}
self.save_manifest(manifest)
return path
def load_contact(self, contact_id: int) -> dict[str, Any]:
path = self.contact_path(contact_id)