"""Cache decorators.""" import datetime import functools import gzip import hashlib import json import time from collections import namedtuple from decimal import Decimal from analytics.connectors import redis 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)) def cache_in_redis(ttl, key=None, concatenate_key_with_args=False): """Wrap a function with a TTL Redis cache. Args: ttl (int): time to live in seconds key (str): unique cache key concatenate_key_with_args (bool): concatenate key with args """ if not ttl: raise ValueError("Missing required arguments ttl") def wrap(orig_func): @functools.wraps(orig_func) def wrapped(*args, **kwargs): if key is None: cache_key = generate_func_key(orig_func, *args, **kwargs) else: cache_key = key if concatenate_key_with_args and key: cache_key = key + cache_key cached_result = 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.client.set( cache_key, gzip.compress( bytes(json.dumps(result, default=_serialize_types), "utf-8") ), ex=ttl, ) return result return wrapped return wrap def create_permissions_key(profile_type, profile_id, permission): """Create a permission key for Redis. Args: profile_type (str): profile type profile_id (int): profile id permission (str): permission Return str: A key for Redis to store permissions """ return ( "profile_type:{profile_type}" "profile_id:{profile_id}" "permission:{permission}".format( profile_type=profile_type, profile_id=profile_id, permission=permission ) ) def create_access_key(account_type, account_id, user_id, access): """Create a key for Redis. Args: account_id (str): Vendor unique identifier account_type (str): Vendor or subaccount user_id (str): Orchard-User-Id, e.g. alw:48071 Return str: A key for Redis to store access status. """ return ( "account_type:{account_type}" "account_id:{account_id}" "user_id:{user_id}" "access:{access}".format( account_type=account_type, account_id=account_id, user_id=user_id, access=access, ) ) 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()