"""EZB-Referenzkurse laden und lokal cachen. Die EZB veröffentlicht die Euro-Referenzkurse werktäglich als XML: ein Tages-Feed, ein 90-Tage-Feed und die volle Historie seit 1999. Wir brauchen nur CHF. Historische Kurse ändern sich nicht – vorhandene Zeilen werden daher nicht überschrieben. """ import xml.etree.ElementTree as ET from datetime import date from decimal import Decimal import httpx from sqlalchemy.orm import Session from ..models import ExchangeRate FEED_90D = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml" FEED_FULL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml" # Welche Währungen wir speichern (Hauptwährungen des Nutzers: EUR ist Pivot). CURRENCIES = {"CHF"} def _parse(xml_bytes: bytes) -> list[tuple[date, str, str]]: """Extrahiert (Datum, Währung, Kurs) aus dem EZB-XML (namespace-tolerant).""" root = ET.fromstring(xml_bytes) rows: list[tuple[date, str, str]] = [] for elem in root.iter(): t = elem.attrib.get("time") if not t: continue try: d = date.fromisoformat(t) except ValueError: continue for child in elem: ccy = child.attrib.get("currency") rate = child.attrib.get("rate") if ccy in CURRENCIES and rate: rows.append((d, ccy, rate)) return rows def _fetch(url: str, timeout: float = 15.0) -> bytes: with httpx.Client(timeout=timeout, follow_redirects=True) as client: resp = client.get(url) resp.raise_for_status() return resp.content def _upsert(db: Session, rows: list[tuple[date, str, str]]) -> int: added = 0 for d, ccy, rate in rows: if db.get(ExchangeRate, (d, ccy)) is None: db.add(ExchangeRate(date=d, currency=ccy, rate_per_eur=Decimal(rate), source="ecb")) added += 1 if added: db.commit() return added def refresh_recent(db: Session) -> int: """Lädt die letzten ~90 Tage (schnell) und ergänzt fehlende Tage.""" return _upsert(db, _parse(_fetch(FEED_90D))) def backfill_full(db: Session) -> int: """Lädt die komplette Historie (einmalig, größer).""" return _upsert(db, _parse(_fetch(FEED_FULL))) def ensure_rates_on_startup(db: Session, enabled: bool) -> None: """Beim App-Start best-effort die aktuellen Kurse nachladen. Blockiert nie hart: Ohne Internet läuft die App weiter, nur ohne frische Kurse. Die volle Historie holt das Install-Skript / der systemd-Timer separat.""" if not enabled: return try: refresh_recent(db) except Exception: # noqa: BLE001 – bewusst tolerant (Netz weg etc.) pass