diff --git a/README.md b/README.md index b77c3c9..76b7679 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ There are different sub-packages available: * pyrogram - Telegram bots with Pyrogram's fork "Pyrofork" * pycord - Discord bots with Pycord * speed - Performance improvements +* cache - Support for Redis and Memcached * dev - Dependencies for package development purposes You can freely choose any sub-package you want, as well as add multiple (comma-separated) or none at all. diff --git a/requirements/_.txt b/requirements/_.txt index 3a5d6be..aa412d5 100644 --- a/requirements/_.txt +++ b/requirements/_.txt @@ -1,2 +1,2 @@ aiofiles>=23.0.0 -typing-extensions~=4.12.2 \ No newline at end of file +typing-extensions~=4.13.0 \ No newline at end of file diff --git a/requirements/cache.txt b/requirements/cache.txt index f0f2e14..6c3d33f 100644 --- a/requirements/cache.txt +++ b/requirements/cache.txt @@ -1,2 +1,2 @@ pymemcache~=4.0.0 -redis~=5.2.1 \ No newline at end of file +redis~=6.1.0 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 167d6ef..54f12e3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,11 +2,11 @@ black==25.1.0 build==1.2.2.post1 isort==5.13.2 mypy==1.15.0 -pylint==3.3.4 -pytest-asyncio==0.25.3 -pytest-cov==6.0.0 -pytest==8.3.4 -tox==4.24.0 +pylint==3.3.7 +pytest-asyncio==0.26.0 +pytest-cov==6.1.1 +pytest==8.3.5 +tox==4.26.0 twine==6.1.0 -types-aiofiles==24.1.0.20241221 -types-ujson==5.10.0.20240515 \ No newline at end of file +types-aiofiles==24.1.0.20250516 +types-ujson==5.10.0.20250326 \ No newline at end of file diff --git a/src/libbot/__init__.py b/src/libbot/__init__.py index 52f592d..fcb6220 100644 --- a/src/libbot/__init__.py +++ b/src/libbot/__init__.py @@ -1,4 +1,4 @@ -__version__ = "4.1.0" +__version__ = "4.2.0" __license__ = "GPL3" __author__ = "Profitroll" diff --git a/src/libbot/cache/classes/cache_memcached.py b/src/libbot/cache/classes/cache_memcached.py index 41b654c..f592b1d 100644 --- a/src/libbot/cache/classes/cache_memcached.py +++ b/src/libbot/cache/classes/cache_memcached.py @@ -1,6 +1,6 @@ import logging from logging import Logger -from typing import Dict, Any +from typing import Dict, Any, Optional from pymemcache import Client @@ -13,21 +13,27 @@ logger: Logger = logging.getLogger(__name__) class CacheMemcached(Cache): client: Client - def __init__(self, client: Client): - self.client = client + def __init__(self, client: Client, prefix: Optional[str] = None): + self.client: Client = client + self.prefix: str | None = prefix logger.info("Initialized Memcached for caching") @classmethod - def from_config(cls, engine_config: Dict[str, Any]) -> "CacheMemcached": + def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> "CacheMemcached": if "uri" not in engine_config: raise KeyError( "Cache configuration is invalid. Please check if all keys are set (engine: memcached)" ) - return cls(Client(engine_config["uri"], default_noreply=True)) + return cls(Client(engine_config["uri"], default_noreply=True), prefix=prefix) + + def _get_prefixed_key(self, key: str) -> str: + return key if self.prefix is None else f"{self.prefix}_{key}" def get_json(self, key: str) -> Any | None: + key = self._get_prefixed_key(key) + try: result: Any | None = self.client.get(key, None) @@ -43,6 +49,8 @@ class CacheMemcached(Cache): return None if result is None else _string_to_json(result) def get_string(self, key: str) -> str | None: + key = self._get_prefixed_key(key) + try: result: str | None = self.client.get(key, None) @@ -62,6 +70,8 @@ class CacheMemcached(Cache): raise NotImplementedError() def set_json(self, key: str, value: Any) -> None: + key = self._get_prefixed_key(key) + try: self.client.set(key, _json_to_string(value)) logger.debug("Set json cache key '%s'", key) @@ -70,6 +80,8 @@ class CacheMemcached(Cache): return None def set_string(self, key: str, value: str) -> None: + key = self._get_prefixed_key(key) + try: self.client.set(key, value) logger.debug("Set string cache key '%s'", key) @@ -82,6 +94,8 @@ class CacheMemcached(Cache): raise NotImplementedError() def delete(self, key: str) -> None: + key = self._get_prefixed_key(key) + try: self.client.delete(key) logger.debug("Deleted cache key '%s'", key) diff --git a/src/libbot/cache/classes/cache_redis.py b/src/libbot/cache/classes/cache_redis.py index 2edfa43..797a866 100644 --- a/src/libbot/cache/classes/cache_redis.py +++ b/src/libbot/cache/classes/cache_redis.py @@ -1,6 +1,6 @@ import logging from logging import Logger -from typing import Dict, Any +from typing import Dict, Any, Optional from redis import Redis @@ -13,21 +13,27 @@ logger: Logger = logging.getLogger(__name__) class CacheRedis(Cache): client: Redis - def __init__(self, client: Redis): - self.client = client + def __init__(self, client: Redis, prefix: Optional[str] = None): + self.client: Redis = client + self.prefix: str | None = prefix logger.info("Initialized Redis for caching") @classmethod - def from_config(cls, engine_config: Dict[str, Any]) -> Any: + def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> Any: if "uri" not in engine_config: raise KeyError( "Cache configuration is invalid. Please check if all keys are set (engine: memcached)" ) - return cls(Redis.from_url(engine_config["uri"])) + return cls(Redis.from_url(engine_config["uri"]), prefix=prefix) + + def _get_prefixed_key(self, key: str) -> str: + return key if self.prefix is None else f"{self.prefix}_{key}" def get_json(self, key: str) -> Any | None: + key = self._get_prefixed_key(key) + try: result: Any | None = self.client.get(key) @@ -43,6 +49,8 @@ class CacheRedis(Cache): return None if result is None else _string_to_json(result) def get_string(self, key: str) -> str | None: + key = self._get_prefixed_key(key) + try: result: str | None = self.client.get(key) @@ -62,6 +70,8 @@ class CacheRedis(Cache): raise NotImplementedError() def set_json(self, key: str, value: Any) -> None: + key = self._get_prefixed_key(key) + try: self.client.set(key, _json_to_string(value)) logger.debug("Set json cache key '%s'", key) @@ -70,6 +80,8 @@ class CacheRedis(Cache): return None def set_string(self, key: str, value: str) -> None: + key = self._get_prefixed_key(key) + try: self.client.set(key, value) logger.debug("Set string cache key '%s'", key) @@ -82,6 +94,8 @@ class CacheRedis(Cache): raise NotImplementedError() def delete(self, key: str) -> None: + key = self._get_prefixed_key(key) + try: self.client.delete(key) logger.debug("Deleted cache key '%s'", key) diff --git a/src/libbot/cache/manager/manager.py b/src/libbot/cache/manager/manager.py index 6b7db2c..67d3f6b 100644 --- a/src/libbot/cache/manager/manager.py +++ b/src/libbot/cache/manager/manager.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, Literal +from typing import Dict, Any, Literal, Optional from ..classes import CacheMemcached, CacheRedis @@ -6,6 +6,7 @@ from ..classes import CacheMemcached, CacheRedis def create_cache_client( config: Dict[str, Any], engine: Literal["memcached", "redis"] | None = None, + prefix: Optional[str] = None, ) -> CacheMemcached | CacheRedis: if engine not in ["memcached", "redis"] or engine is None: raise KeyError(f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'") @@ -17,8 +18,8 @@ def create_cache_client( match engine: case "memcached": - return CacheMemcached.from_config(config["cache"][engine]) + return CacheMemcached.from_config(config["cache"][engine], prefix=prefix) case "redis": - return CacheRedis.from_config(config["cache"][engine]) + return CacheRedis.from_config(config["cache"][engine], prefix=prefix) case _: raise KeyError(f"Cache implementation for the engine '{engine}' is not present.")