Aus Container-Log gefundene Backend-Errors: 1. _job_anniversary_reminders: 'no such column: d.user_id' diary-Tabelle hat keine user_id — User-Bezug geht über dogs.user_id. → JOIN dogs ON dogs.id = d.dog_id ergänzt + SELECT dogs.user_id. Job läuft täglich 09:00 — war seit Tag X kaputt, kein Push für Jahrestage gesendet. 2. RASFF API 404 (EU Rapid Alert System for Food and Feed): webgate.ec.europa.eu/rasff-window/backend/public/... ist umgezogen. → HTTPStatusError mit 404/410/503 wird jetzt nur als WARNING geloggt (vorher ERROR → Error-Digest spammte täglich). Fallback ist eh schon ein leeres Array, App läuft weiter. EU-Endpoint-URL muss nochmal recherchiert werden, dann RASFF_URL aktualisieren — Folge-Sprint.
153 lines
5.5 KiB
Python
153 lines
5.5 KiB
Python
"""BAN YARO — Rückruf-Alarm (Tierfutter)
|
|
RASFF EU Rapid Alert System for Food and Feed
|
|
"""
|
|
|
|
import logging
|
|
import httpx
|
|
from fastapi import APIRouter
|
|
from database import db
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
RASFF_URL = "https://webgate.ec.europa.eu/rasff-window/backend/public/notification/list/with-filters"
|
|
RASFF_PARAMS = {
|
|
"filters": '{"subject.product_category":["pet food and animal feed"]}',
|
|
"pageNumber": 0,
|
|
"pageSize": 20,
|
|
"sortColumn": "notificationDate",
|
|
"sortDirection": "DESC",
|
|
}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/recalls — Letzte 50 Rückrufe
|
|
# ------------------------------------------------------------------
|
|
@router.get("")
|
|
async def list_recalls(q: str = ""):
|
|
with db() as conn:
|
|
if q:
|
|
like = f"%{q}%"
|
|
rows = conn.execute("""
|
|
SELECT id, external_id, titel, produkt, gefahr, herkunft, datum, quelle, url, created_at
|
|
FROM feed_recalls
|
|
WHERE titel LIKE ? OR produkt LIKE ? OR gefahr LIKE ? OR herkunft LIKE ?
|
|
ORDER BY datum DESC
|
|
LIMIT 50
|
|
""", (like, like, like, like)).fetchall()
|
|
else:
|
|
rows = conn.execute("""
|
|
SELECT id, external_id, titel, produkt, gefahr, herkunft, datum, quelle, url, created_at
|
|
FROM feed_recalls
|
|
ORDER BY datum DESC
|
|
LIMIT 50
|
|
""").fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Interne Hilfsfunktion: RASFF API abfragen
|
|
# ------------------------------------------------------------------
|
|
async def fetch_rasff_recalls() -> list[dict]:
|
|
"""Fragt die RASFF API ab und gibt eine Liste normalisierter Einträge zurück.
|
|
|
|
Hinweis: Die EU hat die API mehrfach umgezogen — wenn der Endpoint
|
|
404 oder andere persistent fehler liefert, geben wir [] zurück und
|
|
loggen nur als Warning (nicht Error), damit das Error-Digest nicht
|
|
täglich spammt.
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
resp = await client.get(RASFF_URL, params=RASFF_PARAMS)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
except httpx.HTTPStatusError as e:
|
|
if e.response.status_code in (404, 410, 503):
|
|
# API umgezogen oder temporär unten — Warning, kein Error
|
|
logger.warning(
|
|
f"RASFF API liefert {e.response.status_code} (Endpoint vermutlich umgezogen) — überspringe."
|
|
)
|
|
else:
|
|
logger.error(f"RASFF API-HTTP-Fehler: {e}")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"RASFF API-Fehler: {e}")
|
|
return []
|
|
|
|
entries = []
|
|
try:
|
|
items = data.get("data", {}).get("list", [])
|
|
for item in items:
|
|
reference = item.get("reference", "")
|
|
if not reference:
|
|
continue
|
|
|
|
# Datum
|
|
datum_raw = item.get("notificationDate", "")
|
|
datum = datum_raw[:10] if datum_raw else ""
|
|
|
|
# Produkt
|
|
subject = item.get("subject") or {}
|
|
produkt = subject.get("product", "") or ""
|
|
|
|
# Gefahr
|
|
hazards = subject.get("hazard") or []
|
|
gefahr = ""
|
|
if hazards:
|
|
gefahr = hazards[0].get("hazardDescription", "") or ""
|
|
|
|
# Herkunft
|
|
origin = item.get("origin") or {}
|
|
herkunft = origin.get("name", "") or ""
|
|
|
|
# URL zur RASFF-Seite
|
|
url = f"https://webgate.ec.europa.eu/rasff-window/screen/notificationDetail?notifRef={reference}"
|
|
|
|
entries.append({
|
|
"external_id": reference,
|
|
"titel": produkt or reference,
|
|
"produkt": produkt,
|
|
"gefahr": gefahr,
|
|
"herkunft": herkunft,
|
|
"datum": datum,
|
|
"quelle": "rasff",
|
|
"url": url,
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"RASFF Parsing-Fehler: {e}")
|
|
|
|
return entries
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Interne Hilfsfunktion: Neue Einträge in DB speichern
|
|
# ------------------------------------------------------------------
|
|
def save_new_recalls(entries: list[dict]) -> list[dict]:
|
|
"""Speichert neue Einträge und gibt die Liste der neuen Einträge zurück."""
|
|
new_entries = []
|
|
for entry in entries:
|
|
try:
|
|
with db() as conn:
|
|
exists = conn.execute(
|
|
"SELECT id FROM feed_recalls WHERE external_id=?",
|
|
(entry["external_id"],)
|
|
).fetchone()
|
|
if not exists:
|
|
conn.execute("""
|
|
INSERT INTO feed_recalls
|
|
(external_id, titel, produkt, gefahr, herkunft, datum, quelle, url)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
entry["external_id"],
|
|
entry["titel"],
|
|
entry["produkt"],
|
|
entry["gefahr"],
|
|
entry["herkunft"],
|
|
entry["datum"],
|
|
entry["quelle"],
|
|
entry["url"],
|
|
))
|
|
new_entries.append(entry)
|
|
except Exception as e:
|
|
logger.warning(f"Recall DB-Fehler für {entry.get('external_id')}: {e}")
|
|
return new_entries
|