import arrow from typing import Optional, List, Dict, Union, Iterator from response import RedisResponse from connection import redis_cli from base import RedisRow class MainConfig: DATETIME_FORMAT: str = "YYYY-MM-DD HH:mm:ss" class RedisActions: """Class for handling Redis operations with JSON data.""" @classmethod def get_expiry_time(cls, expiry_kwargs: Dict[str, int]) -> int: """ Calculate expiry time in seconds from kwargs. Args: expiry_kwargs: Dictionary with time units as keys (days, hours, minutes, seconds) and their respective values. Returns: Total expiry time in seconds. """ time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1} return sum( int(expiry_kwargs.get(unit, 0)) * multiplier for unit, multiplier in time_multipliers.items() ) @classmethod def set_expiry_time(cls, expiry_seconds: int) -> Dict[str, int]: """ Convert total seconds back into a dictionary of time units. Args: expiry_seconds: Total expiry time in seconds. Returns: Dictionary with time units and their values. """ time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1} result = {} remaining_seconds = expiry_seconds if expiry_seconds < 0: return {} for unit, multiplier in time_multipliers.items(): if remaining_seconds >= multiplier: result[unit], remaining_seconds = divmod(remaining_seconds, multiplier) return result @classmethod def resolve_expires_at(cls, redis_row: RedisRow) -> str: """ Resolve expiry time for Redis key. Args: redis_row: RedisRow object containing the redis_key. Returns: Formatted expiry time string or message indicating no expiry. """ expiry_time = redis_cli.ttl(redis_row.redis_key) if expiry_time == -1: return "Key has no expiry time." if expiry_time == -2: return "Key does not exist." return arrow.now().shift(seconds=expiry_time).format(MainConfig.DATETIME_FORMAT) @classmethod def key_exists(cls, key: Union[str, bytes]) -> bool: """ Check if a key exists in Redis without retrieving its value. Args: key: Redis key to check. Returns: Boolean indicating if key exists. """ return bool(redis_cli.exists(key)) @classmethod def refresh_ttl( cls, key: Union[str, bytes], expires: Dict[str, int] ) -> RedisResponse: """ Refresh TTL for an existing key. Args: key: Redis key to refresh TTL. expires: Dictionary with time units to set new expiry. Returns: RedisResponse with operation result. """ try: if not cls.key_exists(key): return RedisResponse( status=False, message="Cannot refresh TTL: Key does not exist.", ) expiry_time = cls.get_expiry_time(expiry_kwargs=expires) redis_cli.expire(name=key, time=expiry_time) expires_at_string = ( arrow.now() .shift(seconds=expiry_time) .format(MainConfig.DATETIME_FORMAT) ) return RedisResponse( status=True, message="TTL refreshed successfully.", data={"key": key, "expires_at": expires_at_string}, ) except Exception as e: return RedisResponse( status=False, message="Failed to refresh TTL.", error=str(e), ) @classmethod def delete_key(cls, key: Union[Optional[str], Optional[bytes]]) -> RedisResponse: """ Delete a specific key from Redis. Args: key: Redis key to delete. Returns: RedisResponse with operation result. """ try: deleted_count = redis_cli.delete(key) if deleted_count > 0: return RedisResponse( status=True, message="Key deleted successfully.", data={"deleted_count": deleted_count}, ) return RedisResponse( status=False, message="Key not found or already deleted.", data={"deleted_count": 0}, ) except Exception as e: return RedisResponse( status=False, message="Failed to delete key.", error=str(e), ) @classmethod def delete( cls, list_keys: List[Union[Optional[str], Optional[bytes]]] ) -> RedisResponse: """ Delete multiple keys matching a pattern. Args: list_keys: List of key components to form pattern for deletion. Returns: RedisResponse with operation result. """ try: regex = RedisRow().regex(list_keys=list_keys) json_get = redis_cli.scan_iter(match=regex) deleted_keys, deleted_count = [], 0 # Use pipeline for batch deletion with redis_cli.pipeline() as pipe: for row in json_get: pipe.delete(row) deleted_keys.append(row) results = pipe.execute() deleted_count = sum(results) return RedisResponse( status=True, message="Keys deleted successfully.", data={"deleted_count": deleted_count, "deleted_keys": deleted_keys}, ) except Exception as e: return RedisResponse( status=False, message="Failed to delete keys.", error=str(e), ) @classmethod def set_json( cls, list_keys: List[Union[str, bytes]], value: Optional[Union[Dict, List]], expires: Optional[Dict[str, int]] = None, ) -> RedisResponse: """ Set JSON value in Redis with optional expiry. Args: list_keys: List of key components to form Redis key. value: JSON-serializable data to store. expires: Optional dictionary with time units for expiry. Returns: RedisResponse with operation result. """ redis_row = RedisRow() redis_row.merge(set_values=list_keys) redis_row.feed(value) redis_row.expires_at_string = None redis_row.expires_at = None try: if expires: redis_row.expires_at = expires expiry_time = cls.get_expiry_time(expiry_kwargs=expires) redis_cli.setex( name=redis_row.redis_key, time=expiry_time, value=redis_row.value, ) redis_row.expires_at_string = str( arrow.now() .shift(seconds=expiry_time) .format(MainConfig.DATETIME_FORMAT) ) else: redis_cli.set(name=redis_row.redis_key, value=redis_row.value) return RedisResponse( status=True, message="Value set successfully.", data=redis_row, ) except Exception as e: return RedisResponse( status=False, message="Failed to set value.", error=str(e), ) @classmethod def get_json( cls, list_keys: List[Union[Optional[str], Optional[bytes]]], limit: Optional[int] = None, ) -> RedisResponse: """ Get JSON values from Redis using pattern matching. Args: list_keys: List of key components to form pattern for retrieval. limit: Optional limit on number of results to return. Returns: RedisResponse with operation result. """ try: list_of_rows, count = [], 0 regex = RedisRow.regex(list_keys=list_keys) json_get = redis_cli.scan_iter(match=regex) for row in json_get: if limit is not None and count >= limit: break redis_row = RedisRow() redis_row.set_key(key=row) # Use pipeline for batch retrieval with redis_cli.pipeline() as pipe: pipe.get(row) pipe.ttl(row) redis_value, redis_value_expire = pipe.execute() redis_row.expires_at = cls.set_expiry_time( expiry_seconds=int(redis_value_expire) ) redis_row.expires_at_string = cls.resolve_expires_at( redis_row=redis_row ) redis_row.feed(redis_value) list_of_rows.append(redis_row) count += 1 if list_of_rows: return RedisResponse( status=True, message="Values retrieved successfully.", data=list_of_rows, ) return RedisResponse( status=False, message="No matching keys found.", data=list_of_rows, ) except Exception as e: return RedisResponse( status=False, message="Failed to retrieve values.", error=str(e), ) @classmethod def get_json_iterator( cls, list_keys: List[Union[Optional[str], Optional[bytes]]] ) -> Iterator[RedisRow]: """ Get JSON values from Redis as an iterator for memory-efficient processing of large datasets. Args: list_keys: List of key components to form pattern for retrieval. Returns: Iterator yielding RedisRow objects. Raises: RedisValueError: If there's an error processing a row """ regex = RedisRow.regex(list_keys=list_keys) json_get = redis_cli.scan_iter(match=regex) for row in json_get: try: redis_row = RedisRow() redis_row.set_key(key=row) # Use pipeline for batch retrieval with redis_cli.pipeline() as pipe: pipe.get(row) pipe.ttl(row) redis_value, redis_value_expire = pipe.execute() redis_row.expires_at = cls.set_expiry_time( expiry_seconds=int(redis_value_expire) ) redis_row.expires_at_string = cls.resolve_expires_at( redis_row=redis_row ) redis_row.feed(redis_value) yield redis_row except Exception as e: # Log the error and continue with next row print(f"Error processing row {row}: {str(e)}") continue