import functools import hashlib import flask_caching from flask import request class Cache(flask_caching.Cache): """Customized cache class with extra features""" SCAN_BATCH = 10000 def cached_with( self, strategy, timeout=None, unless=None, forced_update=None, response_filter=None, cache_none=False, ): return super().cached( timeout, make_cache_key=lambda *args, **kwargs: self._make_strategy_key( strategy ), unless=unless, forced_update=forced_update, response_filter=response_filter, cache_none=cache_none, ) @staticmethod def _make_strategy_key(strategy): """Default flask caching logic extended with strategy logic.""" args_as_sorted_tuple = tuple( sorted((pair for pair in request.args.items(multi=True))) ) args_as_bytes = str(args_as_sorted_tuple).encode() cache_hash = hashlib.md5(args_as_bytes) # nosec cache_hash = str(cache_hash.hexdigest()) cache_key = request.path + cache_hash return strategy(request.view_args, cache_key).key def scan_all(self, pattern): """ More efficient alternative to KEYS command with SCAN command, as KEYS command is not recommended for prod envs. """ raw_keys = [ k.decode() for k in self.cache._read_client.scan_iter( f"{self.cache.key_prefix}{pattern}", self.SCAN_BATCH ) ] return [ k[ k.startswith(self.cache.key_prefix) and len(self.cache.key_prefix) : # noqa ] for k in raw_keys ] def delete_by_pattern(self, pattern): keys = self.scan_all(pattern) self.delete_many(*keys) def invalidated_with(self, strategy): """ Decorator for strategy based invalidation for logic services that makes some actions on data. """ def decorator(func): @functools.wraps(func) def decorated_func(*args, **kwargs): result = func(*args, **kwargs) if result.is_right: self.invalidate_related_cache(strategy, result.value) return result return decorated_func return decorator def invalidate_related_cache(self, strategy, obj): obj_strategy = strategy.from_object(obj) for pattern in obj_strategy.invalidation_patterns: self.delete_by_pattern(pattern)