From 2ddfa07f52a4ede57edfae7677de93c39893cd56 Mon Sep 17 00:00:00 2001 From: niqzart Date: Sat, 10 May 2025 21:50:52 +0300 Subject: [PATCH] feat: pochta worker --- app/common/config.py | 23 +- app/common/redis_ext.py | 180 ++++++++++++++++ app/main.py | 10 +- app/pochta/dependencies/__init__.py | 0 app/pochta/dependencies/redis_dep.py | 14 ++ app/pochta/main.py | 8 +- app/pochta/routes/__init__.py | 0 app/pochta/routes/pochta_mub.py | 10 + app/pochta/routes/pochta_rds.py | 49 +++++ app/pochta/workers/__init__.py | 0 docker-compose.yml | 24 +++ poetry.lock | 309 +++++++++++++++++++++++---- pyproject.toml | 4 +- redis.conf | 1 + 14 files changed, 582 insertions(+), 50 deletions(-) create mode 100644 app/common/redis_ext.py create mode 100644 app/pochta/dependencies/__init__.py create mode 100644 app/pochta/dependencies/redis_dep.py create mode 100644 app/pochta/routes/__init__.py create mode 100644 app/pochta/routes/pochta_rds.py create mode 100644 app/pochta/workers/__init__.py create mode 100644 redis.conf diff --git a/app/common/config.py b/app/common/config.py index 35a7438..f3ef1c7 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -1,10 +1,12 @@ import sys from pathlib import Path +from typing import Any from aiosmtplib import SMTP from cryptography.fernet import Fernet -from pydantic import AmqpDsn, BaseModel, Field, PostgresDsn, computed_field +from pydantic import AmqpDsn, BaseModel, Field, PostgresDsn, computed_field, RedisDsn from pydantic_settings import BaseSettings, SettingsConfigDict +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 @@ -105,6 +107,21 @@ def postgres_dsn(self) -> str: path=self.postgres_database, ).unicode_string() + redis_host: str = "localhost" + redis_port: int = 5800 + redis_password: str = "test" + redis_pochta_stream: str = "pochta:send" + + @computed_field + @property + def redis_dsn(self) -> str: + return RedisDsn.build( + scheme="redis", + password=self.redis_password, + host=self.redis_host, + port=self.redis_port, + ).unicode_string() + mq_host: str = "localhost" mq_port: int = 5672 mq_username: str = "guest" @@ -160,6 +177,10 @@ class Base(AsyncAttrs, DeclarativeBase, MappingBase): metadata = db_meta +redis_pool: ConnectionPool[Any] = ConnectionPool.from_url( + settings.redis_dsn, decode_responses=True, max_connections=20 +) + pochta_producer = RabbitDirectProducer(queue_name=settings.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..9570ae7 --- /dev/null +++ b/app/common/redis_ext.py @@ -0,0 +1,180 @@ +import asyncio +import logging +from collections.abc import Callable +from typing import Any, Final, Protocol, TypeVar, get_type_hints, ClassVar + +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, ResponseError, TimeoutError + +from app.common.config import settings + +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 nq remove protocol + + +class ConsumerException(Exception): + message: ClassVar[str] + requeue: ClassVar[bool] + + def __init__(self, message_override: str | None = None) -> None: + super().__init__(message_override or self.message) + + +class SMTPTimeoutException(ConsumerException): + message = "SMTP sad((" + requeue = True + + +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 = stream_name + self.group_name = group_name + self.consumer_name = consumer_name + self.model = model + self.message_handler = message_handler + + self.redis_client = Redis.from_url( + url=settings.redis_dsn, + decode_responses=True, + retry=Retry(backoff=ExponentialBackoff(cap=10, base=1), retries=10), + retry_on_error=[ConnectionError, TimeoutError], + ) + + 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: + # Не поднимаем ошибку о том, что группа уже создана (`BUSYGROUP`) + if "BUSYGROUP" not in str(response_exc): + raise + + async def process_message(self, message_id: str, data: dict[str, str]) -> None: + # TODO nq fixup error messages & expand extras + try: + validated_data = self.model.model_validate(data) + except ValidationError as e: # TODO nq mb move to handle_messages + logging.error( + "Invalid message payload", + extra={"original_message": data}, + exc_info=e, + ) + await self.redis_client.xack( # type: ignore[no-untyped-call] + self.stream_name, + self.group_name, + message_id, + ) + return + + try: + await self.message_handler(validated_data) + # TODO nq ConsumerException? + except Exception as e: # noqa PIE786 # TODO nq mb move to handle_messages + logging.error( + f"Error in {self.consumer_name} while processing message {data}", + exc_info=e, + ) + + await self.redis_client.xack( # type: ignore[no-untyped-call] + 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 = ">" + continue + + message_id, data = messages[0][1][0] + 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() # TODO nq move to destruct? + break + except Exception as e: # noqa PIE786 + logging.error( + f"An error occurred in worker {self.consumer_name}: {e}", + exc_info=e, + ) + await asyncio.sleep(2) + # TODO nq backoff & give up after 10 tries + # or remove `while True` completely + 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())) # TODO nq signature + 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 d7a3dd3..4f4b385 100644 --- a/app/main.py +++ b/app/main.py @@ -12,7 +12,14 @@ from app import pochta, supbot, users from app.common.bridges.config_bdg import public_users_bridge -from app.common.config import Base, engine, pochta_producer, sessionmaker, settings +from app.common.config import ( + Base, + engine, + pochta_producer, + redis_pool, + sessionmaker, + settings, +) from app.common.sqlalchemy_ext import session_context from app.common.starlette_cors_ext import CorrectCORSMiddleware @@ -47,6 +54,7 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: yield await rabbit_connection.close() + await redis_pool.disconnect() app = FastAPI( 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..18ec14f --- /dev/null +++ b/app/pochta/dependencies/redis_dep.py @@ -0,0 +1,14 @@ +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[str]: + # TODO nq add backoff? + return Redis(connection_pool=redis_pool, decode_responses=True) + + +RedisConnection = Annotated[Redis[str], Depends(get_redis_connection)] diff --git a/app/pochta/main.py b/app/pochta/main.py index 56eee62..6bffe5d 100644 --- a/app/pochta/main.py +++ b/app/pochta/main.py @@ -4,7 +4,8 @@ from app.common.config import settings from app.common.fastapi_ext import APIRouterExt -from app.pochta.routes import pochta_mub +from app.common.redis_ext import RedisRouter +from app.pochta.routes import pochta_mub, pochta_rds from app.users.utils.mub import MUBProtection mub_router = APIRouterExt(prefix="/mub", dependencies=[MUBProtection]) @@ -13,9 +14,14 @@ 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]: if settings.production_mode and settings.email is None: logging.warning("Configuration for email service is missing") + 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 index 3a4166e..14b7102 100644 --- a/app/pochta/routes/pochta_mub.py +++ b/app/pochta/routes/pochta_mub.py @@ -5,6 +5,7 @@ from app.common.config import settings, smtp_client from app.common.fastapi_ext import APIRouterExt +from app.pochta.dependencies.redis_dep import RedisConnection router = APIRouterExt(tags=["pochta mub"]) @@ -30,3 +31,12 @@ async def send_email_from_file( async with smtp_client as smtp: await smtp.send_message(message) + + +@router.post("/") +async def home(r: RedisConnection) -> str: + await r.xadd( + settings.redis_pochta_stream, + {"key": "value"}, + ) + return f"Message was added to stream {settings.redis_pochta_stream}" diff --git a/app/pochta/routes/pochta_rds.py b/app/pochta/routes/pochta_rds.py new file mode 100644 index 0000000..ab58a92 --- /dev/null +++ b/app/pochta/routes/pochta_rds.py @@ -0,0 +1,49 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from app.common.config import settings +from app.common.redis_ext import RedisRouter + + +class RegistrationEmailV1Data(BaseModel): + template: Literal["registration-v1"] = "registration-v1" + email_confirmation_token: str + + +class RegistrationEmailV2Data(BaseModel): + template: Literal["registration-v2"] = "registration-v2" + email_confirmation_token: str + username: str + + +class PasswordResetEmailData(BaseModel): + template: Literal["password-reset-v1"] = "password-reset-v1" + reset_confirmation_token: str + + +class EmailSendRequest(BaseModel): + email: str + data: RegistrationEmailV1Data | RegistrationEmailV2Data | PasswordResetEmailData = Field(discriminator="template") + + +# {{ data.email_confirmation_token }} + +# {"email": "test@test.test", "data": {"template": "registration-v1", "email_confirmation_token": ""}} +# {"email": "test@test.test", "data": {"template": "password-reset-v1", "reset_confirmation_token": ""}} + + +class PochtaSchema(BaseModel): + key: str + + +router = RedisRouter() + + +@router.add_consumer( + stream_name=settings.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/app/pochta/workers/__init__.py b/app/pochta/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 194e6f1..b6ccc25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,24 @@ services: + redis: + image: redis:7.4.2-alpine + command: /etc/redis/redis.conf + volumes: + - type: tmpfs + target: /data + - type: bind + source: redis.conf + target: /etc/redis/redis.conf + ports: + - target: 6379 + host_ip: 127.0.0.1 + published: 5800 + healthcheck: + test: "redis-cli ping | grep PONG" + start_period: 60s + interval: 10s + timeout: 60s + retries: 5 + mq: image: rabbitmq:3.12.10-management-alpine volumes: @@ -65,6 +85,8 @@ services: profiles: - app depends_on: + redis: + condition: service_healthy mq: condition: service_healthy db: @@ -91,3 +113,5 @@ services: mq_port: 5672 mq_username: guest mq_password: guest + vacancy_webhook_url: https://discord.com/api/webhooks/1217785134657568808/0A3tBqexVzpWtoqW0PZYfVcE6guRe3nC1NX-NLnZr8Tsbcu1jOD8_S8VBIkB8LL_Lveh + REDIS_URL: redis://redis:6379 diff --git a/poetry.lock b/poetry.lock index 34e2fbe..5337e0b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -283,6 +283,17 @@ files = [ {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + [[package]] name = "attrs" version = "24.3.0" @@ -1636,6 +1647,124 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "hiredis" +version = "3.1.1" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hiredis-3.1.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:6154d5dea2f58542333ef5ffd6765bf36b223210168109a17167cc593dab9c69"}, + {file = "hiredis-3.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:fe49dd39922075536f06aba35f346ad3517561f37e7fb18e247040f44e48c18e"}, + {file = "hiredis-3.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03c6f4501d8559b838be0138e5e312dda17d0db2eebb5fbb741fdc9e73c21c4f"}, + {file = "hiredis-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b865e5c6fba2eef0a60d9e49e70aa59f5809d762fe2a35aa48afe5a314bfa145"}, + {file = "hiredis-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fe7b283b8b1c38e97f8b810015c29af925f25e59fea02d903b2b17fb89c691f"}, + {file = "hiredis-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85bcacc03fdf1435fcdd2610372435782580c3c0f4159633c1a4f4eadf3690c2"}, + {file = "hiredis-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66a25a32a63374efac5242581e60364537777aba81714d5b49527b5a86be0169"}, + {file = "hiredis-3.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20eeaadac0ad7b9390c4bd468954d79626be853c92e8df99158240f403817641"}, + {file = "hiredis-3.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c02fae94b9a2b27bc6656217dd0df9ac6d5d8849e39ae8ceac2658fec1e0b6fe"}, + {file = "hiredis-3.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c70f18645749c339379b3c4e2be794b92e6011aae4ffcc462616e381d9031336"}, + {file = "hiredis-3.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:adf1cb0f7002aea80e3cbd5a76dceb4f419e52f6aba1b290ac924ca599960d42"}, + {file = "hiredis-3.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ed3fb25208535e6470b628f52dc7c4f3b2581d73cc2a162cc704dde26bbc89e5"}, + {file = "hiredis-3.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:72320d24b9f22f0f086103a2dd33f4f1f1c4df70221c422507f67000d6329da8"}, + {file = "hiredis-3.1.1-cp310-cp310-win32.whl", hash = "sha256:a5a6278f254d688099683672bec6ddf1bd3949e732780e8b118d43588152e452"}, + {file = "hiredis-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ee37cff4a3d35207e4c385a03be2adb9649df77eb9578afc4ab37825f1390c0"}, + {file = "hiredis-3.1.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:80d98b1d21002c7045ef4c7bae9a41a7a5f6585d08c345850c32ec08d05bd8fe"}, + {file = "hiredis-3.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:9d943c754273fda5908b6c6f4c64c9dcdc4718bb96fa5c699e7fee687d713566"}, + {file = "hiredis-3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e0b02238141b685de2e8fcf361d79359a77ca9b440e566280e9dda875de03d1"}, + {file = "hiredis-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2737844ca1c2477808f2612093c9fad68b42dd17fba1b348c95232cf895d84"}, + {file = "hiredis-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf6c2ea105477a7ea837381255f884b60775a8f6b527d16416d0e2fc4dd107d6"}, + {file = "hiredis-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32acf786b0e7117b1d8ffc8e5a1cfab57c73798658ed02228b5e9fa71fd4eaff"}, + {file = "hiredis-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98aeff9c038fd456e2e1a789abab775a1fcd1fd993170b1602f224e8fb8bc507"}, + {file = "hiredis-3.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb89a866e61c81ed2da3dc7eaee2b3e70d444aa350aa893321d637e77cda1790"}, + {file = "hiredis-3.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec1e10e02eaa8df9f43d6e4b3d201cfcc33d08d263f3f1ad59e8433bca4c25e8"}, + {file = "hiredis-3.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c32869095b412d401ad8358dbb4d8c50661a301237e55fa865c4de83d1a2b5f2"}, + {file = "hiredis-3.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ef96546415a0ec22534ee5ce30ca5e0fefc1c1b9f69ded167748fa6b2da55a73"}, + {file = "hiredis-3.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7bfbc714b3c48f62064e1ff031495c977d7554d9ff3d799bb3f8c40256af94bb"}, + {file = "hiredis-3.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd0e4b5a0bd8c5c7137b2fb96eac1a36fca65ab822bfd7e7a712c5f7faf956"}, + {file = "hiredis-3.1.1-cp311-cp311-win32.whl", hash = "sha256:de94a0fbdbf1436a94443be8ddf9357d3f6637a1a072a22442135eca634157cc"}, + {file = "hiredis-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:b60488f5ec1d9903b3b0ce744b76c570e82cb1b53d3045df74111a5d5bd2c134"}, + {file = "hiredis-3.1.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:b7c3e47b3eec883add6ff6d8dbcc314e7bacd73c5146e4587aa3610a1d59c1b0"}, + {file = "hiredis-3.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:dba871b974ebd60258cf723a096a4170cc1241d9a32273513fc9da37410ff4a1"}, + {file = "hiredis-3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f444c482e817452ccb598140c6544c803480346f572e0b42fece391ed70ff26"}, + {file = "hiredis-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c63a753a0ba0bb0bc92041682623cab843114a0cf87875cd9aca0ab0d833337"}, + {file = "hiredis-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f1981e0f54e74de525266a2dca3f9740ca2eed03227b4f86d1ae8ef887d37b"}, + {file = "hiredis-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0e371c78b9e4715678ca17a59fc72c37483e53179c9a2d4babf85c383fc55c5"}, + {file = "hiredis-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42cd753d4d85cf806037a01e4e6fa83c8db5b20b8d0cbfc2feec3daad2d563f"}, + {file = "hiredis-3.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76b8f64de36c8607650c47951a1591996dcfe186ae158f88bac7e3976348cccc"}, + {file = "hiredis-3.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a6f2e54bbad9e0831c5d88451676d7f116210f4f302055a84671ef69c5e935b"}, + {file = "hiredis-3.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8619f2a015dd8ba98214e76e7453bcdaaa8b04d81348369ad8975c1ff2792eb3"}, + {file = "hiredis-3.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1fd685aa1f9636da9548fa471abf37138033f1b4ec0d91f515ea5ed4d7d88b62"}, + {file = "hiredis-3.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:24b51d492b6628054eb4dce63eab0cadf483b87227fe6ee3b6de0038caed6544"}, + {file = "hiredis-3.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c7e43d3968000d75d97b2d24a6f1ee37d24b9a4472ba85f670e7d2d94c6b1f2"}, + {file = "hiredis-3.1.1-cp312-cp312-win32.whl", hash = "sha256:b48578047c6bb3d0ea3ce37f0762e35e71d1f7cff8d940e2caa131359a12c5a7"}, + {file = "hiredis-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:87b69e99301a33119cb31b19c6be7aed164c0df6b6343ba57b65deb23ae9251e"}, + {file = "hiredis-3.1.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:f22759efdb02f5e95884b1ad986574be86c7dd2ac4b05fe8e2b93826c6e680b3"}, + {file = "hiredis-3.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:a9524a1f651e2d45eaf33865a0f412a0d1117f49661f09d8243a98c3d3f961a2"}, + {file = "hiredis-3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e6c9c6eb9929ca220002b28ce0976b1a90bb95ffcf08e6e2c51956d37a2318a"}, + {file = "hiredis-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4614cc774bff82c2ed62f13facb732c03a8dc0c5e75cc276af61b5089c434898"}, + {file = "hiredis-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17e5ad9ed8d8913bdac6d567c9cf0c4f259e7950c3b318fe636ebb7383d3f16b"}, + {file = "hiredis-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340f3e9c33ae71e235a63770e339743254c91aba7b55d75a1ab6679e0d502aea"}, + {file = "hiredis-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56f2587e35a4f3f214d6f843e000af45422ebd81721a12add13108c1c4789332"}, + {file = "hiredis-3.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dba00f102b4414a6b33f3aa0ab6192d78c515fc4939a14d9c87461262047883f"}, + {file = "hiredis-3.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4bf5711a5bdbc284c406c8e9dd9154b4d93a54ba758f417c8b8c01133997059c"}, + {file = "hiredis-3.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:434614076066704efae38a538a6e1dcea9678c3de030a6ec2fe72d475e2594de"}, + {file = "hiredis-3.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7db1567a8aef8594c81ad67ff597b8ac86aa1ead585b8e8b51d33e234b817d68"}, + {file = "hiredis-3.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:64cd1f6b5cf2904e64445c3a5e765fcbf30c5a0f132051a3c8d4bd24fa2fa3fa"}, + {file = "hiredis-3.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:330246da5c5c33fd7cb25e87e17eabdb1f03678e652ea0c46861f4318fc56c29"}, + {file = "hiredis-3.1.1-cp313-cp313-win32.whl", hash = "sha256:a227a02b603583c84cb6791b48bc428339ebccd80befed8f00cac5584fc29ca4"}, + {file = "hiredis-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:5fa133c6f0fb09bf5f7dd3d722934f2908d209be1adba5c64b5227c0e875e88c"}, + {file = "hiredis-3.1.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:0f594b790ca6b4cff22c53bcad894386c6bcf1673ead1f2ce6b8a47ab67650ba"}, + {file = "hiredis-3.1.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:747de1f53439c2fd3cee9f0876f197583630950c60afa9521d2a28ff9513b941"}, + {file = "hiredis-3.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b98f8466d3e5e6f09604aceb3a290085e520d0a8ccabbe076b4cc7b08bc17f02"}, + {file = "hiredis-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b6c1780e4839249ab64100af54cd63f6818a1d4a5e2d2a772caf079553902e3"}, + {file = "hiredis-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8dc575624a57a7e8a88e3c72a78c201bedb864a94ab23d97ae2395adb3ec81c"}, + {file = "hiredis-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:217e635245894c8d1bd827e6e0d7a59de703d0884cb41396fd22f73f9d134ee3"}, + {file = "hiredis-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763d36bdd554bea314b6b09b38edfacb579b3a349ddff496a995232f7bd5ee3d"}, + {file = "hiredis-3.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:afca4ce7dca972d990b542bedbc61561d360b5d2565748973c58ad90ff492292"}, + {file = "hiredis-3.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5a4878673d8f1167829ca0cfc204faa480f85acc46a07de4dddfa188bfaa28e0"}, + {file = "hiredis-3.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c9822f42f195397df3554d13e6a4acfec201f68de3b31d35940d6232c43b715"}, + {file = "hiredis-3.1.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8a574484a279827703aa3e8bf8d0907bdc5ad598b09c0b7af356e33babb35813"}, + {file = "hiredis-3.1.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:77df8d0d6d0e98e74afaf1e75d890b32561d3b4614b674e121f30f4e92197d53"}, + {file = "hiredis-3.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cee9a3fdb067054fd23799ba010de03e78788d6cf2bcd1b4edfe9e2363b9cde7"}, + {file = "hiredis-3.1.1-cp38-cp38-win32.whl", hash = "sha256:03f3fa16c7b285bd053ba6b013d7e642a19318f8f291338a35cdd2237da47b70"}, + {file = "hiredis-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:ecb0d7813a576570f9fc9d192f4690f61a151f370c9a3015811a69b99fce56c3"}, + {file = "hiredis-3.1.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:f9d7fc07a083d38d15aebaa3f2b7acab520330b59e74a3aaabe74a25e01bf582"}, + {file = "hiredis-3.1.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f1411e49f247e28f03607fc26fa6c976e935a0e6a9de63c0ae1d80a76fe06f4f"}, + {file = "hiredis-3.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e09cd080c5985a41028093f7365e88194f64ea7f0c37fdc2154f0006f66d540"}, + {file = "hiredis-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b8a3b92fe816b713d18535a34cb5a5aeab3c2d13cb2ad19f115a406b5651bf"}, + {file = "hiredis-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff714f4efc93644481b23fd0da8df7f8d60e93604bb9b66590e29b6bf3642b22"}, + {file = "hiredis-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:727215b75a669eeba3518b37a640c2eaf9f56bcad209eee516afb632a7b19965"}, + {file = "hiredis-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22ae61afcc1d7c561cc927a76cc41b7707eee4861871569a79ce093ac332e429"}, + {file = "hiredis-3.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0af1513d98f80b4edb65f2ec1536c06bd1fa7b553adb4bc5f2e18466203038f"}, + {file = "hiredis-3.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8b83937f8b84310fe4caf89b81622618aa319c0f9d67d9efe340d0e67d61acb8"}, + {file = "hiredis-3.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1ac2efcdc1c5e3ff1add167e4dd9646f266d10dd2a7b1ae1f2a021f5e95795b7"}, + {file = "hiredis-3.1.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:757303f4c8115b338f8317dfeec836525e097dc937f7d43b8dd316c7419b1301"}, + {file = "hiredis-3.1.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:d60e956ebb6960a5db480db7b9382ce3942c660bf922b3803d66896dbaef5dbe"}, + {file = "hiredis-3.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:65d3f1533b4e3fd02dae821f4bfee75e6a1d955bf34c47e32cf7924e145beee8"}, + {file = "hiredis-3.1.1-cp39-cp39-win32.whl", hash = "sha256:96999841cbf20ae015809ac491d387118179270b7d7dda336c7333a2283c9822"}, + {file = "hiredis-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c3b80ef4185f64a7107d8a6954ba756fba2b955700021af7cc1231b7602b3b6f"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c37f00064203ea8c17e06a51971511dda0ce826e5974ebe61cc6c7447cd16d30"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e196647a34e82c5567b982184dff698a8655c9d2997ddd427a2ef542ef8b3864"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5851522b854a7ef9af1c0d5bda04ff1d97e5da28cd93eb332e051acce36d8e3"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4586570efa5c931a9dae47d0ea80968e058ad9a631364c474b316d0d62d54653"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c8b28c9949eb1849bc07e6b03591e43deb25cc244729fa2a53d9c6a9cdbdb0"}, + {file = "hiredis-3.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dc0348c4f464020cc22f9733792d95fc4c09afecd1d3097eada500878133fa0e"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:93027287ad7bef050bdaeec3d9310c6a4cb965b2491ede7a68d9c6121d45f052"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7ff51069030f0ed71521f40828b5966aa8613f40170ddc8929c675ea2b662bf1"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:898a67fd6e07d9c19aa8f25c47954f3c32c4b43d16f6dab3bd93e385eccf93d8"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:412ac8d59134b7bcd1ea0c0378828ec8584c313e35ce6926d0b26f3db8004e58"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36667d8a5947fa382339e2c60fc4947005e07681b569823f13dc227186e16267"}, + {file = "hiredis-3.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8782a8eff272af2cc705b8ee503367f71fb9d2ff370eae0825d77baed91f0aeb"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ff3706f3802248c98ac7e06553f6b3820a6e073899df41f49833d60708fbe1c7"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b25b26064b9b10f75b9765db3e46d7bb5215865e85d991dcc34b793d8193faf1"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b6cd4716a666e544af7b9d9b95e1b66e2ea46c419740856b63237aef28387"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfcde616a8e1aba2f6114a10ebe20d7f8961fff38c0e9b720105ed977d40f977"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f756d4c63bd6e50141dbb5b146c541899ce30911a2d79221752717ef946a4f12"}, + {file = "hiredis-3.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d1eeacc81a8014cc88370f42d1e82bc41591587fe063ff7548645c172713f38"}, + {file = "hiredis-3.1.1.tar.gz", hash = "sha256:63f22cd7b441cbe13d24087b338e4e6a8f454f333cf35a6ed27ef13a60ca8b0b"}, +] + [[package]] name = "httpcore" version = "0.16.3" @@ -2040,58 +2169,51 @@ files = [ [[package]] name = "mypy" -version = "1.14.1" +version = "1.12.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, - {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, - {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, - {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, - {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, - {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, - {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, - {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, - {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, - {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, - {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, - {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, - {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, - {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, - {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, - {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -typing_extensions = ">=4.6.0" + {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, + {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, + {file = "mypy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e"}, + {file = "mypy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a"}, + {file = "mypy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff"}, + {file = "mypy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7"}, + {file = "mypy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57"}, + {file = "mypy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309"}, + {file = "mypy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f"}, + {file = "mypy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9"}, + {file = "mypy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164"}, + {file = "mypy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475"}, + {file = "mypy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9"}, + {file = "mypy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642"}, + {file = "mypy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601"}, + {file = "mypy-1.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521"}, + {file = "mypy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893"}, + {file = "mypy-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721"}, + {file = "mypy-1.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3"}, + {file = "mypy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b"}, + {file = "mypy-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f"}, + {file = "mypy-1.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e"}, + {file = "mypy-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7"}, + {file = "mypy-1.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b"}, + {file = "mypy-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0"}, + {file = "mypy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8"}, + {file = "mypy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0"}, + {file = "mypy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1"}, + {file = "mypy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e"}, + {file = "mypy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa"}, + {file = "mypy-1.12.0-py3-none-any.whl", hash = "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266"}, + {file = "mypy-1.12.0.tar.gz", hash = "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -2770,6 +2892,23 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.9.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pytest" version = "7.4.4" @@ -2912,6 +3051,26 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "redis" +version = "5.3.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.8" +files = [ + {file = "redis-5.3.0-py3-none-any.whl", hash = "sha256:f1deeca1ea2ef25c1e4e46b07f4ea1275140526b1feea4c6459c0ec27a10ef83"}, + {file = "redis-5.3.0.tar.gz", hash = "sha256:8d69d2dde11a12dc85d0dbf5c45577a5af048e2456f7077d87ad35c1c81c310e"}, +] + +[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\""} +PyJWT = ">=2.9.0,<2.10.0" + +[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.32.3" @@ -3185,6 +3344,20 @@ files = [ [package.dependencies] pbr = ">=2.0.0" +[[package]] +name = "types-cffi" +version = "1.17.0.20250326" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.9" +files = [ + {file = "types_cffi-1.17.0.20250326-py3-none-any.whl", hash = "sha256:5af4ecd7374ae0d5fa9e80864e8d4b31088cc32c51c544e3af7ed5b5ed681447"}, + {file = "types_cffi-1.17.0.20250326.tar.gz", hash = "sha256:6c8fea2c2f34b55e5fb77b1184c8ad849d57cf0ddccbc67a62121ac4b8b32254"}, +] + +[package.dependencies] +types-setuptools = "*" + [[package]] name = "types-passlib" version = "1.7.7.20241221" @@ -3196,6 +3369,50 @@ files = [ {file = "types_passlib-1.7.7.20241221.tar.gz", hash = "sha256:c7e7d2d836aef2ef26a650110fc89cff896163767aebd8f5d6d5b2675e460173"}, ] +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +description = "Typing stubs for pyOpenSSL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, + {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-cffi = "*" + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +description = "Typing stubs for redis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, + {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-pyOpenSSL = "*" + +[[package]] +name = "types-setuptools" +version = "80.3.0.20250505" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "types_setuptools-80.3.0.20250505-py3-none-any.whl", hash = "sha256:117c86a82367306388b55310d04da807ff4c3ecdf769656a5fdc0fdd06a2c1b6"}, + {file = "types_setuptools-80.3.0.20250505.tar.gz", hash = "sha256:5fd3d34b8fa3441d68d010fef95e232d1e48f3f5cb578f3477b7aae4f8374502"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -3627,4 +3844,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "713aaca33d6eca918a7ef3831e1020c6e3cdd63facf912230ef07878f184deed" +content-hash = "650936ef706f9020f5606a9f49e689fd022b9f76f5a183b8537e4bc24579633e" diff --git a/pyproject.toml b/pyproject.toml index 4495dce..2bc07d7 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"} aiosmtplib = "^3.0.2" pydantic-settings = "^2.6.1" @@ -31,7 +32,7 @@ pytest = "^7.2.2" flake8 = "4.0.1" black = "24.10.0" pre-commit = "^3.2.1" -mypy = "^1.12.0" +mypy = "1.12.0" flake8-pie = "0.16.0" dlint = "0.14.0" flake8-coding = "1.3.2" @@ -65,6 +66,7 @@ rstr = "3.2.2" watchfiles = "^0.21.0" polyfactory = "^2.15.0" respx = "^0.21.1" +types-redis = "^4.6.0" [tool.isort] profile = "black" diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..acf6474 --- /dev/null +++ b/redis.conf @@ -0,0 +1 @@ +requirepass foobared