- 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
38 lines
1.4 KiB
Python
38 lines
1.4 KiB
Python
"""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}>"
|