from functools import wraps import logging from django.core.cache import cache from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers logger = logging.getLogger(__name__) def invalidate_cache_patterns(*patterns): """ Safely invalidates Redis cache keys matching given glob patterns. Falls back to cache.clear() if the cache backend does not support delete_pattern. """ try: if hasattr(cache, 'delete_pattern'): total_deleted = 0 for pattern in patterns: deleted = cache.delete_pattern(pattern) total_deleted += deleted or 0 logger.info(f"Cache cleared for patterns {patterns} (deleted {total_deleted} keys)") return total_deleted else: cache.clear() logger.info(f"Cache cleared (fallback clear for patterns: {patterns})") return 1 except Exception as e: logger.error(f"Cache invalidation failed for patterns {patterns}: {e}") return 0 def smart_cached_view(timeout=60 * 60 * 2, key_prefix=''): """ Smart cache decorator for API views that: 1. Caches responses per Accept-Language header using Django's cache_page. 2. Bypasses cache when: - Cache-Control / Pragma: no-cache headers are present (e.g., Pull-to-refresh from mobile / web). - Query parameters: no_cache=true, refresh=true, is_bookmark=true, in_playlist=true are passed. """ def decorator(view_func): cached_func = cache_page(timeout, key_prefix=key_prefix)( vary_on_headers('Accept-Language')(view_func) ) @wraps(view_func) def _wrapped_view(request, *args, **kwargs): # 1. Check HTTP headers for no-cache directives cache_control = (request.META.get('HTTP_CACHE_CONTROL') or '').lower() pragma = (request.META.get('HTTP_PRAGMA') or '').lower() if 'no-cache' in cache_control or 'no-store' in cache_control or 'max-age=0' in cache_control or 'no-cache' in pragma: return view_func(request, *args, **kwargs) # 2. Check query params for explicit bypass query_params = getattr(request, 'GET', {}) no_cache_param = (query_params.get('no_cache') or query_params.get('refresh') or '').lower() if no_cache_param in ('1', 'true'): return view_func(request, *args, **kwargs) # 3. Check bookmark & playlist specific bypasses is_bookmark = (query_params.get('is_bookmark') or query_params.get('is_bookmarked') or '').lower() in_playlist = (query_params.get('in_playlist') or '').lower() if is_bookmark in ('1', 'true') or in_playlist in ('1', 'true'): return view_func(request, *args, **kwargs) return cached_func(request, *args, **kwargs) return _wrapped_view return decorator