updated Mongo Postgres Redis Controllers
This commit is contained in:
@@ -12,7 +12,10 @@ import arrow
|
||||
import json
|
||||
|
||||
from connection import redis_cli
|
||||
from typing import Union, Dict, List, Optional, Any, ClassVar
|
||||
from typing import Union, Dict, List, Optional, Any, TypeVar
|
||||
|
||||
|
||||
T = TypeVar('T', Dict[str, Any], List[Any])
|
||||
|
||||
|
||||
class RedisKeyError(Exception):
|
||||
@@ -86,7 +89,7 @@ class RedisRow:
|
||||
self.key = self.delimiter.join(merged).encode()
|
||||
|
||||
@classmethod
|
||||
def regex(cls, list_keys: List[Union[str, bytes, None]]) -> str:
|
||||
def regex(cls, list_keys: List[Union[Optional[str], Optional[bytes]]]) -> str:
|
||||
"""
|
||||
Generate Redis search pattern from list of keys.
|
||||
|
||||
@@ -250,7 +253,7 @@ class RedisRow:
|
||||
try:
|
||||
redis_cli.delete(self.redis_key)
|
||||
except Exception as e:
|
||||
print(f"Error deleting key: {str(e)}")
|
||||
raise RedisKeyError(f"Failed to delete key: {str(e)}")
|
||||
|
||||
@property
|
||||
def keys(self) -> str:
|
||||
@@ -268,9 +271,24 @@ class RedisRow:
|
||||
|
||||
Args:
|
||||
key: Key in string or bytes format
|
||||
|
||||
Raises:
|
||||
RedisKeyError: If key is empty or invalid
|
||||
"""
|
||||
if not key:
|
||||
raise RedisKeyError("Cannot set empty key")
|
||||
|
||||
# Convert to string for validation
|
||||
key_str = key.decode() if isinstance(key, bytes) else str(key)
|
||||
|
||||
# Validate key length (Redis has a 512MB limit for keys)
|
||||
if len(key_str) > 512 * 1024 * 1024:
|
||||
raise RedisKeyError("Key exceeds maximum length of 512MB")
|
||||
|
||||
# Validate key format (basic check for invalid characters)
|
||||
if any(c in key_str for c in ['\n', '\r', '\t', '\0']):
|
||||
raise RedisKeyError("Key contains invalid characters")
|
||||
|
||||
self.key = key if isinstance(key, bytes) else str(key).encode()
|
||||
|
||||
@property
|
||||
|
||||
@@ -27,8 +27,9 @@ class RedisConn:
|
||||
max_retries: Maximum number of connection attempts.
|
||||
"""
|
||||
self.max_retries = max_retries
|
||||
self.config = config
|
||||
self.config = config or {}
|
||||
self._redis = None
|
||||
self._pool = None
|
||||
|
||||
# Add default parameters if not provided
|
||||
if "socket_timeout" not in self.config:
|
||||
@@ -45,6 +46,22 @@ class RedisConn:
|
||||
# Initialize the connection with retry logic
|
||||
self._connect_with_retry()
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup Redis connection and pool on object destruction."""
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close Redis connection and connection pool."""
|
||||
try:
|
||||
if self._redis:
|
||||
self._redis.close()
|
||||
self._redis = None
|
||||
if self._pool:
|
||||
self._pool.disconnect()
|
||||
self._pool = None
|
||||
except Exception as e:
|
||||
print(f"Error closing Redis connection: {str(e)}")
|
||||
|
||||
def _connect_with_retry(self) -> None:
|
||||
"""
|
||||
Attempt to establish a Redis connection with retry logic.
|
||||
@@ -54,7 +71,10 @@ class RedisConn:
|
||||
"""
|
||||
for attempt in range(1, self.max_retries + 1):
|
||||
try:
|
||||
self._redis = Redis(**self.config)
|
||||
if self._pool is None:
|
||||
from redis import ConnectionPool
|
||||
self._pool = ConnectionPool(**self.config)
|
||||
self._redis = Redis(connection_pool=self._pool)
|
||||
if self.check_connection():
|
||||
return
|
||||
except (ConnectionError, TimeoutError) as e:
|
||||
|
||||
@@ -323,6 +323,9 @@ class RedisActions:
|
||||
|
||||
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)
|
||||
@@ -346,5 +349,6 @@ class RedisActions:
|
||||
redis_row.feed(redis_value)
|
||||
yield redis_row
|
||||
except Exception as e:
|
||||
err = e
|
||||
# Log the error and continue with next row
|
||||
print(f"Error processing row {row}: {str(e)}")
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from typing import Dict, List, Optional
|
||||
from database import RedisActions
|
||||
|
||||
def example_set_json() -> None:
|
||||
"""Example of setting JSON data in Redis with and without expiry."""
|
||||
# Example 1: Set JSON without expiry
|
||||
data = {"name": "John", "age": 30, "city": "New York"}
|
||||
keys = ["user", "profile", "123"]
|
||||
result = RedisActions.set_json(list_keys=keys, value=data)
|
||||
print("Set JSON without expiry:", result)
|
||||
|
||||
# Example 2: Set JSON with expiry
|
||||
expiry = {"hours": 1, "minutes": 30}
|
||||
result = RedisActions.set_json(list_keys=keys, value=data, expires=expiry)
|
||||
print("Set JSON with expiry:", result)
|
||||
|
||||
def example_get_json() -> None:
|
||||
"""Example of retrieving JSON data from Redis."""
|
||||
# Example 1: Get all matching keys
|
||||
keys = ["user", "profile", "*"]
|
||||
result = RedisActions.get_json(list_keys=keys)
|
||||
print("Get all matching JSON:", result)
|
||||
|
||||
# Example 2: Get with limit
|
||||
result = RedisActions.get_json(list_keys=keys, limit=5)
|
||||
print("Get JSON with limit:", result)
|
||||
|
||||
def example_get_json_iterator() -> None:
|
||||
"""Example of using the JSON iterator for large datasets."""
|
||||
keys = ["user", "profile", "*"]
|
||||
for row in RedisActions.get_json_iterator(list_keys=keys):
|
||||
print("Iterating over JSON row:", row)
|
||||
|
||||
def example_delete_key() -> None:
|
||||
"""Example of deleting a specific key."""
|
||||
key = "user:profile:123"
|
||||
result = RedisActions.delete_key(key)
|
||||
print("Delete specific key:", result)
|
||||
|
||||
def example_delete() -> None:
|
||||
"""Example of deleting multiple keys matching a pattern."""
|
||||
keys = ["user", "profile", "*"]
|
||||
result = RedisActions.delete(list_keys=keys)
|
||||
print("Delete multiple keys:", result)
|
||||
|
||||
def example_refresh_ttl() -> None:
|
||||
"""Example of refreshing TTL for a key."""
|
||||
key = "user:profile:123"
|
||||
new_expiry = {"hours": 2, "minutes": 0}
|
||||
result = RedisActions.refresh_ttl(key=key, expires=new_expiry)
|
||||
print("Refresh TTL:", result)
|
||||
|
||||
def example_key_exists() -> None:
|
||||
"""Example of checking if a key exists."""
|
||||
key = "user:profile:123"
|
||||
exists = RedisActions.key_exists(key)
|
||||
print(f"Key {key} exists:", exists)
|
||||
|
||||
def example_resolve_expires_at() -> None:
|
||||
"""Example of resolving expiry time for a key."""
|
||||
from base import RedisRow
|
||||
redis_row = RedisRow()
|
||||
redis_row.redis_key = "user:profile:123"
|
||||
expires_at = RedisActions.resolve_expires_at(redis_row)
|
||||
print("Resolve expires at:", expires_at)
|
||||
|
||||
def run_all_examples() -> None:
|
||||
"""Run all example functions to demonstrate RedisActions functionality."""
|
||||
print("\n=== Redis Actions Examples ===\n")
|
||||
|
||||
print("1. Setting JSON data:")
|
||||
example_set_json()
|
||||
|
||||
print("\n2. Getting JSON data:")
|
||||
example_get_json()
|
||||
|
||||
print("\n3. Using JSON iterator:")
|
||||
example_get_json_iterator()
|
||||
|
||||
print("\n4. Deleting specific key:")
|
||||
example_delete_key()
|
||||
|
||||
print("\n5. Deleting multiple keys:")
|
||||
example_delete()
|
||||
|
||||
print("\n6. Refreshing TTL:")
|
||||
example_refresh_ttl()
|
||||
|
||||
print("\n7. Checking key existence:")
|
||||
example_key_exists()
|
||||
|
||||
print("\n8. Resolving expiry time:")
|
||||
example_resolve_expires_at()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all_examples()
|
||||
|
||||
@@ -3,69 +3,194 @@ from base import RedisRow
|
||||
|
||||
|
||||
class RedisResponse:
|
||||
"""Base class for Redis response handling."""
|
||||
"""
|
||||
Base class for Redis response handling.
|
||||
|
||||
Provides a standardized way to return and process Redis operation results,
|
||||
with tools to convert between different data representations.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status: bool,
|
||||
message: str,
|
||||
data: Any = None,
|
||||
error: Optional[str] = None,
|
||||
self,
|
||||
status: bool,
|
||||
message: str,
|
||||
data: Any = None,
|
||||
error: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize a Redis response.
|
||||
|
||||
Args:
|
||||
status: Operation success status
|
||||
message: Human-readable message about the operation
|
||||
data: Response data (can be None, RedisRow, list, or dict)
|
||||
error: Optional error message if operation failed
|
||||
"""
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.data = data
|
||||
self.error = error
|
||||
|
||||
if isinstance(data, Dict):
|
||||
# Determine the data type
|
||||
if isinstance(data, dict):
|
||||
self.data_type = "dict"
|
||||
elif isinstance(data, list):
|
||||
self.data_type = "list"
|
||||
elif isinstance(data, RedisRow):
|
||||
self.data_type = "row"
|
||||
elif data is None:
|
||||
elif isinstance(data, (int, float, str, bool)):
|
||||
self.data_type = "primitive"
|
||||
else:
|
||||
self.data_type = None
|
||||
self.error = error
|
||||
|
||||
def as_dict(self) -> Dict:
|
||||
data = self.all
|
||||
"""
|
||||
Convert the response to a dictionary format suitable for serialization.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the response
|
||||
"""
|
||||
# Base response fields
|
||||
main_dict = {
|
||||
"status": self.status, "message": self.message,
|
||||
"count": self.count, "dataType": getattr(self, "data_type", None),
|
||||
"status": self.status,
|
||||
"message": self.message,
|
||||
"count": self.count,
|
||||
"dataType": self.data_type,
|
||||
}
|
||||
|
||||
# Add error if present
|
||||
if self.error:
|
||||
main_dict["error"] = self.error
|
||||
|
||||
data = self.all
|
||||
|
||||
# Process single RedisRow
|
||||
if isinstance(data, RedisRow):
|
||||
dict_return = {data.keys: data.row}
|
||||
dict_return.update(dict(main_dict))
|
||||
return dict_return
|
||||
result = {**main_dict}
|
||||
if hasattr(data, 'keys') and hasattr(data, 'row'):
|
||||
if not isinstance(data.keys, str):
|
||||
raise ValueError("RedisRow keys must be string type")
|
||||
result[data.keys] = data.row
|
||||
return result
|
||||
|
||||
# Process list of RedisRows
|
||||
elif isinstance(data, list):
|
||||
dict_return = {}
|
||||
result = {**main_dict}
|
||||
|
||||
# Handle list of RedisRow objects
|
||||
rows_dict = {}
|
||||
for row in data:
|
||||
if isinstance(row, RedisRow):
|
||||
dict_return.update({row.keys: row.row})
|
||||
dict_return.update(dict(main_dict))
|
||||
return dict_return
|
||||
if isinstance(row, RedisRow) and hasattr(row, 'keys') and hasattr(row, 'row'):
|
||||
if not isinstance(row.keys, str):
|
||||
raise ValueError("RedisRow keys must be string type")
|
||||
rows_dict[row.keys] = row.row
|
||||
|
||||
if rows_dict:
|
||||
result["data"] = rows_dict
|
||||
elif data: # If it's just a regular list with items
|
||||
result["data"] = data
|
||||
|
||||
return result
|
||||
|
||||
# Process dictionary
|
||||
elif isinstance(data, dict):
|
||||
return {**main_dict, "data": data}
|
||||
|
||||
return main_dict
|
||||
|
||||
@property
|
||||
def all(self) -> Union[Optional[List[RedisRow]]]:
|
||||
return self.data or []
|
||||
def all(self) -> Any:
|
||||
"""
|
||||
Get all data from the response.
|
||||
|
||||
Returns:
|
||||
All data or empty list if None
|
||||
"""
|
||||
return self.data if self.data is not None else []
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
row = self.all
|
||||
if isinstance(row, list):
|
||||
return len(row)
|
||||
elif isinstance(row, RedisRow):
|
||||
"""
|
||||
Count the number of items in the response data.
|
||||
|
||||
Returns:
|
||||
Number of items (0 if no data)
|
||||
"""
|
||||
data = self.all
|
||||
|
||||
if isinstance(data, list):
|
||||
return len(data)
|
||||
elif isinstance(data, (RedisRow, dict)):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@property
|
||||
def first(self) -> Union[RedisRow, Dict, None]:
|
||||
if self.data:
|
||||
if isinstance(self.data, list):
|
||||
if isinstance(self.data[0], RedisRow):
|
||||
return self.data[0].row
|
||||
return self.data[0]
|
||||
elif isinstance(self.data, RedisRow):
|
||||
return self.data.row
|
||||
self.status = False
|
||||
def first(self) -> Union[Dict, None]:
|
||||
"""
|
||||
Get the first item from the response data.
|
||||
|
||||
Returns:
|
||||
First item as a dictionary or None if no data
|
||||
"""
|
||||
if not self.data:
|
||||
return None
|
||||
|
||||
if isinstance(self.data, list) and self.data:
|
||||
item = self.data[0]
|
||||
if isinstance(item, RedisRow) and hasattr(item, 'row'):
|
||||
return item.row
|
||||
return item
|
||||
elif isinstance(self.data, RedisRow) and hasattr(self.data, 'row'):
|
||||
return self.data.row
|
||||
elif isinstance(self.data, dict):
|
||||
return self.data
|
||||
|
||||
return None
|
||||
|
||||
def is_successful(self) -> bool:
|
||||
"""
|
||||
Check if the operation was successful.
|
||||
|
||||
Returns:
|
||||
Boolean indicating success status
|
||||
"""
|
||||
return self.status
|
||||
|
||||
def to_api_response(self) -> Dict:
|
||||
"""
|
||||
Format the response for API consumption.
|
||||
|
||||
Returns:
|
||||
API-friendly response dictionary
|
||||
"""
|
||||
try:
|
||||
response = {
|
||||
"success": self.status,
|
||||
"message": self.message,
|
||||
}
|
||||
|
||||
if self.error:
|
||||
response["error"] = self.error
|
||||
|
||||
if self.data is not None:
|
||||
if self.data_type == "row" and hasattr(self.data, 'to_dict'):
|
||||
response["data"] = self.data.to_dict()
|
||||
elif self.data_type == "list":
|
||||
try:
|
||||
if all(hasattr(item, 'to_dict') for item in self.data):
|
||||
response["data"] = [item.to_dict() for item in self.data]
|
||||
else:
|
||||
response["data"] = self.data
|
||||
except Exception as e:
|
||||
response["error"] = f"Error converting list items: {str(e)}"
|
||||
else:
|
||||
response["data"] = self.data
|
||||
|
||||
response["count"] = self.count
|
||||
return response
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Error formatting response",
|
||||
"error": str(e)
|
||||
}
|
||||
Reference in New Issue
Block a user