109 lines
4.0 KiB
Python
109 lines
4.0 KiB
Python
"""Hilfsclient des Plugins „dedup“ für die BoardGameGeek XML API2.
|
|
|
|
Besorgt zwei Zusatzinformationen für die Prüfung:
|
|
- Alternate-Names eines Spiels (für die deutsche Titel-Empfehlung),
|
|
- die `boardgameexpansion`-Relation — welches Basisspiel eine Erweiterung
|
|
ergänzt (für die Vorgänger-Erkennung).
|
|
|
|
Der Client ist bewusst defensiv: Fehlt das Netzwerk oder liefert BGG
|
|
Unsinn, geben die Methoden leere Listen zurück und die Prüfung fällt auf
|
|
die reine Titel-Heuristik zurück, statt den Eintrage-Vorgang zu blockieren.
|
|
Rate-Limit wie im Neuheiten-Plugin: mindestens 1 Sekunde zwischen Requests;
|
|
Uhr, Schlaf-Funktion und HTTP-Transport sind injizierbar, damit Tests
|
|
netzwerkfrei bleiben.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
from collections.abc import Callable
|
|
|
|
import httpx
|
|
|
|
_logger = logging.getLogger("plugins.dedup")
|
|
|
|
BASIS_URL = "https://boardgamegeek.com/xmlapi2"
|
|
|
|
|
|
class BggPruefClient:
|
|
"""Liest Alternate-Names und Erweiterungs-Relationen von BGG."""
|
|
|
|
def __init__(
|
|
self,
|
|
mindestabstand_sekunden: float = 1.0,
|
|
*,
|
|
token: str | None = None,
|
|
transport: httpx.BaseTransport | None = None,
|
|
uhr: Callable[[], float] = time.monotonic,
|
|
schlafen: Callable[[float], None] = time.sleep,
|
|
timeout: float = 10.0,
|
|
) -> None:
|
|
self._mindestabstand = max(0.0, mindestabstand_sekunden)
|
|
self._zuletzt: float | None = None
|
|
self._uhr = uhr
|
|
self._schlafen = schlafen
|
|
headers: dict[str, str] = {}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
self._client = httpx.Client(
|
|
base_url=BASIS_URL, timeout=timeout, transport=transport, headers=headers
|
|
)
|
|
|
|
def schliessen(self) -> None:
|
|
self._client.close()
|
|
|
|
# ---------- Internas ----------
|
|
|
|
def _hole(self, pfad: str, parameter: dict[str, str]) -> ET.Element | None:
|
|
"""Ein GET mit Rate-Limit; bei jedem Fehler None (Prüfung degradiert)."""
|
|
if self._zuletzt is not None:
|
|
vergangen = self._uhr() - self._zuletzt
|
|
if vergangen < self._mindestabstand:
|
|
self._schlafen(self._mindestabstand - vergangen)
|
|
self._zuletzt = self._uhr()
|
|
try:
|
|
antwort = self._client.get(pfad, params=parameter)
|
|
antwort.raise_for_status()
|
|
return ET.fromstring(antwort.content)
|
|
except Exception as exc:
|
|
_logger.warning("BGG-Hilfsabruf fehlgeschlagen (%s %s): %s", pfad, parameter, exc)
|
|
return None
|
|
|
|
# ---------- Öffentliche API ----------
|
|
|
|
def alternativen(self, bgg_id: int) -> list[str]:
|
|
"""Alternate-Names des Spiels laut BGG (leer bei Problemen)."""
|
|
wurzel = self._hole("/thing", {"id": str(bgg_id), "type": "boardgame"})
|
|
namen: list[str] = []
|
|
if wurzel is None:
|
|
return namen
|
|
for element in wurzel.iter("name"):
|
|
if element.get("type") == "alternate":
|
|
wert = (element.get("value") or "").strip()
|
|
if wert:
|
|
namen.append(wert)
|
|
return namen
|
|
|
|
def vorgaenger(self, bgg_id: int) -> list[tuple[int, str]]:
|
|
"""Basisspiele, die dieses Spiel ergänzt — als (bgg_id, titel).
|
|
|
|
Die XML API2 listet auf der Seite einer Erweiterung die Links vom
|
|
Typ `boardgameexpansion` auf das jeweils ergänzte Basisspiel.
|
|
"""
|
|
wurzel = self._hole("/thing", {"id": str(bgg_id), "type": "boardgame"})
|
|
ergebnis: list[tuple[int, str]] = []
|
|
if wurzel is None:
|
|
return ergebnis
|
|
for element in wurzel.iter("link"):
|
|
if element.get("type") != "boardgameexpansion":
|
|
continue
|
|
try:
|
|
fremd_id = int(element.get("id") or "")
|
|
except ValueError:
|
|
continue
|
|
titel = (element.get("value") or "").strip()
|
|
if titel:
|
|
ergebnis.append((fremd_id, titel))
|
|
return ergebnis
|