import hashlib import json from collections.abc import Callable from functools import wraps from typing import Any, Concatenate, Protocol, cast from cachelib import BaseCache as Cache from fansifter_common.utils.encoders import SafeJSONEncoder class HasCacheProtocol(Protocol): cache: Cache def cached[R, HasCache: HasCacheProtocol, **P]( key: str, *, timeout: int | Callable[[], int] | None = None, timeout_param: str | None = None, ) -> Callable[ [Callable[Concatenate[HasCache, P], R]], Callable[Concatenate[HasCache, P], R], ]: def decorator( func: Callable[Concatenate[HasCache, P], R], ) -> Callable[Concatenate[HasCache, P], R]: @wraps(func) def wrapper(self: HasCache, /, *args: P.args, **kwargs: P.kwargs) -> R: cache_timeout: Any = timeout if cache_timeout is None and timeout_param: cache_timeout = getattr(self, timeout_param, None) cache_key = make_cache_key(key, *args, **kwargs) value = self.cache.get(cache_key) if value is None: value = func(self, *args, **kwargs) if callable(cache_timeout): cache_timeout = cache_timeout() if not isinstance(cache_timeout, int): cache_timeout = None self.cache.set(cache_key, value, timeout=cache_timeout) return cast(R, value) return wrapper return decorator def make_cache_key(key: str, *args: Any, **kwargs: Any) -> str: """Make a cache key from a key and optional args and kwargs.""" if args or kwargs: args_kwargs_str = json.dumps( {"args": args, "kwargs": kwargs}, cls=SafeJSONEncoder, separators=(",", ":"), sort_keys=True, ) hashed_args_kwargs = hashlib.sha256(args_kwargs_str.encode()).hexdigest() return f"{key}:{hashed_args_kwargs}" return key