production-evyos-systems-an.../ServicesApi/Controllers/Redis/database.py

341 lines
11 KiB
Python

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