"""Redis connector — async interface for Redis instance management. Provides connection, info retrieval, key CRUD, command execution, and performance monitoring via redis.asyncio. """ import json from typing import Any, Dict, List, Optional, Tuple import redis.asyncio as aioredis from app.core.crypto import decrypt_password class RedisConnector: """Async Redis connector for instance operations.""" @staticmethod async def get_connection(instance) -> aioredis.Redis: """Create an async Redis connection from an Instance model.""" pwd = decrypt_password(instance.password) return aioredis.Redis( host=instance.host, port=instance.port, password=pwd or None, decode_responses=True, ) @staticmethod async def get_info(instance) -> dict: """Get Redis INFO ALL parsed into a structured dict.""" conn = await RedisConnector.get_connection(instance) try: raw: str = await conn.info("all") return raw finally: await conn.close() @staticmethod async def get_dbsize(instance, db: int = 0) -> int: """Get number of keys in a database.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) return await conn.dbsize() finally: await conn.close() @staticmethod async def scan_keys( instance, db: int = 0, pattern: str = "*", type_filter: Optional[str] = None, cursor: int = 0, count: int = 50, ) -> Tuple[int, List[Dict[str, Any]]]: """SCAN keys with optional type filter. Returns (next_cursor, key_details).""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) next_cursor, keys = await conn.scan( cursor=cursor, match=pattern, count=count ) result = [] for key in keys: key_type = await conn.type(key) if type_filter and key_type != type_filter: continue ttl = await conn.ttl(key) result.append( {"key": key, "type": key_type, "ttl": ttl} ) return next_cursor, result finally: await conn.close() @staticmethod async def get_key_value( instance, db: int, key: str, key_type: Optional[str] = None ) -> Any: """Get value for a key based on its type.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) if key_type is None: key_type = await conn.type(key) if key_type == "string": value = await conn.get(key) elif key_type == "hash": value = await conn.hgetall(key) elif key_type == "list": value = await conn.lrange(key, 0, -1) elif key_type == "set": value = list(await conn.smembers(key)) elif key_type == "zset": raw = await conn.zrange(key, 0, -1, withscores=True) value = [{"member": m, "score": s} for m, s in raw] else: value = None ttl = await conn.ttl(key) return {"key": key, "type": key_type, "value": value, "ttl": ttl} finally: await conn.close() @staticmethod async def set_key_value( instance, db: int, key: str, value: Any, type_: str = "string", ttl: Optional[int] = None, ) -> str: """Set a key value. Type determines how value is stored.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) if type_ == "string": await conn.set(key, value) elif type_ == "hash": # value should be a dict if isinstance(value, str): value = json.loads(value) await conn.hset(key, mapping=value) elif type_ == "list": if isinstance(value, str): value = json.loads(value) await conn.delete(key) if value: await conn.rpush(key, *value) elif type_ == "set": if isinstance(value, str): value = json.loads(value) await conn.delete(key) if value: await conn.sadd(key, *value) elif type_ == "zset": if isinstance(value, str): value = json.loads(value) await conn.delete(key) if isinstance(value, list): for item in value: member = item.get("member", "") score = item.get("score", 0) await conn.zadd(key, {member: score}) if ttl and ttl > 0: await conn.expire(key, ttl) return "OK" finally: await conn.close() @staticmethod async def del_key(instance, db: int, key: str) -> int: """Delete a key. Returns number of keys deleted.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) return await conn.delete(key) finally: await conn.close() @staticmethod async def set_ttl(instance, db: int, key: str, seconds: int) -> bool: """Set TTL on a key.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) return await conn.expire(key, seconds) finally: await conn.close() @staticmethod async def get_ttl(instance, db: int, key: str) -> int: """Get TTL of a key.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) return await conn.ttl(key) finally: await conn.close() @staticmethod async def execute_command(instance, db: int, command: str) -> str: """Execute a raw Redis command.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) parts = command.strip().split() if not parts: return "" cmd = parts[0].upper() args = parts[1:] result = await conn.execute_command(cmd, *args) if result is None: return "(nil)" if isinstance(result, bytes): return result.decode("utf-8", errors="replace") if isinstance(result, (list, set)): formatted = [] for item in result: if isinstance(item, bytes): formatted.append(item.decode("utf-8", errors="replace")) else: formatted.append(str(item)) return "\n".join(formatted) return str(result) finally: await conn.close() @staticmethod async def flush_db(instance, db: int = 0) -> str: """FLUSHDB — dangerous! Admin only.""" conn = await RedisConnector.get_connection(instance) try: await conn.select(db) await conn.flushdb() return "OK" finally: await conn.close() @staticmethod async def get_slowlog(instance, count: int = 10) -> List[dict]: """Get slowlog entries.""" conn = await RedisConnector.get_connection(instance) try: raw = await conn.slowlog_get(count) result = [] for entry in raw: result.append( { "id": entry.get("id", entry.get(0)), "timestamp": entry.get("start_time", entry.get(1)), "duration_us": entry.get("duration", entry.get(2)), "command": " ".join( str(c) if not isinstance(c, bytes) else c.decode("utf-8", errors="replace") for c in entry.get("command", entry.get(3, [])) ), } ) return result finally: await conn.close() @staticmethod async def get_clients(instance) -> List[dict]: """Get client list.""" conn = await RedisConnector.get_connection(instance) try: raw: str = await conn.client_list() clients = [] for line in raw.split("\n"): line = line.strip() if not line: continue client = {} for pair in line.split(" "): if "=" in pair: k, v = pair.split("=", 1) client[k] = v if client: clients.append(client) return clients finally: await conn.close()