feat: Environment Configuration and Logging Baseline

This commit is contained in:
OpenVelo Agent
2026-08-04 17:53:17 +00:00
parent bbad70bed1
commit a8b87179f0
13 changed files with 667 additions and 77 deletions
+92 -23
View File
@@ -1,47 +1,116 @@
from __future__ import annotations
import logging
import sys
import time
from typing import Iterable
from . import __version__
from .config import ConfigError, load_config
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
log = logging.getLogger("tenbackward")
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:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
configure_json_logging()
try:
config = load_config()
except ConfigError as exc:
log.error("configuration error: %s", exc)
log_error("configuration_error", exc=exc)
return 2
config.data_dir.mkdir(parents=True, exist_ok=True)
state = load_posted(config.data_dir)
save_posted(config.data_dir, state)
log.info(
"10backward v%s ready (site=%s, run_at=%s, tz=%s, hashtags=%s, prefix=%r)",
__version__,
config.site_url,
config.run_at,
config.tz,
config.hashtags,
config.throwback_prefix,
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,
)
log.warning(
"post pipeline is not yet implemented; this run only validates the scaffold. "
"Future job will clone the blog and post a throwback via Mastodon.py."
)
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