"""Cache decorators.""" import datetime import functools import gzip import hashlib import json import time from collections import namedtuple from decimal import Decimal from charts import config from charts.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=config.REDIS_CACHE_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): from application import app 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 try: cached_result = redis.client.get(cache_key) except redis.exceptions.ConnectionError: app.logger.warning("Redis connection error") cached_result = None 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: try: redis.client.set( cache_key, gzip.compress( bytes(json.dumps(result, default=_serialize_types), "utf-8") ), ex=ttl, ) except redis.exceptions.ConnectionError: app.logger.warning("Redis connection error") 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()