"""Helpers shared by collectors: failure isolation and timing.""" from __future__ import annotations import logging import time from collections.abc import Callable from us2kr.logging_setup import mask_secrets log = logging.getLogger(__name__) def run_safely[T](name: str, fn: Callable[[], T], default: T, gaps: list[str]) -> T: """Run a collector; on any exception log it, append a gap note and return `default`. The pipeline must never die because one data source is down. """ t0 = time.monotonic() try: result = fn() log.info("collector %s ok in %.1fs", name, time.monotonic() - t0) return result except Exception as exc: # deliberate: isolate every failure log.exception("collector %s failed", name) # Gap notes end up in the briefing / Telegram / LLM prompt: never let a URL with a key through. detail = mask_secrets(str(exc)).split(" for url: ", 1)[0][:120] gaps.append(f"{name}: {type(exc).__name__}: {detail}") return default