119 lines
3.1 KiB
Python
119 lines
3.1 KiB
Python
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())
|