maboto/tests/fakes.py
Jeuner 077a877480 Produkt->Kategorie-Cache: bekannte Produkte ohne LLM (SQLite, modellübergreifend)
Neuer produktcache.py (Stufe 2): speichert pro Produkt (Titel+Marke, mengen-
invariant) die einmal ermittelte Gruppe in SQLite, bulk-load ins dict -> O(1).
Schnitt gewahrt: kein LLM-Import, nur Gruppe (nie Angebotsdaten), Whitelist
beim Lesen+Schreiben, nur SICHERE Zuordnungen gecacht.

kategorisiere(cache=, statistik=): Lookup vor dem LLM, Dedup im Lauf (ein
Produkt = ein Posten), Write-Back danach. Parallel-/id-Logik unverändert.
als_struktur/web/cli verdrahtet (Statistik 'X aus Cache · Y neu', --no-cache).

Live verifiziert (1903 Angebote PLZ 60487): Lauf 1 (gemini) 1551 neu; Lauf 2
(deepseek, anderes Modell) nur 110 neu, 1765 aus Cache -> ~93% weniger LLM-Calls,
modellübergreifend. +12 Tests (Round-Trip, Whitelist, Hit-vermeidet-Call, Dedup,
nur-sichere, Schnitt). 70 Tests grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:37:12 +02:00

72 lines
2.1 KiB
Python

"""Test-Doubles -- halten die Tests offline (kein Netz, kein LLM, kein Schlüssel)."""
from __future__ import annotations
from datetime import date, datetime
from typing import Callable
from angebote.modell import Angebot
from angebote.quellen.basis import Ort
class FakeQuelle:
"""Adapter-Double: liefert vorgegebene Angebote, deckt-Flag, optional Fehler."""
def __init__(self, name, angebote=None, *, deckt=True, fehler=None):
self.name = name
self._angebote = list(angebote or [])
self._deckt = deckt
self._fehler = fehler
def deckt_ab(self, ort: Ort) -> bool:
return self._deckt
def hole(self, ort: Ort):
if self._fehler is not None:
raise self._fehler
return list(self._angebote)
class FakeKategorisierer:
"""Kategorisierer-Double: ruft eine reine Funktion posten->antworten auf."""
def __init__(self, fn: Callable[[list[dict]], list[dict]]):
self._fn = fn
def klassifiziere(self, posten: list[dict]) -> list[dict]:
return self._fn(posten)
class CountingFakeKategorisierer:
"""Zählt die ans LLM gegebenen Posten -- für Cache-Tests (Hit/Dedup)."""
def __init__(self, gruppe: str = "Sonstiges", unsicher: bool = False):
self.gesehen = 0
self.titel: list[str] = []
self._gruppe = gruppe
self._unsicher = unsicher
def klassifiziere(self, posten: list[dict]) -> list[dict]:
self.gesehen += len(posten)
self.titel.extend(p["titel"] for p in posten)
return [
{"id": p["id"], "gruppe": self._gruppe, "unsicher": self._unsicher}
for p in posten
]
def beispiel_angebot(titel="Butter", **kw) -> Angebot:
"""Belegtes Angebot mit Default-Pflichtfeldern; einzeln überschreibbar."""
daten = dict(
titel=titel,
haendler="REWE",
quelle="test:fixture",
abgerufen_am=datetime(2026, 6, 1, 8, 0, 0),
preis=1.49,
marke="Markenbutter",
menge="250 g",
gueltig_von=date(2026, 6, 1),
gueltig_bis=date(2026, 6, 7),
)
daten.update(kw)
return Angebot(**daten)