Plugin neuheiten: BGG-Sync mit APScheduler, Filtern und deutscher UI
- Datenmodell + eigene Migration 0001_neuheiten_tabelle (Tabelle neuheiten: Titel, Verlag, Autor, Erscheinungsjahr, BGG-ID, Status 'neuheit', Quelle, Zeitstempel; bgg_id eindeutig als Merge-Kriterium) - BoardGameGeek XML API2-Client (search + thing, Batches à 20 IDs): Rate-Limit >= 1 s zwischen Requests, Retry mit exponentiellem Backoff bei 5xx/429/Netzwerkfehlern, HTTP 202 gemäß Retry-After, robustes XML-Parsing; Transport/Uhr/Sleep injizierbar (keine echten Calls in Tests) - Filter: Erweiterungen (boardgameexpansion) auf Request- und Elementebene ausgeschlossen; Prototypen per Titel-Heuristik (BGG hat keinen Marker) - Sync-Service mit Update-statt-Duplikat-Logik über die eindeutige BGG-ID (Status bleibt erhalten); Fehler je Suchbegriff brechen den Lauf nicht ab - APScheduler-Hintergrundjob (Standard 24 h) mit Überlappungsschutz, abschaltbar/intervallkonfigurierbar per Env; manueller 'Jetzt synchronisieren'-Endpunkt nur für Admin/Redakteur, optional mit Sofort-Suchbegriff - UI /neuheiten: sortier-/filterbare Tabelle mit Volltextsuche (HTMX-Teilladung, noscript-fähig), deutsche Oberfläche, BGG-Links, Ergebnis-Banner - Plugin-Loader: idempotentes Laden (Modul-Caching), damit mehrere create_app()-Aufrufe dieselben Plugin-Klassen/Tabellen nutzen - Tests: Parsing, Erweiterungs-/Prototyp-Filter, Rate-Limit/Backoff/202, Update-statt-Duplikat, Rollen am Sync-Endpunkt, Scheduler-Lifecycle — ausschließlich mit gemockten BGG-Antworten (uv run pytest: 96 grün) - README/AGENTS: Plugin-Doku, Env-Variablen, Fortschrittstabelle aktualisiert
This commit is contained in:
37
plugins/neuheiten/models.py
Normal file
37
plugins/neuheiten/models.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Datenmodell des Plugins „neuheiten“.
|
||||
|
||||
Ein Eintrag repräsentiert ein Spiel der Neuheitenliste. Die BGG-ID ist das
|
||||
eindeutige Merge-Kriterium: Beim Sync werden bestehende Einträge aktualisiert,
|
||||
statt Duplikate anzulegen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from redaktionskern.db import Base
|
||||
|
||||
STATUS_NEUHEIT = "neuheit"
|
||||
|
||||
|
||||
class Neuheit(Base):
|
||||
__tablename__ = "neuheiten"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
titel: Mapped[str] = mapped_column(String(300), index=True)
|
||||
verlag: Mapped[str | None] = mapped_column(String(300))
|
||||
autor: Mapped[str | None] = mapped_column(String(300))
|
||||
# BoardGameGeek liefert nur das Erscheinungsjahr (kein genaues Datum).
|
||||
erscheinungsjahr: Mapped[int | None] = mapped_column(Integer)
|
||||
bgg_id: Mapped[int] = mapped_column(Integer, unique=True, index=True)
|
||||
status: Mapped[str] = mapped_column(String(50), default=STATUS_NEUHEIT, index=True)
|
||||
quelle: Mapped[str] = mapped_column(String(100), default="boardgamegeek")
|
||||
erstellt_am: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
aktualisiert_am: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe
|
||||
return f"<Neuheit {self.bgg_id} {self.titel!r}>"
|
||||
Reference in New Issue
Block a user