from atlas_um.logs import logger class BaseCachingStrategy: """ Base caching strategy class. The main idea is provide some interface and base logic for building sets of keys required for caching objects and invalidation of all dependent cache items. Lets say we need to cache 2 endpoints that both depends on same object, e.g. claim value: 1. /api/resource_groups/{resource_group_id}/claims/{claim_name_id} 2. /api/users/{user_id}/resource_groups/{resource_group_id}/claims/{claim_name_id} # noqa In this case logic behind the keys for endpoints would be the following: 1. /claim_values/resource_group_id=:claim_name_id=/ # noqa 2. /users/user_id=:resource_group_id=:claim_name_id:/ # noqa Each endpoint would have it`s own keys series. Then if the claim value will have some changes, e.g. we change it`s content, so it would be possible to invalidate cache parts for both endpoints from somewhere inside of business logic using only some core params, without to know anything about where related data are cached. We`ll just use keys logic from strategies for this, so the invalidation patterns would be the following: /claim_values* /*resource_group_id=* /*claim_name_id=* And this allow to clear all cache related to changed claim value, e.g. generic claim values list endpoint and user claim values endpoint, and any other endpoint in dependencies chain. """ BASE_PREFIX = "" PREFIX_ARGS = () PREFIX_TO_OBJECT_MAPPING = {} def __init__(self, args, key=""): self._args = args self._key = key @property def key(self): return f"/{self.prefix}/{self._key}" @property def prefix(self): return f"{self.BASE_PREFIX}/{self.params_string}" @property def invalidation_patterns(self): return [f"*{self.BASE_PREFIX}*", *(f"*{p}*" for p in self.params if p)] @property def params_string(self): return ":".join(self.params) @property def params(self): items = [] for k, v in self._args.items(): if k not in self.PREFIX_ARGS: continue if isinstance(v, list): items.extend(f"{k}={vv}" for vv in v if vv) else: items.append(f"{k}={v}") return set(items) @classmethod def from_object(cls, obj): args = {} for arg, attr in cls.PREFIX_TO_OBJECT_MAPPING.items(): try: args[arg] = cls._traverse_attr(obj, attr) except AttributeError: logger.bind(arg=arg, attr=attr, obj=obj).warning( "Can`t find corresponding attribute" ) return cls(args) @staticmethod def _traverse_attr(obj, attr): """Traversing all nested object attributes.""" vals = [] val = None if "." not in attr: val = getattr(obj, attr) else: bits = attr.split(".") for bit in bits: if isinstance(obj, list): for item in obj: val = getattr(item, bit) vals.append(val) return vals else: val = getattr(obj, bit) obj = val vals.append(val) return vals class ResourceGroupsCache(BaseCachingStrategy): BASE_PREFIX = "resource_groups_cache" PREFIX_ARGS = ("resource_group_id",) PREFIX_TO_OBJECT_MAPPING = {"resource_group_id": "external_id"} class ClaimNamesCache(BaseCachingStrategy): BASE_PREFIX = "claim_names_cache" PREFIX_ARGS = ("resource_group_id", "claim_name_id") PREFIX_TO_OBJECT_MAPPING = { "claim_name_id": "external_id", "resource_group_id": "resource_group.external_id", } class ClaimValuesCache(BaseCachingStrategy): BASE_PREFIX = "claim_values_cache" PREFIX_ARGS = ("resource_group_id", "claim_name_id", "claim_id") PREFIX_TO_OBJECT_MAPPING = { "claim_value_id": "external_id", "claim_name_id": "claim_name.external_id", } class DNAAccountsCache(BaseCachingStrategy): BASE_PREFIX = "users_cache" PREFIX_ARGS = ( "user_id", "resource_group_id", "claim_name_id", "claim_id", ) PREFIX_TO_OBJECT_MAPPING = { "user_id": "sub", } @property def invalidation_patterns(self): patterns = super().invalidation_patterns return [*patterns, "*claim_id*"] class DNAAccountsDetailsCache(BaseCachingStrategy): BASE_PREFIX = "users_cache" PREFIX_ARGS = () @property def invalidation_patterns(self): patterns = super().invalidation_patterns return [*patterns, "*user_id*"]