import inspect from base64 import urlsafe_b64encode from collections.abc import Callable from dataclasses import dataclass from functools import wraps from itertools import chain from typing import Iterable, ParamSpec, TypeVar from redis import Redis @dataclass(frozen=True, kw_only=True, slots=True) class Config: redis: Redis prefix: str | None = "cache" ttl_fresh: int = 600 ttl_stale: int | None = None def qualified_name(callable_obj) -> str: # callable classes/instances if hasattr(callable_obj, "__call__") and not inspect.isroutine(callable_obj): # callable instance: use its class cls = callable_obj.__class__ module = getattr(cls, "__module__", None) qual = getattr(cls, "__qualname__", cls.__name__) return f"{module}.{qual}" if module else qual # functions, methods, builtins # unwrap descriptors like staticmethod/classmethod if isinstance(callable_obj, staticmethod | classmethod): callable_obj = callable_obj.__func__ # bound method if inspect.ismethod(callable_obj): func = callable_obj.__func__ owner = getattr(func, "__qualname__", func.__name__).rsplit(".", 1)[0] module = getattr(func, "__module__", None) qual = f"{owner}.{func.__name__}" return f"{module}.{qual}" if module else qual # regular function or builtin if inspect.isfunction(callable_obj) or inspect.isbuiltin(callable_obj): module = getattr(callable_obj, "__module__", None) qual = getattr(callable_obj, "__qualname__", callable_obj.__name__) return f"{module}.{qual}" if module else qual # fallback for other callables (functors, functools.partial, etc.) try: module = getattr(callable_obj, "__module__", None) qual = getattr(callable_obj, "__qualname__", None) or getattr( callable_obj, "__name__", type(callable_obj).__name__ ) return f"{module}.{qual}" if module else qual except Exception: return urlsafe_b64encode(repr(callable_obj).encode("utf-8")).decode("utf-8") P = ParamSpec("P") R = TypeVar("R") def args_slice(func: Callable[P, R], *args: Iterable) -> tuple: if hasattr(func, "__call__") and not inspect.isroutine(func): return args[1:] if isinstance(func, staticmethod | classmethod): func = func.__func__ if inspect.ismethod(func): return args[1:] return tuple(*args) def cache_key(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> str: """Return a cache key for use with cached methods.""" kwargs_by_key = sorted(kwargs.items(), key=lambda kv: kv[0]) parts = chain( (qualified_name(func),), # positional args (repr(arg) for arg in args_slice(func, args)), # keyword args (f"{k}={v!r}" for k, v in kwargs_by_key), ) return ":".join(parts) def redis_cached(cfg: Config) -> Callable[[Callable[P, R]], Callable[P, R]]: """ """ def decorator(func: Callable[P, R]) -> Callable[P, R]: """ """ @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: key = cache_key(func, *args, **kwargs) if cfg.prefix is not None: key = f"{cfg.prefix}:{key}" # pre-hook result = func(*args, **kwargs) # post-hook return result return wrapper return decorator