Plugin neuheiten: Web-Quellen-Crawler (4 Adapter, Rate-Limit, Dedup-Gate, Sync-UI)
This commit is contained in:
@@ -6,9 +6,10 @@ statt Duplikate anzulegen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, func
|
||||
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from redaktionskern.db import Base
|
||||
@@ -25,7 +26,10 @@ class Neuheit(Base):
|
||||
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)
|
||||
# Eindeutiges Merge-Kriterium der BGG-Syncs; Web-Quellen haben keine
|
||||
# BGG-ID und speichern NULL (Migration 0002 macht die Spalte nullable,
|
||||
# Duplikate werden dort über den Titel gematcht statt über die ID).
|
||||
bgg_id: Mapped[int | None] = 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())
|
||||
@@ -34,4 +38,50 @@ class Neuheit(Base):
|
||||
)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe
|
||||
return f"<Neuheit {self.bgg_id} {self.titel!r}>"
|
||||
return f"<Neuheit {self.bgg_id or '?'} {self.titel!r}>"
|
||||
|
||||
|
||||
class QuellenStatus(Base):
|
||||
"""Sync-Status einer Web-Quelle (Übersicht im UI + HTTP-Validatoren).
|
||||
|
||||
Pro Quelle eine Zeile: Ergebnis des letzten Laufes (Zeitpunkt, Dauer,
|
||||
neu/aktualisiert/gefiltert/Fehler) plus die konditionalen
|
||||
Request-Header (ETag/Last-Modified je abgerufener URL), damit 304er
|
||||
auch über Neustarts hinweg respektiert werden.
|
||||
"""
|
||||
|
||||
__tablename__ = "neuheiten_quellen_status"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
quelle: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
||||
url: Mapped[str | None] = mapped_column(String(500))
|
||||
letzte_laufzeit: Mapped[datetime | None] = mapped_column(DateTime)
|
||||
dauer_sekunden: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
anzahl_neu: Mapped[int] = mapped_column(Integer, default=0)
|
||||
anzahl_aktualisiert: Mapped[int] = mapped_column(Integer, default=0)
|
||||
anzahl_gefiltert: Mapped[int] = mapped_column(Integer, default=0)
|
||||
anzahl_fehler: Mapped[int] = mapped_column(Integer, default=0)
|
||||
fehlermeldung: Mapped[str | None] = mapped_column(Text)
|
||||
unveraendert: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
#: JSON-Objekt URL → [etag, last_modified]
|
||||
validatoren_json: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
@property
|
||||
def validatoren(self) -> dict[str, tuple[str | None, str | None]]:
|
||||
if not self.validatoren_json:
|
||||
return {}
|
||||
try:
|
||||
roh = json.loads(self.validatoren_json)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {
|
||||
str(url): (paar[0], paar[1]) if isinstance(paar, (list, tuple)) else (None, None)
|
||||
for url, paar in roh.items()
|
||||
}
|
||||
|
||||
@validatoren.setter
|
||||
def validatoren(self, wert: dict[str, tuple[str | None, str | None]]) -> None:
|
||||
self.validatoren_json = json.dumps(wert or {})
|
||||
|
||||
|
||||
STATUS_UNVERAENDERT_HINWEIS = "unverändert (304) — übersprungen"
|
||||
|
||||
Reference in New Issue
Block a user