diff --git a/.flake8 b/.flake8 index 13b9e8d..657d671 100644 --- a/.flake8 +++ b/.flake8 @@ -37,8 +37,8 @@ extend-ignore = WPS428 # fails to understand overloading WPS465 # fails to understand pipe-unions for types WPS601 # fails to same-name class and instance attributes (pydantic & sqlalchemy) - # to many - WPS201 WPS202 WPS204 WPS210 WPS214 WPS217 WPS218 WPS220 WPS221 WPS234 WPS235 + # too many + WPS201 WPS202 WPS204 WPS210 WPS211 WPS214 WPS217 WPS218 WPS220 WPS221 WPS230 WPS231 WPS234 WPS235 # don't block features WPS100 # utils is a module name diff --git a/app/common/config.py b/app/common/config.py index d00b39c..2de5d97 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -5,6 +5,7 @@ from cryptography.fernet import Fernet from dotenv import load_dotenv +from redis.asyncio import ConnectionPool from sqlalchemy import MetaData from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase @@ -31,6 +32,9 @@ DB_URL: str = getenv("DB_LINK", "postgresql+psycopg://test:test@localhost:5432/test") DB_SCHEMA: str | None = getenv("DB_SCHEMA", None) +REDIS_URL: str = getenv("REDIS_URL", "redis://localhost:6379") +REDIS_POCHTA_STREAM: str = getenv("REDIS_POCHTA_STREAM", "pochta:send") + MQ_URL: str = getenv("MQ_URL", "amqp://guest:guest@localhost/") MQ_POCHTA_QUEUE: str = getenv("MQ_POCHTA_QUEUE", "pochta.send") @@ -77,6 +81,10 @@ class Base(AsyncAttrs, DeclarativeBase, MappingBase): metadata = db_meta +redis_pool = ConnectionPool.from_url( + REDIS_URL, decode_responses=True, max_connections=20 +) + pochta_producer = RabbitDirectProducer(queue_name=MQ_POCHTA_QUEUE) password_reset_cryptography = CryptographyProvider( diff --git a/app/common/redis_ext.py b/app/common/redis_ext.py new file mode 100644 index 0000000..b35fd9f --- /dev/null +++ b/app/common/redis_ext.py @@ -0,0 +1,159 @@ +import asyncio +import logging +from collections.abc import Callable +from typing import Any, Final, Protocol, TypeVar, get_type_hints + +from pydantic import BaseModel, ValidationError +from redis.asyncio import Redis +from redis.asyncio.retry import Retry +from redis.backoff import ExponentialBackoff +from redis.exceptions import ( + ConnectionError, + MaxConnectionsError, + ResponseError, + TimeoutError, +) + +from app.common.config import REDIS_URL + +BLOCK_TIME_MS: Final[int] = 2000 + +T = TypeVar("T", bound=BaseModel, contravariant=True) + + +class MessageHandlerProtocol(Protocol[T]): + async def __call__(self, message: T) -> None: + pass + # TODO remove protocol + + +class RedisStreamConsumer: + def __init__( + self, + stream_name: str, + group_name: str, + consumer_name: str, + model: type[T], + message_handler: MessageHandlerProtocol[T], + ) -> None: + self.stream_name: str = stream_name + self.group_name: str = group_name + self.consumer_name: str = consumer_name + self.model: type[T] = model + self.message_handler: MessageHandlerProtocol[T] = message_handler + + self.redis_client = Redis.from_url( + REDIS_URL, + decode_responses=True, + retry=Retry(ExponentialBackoff(cap=10, base=1), 10), + retry_on_error=(ConnectionError, TimeoutError, MaxConnectionsError), + ) + + async def create_group_if_not_exist(self) -> None: + try: + await self.redis_client.xgroup_create( + name=self.stream_name, + groupname=self.group_name, + id="$", + mkstream=True, + ) + except ResponseError as response_exc: + if "BUSYGROUP" not in str(response_exc): + raise + + async def process_message(self, message_id: str, data: dict[str, str]) -> None: + try: + validated_data = self.model.model_validate(data) + except ValidationError: + logging.error( + "Invalid message payload", + extra={"original_message": data}, + ) + await self.redis_client.xack(self.stream_name, self.group_name, message_id) + return + + try: + await self.message_handler(validated_data) + except Exception as handling_exc: # noqa PIE786 + logging.error( + f"Error in {self.consumer_name} while processing message {data}", + exc_info=handling_exc, + ) + return + await self.redis_client.xack(self.stream_name, self.group_name, message_id) + + async def handle_messages(self) -> None: + await self.create_group_if_not_exist() + + last_message_id: str = "0" + + while True: # noqa WPS457 # required for continuous message handling + messages = await self.redis_client.xreadgroup( + groupname=self.group_name, + consumername=self.consumer_name, + streams={self.stream_name: last_message_id}, + count=1, + block=BLOCK_TIME_MS, + ) + if len(messages) == 0: + continue + elif len(messages[0][1]) == 0: + last_message_id = ">" + for message_id, data in messages[0][1]: + await self.process_message(message_id, data) + if last_message_id != ">": + last_message_id = message_id + + async def run(self) -> None: + while True: # noqa WPS457 # required for continuous running + try: + await self.handle_messages() + except asyncio.CancelledError: + await self.redis_client.close() + break + except (ConnectionError, TimeoutError, MaxConnectionsError): + await asyncio.sleep(10) + continue + except Exception as handling_exc: # noqa PIE786 + logging.error( + f"An error occurred in worker {self.consumer_name}: {handling_exc}", + exc_info=handling_exc, + ) + await asyncio.sleep(2) + continue + + +class RedisRouter: + def __init__(self) -> None: + self.consumers: list[RedisStreamConsumer] = [] + self.tasks: list[asyncio.Task[Any]] = [] + + def add_consumer( + self, stream_name: str, group_name: str, consumer_name: str + ) -> Callable[[MessageHandlerProtocol[T]], None]: + def redis_consumer_wrapper(func: MessageHandlerProtocol[T]) -> None: + model = next(iter(get_type_hints(func).values())) + if not issubclass(model, BaseModel): + raise TypeError(f"Expected a subclass of BaseModel, got {model}.") + worker_instance = RedisStreamConsumer( + stream_name=stream_name, + group_name=group_name, + consumer_name=consumer_name, + model=model, + message_handler=func, + ) + self.consumers.append(worker_instance) + + return redis_consumer_wrapper + + def include_router(self, router: "RedisRouter") -> None: + self.consumers.extend(router.consumers) + + async def run_consumers(self) -> None: + self.tasks.extend( + [asyncio.create_task(consumer.run()) for consumer in self.consumers] + ) + + async def terminate_consumers(self) -> None: + for task in self.tasks: + task.cancel() diff --git a/app/main.py b/app/main.py index c5e90eb..e0c3ce7 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ from starlette.responses import Response from starlette.staticfiles import StaticFiles -from app import supbot, users +from app import pochta, supbot, users from app.common.config import ( DATABASE_MIGRATED, MQ_URL, @@ -18,6 +18,7 @@ Base, engine, pochta_producer, + redis_pool, sessionmaker, ) from app.common.sqlalchemy_ext import session_context @@ -47,10 +48,11 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: async with AsyncExitStack() as stack: await stack.enter_async_context(users.lifespan()) await stack.enter_async_context(supbot.lifespan()) - + await stack.enter_async_context(pochta.lifespan()) yield await rabbit_connection.close() + await redis_pool.disconnect() app = FastAPI( @@ -86,6 +88,7 @@ async def custom_swagger_ui_html() -> Response: allow_headers=["*"], ) +app.include_router(pochta.api_router) app.include_router(users.api_router) app.include_router(supbot.api_router) diff --git a/app/pochta/__init__.py b/app/pochta/__init__.py new file mode 100644 index 0000000..81c7620 --- /dev/null +++ b/app/pochta/__init__.py @@ -0,0 +1,3 @@ +from app.pochta.main import api_router, lifespan + +__all__ = ["api_router", "lifespan"] diff --git a/app/pochta/dependencies/__init__.py b/app/pochta/dependencies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/pochta/dependencies/redis_dep.py b/app/pochta/dependencies/redis_dep.py new file mode 100644 index 0000000..09ec0d3 --- /dev/null +++ b/app/pochta/dependencies/redis_dep.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends +from redis.asyncio import Redis + +from app.common.config import redis_pool + + +async def get_redis_connection() -> Redis: + return Redis(connection_pool=redis_pool) + + +RedisConnection = Annotated[Redis, Depends(get_redis_connection)] diff --git a/app/pochta/main.py b/app/pochta/main.py new file mode 100644 index 0000000..b86ef6f --- /dev/null +++ b/app/pochta/main.py @@ -0,0 +1,24 @@ +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +from app.common.fastapi_ext import APIRouterExt +from app.common.redis_ext import RedisRouter +from app.pochta.routes import pochta_mub +from app.pochta.workers import pochta_rds +from app.users.utils.mub import MUBProtection + +mub_router = APIRouterExt(prefix="/mub", dependencies=[MUBProtection]) +mub_router.include_router(pochta_mub.router, prefix="/pochta-service") + +api_router = APIRouterExt() +api_router.include_router(mub_router) + +redis_router = RedisRouter() +redis_router.include_router(pochta_rds.router) + + +@asynccontextmanager +async def lifespan() -> AsyncIterator[None]: + await redis_router.run_consumers() + yield + await redis_router.terminate_consumers() diff --git a/app/pochta/routes/__init__.py b/app/pochta/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/pochta/routes/pochta_mub.py b/app/pochta/routes/pochta_mub.py new file mode 100644 index 0000000..f7fa956 --- /dev/null +++ b/app/pochta/routes/pochta_mub.py @@ -0,0 +1,14 @@ +from app.common.config import REDIS_POCHTA_STREAM +from app.common.fastapi_ext import APIRouterExt +from app.pochta.dependencies.redis_dep import RedisConnection + +router = APIRouterExt(tags=["pochta mub"]) + + +@router.post("/") +async def home(r: RedisConnection) -> dict[str, str]: + await r.xadd( + REDIS_POCHTA_STREAM, + {"key": "value"}, + ) + return {"msg": f"Message was added to stream {REDIS_POCHTA_STREAM}"} diff --git a/app/pochta/workers/__init__.py b/app/pochta/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/pochta/workers/pochta_rds.py b/app/pochta/workers/pochta_rds.py new file mode 100644 index 0000000..72aeb7d --- /dev/null +++ b/app/pochta/workers/pochta_rds.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + +from app.common.config import REDIS_POCHTA_STREAM +from app.common.redis_ext import RedisRouter + + +class PochtaSchema(BaseModel): + key: str + + +router = RedisRouter() + + +@router.add_consumer( + stream_name=REDIS_POCHTA_STREAM, + group_name="pochta:group", + consumer_name="pochta_consumer", +) +async def process_email_message(message: PochtaSchema) -> None: + print(f"Message: {message}") # noqa T201 Temporary print for debugging diff --git a/docker-compose.yml b/docker-compose.yml index 5b20410..1db2a77 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,15 @@ services: + redis: + image: redis:7.4.0-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + start_period: 60s + interval: 10s + timeout: 60s + retries: 5 + mq: image: rabbitmq:3.12.10-management-alpine volumes: @@ -62,6 +73,8 @@ services: profiles: - app depends_on: + redis: + condition: service_healthy mq: condition: service_healthy db: @@ -80,6 +93,7 @@ services: environment: WATCHFILES_FORCE_POLLING: true DB_LINK: postgresql+psycopg://test:test@db:5432/test + REDIS_URL: redis://redis:6379 MQ_URL: amqp://guest:guest@mq DB_SCHEMA: xi_auth # DATABASE_MIGRATED: "1" diff --git a/poetry.lock b/poetry.lock index 9b6ea64..1695e37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -263,6 +263,18 @@ files = [ {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, ] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "attrs" version = "23.1.0" @@ -1639,6 +1651,110 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "hiredis" +version = "3.0.0" +description = "Python wrapper for hiredis" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, + {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, + {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, + {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, + {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, + {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, + {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, + {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, + {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, + {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, + {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, + {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, +] + [[package]] name = "httpcore" version = "0.16.3" @@ -2813,6 +2929,26 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "redis" +version = "5.1.1" +description = "Python client for Redis database and key-value store" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, + {file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} +hiredis = {version = ">=3.0.0", optional = true, markers = "extra == \"hiredis\""} + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] + [[package]] name = "requests" version = "2.31.0" @@ -3544,4 +3680,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "7bc7587c5c5ab4b7830cb7289ffb85f915d79601826b1c751e345e0bb92df372" +content-hash = "0423cefb0cba2f10d05c994483b659b8294a3b9a0a19c7e8f6a7e4b060473316" diff --git a/pyproject.toml b/pyproject.toml index 4045cb3..95cb705 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ discord-webhook = {extras = ["async"], version = "^1.3.1"} aiogram = "^3.4.1" httpx = "^0.23.0" psycopg = {extras = ["binary"], version = "^3.1.19"} +redis = {extras = ["hiredis"], version = "^5.1.1"} [tool.poetry.group.dev.dependencies]