"""Cache decorators.""" import datetime import functools import gzip import hashlib import json import time from collections import namedtuple from decimal import Decimal from typing import Callable from playlist.connectors import redis from playlist.features import is_disable_playlists_cache_enabled def cached(fn: Callable, key: str, ttl: int, *args, **kwargs): @functools.wraps(fn) def wrapped(args, kwargs): cached = redis.redis_client.get(key) if cached: return json.loads(cached) result = fn(*args, **kwargs) if result is not None: redis.redis_client.set(key, json.dumps(result), ex=ttl) return result return wrapped(args, kwargs) def cache_in_redis(ttl, key=None): """Wrap a function with a TTL Redis cache. Args: ttl (int): time to live in seconds key (str): unique cache key """ if not ttl: raise ValueError("Missing required arguments ttl") def wrap(orig_func): @functools.wraps(orig_func) def wrapped(*args, **kwargs): # Check if force_refresh is requested via kwargs force_refresh = kwargs.pop("force_refresh", False) if is_disable_playlists_cache_enabled(): return orig_func(*args, **kwargs) if key is None: cache_key = _generate_func_key(orig_func, *args, **kwargs) else: cache_key = key # Skip cache read if force_refresh is True if not force_refresh: cached_result = redis.redis_client.get(cache_key) if cached_result: return json.loads( gzip.decompress(cached_result).decode("utf-8"), object_hook=_deserialize_types, ) result = orig_func(*args, **kwargs) if result is not None: redis.redis_client.set( cache_key, gzip.compress( bytes(json.dumps(result, default=_serialize_types), "utf-8") ), ex=ttl, ) return result return wrapped return wrap def _generate_func_key(func, *args, **kwargs): """Create a key for Redis. Args: func (function): function object Return str: A key for Redis to store function return values. """ func_dict = {"func_name": func.__qualname__, "args": args, "kwargs": kwargs} return hashlib.sha256(repr(func_dict).encode()).hexdigest() def _serialize_types(obj): if isinstance(obj, (datetime.datetime)): return {"val": obj.timestamp(), "_spec_type": "datetime"} if isinstance(obj, (datetime.date)): return {"val": time.mktime(obj.timetuple()), "_spec_type": "date"} if isinstance(obj, Decimal): return {"val": str(obj), "_spec_type": "Decimal"} if hasattr(obj, "__dict__"): return {"val": vars(obj), "_spec_type": "obj_dict"} else: return str(obj) def _deserialize_types(obj): _spec_type = obj.get("_spec_type") if not _spec_type: return obj if _spec_type == "datetime": return datetime.datetime.fromtimestamp(obj["val"]) if _spec_type == "date": return datetime.date.fromtimestamp(obj["val"]) if _spec_type == "Decimal": return Decimal(obj["val"]) if _spec_type == "obj_dict": ObjTuple = namedtuple("ObjTuple", [*obj["val"]]) return ObjTuple(**obj["val"]) else: raise Exception("Unknown {}".format(_spec_type))