"""BAN YARO — In-Memory TTL-Cache für statische DB-Daten. Hintergrund: Routes wie /api/training/exercises, /api/help, /api/wiki/stats laden bei jedem Request statische Daten aus der DB. Das ist verschwendete Energie. Diese Daten ändern sich nur durch Admin-Aktionen. Verwendung: from cache import ttl_cache @ttl_cache(ttl=3600) def my_func(arg1, arg2): ... API: @ttl_cache(ttl=3600) Decorator – cached pro Argumenten-Signatur my_func.cache_clear() Komplett leeren (z.B. nach Admin-Update) Hinweis: Diese Implementierung ist absichtlich klein und ohne externe Dependency (kein cachetools nötig). Thread-safe via Lock. Reicht für Read-Only- Listen, die sich selten ändern. Niemals für user-spezifische Daten verwenden! """ from __future__ import annotations import functools import threading import time from typing import Any, Callable def ttl_cache(ttl: int = 3600, maxsize: int = 128) -> Callable: """Decorator: cached Rückgabewert pro Argumenten-Signatur für `ttl` Sek. - ttl: Time-to-live in Sekunden (Default: 1 Stunde) - maxsize: max. Anzahl Einträge im Cache (FIFO-Eviction bei Überlauf) Die dekorierte Funktion bekommt zusätzlich: .cache_clear() – leert den gesamten Cache .cache_info() – {hits, misses, size, ttl, maxsize} """ def decorator(func: Callable) -> Callable: store: dict[tuple, tuple[float, Any]] = {} lock = threading.Lock() stats = {"hits": 0, "misses": 0} def _make_key(args: tuple, kwargs: dict) -> tuple: # kwargs als sortiertes Tuple in den Key packen if kwargs: return args + tuple(sorted(kwargs.items())) return args @functools.wraps(func) def wrapper(*args, **kwargs): key = _make_key(args, kwargs) now = time.monotonic() with lock: cached = store.get(key) if cached is not None: expires_at, value = cached if expires_at > now: stats["hits"] += 1 return value # abgelaufen → raus del store[key] stats["misses"] += 1 # Außerhalb des Locks ausführen (kann DB-Calls machen) value = func(*args, **kwargs) with lock: # FIFO-Eviction, wenn maxsize überschritten if len(store) >= maxsize: try: oldest_key = next(iter(store)) del store[oldest_key] except StopIteration: pass store[key] = (now + ttl, value) return value def cache_clear() -> None: with lock: store.clear() stats["hits"] = 0 stats["misses"] = 0 def cache_info() -> dict: with lock: return { "hits": stats["hits"], "misses": stats["misses"], "size": len(store), "ttl": ttl, "maxsize": maxsize, } wrapper.cache_clear = cache_clear # type: ignore[attr-defined] wrapper.cache_info = cache_info # type: ignore[attr-defined] return wrapper return decorator