feat: enforce community source cooldown
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user