feat: enforce community source cooldown
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-05 07:52:37 +07:00
parent 1a09bd59f3
commit 3f468799bd
10 changed files with 114 additions and 8 deletions
+39
View File
@@ -2,8 +2,11 @@ from __future__ import annotations
import argparse
import json
import os
import sys
import time
from dataclasses import asdict
from pathlib import Path
from urllib.request import Request, urlopen
from .community_sources import (
@@ -25,6 +28,35 @@ DETAIL_SOURCES = {
"rf4posts-spot": parse_rf4posts_spot,
}
USER_AGENT = "RF4-Spotter/0.1 (authorized data integration)"
MIN_FETCH_INTERVAL_SECONDS = 30 * 60
DEFAULT_STATE_FILE = Path(".cache/community-fetch-state.json")
def enforce_fetch_interval(
source: str, *, state_file: Path, now: float | None = None,
) -> None:
now = time.time() if now is None else now
try:
state = json.loads(state_file.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
state = {}
last_fetch = state.get(source)
if isinstance(last_fetch, (int, float)) and now - last_fetch < MIN_FETCH_INTERVAL_SECONDS:
wait = int(MIN_FETCH_INTERVAL_SECONDS - (now - last_fetch))
raise RuntimeError(f"source cooldown is active; retry in {wait} seconds")
def mark_fetch(source: str, *, state_file: Path, now: float | None = None) -> None:
now = time.time() if now is None else now
try:
state = json.loads(state_file.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
state = {}
state[source] = now
state_file.parent.mkdir(parents=True, exist_ok=True)
temporary = state_file.with_suffix(".tmp")
temporary.write_text(json.dumps(state, sort_keys=True), encoding="utf-8")
temporary.replace(state_file)
def fetch_html(url: str, *, timeout: float = 30) -> str:
@@ -40,13 +72,20 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("source", choices=(*SOURCES, *DETAIL_SOURCES))
parser.add_argument("--url", help="Override the configured public page URL")
parser.add_argument("--limit", type=int, default=100, choices=range(1, 501), metavar="1..500")
parser.add_argument(
"--state-file", type=Path,
default=Path(os.environ.get("RF4_COMMUNITY_FETCH_STATE", DEFAULT_STATE_FILE)),
help="Persistent per-source cooldown state",
)
args = parser.parse_args(argv)
if args.source in DETAIL_SOURCES and not args.url:
parser.error(f"--url is required for {args.source}")
default_url, parse = SOURCES.get(args.source, (None, DETAIL_SOURCES.get(args.source)))
url = args.url or default_url
try:
enforce_fetch_interval(args.source, state_file=args.state_file)
html = fetch_html(url)
mark_fetch(args.source, state_file=args.state_file)
records = (parse(html, source_url=url) if args.source in DETAIL_SOURCES else parse(html))[:args.limit]
except Exception as exc:
print(f"community source failed: {exc}", file=sys.stderr)