from typing import Any, Dict, List, Optional, TypeVar, Generic, Type from dataclasses import dataclass from flask import Flask, current_app from dogpile.cache import CacheRegion from abc import ABC, abstractmethod T = TypeVar('T') # Generic type parameter for cached data @dataclass class CacheKey: """ Represents a composite cache key made up of multiple components. Enables structured and consistent key generation for cache entries. Attributes: components (Dict[str, Any]): Dictionary of key components and their values Example: key = CacheKey({'tenant_id': 123, 'user_id': 456}) str(key) -> "tenant_id=123:user_id=456" """ components: Dict[str, Any] def __str__(self) -> str: """ Converts components into a deterministic string representation. Components are sorted alphabetically to ensure consistent key generation. """ return ":".join(f"{k}={v}" for k, v in sorted(self.components.items())) class CacheHandler(Generic[T]): """ Base cache handler implementation providing structured caching functionality. Uses generics to ensure type safety of cached data. Type Parameters: T: Type of data being cached Attributes: region (CacheRegion): Dogpile cache region for storage prefix (str): Prefix for all cache keys managed by this handler """ def __init__(self, region: CacheRegion, prefix: str): self.region = region self.prefix = prefix self._key_components = [] # List of required key components @abstractmethod def _to_cache_data(self, instance: T) -> Any: """ Convert the data to a cacheable format for internal use. Args: instance: The data to be cached. Returns: A serializable format of the instance. """ raise NotImplementedError @abstractmethod def _from_cache_data(self, data: Any, **kwargs) -> T: """ Convert cached data back to usable format for internal use. Args: data: The cached data. **kwargs: Additional context. Returns: The data in its usable format. """ raise NotImplementedError @abstractmethod def _should_cache(self, value: T) -> bool: """ Validate if the value should be cached for internal use. Args: value: The value to be cached. Returns: True if the value should be cached, False otherwise. """ raise NotImplementedError def configure_keys(self, *components: str): """ Configure required components for cache key generation. Args: *components: Required key component names Returns: self for method chaining """ self._key_components = components return self def generate_key(self, **identifiers) -> str: """ Generate a cache key from provided identifiers. Args: **identifiers: Key-value pairs for key components Returns: Formatted cache key string Raises: ValueError: If required components are missing """ missing = set(self._key_components) - set(identifiers.keys()) if missing: raise ValueError(f"Missing key components: {missing}") region_name = getattr(self.region, 'name', 'default_region') key = CacheKey({k: identifiers[k] for k in self._key_components}) return f"{region_name}:{self.prefix}:{str(key)}" def get(self, creator_func, **identifiers) -> T: """ Get or create a cached value. Args: creator_func: Function to create value if not cached **identifiers: Key components for cache key Returns: Cached or newly created value """ cache_key = self.generate_key(**identifiers) def creator(): instance = creator_func(**identifiers) serialized_instance = self._to_cache_data(instance) return serialized_instance cached_data = self.region.get_or_create( cache_key, creator, should_cache_fn=self._should_cache ) return self._from_cache_data(cached_data, **identifiers) def invalidate(self, **identifiers): """ Invalidate a specific cache entry. Args: **identifiers: Key components for the cache entry """ cache_key = self.generate_key(**identifiers) self.region.delete(cache_key) def invalidate_by_model(self, model: str, **identifiers): """ Invalidate cache entry based on model changes. Args: model: Changed model name **identifiers: Model instance identifiers """ try: self.invalidate(**identifiers) except ValueError: pass # Skip if cache key can't be generated from provided identifiers def invalidate_region(self): """ Invalidate all cache entries within this region. Deletes all keys that start with the region prefix. """ # Construct the pattern for all keys in this region pattern = f"{self.region}:{self.prefix}:*" # Assuming Redis backend with dogpile, use `delete_multi` or direct Redis access if hasattr(self.region.backend, 'client'): redis_client = self.region.backend.client keys_to_delete = redis_client.keys(pattern) if keys_to_delete: redis_client.delete(*keys_to_delete) else: # Fallback for other backends raise NotImplementedError("Region invalidation is only supported for Redis backend.")