from __future__ import annotations import sys import time from typing import Iterable from . import __version__ from .config import Config, ConfigError, load_config from .logging_setup import ( configure_json_logging, log_error, log_run_summary, log_startup, ) from .state import load_posted, save_posted def _iter_candidates(config: Config) -> Iterable[str]: """Yield candidate post identifiers. This is the extension seam for the future blog-clone + matching pipeline. Job 1083 leaves it empty so the silent-on-no-matches contract is the default behaviour. """ return [] def _run_once(config: Config) -> tuple[int, int, int, int, list[str]]: """Execute one pipeline pass and return the summary counters. Returns ``(scanned, matched, posted, skipped, posted_ids)``. """ already_posted = set(load_posted(config.data_dir).keys()) scanned = 0 matched = 0 posted = 0 skipped = 0 posted_ids: list[str] = [] state = load_posted(config.data_dir) for candidate_id in _iter_candidates(config): scanned += 1 matched += 1 if candidate_id in already_posted: skipped += 1 continue posted_ids.append(candidate_id) state[candidate_id] = {"posted_at": _now_iso()} posted += 1 if posted_ids: save_posted(config.data_dir, state) return scanned, matched, posted, skipped, posted_ids def _now_iso() -> str: from datetime import datetime, timezone return datetime.now(tz=timezone.utc).isoformat() def _run_with_retry(config: Config) -> tuple[int, int, int, int, list[str]] | None: """Execute the pipeline with retries. Returns the summary counters on success or ``None`` when the retry budget is exhausted.""" attempts = max(1, config.max_retries + 1) last_exc: BaseException | None = None for attempt in range(1, attempts + 1): try: return _run_once(config) except Exception as exc: # noqa: BLE001 — broad on purpose last_exc = exc log_error( "pipeline_error", exc=exc, attempt=attempt, max_attempts=attempts, ) if attempt < attempts: time.sleep(0) assert last_exc is not None log_error("pipeline_failed", exc=last_exc, attempts=attempts) return None def main() -> int: configure_json_logging() try: config = load_config() except ConfigError as exc: log_error("configuration_error", exc=exc) return 2 config.data_dir.mkdir(parents=True, exist_ok=True) log_startup( version=__version__, site_url=config.site_url, run_at=config.run_at, tz=config.tz, hashtags=config.hashtags, throwback_prefix=config.throwback_prefix, ) result = _run_with_retry(config) if result is None: return 1 scanned, matched, posted, skipped, posted_ids = result log_run_summary(scanned, matched, posted, skipped, posted_ids) return 0 if __name__ == "__main__": sys.exit(main())