""" Redis Service для кеширования, rate limiting, сессий """ import redis.asyncio as redis from typing import Optional, Any import json from ..config import settings import logging logger = logging.getLogger(__name__) class RedisService: """Сервис для работы с Redis""" def __init__(self): self.client: Optional[redis.Redis] = None async def connect(self): """Подключение к Redis""" try: self.client = await redis.from_url( settings.redis_url, encoding="utf-8", decode_responses=True ) await self.client.ping() logger.info(f"✅ Redis connected: {settings.redis_host}:{settings.redis_port}") except Exception as e: logger.error(f"❌ Redis connection error: {e}") raise async def disconnect(self): """Отключение от Redis""" if self.client: await self.client.close() logger.info("Redis connection closed") async def get(self, key: str) -> Optional[str]: """Получить значение по ключу""" full_key = f"{settings.redis_prefix}{key}" return await self.client.get(full_key) async def set(self, key: str, value: Any, expire: Optional[int] = None): """Установить значение с опциональным TTL (в секундах)""" full_key = f"{settings.redis_prefix}{key}" if isinstance(value, (dict, list)): value = json.dumps(value) if expire: await self.client.setex(full_key, expire, value) else: await self.client.set(full_key, value) async def publish(self, channel: str, message: str): """Публикация сообщения в канал Redis Pub/Sub""" try: await self.client.publish(channel, message) except Exception as e: logger.error(f"❌ Redis publish error: {e}") async def delete(self, key: str) -> bool: """Удалить ключ""" full_key = f"{settings.redis_prefix}{key}" result = await self.client.delete(full_key) return result > 0 async def exists(self, key: str) -> bool: """Проверить существование ключа""" full_key = f"{settings.redis_prefix}{key}" return await self.client.exists(full_key) > 0 async def increment(self, key: str, amount: int = 1) -> int: """Инкремент значения""" full_key = f"{settings.redis_prefix}{key}" return await self.client.incrby(full_key, amount) async def expire(self, key: str, seconds: int): """Установить TTL для ключа""" full_key = f"{settings.redis_prefix}{key}" await self.client.expire(full_key, seconds) async def get_json(self, key: str) -> Optional[dict]: """Получить JSON значение""" value = await self.get(key) if value: try: return json.loads(value) except json.JSONDecodeError: return None return None async def set_json(self, key: str, value: dict, expire: Optional[int] = None): """Установить JSON значение""" await self.set(key, json.dumps(value), expire) async def health_check(self) -> bool: """Проверка здоровья Redis""" try: return await self.client.ping() except Exception as e: logger.error(f"Redis health check failed: {e}") return False # ============================================ # RATE LIMITING # ============================================ async def check_rate_limit(self, identifier: str, max_requests: int, window_seconds: int) -> tuple[bool, int]: """ Проверка rate limiting Returns: (allowed: bool, remaining: int) """ key = f"ratelimit:{identifier}" full_key = f"{settings.redis_prefix}{key}" current = await self.client.get(full_key) if current is None: # Первый запрос в окне await self.client.setex(full_key, window_seconds, 1) return True, max_requests - 1 current_count = int(current) if current_count >= max_requests: # Лимит превышен ttl = await self.client.ttl(full_key) return False, 0 # Инкремент счетчика new_count = await self.client.incr(full_key) return True, max_requests - new_count # ============================================ # CACHE # ============================================ async def cache_get(self, cache_key: str) -> Optional[Any]: """Получить из кеша""" return await self.get_json(f"cache:{cache_key}") async def cache_set(self, cache_key: str, value: Any, ttl: int = 3600): """Сохранить в кеш (TTL по умолчанию 1 час)""" await self.set_json(f"cache:{cache_key}", value, ttl) async def cache_delete(self, cache_key: str): """Удалить из кеша""" await self.delete(f"cache:{cache_key}") # Глобальный экземпляр redis_service = RedisService()