gwoe-antragspruefer/app/og_card.py

122 lines
3.8 KiB
Python
Raw Normal View History

"""Open-Graph-Bild-Rendering via Playwright (#141).
Rendert /v2/og-template?drucksache=X als PNG 1200×630.
Cache in data/og-cache/ mit Key SHA256(drucksache + updated_at).
Öffentliche API:
``render_og_card(drucksache, updated_at, base_url)``
PNG-Bytes oder None bei Fehler
``cache_key(drucksache, updated_at)``
Hex-String (SHA-256 Kurzform, 16 Zeichen)
``get_cached(drucksache, updated_at, cache_dir)``
Path der gecacheten Datei oder None
"""
from __future__ import annotations
import hashlib
import logging
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
_DEFAULT_CACHE_DIR = Path(__file__).resolve().parent.parent / "data" / "og-cache"
def cache_key(drucksache: str, updated_at: str) -> str:
"""Berechnet den Cache-Schlüssel als 16-stelligen SHA-256-Präfix.
Args:
drucksache: Drucksachen-ID (z.B. "NRW-18/1234").
updated_at: ISO-Zeitstempel des letzten Updates aus der Datenbank.
Returns:
16 Hex-Zeichen (64-Bit-Präfix des SHA-256).
"""
raw = f"{drucksache}|{updated_at}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
def _cache_path(drucksache: str, updated_at: str, cache_dir: Path) -> Path:
key = cache_key(drucksache, updated_at)
safe_name = drucksache.replace("/", "_").replace(" ", "_")
return cache_dir / f"{safe_name}_{key}.png"
def get_cached(
drucksache: str,
updated_at: str,
cache_dir: Optional[Path] = None,
) -> Optional[Path]:
"""Gibt den Pfad der gecacheten PNG-Datei zurück, wenn sie existiert.
Args:
drucksache: Drucksachen-ID.
updated_at: ISO-Zeitstempel ändert sich dieser, ist der Cache ungültig.
cache_dir: Verzeichnis für den Cache. Standard: data/og-cache/.
Returns:
Path-Objekt wenn Treffer, sonst None.
"""
cache_dir = cache_dir or _DEFAULT_CACHE_DIR
path = _cache_path(drucksache, updated_at, cache_dir)
return path if path.exists() else None
def render_og_card(
drucksache: str,
updated_at: str,
base_url: str = "http://127.0.0.1:8000",
cache_dir: Optional[Path] = None,
) -> Optional[bytes]:
"""Rendert die OG-Karte als PNG via Playwright und legt sie im Cache ab.
Bei Cache-Hit wird das Rendering übersprungen.
Args:
drucksache: Drucksachen-ID (URL-kodierbar).
updated_at: ISO-Zeitstempel für den Cache-Key.
base_url: Interne Basis-URL der App (Playwright greift darauf zu).
cache_dir: Cache-Verzeichnis. Standard: data/og-cache/.
Returns:
PNG-Bytes bei Erfolg, None bei Fehler.
"""
cache_dir = cache_dir or _DEFAULT_CACHE_DIR
cache_dir.mkdir(parents=True, exist_ok=True)
cached = get_cached(drucksache, updated_at, cache_dir)
if cached:
logger.debug("OG-Cache-Hit für %s", drucksache)
return cached.read_bytes()
dest = _cache_path(drucksache, updated_at, cache_dir)
try:
from playwright.sync_api import sync_playwright
import urllib.parse
encoded = urllib.parse.quote(drucksache, safe="")
url = f"{base_url}/v2/og-template?drucksache={encoded}"
with sync_playwright() as pw:
browser = pw.chromium.launch(args=["--no-sandbox"])
page = browser.new_page(viewport={"width": 1200, "height": 630})
page.goto(url, wait_until="networkidle", timeout=15000)
png_bytes = page.screenshot(
clip={"x": 0, "y": 0, "width": 1200, "height": 630},
type="png",
)
browser.close()
dest.write_bytes(png_bytes)
logger.info("OG-Karte gerendert: %s%s", drucksache, dest.name)
return png_bytes
except Exception:
logger.exception("Playwright-Render fehlgeschlagen für %s", drucksache)
return None