Compare commits

..

1 Commits

Author SHA1 Message Date
0593d7b21e Update dependency types-ujson to v5.10.0.20250326
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 47s
Tests / Build and Test (3.11) (pull_request) Successful in 1m13s
Tests / Build and Test (3.12) (pull_request) Successful in 1m8s
Tests / Build and Test (3.13) (pull_request) Successful in 1m4s
2025-03-26 06:30:39 +02:00
10 changed files with 41 additions and 87 deletions

View File

@@ -16,7 +16,6 @@ There are different sub-packages available:
* pyrogram - Telegram bots with Pyrogram's fork "Pyrofork" * pyrogram - Telegram bots with Pyrogram's fork "Pyrofork"
* pycord - Discord bots with Pycord * pycord - Discord bots with Pycord
* speed - Performance improvements * speed - Performance improvements
* cache - Support for Redis and Memcached
* dev - Dependencies for package development purposes * 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. You can freely choose any sub-package you want, as well as add multiple (comma-separated) or none at all.

View File

@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools>=77.0.3", "wheel"] requires = ["setuptools>=62.6", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@@ -9,11 +9,11 @@ authors = [{ name = "Profitroll" }]
description = "Universal bot library with functions needed for basic Discord/Telegram bot development." description = "Universal bot library with functions needed for basic Discord/Telegram bot development."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
license = "GPL-3.0" license = { text = "GPLv3" }
license-files = ["LICENSE"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",

View File

@@ -1,2 +1,2 @@
aiofiles>=23.0.0 aiofiles>=23.0.0
typing-extensions~=4.14.0 typing-extensions~=4.12.2

View File

@@ -1,2 +1,2 @@
pymemcache~=4.0.0 pymemcache~=4.0.0
redis~=6.2.0 redis~=5.2.1

View File

@@ -1,12 +1,12 @@
black==25.1.0 black==25.1.0
build==1.2.2.post1 build==1.2.2.post1
isort==5.13.2 isort==5.13.2
mypy==1.16.1 mypy==1.15.0
pylint==3.3.7 pylint==3.3.6
pytest-asyncio==1.0.0 pytest-asyncio==0.26.0
pytest-cov==6.2.1 pytest-cov==6.0.0
pytest==8.4.1 pytest==8.3.5
tox==4.27.0 tox==4.24.2
twine==6.1.0 twine==6.1.0
types-aiofiles==24.1.0.20250606 types-aiofiles==24.1.0.20250326
types-ujson==5.10.0.20250326 types-ujson==5.10.0.20250326

View File

@@ -1,4 +1,4 @@
__version__ = "4.3.0" __version__ = "4.1.0"
__license__ = "GPL3" __license__ = "GPL3"
__author__ = "Profitroll" __author__ = "Profitroll"

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Dict, Optional from typing import Any, Dict
import pymemcache import pymemcache
import redis import redis
@@ -27,16 +27,16 @@ class Cache(ABC):
pass pass
@abstractmethod @abstractmethod
def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_json(self, key: str, value: Any) -> None:
# TODO This method must also carry out ObjectId conversion! # TODO This method must also carry out ObjectId conversion!
pass pass
@abstractmethod @abstractmethod
def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None: def set_string(self, key: str, value: str) -> None:
pass pass
@abstractmethod @abstractmethod
def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_object(self, key: str, value: Any) -> None:
pass pass
@abstractmethod @abstractmethod

View File

@@ -1,6 +1,6 @@
import logging import logging
from logging import Logger from logging import Logger
from typing import Dict, Any, Optional from typing import Dict, Any
from pymemcache import Client from pymemcache import Client
@@ -13,30 +13,21 @@ logger: Logger = logging.getLogger(__name__)
class CacheMemcached(Cache): class CacheMemcached(Cache):
client: Client client: Client
def __init__( def __init__(self, client: Client):
self, client: Client, prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None self.client = client
) -> None:
self.client: Client = client
self.prefix: str | None = prefix
self.default_ttl_seconds: int = default_ttl_seconds if default_ttl_seconds is not None else 0
logger.info("Initialized Memcached for caching") logger.info("Initialized Memcached for caching")
@classmethod @classmethod
def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> "CacheMemcached": def from_config(cls, engine_config: Dict[str, Any]) -> "CacheMemcached":
if "uri" not in engine_config: if "uri" not in engine_config:
raise KeyError( raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)" "Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
) )
return cls(Client(engine_config["uri"], default_noreply=True), prefix=prefix) return cls(Client(engine_config["uri"], default_noreply=True))
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: def get_json(self, key: str) -> Any | None:
key = self._get_prefixed_key(key)
try: try:
result: Any | None = self.client.get(key, None) result: Any | None = self.client.get(key, None)
@@ -52,8 +43,6 @@ class CacheMemcached(Cache):
return None if result is None else _string_to_json(result) return None if result is None else _string_to_json(result)
def get_string(self, key: str) -> str | None: def get_string(self, key: str) -> str | None:
key = self._get_prefixed_key(key)
try: try:
result: str | None = self.client.get(key, None) result: str | None = self.client.get(key, None)
@@ -72,39 +61,27 @@ class CacheMemcached(Cache):
def get_object(self, key: str) -> Any | None: def get_object(self, key: str) -> Any | None:
raise NotImplementedError() raise NotImplementedError()
def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_json(self, key: str, value: Any) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.set( self.client.set(key, _json_to_string(value))
key,
_json_to_string(value),
expire=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds,
)
logger.debug("Set json cache key '%s'", key) logger.debug("Set json cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc) logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None return None
def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None: def set_string(self, key: str, value: str) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.set( self.client.set(key, value)
key, value, expire=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds
)
logger.debug("Set string cache key '%s'", key) logger.debug("Set string cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc) logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None return None
# TODO Implement binary serialization # TODO Implement binary serialization
def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_object(self, key: str, value: Any) -> None:
raise NotImplementedError() raise NotImplementedError()
def delete(self, key: str) -> None: def delete(self, key: str) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.delete(key) self.client.delete(key)
logger.debug("Deleted cache key '%s'", key) logger.debug("Deleted cache key '%s'", key)

View File

@@ -1,11 +1,11 @@
import logging import logging
from logging import Logger from logging import Logger
from typing import Dict, Any, Optional from typing import Dict, Any
from redis import Redis from redis import Redis
from .cache import Cache from .cache import Cache
from ..utils._objects import _json_to_string, _string_to_json from ..utils._objects import _string_to_json, _json_to_string
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
@@ -13,30 +13,21 @@ logger: Logger = logging.getLogger(__name__)
class CacheRedis(Cache): class CacheRedis(Cache):
client: Redis client: Redis
def __init__( def __init__(self, client: Redis):
self, client: Redis, prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None self.client = client
) -> None:
self.client: Redis = client
self.prefix: str | None = prefix
self.default_ttl_seconds: int | None = default_ttl_seconds
logger.info("Initialized Redis for caching") logger.info("Initialized Redis for caching")
@classmethod @classmethod
def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> Any: def from_config(cls, engine_config: Dict[str, Any]) -> Any:
if "uri" not in engine_config: if "uri" not in engine_config:
raise KeyError( raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)" "Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
) )
return cls(Redis.from_url(engine_config["uri"]), prefix=prefix) return cls(Redis.from_url(engine_config["uri"]))
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: def get_json(self, key: str) -> Any | None:
key = self._get_prefixed_key(key)
try: try:
result: Any | None = self.client.get(key) result: Any | None = self.client.get(key)
@@ -52,8 +43,6 @@ class CacheRedis(Cache):
return None if result is None else _string_to_json(result) return None if result is None else _string_to_json(result)
def get_string(self, key: str) -> str | None: def get_string(self, key: str) -> str | None:
key = self._get_prefixed_key(key)
try: try:
result: str | None = self.client.get(key) result: str | None = self.client.get(key)
@@ -72,37 +61,27 @@ class CacheRedis(Cache):
def get_object(self, key: str) -> Any | None: def get_object(self, key: str) -> Any | None:
raise NotImplementedError() raise NotImplementedError()
def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_json(self, key: str, value: Any) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.set( self.client.set(key, _json_to_string(value))
key,
_json_to_string(value),
ex=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds,
)
logger.debug("Set json cache key '%s'", key) logger.debug("Set json cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc) logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None return None
def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None: def set_string(self, key: str, value: str) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.set(key, value, ex=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds) self.client.set(key, value)
logger.debug("Set string cache key '%s'", key) logger.debug("Set string cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc) logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None return None
# TODO Implement binary serialization # TODO Implement binary serialization
def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None: def set_object(self, key: str, value: Any) -> None:
raise NotImplementedError() raise NotImplementedError()
def delete(self, key: str) -> None: def delete(self, key: str) -> None:
key = self._get_prefixed_key(key)
try: try:
self.client.delete(key) self.client.delete(key)
logger.debug("Deleted cache key '%s'", key) logger.debug("Deleted cache key '%s'", key)

View File

@@ -1,4 +1,4 @@
from typing import Dict, Any, Literal, Optional from typing import Dict, Any, Literal
from ..classes import CacheMemcached, CacheRedis from ..classes import CacheMemcached, CacheRedis
@@ -6,7 +6,6 @@ from ..classes import CacheMemcached, CacheRedis
def create_cache_client( def create_cache_client(
config: Dict[str, Any], config: Dict[str, Any],
engine: Literal["memcached", "redis"] | None = None, engine: Literal["memcached", "redis"] | None = None,
prefix: Optional[str] = None,
) -> CacheMemcached | CacheRedis: ) -> CacheMemcached | CacheRedis:
if engine not in ["memcached", "redis"] or engine is None: if engine not in ["memcached", "redis"] or engine is None:
raise KeyError(f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'") raise KeyError(f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'")
@@ -18,8 +17,8 @@ def create_cache_client(
match engine: match engine:
case "memcached": case "memcached":
return CacheMemcached.from_config(config["cache"][engine], prefix=prefix) return CacheMemcached.from_config(config["cache"][engine])
case "redis": case "redis":
return CacheRedis.from_config(config["cache"][engine], prefix=prefix) return CacheRedis.from_config(config["cache"][engine])
case _: case _:
raise KeyError(f"Cache implementation for the engine '{engine}' is not present.") raise KeyError(f"Cache implementation for the engine '{engine}' is not present.")