341 lines
11 KiB
Python
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
|