From 6d77c34252d8d28d004a0cd053cfac8a11b4b239 Mon Sep 17 00:00:00 2001 From: berkay Date: Mon, 13 Jan 2025 19:05:25 +0300 Subject: [PATCH] updated Redis --- .idea/vcs.xml | 1 + AllConfigs/Email/email_send_model.py | 1 + AllConfigs/Redis/configs.py | 9 ++ AllConfigs/main.py | 4 + ApiServiceRedis/Email/send_email.py | 5 +- ApiServiceRedis/Redis/Actions/actions.py | 145 +++++++---------------- ApiServiceRedis/Redis/Models/base.py | 99 ++++++++++++++++ ApiServiceRedis/Redis/Models/response.py | 22 +++- ApiServiceRedis/Redis/Models/row.py | 2 +- ApiServiceRedis/Redis/conn.py | 9 +- ApiServiceRedis/Redis/howto.py | 42 ++++++- test.py | 40 +++++++ 12 files changed, 257 insertions(+), 122 deletions(-) create mode 100644 ApiServiceRedis/Redis/Models/base.py create mode 100644 test.py diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 49d9f73..33f261a 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/AllConfigs/Email/email_send_model.py b/AllConfigs/Email/email_send_model.py index 2b25428..3108072 100644 --- a/AllConfigs/Email/email_send_model.py +++ b/AllConfigs/Email/email_send_model.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from typing import List, Dict, Optional + class EmailSendModel(BaseModel): subject: str html: str = "" diff --git a/AllConfigs/Redis/configs.py b/AllConfigs/Redis/configs.py index a6531b8..f7c6a65 100644 --- a/AllConfigs/Redis/configs.py +++ b/AllConfigs/Redis/configs.py @@ -6,3 +6,12 @@ class WagRedis: REDIS_PASSWORD: str = "commercial_redis_password" REDIS_PORT: int = 11222 REDIS_DB: int = 0 + + @classmethod + def as_dict(cls): + return dict( + host=WagRedis.REDIS_HOST, + password=WagRedis.REDIS_PASSWORD, + port=WagRedis.REDIS_PORT, + db=WagRedis.REDIS_DB, + ) diff --git a/AllConfigs/main.py b/AllConfigs/main.py index 2e3d78d..460ec4e 100644 --- a/AllConfigs/main.py +++ b/AllConfigs/main.py @@ -1,3 +1,7 @@ class HostConfig: MAIN_HOST = "10.10.2.36" # http://10.10.2.36 EMAIL_HOST = "10.10.2.34" # http://10.10.2.34 + + +class MainConfig: + DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss Z" diff --git a/ApiServiceRedis/Email/send_email.py b/ApiServiceRedis/Email/send_email.py index 1c22a7d..9c76377 100644 --- a/ApiServiceRedis/Email/send_email.py +++ b/ApiServiceRedis/Email/send_email.py @@ -8,10 +8,7 @@ email_sender = EmailSender(**EmailConfig.as_dict()) class EmailService: @classmethod - def send_email( - cls, - params : EmailSendModel - ) -> bool: + def send_email(cls, params: EmailSendModel) -> bool: if not EmailConfig.EMAIL_SEND: print("Email sending is disabled", params) return False diff --git a/ApiServiceRedis/Redis/Actions/actions.py b/ApiServiceRedis/Redis/Actions/actions.py index f8985b4..182cb9a 100644 --- a/ApiServiceRedis/Redis/Actions/actions.py +++ b/ApiServiceRedis/Redis/Actions/actions.py @@ -1,102 +1,33 @@ -import json import arrow -from typing import ( - Any, - Optional, - List, - Dict, -) +from typing import Optional, List, Dict, Union + +from AllConfigs.main import MainConfig from ApiServiceRedis.Redis.conn import redis_cli +from ApiServiceRedis.Redis.Models.base import RedisRow from ApiServiceRedis.Redis.Models.response import RedisResponse -class RedisRow: - - key: str | bytes - value: Any - delimiter: str = "*" - expires_at: Optional[str] = None - - @classmethod - def merge(cls, set_values: list[str | bytes]): - for key, set_value in enumerate(set_values): - set_value = str(set_value) if isinstance(set_value, bytes) else set_value - cls.key += f"{set_value}" if key == len(set_values) - 1 else f"{set_value}{cls.delimiter}" - cls.key = cls.key.encode() - - @classmethod - def regex(cls, list_keys: list[str | bytes]): - search_regex = "" - for key, list_key in enumerate(list_keys): - list_key = list_key.decode() if isinstance(list_key, bytes) else str(list_key) - if key == 0 and list_key: - search_regex += f"{list_key}{cls.delimiter}*" - elif key == len(list_keys) - 1 and list_key: - search_regex += f"*{cls.delimiter}{list_key}" - else: - if list_key: - search_regex += f"*{cls.delimiter}{list_key}{cls.delimiter}*" - return search_regex - - @classmethod - def parse(cls): - return cls.key.split(cls.delimiter) if cls.key else [] - - @classmethod - def feed(cls, value: Any): - cls.value = json.dumps(value) - - @classmethod - def modify(cls, add_dict): - value = cls.data or {} - cls.feed({**value, **add_dict}) - - @classmethod - def remove(cls, key): - value = cls.data or {} - value.pop(key) - cls.feed(value) - - @property - def keys(self): - return self.key if isinstance(self.key, str) else self.key.decode() - - @property - def redis_key(self): - return self.key if isinstance(self.key, bytes) else self.key.encode() - - @property - def data(self): - return json.loads(self.value) - - @property - def as_dict(self): - return { - "keys": self.keys, - "value": self.data, - } - - class RedisActions: + """Class for handling Redis operations with JSON data.""" @classmethod - def get_expiry_time(cls, expiry_kwargs: dict) -> int: - expiry_time = 0 - if "days" in expiry_kwargs: - expiry_time += int(expiry_kwargs["days"]) * 24 * 60 * 60 - if "hours" in expiry_kwargs: - expiry_time += int(expiry_kwargs["hours"]) * 60 * 60 - if "minutes" in expiry_kwargs: - expiry_time += int(expiry_kwargs["minutes"]) * 60 - if "seconds" in expiry_kwargs: - expiry_time += int(expiry_kwargs["seconds"]) - return expiry_time + def get_expiry_time(cls, expiry_kwargs: Dict[str, int]) -> int: + """Calculate expiry time in seconds from kwargs.""" + 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_json( - cls, list_keys: list[str | bytes], value: Optional[dict | list], expires: Optional[dict] = None - ): + 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.""" redis_row = RedisRow() redis_row.merge(set_values=list_keys) redis_row.feed(value) @@ -108,9 +39,15 @@ class RedisActions: time=expiry_time, value=redis_row.value, ) - redis_row.expires_at = arrow.now().shift(seconds=expiry_time).format("YYYY-MM-DD HH:mm:ss") + redis_row.expires_at = ( + 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 is set successfully.", @@ -124,21 +61,31 @@ class RedisActions: ) @classmethod - def get_json(cls, list_keys: list[str | bytes]): + def resolve_expires_at(cls, redis_row: RedisRow) -> str: + """Resolve expiry time for Redis key.""" + expiry_time = redis_cli.ttl(redis_row.redis_key) + if expiry_time == -1: + return "Key has no expiry time." + return arrow.now().shift(seconds=expiry_time).format(MainConfig.DATETIME_FORMAT) + + @classmethod + def get_json(cls, list_keys: List[Union[str, bytes]]) -> RedisResponse: + """Get JSON values from Redis using pattern matching.""" try: - redis_row = RedisRow() - json_get = redis_cli.get(redis_row.regex(list_keys=list_keys)) - redis_row.key = json_get - if not json_get: - return RedisResponse( - status=False, - message="Value is not get successfully.", - error="Value is not found in the redis.", - ) + list_of_rows = [] + regex = RedisRow.regex(list_keys=list_keys) + json_get = redis_cli.scan_iter(match=regex) + + for row in list(json_get): + redis_row = RedisRow() + redis_row.set_key(key=row) + redis_row.expires_at = cls.resolve_expires_at(redis_row=redis_row) + redis_row.feed(redis_cli.get(redis_row.redis_key)) + list_of_rows.append(redis_row) return RedisResponse( status=True, message="Value is get successfully.", - data=json.loads(json_get), + data=list_of_rows, ) except Exception as e: return RedisResponse( diff --git a/ApiServiceRedis/Redis/Models/base.py b/ApiServiceRedis/Redis/Models/base.py new file mode 100644 index 0000000..529b0ab --- /dev/null +++ b/ApiServiceRedis/Redis/Models/base.py @@ -0,0 +1,99 @@ +import json +from typing import Union, Dict, List, Optional, Any + + +class RedisRow: + """Class for handling Redis key-value operations with structured data.""" + + key: Union[str, bytes] + value: Any + delimiter: str = ":" + expires_at: Optional[str] = None + + @classmethod + def merge(cls, set_values: List[Union[str, bytes]]) -> None: + """Merge list of values into a single delimited key.""" + cls.key = "" + for key, set_value in enumerate(set_values): + set_value = ( + set_value.decode() if isinstance(set_value, bytes) else str(set_value) + ) + cls.key += ( + f"{set_value}" + if key == len(set_values) - 1 + else f"{set_value}{cls.delimiter}" + ) + cls.key = cls.key.encode() + + @classmethod + def regex(cls, list_keys: List[Union[str, bytes]]) -> str: + """Generate Redis search pattern from list of keys.""" + search_regex = "" + for key, list_key in enumerate(list_keys): + if not list_key: + continue + + list_key = ( + list_key.decode() if isinstance(list_key, bytes) else str(list_key) + ) + if key == 0: + search_regex += f"{list_key}{cls.delimiter}*" + elif key == len(list_keys) - 1: + search_regex += f"*{cls.delimiter}{list_key}" + else: + search_regex += f"*{cls.delimiter}{list_key}{cls.delimiter}*" + return search_regex + + @classmethod + def parse(cls) -> List[str]: + """Parse the key into its component parts.""" + return cls.key.split(cls.delimiter) if cls.key else [] + + @classmethod + def feed(cls, value: Union[bytes, Dict, List]) -> None: + """Convert and store value in JSON format.""" + if isinstance(value, (dict, list)): + cls.value = json.dumps(value) + else: + cls.value = json.dumps(json.loads(value.decode())) + + @classmethod + def modify(cls, add_dict: Dict) -> None: + """Modify existing data by merging with new dictionary.""" + value = cls.data or {} + cls.feed({**value, **add_dict}) + + @classmethod + def remove(cls, key: str) -> None: + """Remove a key from the stored dictionary.""" + value = cls.data or {} + value.pop(key) + cls.feed(value) + + @property + def keys(self) -> str: + """Get key as string.""" + return self.key.decode() if isinstance(self.key, bytes) else self.key + + @classmethod + def set_key(cls, key: Union[str, bytes]) -> None: + """Set key ensuring bytes format.""" + cls.key = key if isinstance(key, bytes) else key.encode() + + @property + def redis_key(self) -> bytes: + """Get key in bytes format for Redis operations.""" + return self.key if isinstance(self.key, bytes) else self.key.encode() + + @property + def data(self) -> Union[Dict, List]: + """Get stored value as Python object.""" + return json.loads(self.value) + + @property + def as_dict(self) -> Dict: + """Get row data as dictionary.""" + return { + "keys": self.keys, + "value": self.data, + } diff --git a/ApiServiceRedis/Redis/Models/response.py b/ApiServiceRedis/Redis/Models/response.py index 5c02bf2..c25b6ab 100644 --- a/ApiServiceRedis/Redis/Models/response.py +++ b/ApiServiceRedis/Redis/Models/response.py @@ -1,19 +1,21 @@ -from typing import Union, Dict, List -from ApiServiceRedis.Redis.Actions.actions import RedisRow +from typing import Union, Dict, List, Optional, Any +from ApiServiceRedis.Redis.Models.base import RedisRow class RedisResponse: + """Base class for Redis response handling.""" def __init__( self, status: bool, message: str, - data: RedisRow = None, - error: str = None, + data: Any = None, + error: Optional[str] = None, ): self.status = status self.message = message self.data = data + if isinstance(data, dict): self.data_type = "dict" elif isinstance(data, list): @@ -22,7 +24,7 @@ class RedisResponse: self.data_type = None self.error = error - def as_dict(self): + def as_dict(self) -> Dict: return { "status": self.status, "message": self.message, @@ -30,3 +32,13 @@ class RedisResponse: "dataType": self.data_type, "error": self.error, } + + @property + def all(self) -> Union[Optional[List[RedisRow]]]: + return self.data + + @property + def first(self) -> Union[RedisRow, None]: + if self.data: + return self.data[0] + return None diff --git a/ApiServiceRedis/Redis/Models/row.py b/ApiServiceRedis/Redis/Models/row.py index f36a998..189dae1 100644 --- a/ApiServiceRedis/Redis/Models/row.py +++ b/ApiServiceRedis/Redis/Models/row.py @@ -22,4 +22,4 @@ class AccessToken(BaseModel): # "accessToken": "token", # "userUUID": "uuid" # }) -# access_token_obj.to_list() \ No newline at end of file +# access_token_obj.to_list() diff --git a/ApiServiceRedis/Redis/conn.py b/ApiServiceRedis/Redis/conn.py index 4713218..efc22bc 100644 --- a/ApiServiceRedis/Redis/conn.py +++ b/ApiServiceRedis/Redis/conn.py @@ -1,16 +1,11 @@ from redis import Redis -from api_configs import WagRedis +from AllConfigs.Redis.configs import WagRedis class RedisConn: def __init__(self): - self.redis = Redis( - host=WagRedis.REDIS_HOST, - password=WagRedis.REDIS_PASSWORD, - port=WagRedis.REDIS_PORT, - db=WagRedis.REDIS_DB, - ) + self.redis = Redis(**WagRedis.as_dict()) if not self.check_connection(): raise Exception("Connection error") diff --git a/ApiServiceRedis/Redis/howto.py b/ApiServiceRedis/Redis/howto.py index 5b2fca4..f9dde51 100644 --- a/ApiServiceRedis/Redis/howto.py +++ b/ApiServiceRedis/Redis/howto.py @@ -1,10 +1,40 @@ +import secrets +import uuid + from ApiServiceRedis.Redis.Actions.actions import RedisActions from ApiServiceRedis.Redis.Models.row import AccessToken - -redis_cli_actions = RedisActions() - -access_object = AccessToken( - accessToken="token", - userUUID="uuid" +first_user = AccessToken( + accessToken=secrets.token_urlsafe(90), + userUUID=uuid.uuid4().__str__(), ) +second_user = AccessToken( + accessToken=secrets.token_urlsafe(90), + userUUID=uuid.uuid4().__str__(), +) + +json_data = lambda uu_id, access: { + "uu_id": uu_id, + "access_token": access, + "user_type": 1, + "selected_company": None, + "selected_occupant": None, + "reachable_event_list_id": [], +} +set_response_first_json = json_data(first_user.userUUID, first_user.accessToken) +set_response_second_json = json_data(second_user.userUUID, second_user.accessToken) +set_response_first = RedisActions.set_json( + list_keys=first_user.to_list(), + value=set_response_first_json, + expires={"seconds": 140}, +) + +set_response_second = RedisActions.set_json( + list_keys=second_user.to_list(), + value=set_response_second_json, + expires={"seconds": 190}, +) + +search_keys = [None, set_response_first_json["uu_id"]] +get_response = RedisActions.get_json(list_keys=search_keys) +print("get_response", [data.expires_at for data in get_response.all]) diff --git a/test.py b/test.py new file mode 100644 index 0000000..f9dde51 --- /dev/null +++ b/test.py @@ -0,0 +1,40 @@ +import secrets +import uuid + +from ApiServiceRedis.Redis.Actions.actions import RedisActions +from ApiServiceRedis.Redis.Models.row import AccessToken + +first_user = AccessToken( + accessToken=secrets.token_urlsafe(90), + userUUID=uuid.uuid4().__str__(), +) +second_user = AccessToken( + accessToken=secrets.token_urlsafe(90), + userUUID=uuid.uuid4().__str__(), +) + +json_data = lambda uu_id, access: { + "uu_id": uu_id, + "access_token": access, + "user_type": 1, + "selected_company": None, + "selected_occupant": None, + "reachable_event_list_id": [], +} +set_response_first_json = json_data(first_user.userUUID, first_user.accessToken) +set_response_second_json = json_data(second_user.userUUID, second_user.accessToken) +set_response_first = RedisActions.set_json( + list_keys=first_user.to_list(), + value=set_response_first_json, + expires={"seconds": 140}, +) + +set_response_second = RedisActions.set_json( + list_keys=second_user.to_list(), + value=set_response_second_json, + expires={"seconds": 190}, +) + +search_keys = [None, set_response_first_json["uu_id"]] +get_response = RedisActions.get_json(list_keys=search_keys) +print("get_response", [data.expires_at for data in get_response.all])