""" Redis Cache for Strapi MCP Server """ import json import hashlib from typing import Any, Optional, Callable from redis import asyncio as aioredis from pydantic_settings import BaseSettings from pydantic import ConfigDict class CacheSettings(BaseSettings): """Cache configuration""" redis_host: str = "localhost" redis_port: int = 6379 redis_password: Optional[str] = None redis_db: int = 1 # 使用不同的 DB 避免 key 冲突 cache_ttl: int = 3600 # 默认缓存 1 小时 model_config = ConfigDict(env_file=".env") cache_settings = CacheSettings() class StrapiCache: """Redis cache wrapper for Strapi responses""" def __init__( self, host: Optional[str] = None, port: Optional[int] = None, password: Optional[str] = None, db: Optional[int] = None, ttl: Optional[int] = None ): self.host = host or cache_settings.redis_host self.port = port or cache_settings.redis_port self.password = password or cache_settings.redis_password self.db = db or cache_settings.redis_db self.ttl = ttl or cache_settings.cache_ttl self._redis: Optional[aioredis.Redis] = None async def _get_redis(self) -> aioredis.Redis: """Get or create Redis connection""" if self._redis is None: self._redis = aioredis.from_url( f"redis://{':' + self.password if self.password else ''}@{self.host}:{self.port}/{self.db}", encoding="utf-8", decode_responses=True ) return self._redis def _generate_key(self, category: str, locale: str, **kwargs) -> str: """Generate cache key from parameters""" # 创建唯一 key key_parts = [category, locale] for k, v in sorted(kwargs.items()): key_parts.append(f"{k}:{v}") key_string = ":".join(key_parts) # 使用 MD5 hash 缩短 key 长度 return f"strapi:{hashlib.md5(key_string.encode()).hexdigest()}" async def get(self, key: str) -> Optional[Any]: """Get value from cache""" try: redis = await self._get_redis() value = await redis.get(key) if value: return json.loads(value) except Exception: # Redis 不可用时降级,不影响业务 pass return None async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool: """Set value in cache""" try: redis = await self._get_redis() ttl = ttl or self.ttl await redis.setex(key, ttl, json.dumps(value, ensure_ascii=False)) return True except Exception: # Redis 不可用时降级 return False async def delete(self, key: str) -> bool: """Delete value from cache""" try: redis = await self._get_redis() await redis.delete(key) return True except Exception: return False async def clear_pattern(self, pattern: str) -> int: """Clear all keys matching pattern""" try: redis = await self._get_redis() keys = await redis.keys(f"{pattern}*") if keys: await redis.delete(*keys) return len(keys) except Exception: return 0 async def close(self): """Close Redis connection""" if self._redis: await self._redis.close() self._redis = None # 全局缓存实例 cache = StrapiCache() async def cached_query( cache_key: str, query_func: Callable, ttl: Optional[int] = None ) -> Any: """Execute cached query Args: cache_key: Cache key query_func: Async function to fetch data ttl: Cache TTL in seconds (overrides default) Returns: Cached or fresh data """ # Try to get from cache cached_value = await cache.get(cache_key) if cached_value is not None: return cached_value # Cache miss, execute query result = await query_func() # Store in cache if result is not None: await cache.set(cache_key, result, ttl) return result async def clear_strapi_cache(pattern: Optional[str] = None) -> int: """Clear Strapi cache Args: pattern: Key pattern to clear (default: all strapi keys) Returns: Number of keys deleted """ if pattern: return await cache.clear_pattern(f"strapi:{pattern}") else: return await cache.clear_pattern("strapi:")