Harden portable Redmine CLI runtime and cache writes
This commit is contained in:
+42
-24
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from functools import wraps
|
||||
from redmine_cache_io import CacheError, cache_lock, atomic_write_json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -497,28 +499,29 @@ def main() -> int:
|
||||
|
||||
try:
|
||||
if args.command == "fetch":
|
||||
client = client_from_args(args)
|
||||
contacts = client.fetch_contacts(limit=args.limit)
|
||||
filtered_contacts = filter_contacts_from_args(contacts, args)
|
||||
cache_path = resolve_cache_path(args)
|
||||
effective_cache_mode = resolve_fetch_cache_mode(args)
|
||||
with cache_lock(resolve_cache_path(args)):
|
||||
client = client_from_args(args)
|
||||
contacts = client.fetch_contacts(limit=args.limit)
|
||||
filtered_contacts = filter_contacts_from_args(contacts, args)
|
||||
cache_path = resolve_cache_path(args)
|
||||
effective_cache_mode = resolve_fetch_cache_mode(args)
|
||||
|
||||
cache_contacts = filtered_contacts if effective_cache_mode == "filtered" else contacts
|
||||
write_cache(cache_path, args, cache_contacts)
|
||||
cache_contacts = filtered_contacts if effective_cache_mode == "filtered" else contacts
|
||||
write_cache(cache_path, args, cache_contacts)
|
||||
|
||||
if effective_cache_mode == "filtered":
|
||||
print(
|
||||
f"Cached {len(filtered_contacts)} filtered contacts "
|
||||
f"(from {len(contacts)} fetched) in {cache_path}"
|
||||
)
|
||||
elif has_contact_filters(args):
|
||||
print(
|
||||
f"Cached {len(contacts)} contacts in {cache_path}. "
|
||||
f"{len(filtered_contacts)} contacts currently match the supplied filters."
|
||||
)
|
||||
else:
|
||||
print(f"Cached {len(contacts)} contacts in {cache_path}")
|
||||
return 0
|
||||
if effective_cache_mode == "filtered":
|
||||
print(
|
||||
f"Cached {len(filtered_contacts)} filtered contacts "
|
||||
f"(from {len(contacts)} fetched) in {cache_path}"
|
||||
)
|
||||
elif has_contact_filters(args):
|
||||
print(
|
||||
f"Cached {len(contacts)} contacts in {cache_path}. "
|
||||
f"{len(filtered_contacts)} contacts currently match the supplied filters."
|
||||
)
|
||||
else:
|
||||
print(f"Cached {len(contacts)} contacts in {cache_path}")
|
||||
return 0
|
||||
|
||||
if args.command == "search":
|
||||
contacts = load_or_refresh(args)
|
||||
@@ -627,7 +630,7 @@ def main() -> int:
|
||||
client = client_from_args(args)
|
||||
print_json(client.helpdesk_timeline(args.contact_id, args.limit))
|
||||
return 0
|
||||
except RedmineError as exc:
|
||||
except (RedmineError, CacheError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -683,6 +686,21 @@ def add_contact_filter_args(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def snapshot_transaction(fn):
|
||||
@wraps(fn)
|
||||
def wrapped(args, *a, **kw):
|
||||
path = resolve_cache_path(args)
|
||||
refresh = bool(getattr(args, "refresh", False) or getattr(args, "refresh_cache", False))
|
||||
if refresh or not path.exists():
|
||||
# Includes fetch and replacement; serializing replacement alone lets
|
||||
# a slower old fetch clobber the newer snapshot.
|
||||
with cache_lock(path):
|
||||
return fn(args, *a, **kw)
|
||||
return fn(args, *a, **kw)
|
||||
return wrapped
|
||||
|
||||
|
||||
@snapshot_transaction
|
||||
def load_or_refresh(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
cache_path = resolve_cache_path(args)
|
||||
if args.refresh or not cache_path.exists():
|
||||
@@ -986,6 +1004,7 @@ def read_cache_payload(cache_path: Path, args: argparse.Namespace) -> dict[str,
|
||||
return data
|
||||
|
||||
|
||||
@snapshot_transaction
|
||||
def resolve_contact_from_args(args: argparse.Namespace) -> dict[str, Any]:
|
||||
source = args.source
|
||||
cache_path = resolve_cache_path(args)
|
||||
@@ -1026,6 +1045,7 @@ def has_update_changes(args: argparse.Namespace) -> bool:
|
||||
)
|
||||
|
||||
|
||||
@snapshot_transaction
|
||||
def load_contacts_for_batch(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
cache_path = resolve_cache_path(args)
|
||||
|
||||
@@ -1304,9 +1324,7 @@ def write_cache(cache_path: Path, args: argparse.Namespace, contacts: list[dict[
|
||||
"cached_at": int(time.time()),
|
||||
"contacts": contacts,
|
||||
}
|
||||
with cache_path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
atomic_write_json(cache_path, payload)
|
||||
|
||||
|
||||
def resolve_cache_path(args: argparse.Namespace) -> Path:
|
||||
|
||||
Reference in New Issue
Block a user