Files
ARR-2.0-0918/integrations/ohip/acquisition_progress.py
T

48 lines
2.4 KiB
Python

"""Private, best-effort acquisition observations, separate from source evidence."""
from datetime import datetime, timezone
from .capture_job import atomic_json, fingerprint
VERSION = "arr-acquisition-progress/v1"
PHASES = frozenset({"queued", "searching", "fetching", "verifying", "completed", "failed", "interrupted"})
PUBLIC_FIELDS = frozenset({"phase", "completed", "total", "percent", "updated_at"})
def snapshot(phase, completed=0, total=None, *, updated_at=None):
percent = (100 if phase == "completed" else 0 if phase == "queued" else
min(99, completed * 100 // total) if total else 99 if total == 0 else None)
return validate({"phase": phase, "completed": completed, "total": total, "percent": percent,
"updated_at": updated_at or datetime.now(timezone.utc).isoformat()})
def validate(value):
"""Return only the fixed privacy-safe public contract; reject malformed data."""
if type(value) is not dict or set(value) != PUBLIC_FIELDS or value["phase"] not in PHASES:
raise ValueError("invalid acquisition progress")
completed, total, percent = value["completed"], value["total"], value["percent"]
if type(completed) is not int or not 0 <= completed <= 10000000:
raise ValueError("invalid acquisition count")
if total is not None and (type(total) is not int or not completed <= total <= 10000000):
raise ValueError("invalid acquisition total")
if percent is not None and (type(percent) is not int or not 0 <= percent <= 100):
raise ValueError("invalid acquisition percent")
if value["phase"] == "completed":
if total is None or completed != total or percent != 100:
raise ValueError("incomplete acquisition")
elif percent == 100:
raise ValueError("premature acquisition completion")
stamp = value["updated_at"]
if type(stamp) is not str or len(stamp) > 40 or datetime.fromisoformat(stamp).tzinfo is None:
raise ValueError("invalid acquisition timestamp")
return dict(value)
def publish(folder, identity, phase, completed=0, total=None):
"""An observation failure must never change the source collection outcome."""
try:
atomic_json(folder / "progress.json", {"version": VERSION, "identity_sha256": fingerprint(identity),
"progress": snapshot(phase, completed, total)}, replace=True)
except Exception:
pass