diff --git a/.apiversion b/.apiversion index 9ad974f6..760606e1 100644 --- a/.apiversion +++ b/.apiversion @@ -1 +1 @@ -5.5 +5.7 diff --git a/.coveragerc b/.coveragerc index 9feee202..e1862099 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,4 @@ exclude_lines = pragma: no cover if TYPE_CHECKING: @abstractmethod + @overload diff --git a/CHANGES/830.misc b/CHANGES/830.misc new file mode 100644 index 00000000..f01d8b98 --- /dev/null +++ b/CHANGES/830.misc @@ -0,0 +1 @@ +Logger name for processing events is changed to :code:`aiogram.events`. diff --git a/CHANGES/835.misc b/CHANGES/835.misc new file mode 100644 index 00000000..83be3cb6 --- /dev/null +++ b/CHANGES/835.misc @@ -0,0 +1 @@ +Added full support of Telegram Bot API 5.6 and 5.7 diff --git a/CHANGES/836.feature b/CHANGES/836.feature new file mode 100644 index 00000000..17c9cd49 --- /dev/null +++ b/CHANGES/836.feature @@ -0,0 +1 @@ +Added possibility to add handler flags via decorator (like `pytest.mark` decorator but `aiogram.flags`) diff --git a/CHANGES/837.feature b/CHANGES/837.feature new file mode 100644 index 00000000..57ce165b --- /dev/null +++ b/CHANGES/837.feature @@ -0,0 +1,3 @@ +Added :code:`ChatActionSender` utility to automatically sends chat action while long process is running. + +It also can be used as message middleware and can be customized via :code:`chat_action` flag. diff --git a/CHANGES/838.misc b/CHANGES/838.misc new file mode 100644 index 00000000..d4af8385 --- /dev/null +++ b/CHANGES/838.misc @@ -0,0 +1,2 @@ +**BREAKING** +Events isolation mechanism is moved from FSM storages to standalone managers diff --git a/CHANGES/839.bugix b/CHANGES/839.bugix new file mode 100644 index 00000000..89bbe247 --- /dev/null +++ b/CHANGES/839.bugix @@ -0,0 +1 @@ +Fixed I18n lazy-proxy. Disabled caching. diff --git a/Makefile b/Makefile index 63b117c0..c0508fea 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ clean: rm -rf `find . -name .pytest_cache` rm -rf *.egg-info rm -f report.html - rm -f .coverage* + rm -f .coverage rm -rf {build,dist,site,.cache,.mypy_cache,reports} # ================================================================================================= @@ -84,7 +84,7 @@ reformat: # ================================================================================================= .PHONY: test-run-services test-run-services: - docker-compose -f tests/docker-compose.yml -p aiogram3-dev up -d + @#docker-compose -f tests/docker-compose.yml -p aiogram3-dev up -d .PHONY: test test: test-run-services diff --git a/README.rst b/README.rst index 7e0dd527..99b88c07 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ aiogram |beta badge| :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.7-blue.svg?logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a820ee6c..d734cb83 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -2,6 +2,7 @@ from .client import session from .client.bot import Bot from .dispatcher import filters, handler from .dispatcher.dispatcher import Dispatcher +from .dispatcher.flags.flag import FlagGenerator from .dispatcher.middlewares.base import BaseMiddleware from .dispatcher.router import Router from .utils.magic_filter import MagicFilter @@ -18,6 +19,7 @@ except ImportError: # pragma: no cover F = MagicFilter() html = _html_decoration md = _markdown_decoration +flags = FlagGenerator() __all__ = ( "__api_version__", @@ -34,7 +36,8 @@ __all__ = ( "F", "html", "md", + "flags", ) -__version__ = "3.0.0b1" -__api_version__ = "5.5" +__version__ = "3.0.0b2" +__api_version__ = "5.7" diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index d6378215..94feb7b8 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -311,7 +311,9 @@ class Bot(ContextInstanceMixin["Bot"]): if isinstance(file, str): file_id = file else: - file_id = getattr(file, "file_id", None) + # type is ignored in due to: + # Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") + file_id = getattr(file, "file_id", None) # type: ignore if file_id is None: raise TypeError("file can only be of the string or Downloadable type") @@ -533,6 +535,7 @@ class Bot(ContextInstanceMixin["Bot"]): entities: Optional[List[MessageEntity]] = None, disable_web_page_preview: Optional[bool] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -551,6 +554,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param entities: A JSON-serialized list of special entities that appear in message text, which can be specified instead of *parse_mode* :param disable_web_page_preview: Disables link previews for links in this message :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -564,6 +568,7 @@ class Bot(ContextInstanceMixin["Bot"]): entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -576,6 +581,7 @@ class Bot(ContextInstanceMixin["Bot"]): from_chat_id: Union[int, str], message_id: int, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, request_timeout: Optional[int] = None, ) -> Message: """ @@ -587,6 +593,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format :code:`@channelusername`) :param message_id: Message identifier in the chat specified in *from_chat_id* :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the forwarded message from forwarding and saving :param request_timeout: Request timeout :return: On success, the sent Message is returned. """ @@ -595,6 +602,7 @@ class Bot(ContextInstanceMixin["Bot"]): from_chat_id=from_chat_id, message_id=message_id, disable_notification=disable_notification, + protect_content=protect_content, ) return await self(call, request_timeout=request_timeout) @@ -607,6 +615,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode: Optional[str] = UNSET, caption_entities: Optional[List[MessageEntity]] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -626,6 +635,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the new caption. See `formatting options `_ for more details. :param caption_entities: A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of *parse_mode* :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -640,6 +650,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -654,6 +665,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode: Optional[str] = UNSET, caption_entities: Optional[List[MessageEntity]] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -672,6 +684,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the photo caption. See `formatting options `_ for more details. :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode* :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -685,6 +698,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -703,6 +717,7 @@ class Bot(ContextInstanceMixin["Bot"]): title: Optional[str] = None, thumb: Optional[Union[InputFile, str]] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -726,6 +741,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param title: Track name :param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files » ` :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -743,6 +759,7 @@ class Bot(ContextInstanceMixin["Bot"]): title=title, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -759,6 +776,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities: Optional[List[MessageEntity]] = None, disable_content_type_detection: Optional[bool] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -779,6 +797,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode* :param disable_content_type_detection: Disables automatic server-side content type detection for files uploaded using multipart/form-data :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -794,6 +813,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities=caption_entities, disable_content_type_detection=disable_content_type_detection, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -813,6 +833,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities: Optional[List[MessageEntity]] = None, supports_streaming: Optional[bool] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -836,6 +857,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode* :param supports_streaming: Pass :code:`True`, if the uploaded video is suitable for streaming :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -854,6 +876,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities=caption_entities, supports_streaming=supports_streaming, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -872,6 +895,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode: Optional[str] = UNSET, caption_entities: Optional[List[MessageEntity]] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -894,6 +918,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the animation caption. See `formatting options `_ for more details. :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode* :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -911,6 +936,7 @@ class Bot(ContextInstanceMixin["Bot"]): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -926,6 +952,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities: Optional[List[MessageEntity]] = None, duration: Optional[int] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -945,6 +972,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode* :param duration: Duration of the voice message in seconds :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -959,6 +987,7 @@ class Bot(ContextInstanceMixin["Bot"]): caption_entities=caption_entities, duration=duration, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -973,6 +1002,7 @@ class Bot(ContextInstanceMixin["Bot"]): length: Optional[int] = None, thumb: Optional[Union[InputFile, str]] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -991,6 +1021,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param length: Video width and height, i.e. diameter of the video message :param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files » ` :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1004,6 +1035,7 @@ class Bot(ContextInstanceMixin["Bot"]): length=length, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1015,6 +1047,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id: Union[int, str], media: List[Union[InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo]], disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, request_timeout: Optional[int] = None, @@ -1027,6 +1060,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`) :param media: A JSON-serialized array describing messages to be sent, must include 2-10 items :param disable_notification: Sends messages `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent messages from forwarding and saving :param reply_to_message_id: If the messages are a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param request_timeout: Request timeout @@ -1036,6 +1070,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id=chat_id, media=media, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, ) @@ -1051,6 +1086,7 @@ class Bot(ContextInstanceMixin["Bot"]): heading: Optional[int] = None, proximity_alert_radius: Optional[int] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -1071,6 +1107,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param heading: For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. :param proximity_alert_radius: For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1086,6 +1123,7 @@ class Bot(ContextInstanceMixin["Bot"]): heading=heading, proximity_alert_radius=proximity_alert_radius, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1177,6 +1215,7 @@ class Bot(ContextInstanceMixin["Bot"]): google_place_id: Optional[str] = None, google_place_type: Optional[str] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -1199,6 +1238,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param google_place_id: Google Places identifier of the venue :param google_place_type: Google Places type of the venue. (See `supported types `_.) :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1216,6 +1256,7 @@ class Bot(ContextInstanceMixin["Bot"]): google_place_id=google_place_id, google_place_type=google_place_type, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1230,6 +1271,7 @@ class Bot(ContextInstanceMixin["Bot"]): last_name: Optional[str] = None, vcard: Optional[str] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -1248,6 +1290,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param last_name: Contact's last name :param vcard: Additional data about the contact in the form of a `vCard `_, 0-2048 bytes :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove keyboard or to force a reply from the user. @@ -1261,6 +1304,7 @@ class Bot(ContextInstanceMixin["Bot"]): last_name=last_name, vcard=vcard, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1283,6 +1327,7 @@ class Bot(ContextInstanceMixin["Bot"]): close_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None, is_closed: Optional[bool] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -1309,6 +1354,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with *open_period*. :param is_closed: Pass :code:`True`, if the poll needs to be immediately closed. This can be useful for poll preview. :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1330,6 +1376,7 @@ class Bot(ContextInstanceMixin["Bot"]): close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1341,6 +1388,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id: Union[int, str], emoji: Optional[str] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -1356,6 +1404,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`) :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '⚽', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '⚽', and values 1-64 for '🎰'. Defaults to '🎲' :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1366,6 +1415,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id=chat_id, emoji=emoji, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1517,7 +1567,7 @@ class Bot(ContextInstanceMixin["Bot"]): Source: https://core.telegram.org/bots/api#unbanchatmember - :param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@username`) + :param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`) :param user_id: Unique identifier of the target user :param only_if_banned: Do nothing if the user is not banned :param request_timeout: Request timeout @@ -2512,6 +2562,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id: Union[int, str], sticker: Union[InputFile, str], disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[ @@ -2520,13 +2571,14 @@ class Bot(ContextInstanceMixin["Bot"]): request_timeout: Optional[int] = None, ) -> Message: """ - Use this method to send static .WEBP or `animated `_ .TGS stickers. On success, the sent :class:`aiogram.types.message.Message` is returned. + Use this method to send static .WEBP, `animated `_ .TGS, or `video `_ .WEBM stickers. On success, the sent :class:`aiogram.types.message.Message` is returned. Source: https://core.telegram.org/bots/api#sendsticker :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`) :param sticker: Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » ` :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -2537,6 +2589,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id=chat_id, sticker=sticker, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2592,12 +2645,13 @@ class Bot(ContextInstanceMixin["Bot"]): emojis: str, png_sticker: Optional[Union[InputFile, str]] = None, tgs_sticker: Optional[InputFile] = None, + webm_sticker: Optional[InputFile] = None, contains_masks: Optional[bool] = None, mask_position: Optional[MaskPosition] = None, request_timeout: Optional[int] = None, ) -> bool: """ - Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Returns :code:`True` on success. + Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#createnewstickerset @@ -2606,7 +2660,8 @@ class Bot(ContextInstanceMixin["Bot"]): :param title: Sticker set title, 1-64 characters :param emojis: One or more emoji corresponding to the sticker :param png_sticker: **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » ` - :param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for technical requirements + :param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for technical requirements + :param webm_sticker: **WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for technical requirements :param contains_masks: Pass :code:`True`, if a set of mask stickers should be created :param mask_position: A JSON-serialized object for position where the mask should be placed on faces :param request_timeout: Request timeout @@ -2619,6 +2674,7 @@ class Bot(ContextInstanceMixin["Bot"]): emojis=emojis, png_sticker=png_sticker, tgs_sticker=tgs_sticker, + webm_sticker=webm_sticker, contains_masks=contains_masks, mask_position=mask_position, ) @@ -2631,11 +2687,12 @@ class Bot(ContextInstanceMixin["Bot"]): emojis: str, png_sticker: Optional[Union[InputFile, str]] = None, tgs_sticker: Optional[InputFile] = None, + webm_sticker: Optional[InputFile] = None, mask_position: Optional[MaskPosition] = None, request_timeout: Optional[int] = None, ) -> bool: """ - Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success. + Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#addstickertoset @@ -2643,7 +2700,8 @@ class Bot(ContextInstanceMixin["Bot"]): :param name: Sticker set name :param emojis: One or more emoji corresponding to the sticker :param png_sticker: **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » ` - :param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for technical requirements + :param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for technical requirements + :param webm_sticker: **WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for technical requirements :param mask_position: A JSON-serialized object for position where the mask should be placed on faces :param request_timeout: Request timeout :return: Returns True on success. @@ -2654,6 +2712,7 @@ class Bot(ContextInstanceMixin["Bot"]): emojis=emojis, png_sticker=png_sticker, tgs_sticker=tgs_sticker, + webm_sticker=webm_sticker, mask_position=mask_position, ) return await self(call, request_timeout=request_timeout) @@ -2707,13 +2766,13 @@ class Bot(ContextInstanceMixin["Bot"]): request_timeout: Optional[int] = None, ) -> bool: """ - Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns :code:`True` on success. + Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#setstickersetthumb :param name: Sticker set name :param user_id: User identifier of the sticker set owner - :param thumb: A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for animated sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `. Animated sticker set thumbnail can't be uploaded via HTTP URL. + :param thumb: A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for animated sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for video sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `. Animated sticker set thumbnails can't be uploaded via HTTP URL. :param request_timeout: Request timeout :return: Returns True on success. """ @@ -2798,6 +2857,7 @@ class Bot(ContextInstanceMixin["Bot"]): send_email_to_provider: Optional[bool] = None, is_flexible: Optional[bool] = None, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, @@ -2831,6 +2891,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param send_email_to_provider: Pass :code:`True`, if user's email address should be sent to provider :param is_flexible: Pass :code:`True`, if the final price depends on the shipping method :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: A JSON-serialized object for an `inline keyboard `_. If empty, one 'Pay :code:`total price`' button will be shown. If not empty, the first button must be a Pay button. @@ -2861,6 +2922,7 @@ class Bot(ContextInstanceMixin["Bot"]): send_email_to_provider=send_email_to_provider, is_flexible=is_flexible, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2960,6 +3022,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id: int, game_short_name: str, disable_notification: Optional[bool] = None, + protect_content: Optional[bool] = None, reply_to_message_id: Optional[int] = None, allow_sending_without_reply: Optional[bool] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, @@ -2973,6 +3036,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param chat_id: Unique identifier for the target chat :param game_short_name: Short name of the game, serves as the unique identifier for the game. Set up your games via `Botfather `_. :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. + :param protect_content: Protects the contents of the sent message from forwarding and saving :param reply_to_message_id: If the message is a reply, ID of the original message :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param reply_markup: A JSON-serialized object for an `inline keyboard `_. If empty, one 'Play game_title' button will be shown. If not empty, the first button must launch the game. @@ -2983,6 +3047,7 @@ class Bot(ContextInstanceMixin["Bot"]): chat_id=chat_id, game_short_name=game_short_name, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 04941bf4..610053db 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -16,8 +16,8 @@ from ..utils.backoff import Backoff, BackoffConfig from .event.bases import UNHANDLED, SkipHandler from .event.telegram import TelegramEventObserver from .fsm.middleware import FSMContextMiddleware -from .fsm.storage.base import BaseStorage -from .fsm.storage.memory import MemoryStorage +from .fsm.storage.base import BaseEventIsolation, BaseStorage +from .fsm.storage.memory import DisabledEventIsolation, MemoryStorage from .fsm.strategy import FSMStrategy from .middlewares.error import ErrorsMiddleware from .middlewares.user_context import UserContextMiddleware @@ -35,7 +35,7 @@ class Dispatcher(Router): self, storage: Optional[BaseStorage] = None, fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT, - isolate_events: bool = False, + events_isolation: Optional[BaseEventIsolation] = None, **kwargs: Any, ) -> None: super(Dispatcher, self).__init__(**kwargs) @@ -48,19 +48,22 @@ class Dispatcher(Router): ) self.update.register(self._listen_update) - # Error handlers should works is out of all other functions and be registered before all other middlewares + # Error handlers should work is out of all other functions and be registered before all others middlewares self.update.outer_middleware(ErrorsMiddleware(self)) + # User context middleware makes small optimization for all other builtin # middlewares via caching the user and chat instances in the event context self.update.outer_middleware(UserContextMiddleware()) + # FSM middleware should always be registered after User context middleware # because here is used context from previous step self.fsm = FSMContextMiddleware( storage=storage if storage else MemoryStorage(), strategy=fsm_strategy, - isolate_events=isolate_events, + events_isolation=events_isolation if events_isolation else DisabledEventIsolation(), ) self.update.outer_middleware(self.fsm) + self.shutdown.register(self.fsm.close) self._running_lock = Lock() @@ -104,7 +107,7 @@ class Dispatcher(Router): finally: finish_time = loop.time() duration = (finish_time - start_time) * 1000 - loggers.dispatcher.info( + loggers.event.info( "Update id=%s is %s. Duration %d ms by bot id=%d", update.update_id, "handled" if handled else "not handled", @@ -213,11 +216,11 @@ class Dispatcher(Router): try: await bot(result) except TelegramAPIError as e: - # In due to WebHook mechanism doesn't allows to get response for + # In due to WebHook mechanism doesn't allow getting response for # requests called in answer to WebHook request. # Need to skip unsuccessful responses. # For debugging here is added logging. - loggers.dispatcher.error("Failed to make answer: %s: %s", e.__class__.__name__, e) + loggers.event.error("Failed to make answer: %s: %s", e.__class__.__name__, e) async def _process_update( self, bot: Bot, update: Update, call_answer: bool = True, **kwargs: Any @@ -238,7 +241,7 @@ class Dispatcher(Router): return response is not UNHANDLED except Exception as e: - loggers.dispatcher.exception( + loggers.event.exception( "Cause exception while process update id=%d by bot id=%d\n%s: %s", update.update_id, bot.id, @@ -282,7 +285,7 @@ class Dispatcher(Router): try: return await self.feed_update(bot, update, **kwargs) except Exception as e: - loggers.dispatcher.exception( + loggers.event.exception( "Cause exception while process update id=%d by bot id=%d\n%s: %s", update.update_id, bot.id, diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index 7937d209..813ddf51 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -8,6 +8,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, from magic_filter import MagicFilter from aiogram.dispatcher.filters.base import BaseFilter +from aiogram.dispatcher.flags.getter import extract_flags_from_object from aiogram.dispatcher.handler.base import BaseHandler CallbackType = Callable[..., Awaitable[Any]] @@ -71,6 +72,7 @@ class HandlerObject(CallableMixin): callback = inspect.unwrap(self.callback) if inspect.isclass(callback) and issubclass(callback, BaseHandler): self.awaitable = True + self.flags.update(extract_flags_from_object(callback)) async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]: if not self.filters: diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 74f175ac..a38b57af 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,6 +1,21 @@ from typing import Dict, Tuple, Type from .base import BaseFilter +from .chat_member_updated import ( + ADMINISTRATOR, + CREATOR, + IS_ADMIN, + IS_MEMBER, + IS_NOT_MEMBER, + JOIN_TRANSITION, + KICKED, + LEAVE_TRANSITION, + LEFT, + MEMBER, + PROMOTED_TRANSITION, + RESTRICTED, + ChatMemberUpdatedFilter, +) from .command import Command, CommandObject from .content_types import ContentTypesFilter from .exception import ExceptionMessageFilter, ExceptionTypeFilter @@ -19,6 +34,19 @@ __all__ = ( "ExceptionTypeFilter", "StateFilter", "MagicData", + "ChatMemberUpdatedFilter", + "CREATOR", + "ADMINISTRATOR", + "MEMBER", + "RESTRICTED", + "LEFT", + "KICKED", + "IS_MEMBER", + "IS_ADMIN", + "PROMOTED_TRANSITION", + "IS_NOT_MEMBER", + "JOIN_TRANSITION", + "LEAVE_TRANSITION", ) _ALL_EVENTS_FILTERS: Tuple[Type[BaseFilter], ...] = (MagicData,) @@ -84,10 +112,12 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { "my_chat_member": ( *_ALL_EVENTS_FILTERS, *_TELEGRAM_EVENTS_FILTERS, + ChatMemberUpdatedFilter, ), "chat_member": ( *_ALL_EVENTS_FILTERS, *_TELEGRAM_EVENTS_FILTERS, + ChatMemberUpdatedFilter, ), "chat_join_request": ( *_ALL_EVENTS_FILTERS, diff --git a/aiogram/dispatcher/filters/chat_member_updated.py b/aiogram/dispatcher/filters/chat_member_updated.py new file mode 100644 index 00000000..3f520cb4 --- /dev/null +++ b/aiogram/dispatcher/filters/chat_member_updated.py @@ -0,0 +1,179 @@ +from typing import Any, Dict, Optional, TypeVar, Union + +from aiogram.dispatcher.filters import BaseFilter +from aiogram.types import ChatMember, ChatMemberUpdated + +MarkerT = TypeVar("MarkerT", bound="_MemberStatusMarker") +MarkerGroupT = TypeVar("MarkerGroupT", bound="_MemberStatusGroupMarker") +TransitionT = TypeVar("TransitionT", bound="_MemberStatusTransition") + + +class _MemberStatusMarker: + def __init__(self, name: str, *, is_member: Optional[bool] = None) -> None: + self.name = name + self.is_member = is_member + + def __str__(self) -> str: + result = self.name.upper() + if self.is_member is not None: + result = ("+" if self.is_member else "-") + result + return result + + def __pos__(self: MarkerT) -> MarkerT: + return type(self)(name=self.name, is_member=True) + + def __neg__(self: MarkerT) -> MarkerT: + return type(self)(name=self.name, is_member=False) + + def __or__( + self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> "_MemberStatusGroupMarker": + if isinstance(other, _MemberStatusMarker): + return _MemberStatusGroupMarker(self, other) + if isinstance(other, _MemberStatusGroupMarker): + return other | self + raise TypeError( + f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + __ror__ = __or__ + + def __rshift__( + self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> "_MemberStatusTransition": + old = _MemberStatusGroupMarker(self) + if isinstance(other, _MemberStatusMarker): + return _MemberStatusTransition(old=old, new=_MemberStatusGroupMarker(other)) + if isinstance(other, _MemberStatusGroupMarker): + return _MemberStatusTransition(old=old, new=other) + raise TypeError( + f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + def __lshift__( + self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> "_MemberStatusTransition": + new = _MemberStatusGroupMarker(self) + if isinstance(other, _MemberStatusMarker): + return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=new) + if isinstance(other, _MemberStatusGroupMarker): + return _MemberStatusTransition(old=other, new=new) + raise TypeError( + f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + def __hash__(self) -> int: + return hash((self.name, self.is_member)) + + def check(self, *, member: ChatMember) -> bool: + if self.is_member is not None and member.is_member != self.is_member: + return False + return self.name == member.status + + +class _MemberStatusGroupMarker: + def __init__(self, *statuses: _MemberStatusMarker) -> None: + if not statuses: + raise ValueError("Member status group should have at least one status included") + self.statuses = frozenset(statuses) + + def __or__( + self: MarkerGroupT, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> MarkerGroupT: + if isinstance(other, _MemberStatusMarker): + return type(self)(*self.statuses, other) + elif isinstance(other, _MemberStatusGroupMarker): + return type(self)(*self.statuses, *other.statuses) + raise TypeError( + f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + def __rshift__( + self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> "_MemberStatusTransition": + if isinstance(other, _MemberStatusMarker): + return _MemberStatusTransition(old=self, new=_MemberStatusGroupMarker(other)) + if isinstance(other, _MemberStatusGroupMarker): + return _MemberStatusTransition(old=self, new=other) + raise TypeError( + f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + def __lshift__( + self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"] + ) -> "_MemberStatusTransition": + if isinstance(other, _MemberStatusMarker): + return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=self) + if isinstance(other, _MemberStatusGroupMarker): + return _MemberStatusTransition(old=other, new=self) + raise TypeError( + f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}" + ) + + def __str__(self) -> str: + result = " | ".join(map(str, sorted(self.statuses, key=str))) + if len(self.statuses) != 1: + return f"({result})" + return result + + def check(self, *, member: ChatMember) -> bool: + for status in self.statuses: + if status.check(member=member): + return True + return False + + +class _MemberStatusTransition: + def __init__(self, *, old: _MemberStatusGroupMarker, new: _MemberStatusGroupMarker) -> None: + self.old = old + self.new = new + + def __str__(self) -> str: + return f"{self.old} >> {self.new}" + + def __invert__(self: TransitionT) -> TransitionT: + return type(self)(old=self.new, new=self.old) + + def check(self, *, old: ChatMember, new: ChatMember) -> bool: + return self.old.check(member=old) and self.new.check(member=new) + + +CREATOR = _MemberStatusMarker("creator") +ADMINISTRATOR = _MemberStatusMarker("administrator") +MEMBER = _MemberStatusMarker("member") +RESTRICTED = _MemberStatusMarker("restricted") +LEFT = _MemberStatusMarker("left") +KICKED = _MemberStatusMarker("kicked") + +IS_MEMBER = CREATOR | ADMINISTRATOR | MEMBER | +RESTRICTED +IS_ADMIN = CREATOR | ADMINISTRATOR +IS_NOT_MEMBER = LEFT | KICKED | -RESTRICTED + +JOIN_TRANSITION = IS_NOT_MEMBER >> IS_MEMBER +LEAVE_TRANSITION = ~JOIN_TRANSITION +PROMOTED_TRANSITION = (MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR + + +class ChatMemberUpdatedFilter(BaseFilter): + member_status_changed: Union[ + _MemberStatusMarker, + _MemberStatusGroupMarker, + _MemberStatusTransition, + ] + """Accepts the status transition or new status of the member (see usage in docs)""" + + class Config: + arbitrary_types_allowed = True + + async def __call__(self, member_updated: ChatMemberUpdated) -> Union[bool, Dict[str, Any]]: + old = member_updated.old_chat_member + new = member_updated.new_chat_member + rule = self.member_status_changed + + if isinstance(rule, (_MemberStatusMarker, _MemberStatusGroupMarker)): + return rule.check(member=new) + if isinstance(rule, _MemberStatusTransition): + return rule.check(old=old, new=new) + + # Impossible variant in due to pydantic validation + return False # pragma: no cover diff --git a/aiogram/dispatcher/flags/__init__.py b/aiogram/dispatcher/flags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/flags/flag.py b/aiogram/dispatcher/flags/flag.py new file mode 100644 index 00000000..845b898b --- /dev/null +++ b/aiogram/dispatcher/flags/flag.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from typing import Any, Callable, Optional, Union, cast, overload + +from magic_filter import AttrDict + +from aiogram.dispatcher.flags.getter import extract_flags_from_object + + +@dataclass(frozen=True) +class Flag: + name: str + value: Any + + +@dataclass(frozen=True) +class FlagDecorator: + flag: Flag + + @classmethod + def _with_flag(cls, flag: Flag) -> "FlagDecorator": + return cls(flag) + + def _with_value(self, value: Any) -> "FlagDecorator": + new_flag = Flag(self.flag.name, value) + return self._with_flag(new_flag) + + @overload + def __call__(self, value: Callable[..., Any]) -> Callable[..., Any]: # type: ignore + pass + + @overload + def __call__(self, value: Any) -> "FlagDecorator": + pass + + @overload + def __call__(self, **kwargs: Any) -> "FlagDecorator": + pass + + def __call__( + self, + value: Optional[Any] = None, + **kwargs: Any, + ) -> Union[Callable[..., Any], "FlagDecorator"]: + if value and kwargs: + raise ValueError("The arguments `value` and **kwargs can not be used together") + + if value is not None and callable(value): + value.aiogram_flag = { + **extract_flags_from_object(value), + self.flag.name: self.flag.value, + } + return cast(Callable[..., Any], value) + return self._with_value(AttrDict(kwargs) if value is None else value) + + +class FlagGenerator: + def __getattr__(self, name: str) -> FlagDecorator: + if name[0] == "_": + raise AttributeError("Flag name must NOT start with underscore") + return FlagDecorator(Flag(name, True)) diff --git a/aiogram/dispatcher/flags/getter.py b/aiogram/dispatcher/flags/getter.py new file mode 100644 index 00000000..0c2c90dd --- /dev/null +++ b/aiogram/dispatcher/flags/getter.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast + +from magic_filter import AttrDict, MagicFilter + +if TYPE_CHECKING: + from aiogram.dispatcher.event.handler import HandlerObject + + +def extract_flags_from_object(obj: Any) -> Dict[str, Any]: + if not hasattr(obj, "aiogram_flag"): + return {} + return cast(Dict[str, Any], obj.aiogram_flag) + + +def extract_flags(handler: Union["HandlerObject", Dict[str, Any]]) -> Dict[str, Any]: + """ + Extract flags from handler or middleware context data + + :param handler: handler object or data + :return: dictionary with all handler flags + """ + if isinstance(handler, dict) and "handler" in handler: + handler = handler["handler"] + if not hasattr(handler, "flags"): + return {} + return handler.flags # type: ignore + + +def get_flag( + handler: Union["HandlerObject", Dict[str, Any]], + name: str, + *, + default: Optional[Any] = None, +) -> Any: + """ + Get flag by name + + :param handler: handler object or data + :param name: name of the flag + :param default: default value (None) + :return: value of the flag or default + """ + flags = extract_flags(handler) + return flags.get(name, default) + + +def check_flags(handler: Union["HandlerObject", Dict[str, Any]], magic: MagicFilter) -> Any: + """ + Check flags via magic filter + + :param handler: handler object or data + :param magic: instance of the magic + :return: the result of magic filter check + """ + flags = extract_flags(handler) + return magic.resolve(AttrDict(flags)) diff --git a/aiogram/dispatcher/fsm/middleware.py b/aiogram/dispatcher/fsm/middleware.py index 8d59ff67..29db32ee 100644 --- a/aiogram/dispatcher/fsm/middleware.py +++ b/aiogram/dispatcher/fsm/middleware.py @@ -2,7 +2,12 @@ from typing import Any, Awaitable, Callable, Dict, Optional, cast from aiogram import Bot from aiogram.dispatcher.fsm.context import FSMContext -from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, BaseStorage, StorageKey +from aiogram.dispatcher.fsm.storage.base import ( + DEFAULT_DESTINY, + BaseEventIsolation, + BaseStorage, + StorageKey, +) from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.types import TelegramObject @@ -12,12 +17,12 @@ class FSMContextMiddleware(BaseMiddleware): def __init__( self, storage: BaseStorage, + events_isolation: BaseEventIsolation, strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT, - isolate_events: bool = True, ) -> None: self.storage = storage self.strategy = strategy - self.isolate_events = isolate_events + self.events_isolation = events_isolation async def __call__( self, @@ -30,9 +35,8 @@ class FSMContextMiddleware(BaseMiddleware): data["fsm_storage"] = self.storage if context: data.update({"state": context, "raw_state": await context.get_state()}) - if self.isolate_events: - async with self.storage.lock(bot=bot, key=context.key): - return await handler(event, data) + async with self.events_isolation.lock(bot=bot, key=context.key): + return await handler(event, data) return await handler(event, data) def resolve_event_context( @@ -81,3 +85,7 @@ class FSMContextMiddleware(BaseMiddleware): destiny=destiny, ), ) + + async def close(self) -> None: + await self.storage.close() + await self.events_isolation.close() diff --git a/aiogram/dispatcher/fsm/storage/base.py b/aiogram/dispatcher/fsm/storage/base.py index f4830e0f..71d6ff16 100644 --- a/aiogram/dispatcher/fsm/storage/base.py +++ b/aiogram/dispatcher/fsm/storage/base.py @@ -24,19 +24,6 @@ class BaseStorage(ABC): Base class for all FSM storages """ - @abstractmethod - @asynccontextmanager - async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]: - """ - Isolate events with lock. - Will be used as context manager - - :param bot: instance of the current bot - :param key: storage key - :return: An async generator - """ - yield None - @abstractmethod async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None: """ @@ -101,3 +88,22 @@ class BaseStorage(ABC): Close storage (database connection, file or etc.) """ pass + + +class BaseEventIsolation(ABC): + @abstractmethod + @asynccontextmanager + async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]: + """ + Isolate events with lock. + Will be used as context manager + + :param bot: instance of the current bot + :param key: storage key + :return: An async generator + """ + yield None + + @abstractmethod + async def close(self) -> None: + pass diff --git a/aiogram/dispatcher/fsm/storage/memory.py b/aiogram/dispatcher/fsm/storage/memory.py index 19b43fa9..b65b5d11 100644 --- a/aiogram/dispatcher/fsm/storage/memory.py +++ b/aiogram/dispatcher/fsm/storage/memory.py @@ -2,18 +2,22 @@ from asyncio import Lock from collections import defaultdict from contextlib import asynccontextmanager from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, DefaultDict, Dict, Optional +from typing import Any, AsyncGenerator, DefaultDict, Dict, Hashable, Optional from aiogram import Bot from aiogram.dispatcher.fsm.state import State -from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType, StorageKey +from aiogram.dispatcher.fsm.storage.base import ( + BaseEventIsolation, + BaseStorage, + StateType, + StorageKey, +) @dataclass class MemoryStorageRecord: data: Dict[str, Any] = field(default_factory=dict) state: Optional[str] = None - lock: Lock = field(default_factory=Lock) class MemoryStorage(BaseStorage): @@ -34,11 +38,6 @@ class MemoryStorage(BaseStorage): async def close(self) -> None: pass - @asynccontextmanager - async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]: - async with self.storage[key].lock: - yield None - async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None: self.storage[key].state = state.state if isinstance(state, State) else state @@ -50,3 +49,27 @@ class MemoryStorage(BaseStorage): async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]: return self.storage[key].data.copy() + + +class DisabledEventIsolation(BaseEventIsolation): + @asynccontextmanager + async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]: + yield + + async def close(self) -> None: + pass + + +class SimpleEventIsolation(BaseEventIsolation): + def __init__(self) -> None: + # TODO: Unused locks cleaner is needed + self._locks: DefaultDict[Hashable, Lock] = defaultdict(Lock) + + @asynccontextmanager + async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]: + lock = self._locks[key] + async with lock: + yield + + async def close(self) -> None: + self._locks.clear() diff --git a/aiogram/dispatcher/fsm/storage/redis.py b/aiogram/dispatcher/fsm/storage/redis.py index 8828691f..5ab880de 100644 --- a/aiogram/dispatcher/fsm/storage/redis.py +++ b/aiogram/dispatcher/fsm/storage/redis.py @@ -6,7 +6,13 @@ from aioredis import ConnectionPool, Redis from aiogram import Bot from aiogram.dispatcher.fsm.state import State -from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, BaseStorage, StateType, StorageKey +from aiogram.dispatcher.fsm.storage.base import ( + DEFAULT_DESTINY, + BaseEventIsolation, + BaseStorage, + StateType, + StorageKey, +) DEFAULT_REDIS_LOCK_KWARGS = {"timeout": 60} @@ -121,19 +127,12 @@ class RedisStorage(BaseStorage): redis = Redis(connection_pool=pool) return cls(redis=redis, **kwargs) + def create_isolation(self, **kwargs: Any) -> "RedisEventIsolation": + return RedisEventIsolation(redis=self.redis, key_builder=self.key_builder, **kwargs) + async def close(self) -> None: await self.redis.close() # type: ignore - @asynccontextmanager - async def lock( - self, - bot: Bot, - key: StorageKey, - ) -> AsyncGenerator[None, None]: - redis_key = self.key_builder.build(key, "lock") - async with self.redis.lock(name=redis_key, **self.lock_kwargs): - yield None - async def set_state( self, bot: Bot, @@ -146,8 +145,8 @@ class RedisStorage(BaseStorage): else: await self.redis.set( redis_key, - state.state if isinstance(state, State) else state, # type: ignore[arg-type] - ex=self.state_ttl, # type: ignore[arg-type] + cast(str, state.state if isinstance(state, State) else state), + ex=self.state_ttl, ) async def get_state( @@ -174,7 +173,7 @@ class RedisStorage(BaseStorage): await self.redis.set( redis_key, bot.session.json_dumps(data), - ex=self.data_ttl, # type: ignore[arg-type] + ex=self.data_ttl, ) async def get_data( @@ -189,3 +188,43 @@ class RedisStorage(BaseStorage): if isinstance(value, bytes): value = value.decode("utf-8") return cast(Dict[str, Any], bot.session.json_loads(value)) + + +class RedisEventIsolation(BaseEventIsolation): + def __init__( + self, + redis: Redis, + key_builder: Optional[KeyBuilder] = None, + lock_kwargs: Optional[Dict[str, Any]] = None, + ) -> None: + if key_builder is None: + key_builder = DefaultKeyBuilder() + self.redis = redis + self.key_builder = key_builder + self.lock_kwargs = lock_kwargs or {} + + @classmethod + def from_url( + cls, + url: str, + connection_kwargs: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> "RedisEventIsolation": + if connection_kwargs is None: + connection_kwargs = {} + pool = ConnectionPool.from_url(url, **connection_kwargs) + redis = Redis(connection_pool=pool) + return cls(redis=redis, **kwargs) + + @asynccontextmanager + async def lock( + self, + bot: Bot, + key: StorageKey, + ) -> AsyncGenerator[None, None]: + redis_key = self.key_builder.build(key, "lock") + async with self.redis.lock(name=redis_key, **self.lock_kwargs): + yield None + + async def close(self) -> None: + pass diff --git a/aiogram/dispatcher/webhook/aiohttp_server.py b/aiogram/dispatcher/webhook/aiohttp_server.py index 105e0b38..a8d084f8 100644 --- a/aiogram/dispatcher/webhook/aiohttp_server.py +++ b/aiogram/dispatcher/webhook/aiohttp_server.py @@ -134,7 +134,7 @@ class BaseRequestHandler(ABC): bot=bot, update=await request.json(loads=bot.session.json_loads) ) ) - return web.json_response({}) + return web.json_response({}, dumps=bot.session.json_dumps) async def _handle_request(self, bot: Bot, request: web.Request) -> web.Response: result = await self.dispatcher.feed_webhook_update( @@ -143,8 +143,8 @@ class BaseRequestHandler(ABC): **self.data, ) if result: - return web.json_response(result) - return web.json_response({}) + return web.json_response(result, dumps=bot.session.json_dumps) + return web.json_response({}, dumps=bot.session.json_dumps) async def handle(self, request: web.Request) -> web.Response: bot = await self.resolve_bot(request) diff --git a/aiogram/loggers.py b/aiogram/loggers.py index eec44216..ae871eaf 100644 --- a/aiogram/loggers.py +++ b/aiogram/loggers.py @@ -1,5 +1,6 @@ import logging dispatcher = logging.getLogger("aiogram.dispatcher") +event = logging.getLogger("aiogram.event") middlewares = logging.getLogger("aiogram.middlewares") webhook = logging.getLogger("aiogram.webhook") diff --git a/aiogram/methods/add_sticker_to_set.py b/aiogram/methods/add_sticker_to_set.py index 43499324..7b676674 100644 --- a/aiogram/methods/add_sticker_to_set.py +++ b/aiogram/methods/add_sticker_to_set.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class AddStickerToSet(TelegramMethod[bool]): """ - Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success. + Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#addstickertoset """ @@ -27,15 +27,18 @@ class AddStickerToSet(TelegramMethod[bool]): png_sticker: Optional[Union[InputFile, str]] = None """**PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `""" tgs_sticker: Optional[InputFile] = None - """**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for technical requirements""" + """**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for technical requirements""" + webm_sticker: Optional[InputFile] = None + """**WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for technical requirements""" mask_position: Optional[MaskPosition] = None """A JSON-serialized object for position where the mask should be placed on faces""" def build_request(self, bot: Bot) -> Request: - data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"}) + data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker", "webm_sticker"}) files: Dict[str, InputFile] = {} prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker) prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker) + prepare_file(data=data, files=files, name="webm_sticker", value=self.webm_sticker) return Request(method="addStickerToSet", data=data, files=files) diff --git a/aiogram/methods/copy_message.py b/aiogram/methods/copy_message.py index 3c90baee..9a5cddae 100644 --- a/aiogram/methods/copy_message.py +++ b/aiogram/methods/copy_message.py @@ -40,6 +40,8 @@ class CopyMessage(TelegramMethod[MessageId]): """A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of *parse_mode*""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/create_new_sticker_set.py b/aiogram/methods/create_new_sticker_set.py index baa29ad8..5c807963 100644 --- a/aiogram/methods/create_new_sticker_set.py +++ b/aiogram/methods/create_new_sticker_set.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class CreateNewStickerSet(TelegramMethod[bool]): """ - Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Returns :code:`True` on success. + Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#createnewstickerset """ @@ -29,17 +29,20 @@ class CreateNewStickerSet(TelegramMethod[bool]): png_sticker: Optional[Union[InputFile, str]] = None """**PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `""" tgs_sticker: Optional[InputFile] = None - """**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for technical requirements""" + """**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for technical requirements""" + webm_sticker: Optional[InputFile] = None + """**WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for technical requirements""" contains_masks: Optional[bool] = None """Pass :code:`True`, if a set of mask stickers should be created""" mask_position: Optional[MaskPosition] = None """A JSON-serialized object for position where the mask should be placed on faces""" def build_request(self, bot: Bot) -> Request: - data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"}) + data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker", "webm_sticker"}) files: Dict[str, InputFile] = {} prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker) prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker) + prepare_file(data=data, files=files, name="webm_sticker", value=self.webm_sticker) return Request(method="createNewStickerSet", data=data, files=files) diff --git a/aiogram/methods/forward_message.py b/aiogram/methods/forward_message.py index 6ea5b233..ec4e7623 100644 --- a/aiogram/methods/forward_message.py +++ b/aiogram/methods/forward_message.py @@ -26,6 +26,8 @@ class ForwardMessage(TelegramMethod[Message]): """Message identifier in the chat specified in *from_chat_id*""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the forwarded message from forwarding and saving""" def build_request(self, bot: Bot) -> Request: data: Dict[str, Any] = self.dict() diff --git a/aiogram/methods/send_animation.py b/aiogram/methods/send_animation.py index 1f0971f6..97b483a4 100644 --- a/aiogram/methods/send_animation.py +++ b/aiogram/methods/send_animation.py @@ -47,6 +47,8 @@ class SendAnimation(TelegramMethod[Message]): """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_audio.py b/aiogram/methods/send_audio.py index 8b61e75b..16b24723 100644 --- a/aiogram/methods/send_audio.py +++ b/aiogram/methods/send_audio.py @@ -48,6 +48,8 @@ class SendAudio(TelegramMethod[Message]): """Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files » `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_contact.py b/aiogram/methods/send_contact.py index 182654d2..d0cb4ccb 100644 --- a/aiogram/methods/send_contact.py +++ b/aiogram/methods/send_contact.py @@ -36,6 +36,8 @@ class SendContact(TelegramMethod[Message]): """Additional data about the contact in the form of a `vCard `_, 0-2048 bytes""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_dice.py b/aiogram/methods/send_dice.py index be3d567c..dd844534 100644 --- a/aiogram/methods/send_dice.py +++ b/aiogram/methods/send_dice.py @@ -30,6 +30,8 @@ class SendDice(TelegramMethod[Message]): """Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '⚽', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '⚽', and values 1-64 for '🎰'. Defaults to '🎲'""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_document.py b/aiogram/methods/send_document.py index 9fff7d5e..17f38dfb 100644 --- a/aiogram/methods/send_document.py +++ b/aiogram/methods/send_document.py @@ -43,6 +43,8 @@ class SendDocument(TelegramMethod[Message]): """Disables automatic server-side content type detection for files uploaded using multipart/form-data""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_game.py b/aiogram/methods/send_game.py index d4957707..ba430845 100644 --- a/aiogram/methods/send_game.py +++ b/aiogram/methods/send_game.py @@ -24,6 +24,8 @@ class SendGame(TelegramMethod[Message]): """Short name of the game, serves as the unique identifier for the game. Set up your games via `Botfather `_.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_invoice.py b/aiogram/methods/send_invoice.py index 88a1bc5a..95c61ef6 100644 --- a/aiogram/methods/send_invoice.py +++ b/aiogram/methods/send_invoice.py @@ -64,6 +64,8 @@ class SendInvoice(TelegramMethod[Message]): """Pass :code:`True`, if the final price depends on the shipping method""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_location.py b/aiogram/methods/send_location.py index 31385884..225f23eb 100644 --- a/aiogram/methods/send_location.py +++ b/aiogram/methods/send_location.py @@ -40,6 +40,8 @@ class SendLocation(TelegramMethod[Message]): """For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_media_group.py b/aiogram/methods/send_media_group.py index 6a35e934..7d5b4502 100644 --- a/aiogram/methods/send_media_group.py +++ b/aiogram/methods/send_media_group.py @@ -31,6 +31,8 @@ class SendMediaGroup(TelegramMethod[List[Message]]): """A JSON-serialized array describing messages to be sent, must include 2-10 items""" disable_notification: Optional[bool] = None """Sends messages `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent messages from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the messages are a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_message.py b/aiogram/methods/send_message.py index bd7bfee0..d0269317 100644 --- a/aiogram/methods/send_message.py +++ b/aiogram/methods/send_message.py @@ -38,6 +38,8 @@ class SendMessage(TelegramMethod[Message]): """Disables link previews for links in this message""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_photo.py b/aiogram/methods/send_photo.py index 82eb06ac..faf9353f 100644 --- a/aiogram/methods/send_photo.py +++ b/aiogram/methods/send_photo.py @@ -39,6 +39,8 @@ class SendPhoto(TelegramMethod[Message]): """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_poll.py b/aiogram/methods/send_poll.py index 11e0eec2..85455b0d 100644 --- a/aiogram/methods/send_poll.py +++ b/aiogram/methods/send_poll.py @@ -55,6 +55,8 @@ class SendPoll(TelegramMethod[Message]): """Pass :code:`True`, if the poll needs to be immediately closed. This can be useful for poll preview.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_sticker.py b/aiogram/methods/send_sticker.py index c4435e77..573040e1 100644 --- a/aiogram/methods/send_sticker.py +++ b/aiogram/methods/send_sticker.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: class SendSticker(TelegramMethod[Message]): """ - Use this method to send static .WEBP or `animated `_ .TGS stickers. On success, the sent :class:`aiogram.types.message.Message` is returned. + Use this method to send static .WEBP, `animated `_ .TGS, or `video `_ .WEBM stickers. On success, the sent :class:`aiogram.types.message.Message` is returned. Source: https://core.telegram.org/bots/api#sendsticker """ @@ -31,6 +31,8 @@ class SendSticker(TelegramMethod[Message]): """Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_venue.py b/aiogram/methods/send_venue.py index dc62b2d0..df708ff8 100644 --- a/aiogram/methods/send_venue.py +++ b/aiogram/methods/send_venue.py @@ -44,6 +44,8 @@ class SendVenue(TelegramMethod[Message]): """Google Places type of the venue. (See `supported types `_.)""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_video.py b/aiogram/methods/send_video.py index 6867f37b..4ed09cf1 100644 --- a/aiogram/methods/send_video.py +++ b/aiogram/methods/send_video.py @@ -49,6 +49,8 @@ class SendVideo(TelegramMethod[Message]): """Pass :code:`True`, if the uploaded video is suitable for streaming""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_video_note.py b/aiogram/methods/send_video_note.py index 99e3651a..7431d582 100644 --- a/aiogram/methods/send_video_note.py +++ b/aiogram/methods/send_video_note.py @@ -37,6 +37,8 @@ class SendVideoNote(TelegramMethod[Message]): """Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files » `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/send_voice.py b/aiogram/methods/send_voice.py index c991c12f..e753e9e1 100644 --- a/aiogram/methods/send_voice.py +++ b/aiogram/methods/send_voice.py @@ -41,6 +41,8 @@ class SendVoice(TelegramMethod[Message]): """Duration of the voice message in seconds""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" + protect_content: Optional[bool] = None + """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" allow_sending_without_reply: Optional[bool] = None diff --git a/aiogram/methods/set_sticker_set_thumb.py b/aiogram/methods/set_sticker_set_thumb.py index 5ab66cd5..49ef7971 100644 --- a/aiogram/methods/set_sticker_set_thumb.py +++ b/aiogram/methods/set_sticker_set_thumb.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class SetStickerSetThumb(TelegramMethod[bool]): """ - Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns :code:`True` on success. + Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#setstickersetthumb """ @@ -23,7 +23,7 @@ class SetStickerSetThumb(TelegramMethod[bool]): user_id: int """User identifier of the sticker set owner""" thumb: Optional[Union[InputFile, str]] = None - """A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/animated_stickers#technical-requirements `_`https://core.telegram.org/animated_stickers#technical-requirements `_ for animated sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `. Animated sticker set thumbnail can't be uploaded via HTTP URL.""" + """A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#animated-sticker-requirements `_`https://core.telegram.org/stickers#animated-sticker-requirements `_ for animated sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#video-sticker-requirements `_`https://core.telegram.org/stickers#video-sticker-requirements `_ for video sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » `. Animated sticker set thumbnails can't be uploaded via HTTP URL.""" def build_request(self, bot: Bot) -> Request: data: Dict[str, Any] = self.dict(exclude={"thumb"}) diff --git a/aiogram/methods/unban_chat_member.py b/aiogram/methods/unban_chat_member.py index e9938a84..3fce083b 100644 --- a/aiogram/methods/unban_chat_member.py +++ b/aiogram/methods/unban_chat_member.py @@ -18,7 +18,7 @@ class UnbanChatMember(TelegramMethod[bool]): __returning__ = bool chat_id: Union[int, str] - """Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@username`)""" + """Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`)""" user_id: int """Unique identifier of the target user""" only_if_banned: Optional[bool] = None diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py index 844abffb..1bf65eba 100644 --- a/aiogram/types/bot_command.py +++ b/aiogram/types/bot_command.py @@ -11,6 +11,6 @@ class BotCommand(MutableTelegramObject): """ command: str - """Text of the command, 1-32 characters. Can contain only lowercase English letters, digits and underscores.""" + """Text of the command; 1-32 characters. Can contain only lowercase English letters, digits and underscores.""" description: str - """Description of the command, 3-256 characters.""" + """Description of the command; 1-256 characters.""" diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 018bebda..b3d1419c 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,7 +1,13 @@ from __future__ import annotations +import datetime +from typing import TYPE_CHECKING, Optional, Union + from .base import TelegramObject +if TYPE_CHECKING: + from .user import User + class ChatMember(TelegramObject): """ @@ -16,3 +22,48 @@ class ChatMember(TelegramObject): Source: https://core.telegram.org/bots/api#chatmember """ + + status: str + """...""" + user: Optional[User] = None + """*Optional*. Information about the user""" + is_anonymous: Optional[bool] = None + """*Optional*. :code:`True`, if the user's presence in the chat is hidden""" + custom_title: Optional[str] = None + """*Optional*. Custom title for this user""" + can_be_edited: Optional[bool] = None + """*Optional*. :code:`True`, if the bot is allowed to edit administrator privileges of that user""" + can_manage_chat: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege""" + can_delete_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can delete messages of other users""" + can_manage_voice_chats: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can manage voice chats""" + can_restrict_members: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can restrict, ban or unban chat members""" + can_promote_members: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)""" + can_change_info: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to change the chat title, photo and other settings""" + can_invite_users: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to invite new users to the chat""" + can_post_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can post in the channel; channels only""" + can_edit_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can edit messages of other users and can pin messages; channels only""" + can_pin_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to pin messages; groups and supergroups only""" + is_member: Optional[bool] = None + """*Optional*. :code:`True`, if the user is a member of the chat at the moment of the request""" + can_send_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to send text messages, contacts, locations and venues""" + can_send_media_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes""" + can_send_polls: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to send polls""" + can_send_other_messages: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to send animations, games, stickers and use inline bots""" + can_add_web_page_previews: Optional[bool] = None + """*Optional*. :code:`True`, if the user is allowed to add web page previews to their messages""" + until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None + """*Optional*. Date when restrictions will be lifted for this user; unix time. If 0, then the user is restricted forever""" diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 81ea168a..24435025 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -95,7 +95,7 @@ class Message(TelegramObject): forward_from_message_id: Optional[int] = None """*Optional*. For messages forwarded from channels, identifier of the original message in the channel""" forward_signature: Optional[str] = None - """*Optional*. For messages forwarded from channels, signature of the post author if present""" + """*Optional*. For forwarded messages that were originally sent in channels or by an anonymous chat administrator, signature of the message sender if present""" forward_sender_name: Optional[str] = None """*Optional*. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages""" forward_date: Optional[int] = None diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index ddaac506..28226286 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -18,7 +18,7 @@ class MessageEntity(MutableTelegramObject): """ type: str - """Type of the entity. Can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag`), 'cashtag' (:code:`$USD`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_)""" + """Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag`), 'cashtag' (:code:`$USD`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_)""" offset: int """Offset in UTF-16 code units to the start of the entity""" length: int diff --git a/aiogram/utils/chat_action.py b/aiogram/utils/chat_action.py new file mode 100644 index 00000000..2a9dddcc --- /dev/null +++ b/aiogram/utils/chat_action.py @@ -0,0 +1,347 @@ +import asyncio +import logging +import time +from asyncio import Event, Lock +from contextlib import suppress +from types import TracebackType +from typing import Any, Awaitable, Callable, Dict, Optional, Type, Union + +from aiogram import BaseMiddleware, Bot +from aiogram.dispatcher.flags.getter import get_flag +from aiogram.types import Message, TelegramObject + +logger = logging.getLogger(__name__) +DEFAULT_INTERVAL = 5.0 +DEFAULT_INITIAL_SLEEP = 0.1 + + +class ChatActionSender: + """ + This utility helps to automatically send chat action until long actions is done + to take acknowledge bot users the bot is doing something and not crashed. + + Provides simply to use context manager. + + Technically sender start background task with infinity loop which works + until action will be finished and sends the `chat action `_ + every 5 seconds. + """ + + def __init__( + self, + *, + chat_id: Union[str, int], + action: str = "typing", + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + bot: Optional[Bot] = None, + ) -> None: + """ + :param chat_id: target chat id + :param action: chat action type + :param interval: interval between iterations + :param initial_sleep: sleep before first iteration + :param bot: instance of the bot, can be omitted from the context + """ + if bot is None: + bot = Bot.get_current(False) + + self.chat_id = chat_id + self.action = action + self.interval = interval + self.initial_sleep = initial_sleep + self.bot = bot + + self._lock = Lock() + self._close_event = Event() + self._closed_event = Event() + self._task: Optional[asyncio.Task[Any]] = None + + @property + def running(self) -> bool: + return bool(self._task) + + async def _wait(self, interval: float) -> None: + with suppress(asyncio.TimeoutError): + await asyncio.wait_for(self._close_event.wait(), interval) + + async def _worker(self) -> None: + logger.debug( + "Started chat action %r sender in chat_id=%s via bot id=%d", + self.action, + self.chat_id, + self.bot.id, + ) + try: + counter = 0 + await self._wait(self.initial_sleep) + while not self._close_event.is_set(): + start = time.monotonic() + logger.debug( + "Sent chat action %r to chat_id=%s via bot %d (already sent actions %d)", + self.action, + self.chat_id, + self.bot.id, + counter, + ) + await self.bot.send_chat_action(chat_id=self.chat_id, action=self.action) + counter += 1 + + interval = self.interval - (time.monotonic() - start) + await self._wait(interval) + finally: + logger.debug( + "Finished chat action %r sender in chat_id=%s via bot id=%d", + self.action, + self.chat_id, + self.bot.id, + ) + self._closed_event.set() + + async def _run(self) -> None: + async with self._lock: + self._close_event.clear() + self._closed_event.clear() + if self.running: + raise RuntimeError("Already running") + self._task = asyncio.create_task(self._worker()) + + async def _stop(self) -> None: + async with self._lock: + if not self.running: + return + if not self._close_event.is_set(): + self._close_event.set() + await self._closed_event.wait() + self._task = None + + async def __aenter__(self) -> "ChatActionSender": + await self._run() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Any: + await self._stop() + + @classmethod + def typing( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `typing` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="typing", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_photo( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `upload_photo` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="upload_photo", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_video( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `record_video` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="record_video", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_video( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `upload_video` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="upload_video", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_voice( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `record_voice` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="record_voice", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_voice( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `upload_voice` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="upload_voice", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_document( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `upload_document` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="upload_document", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def choose_sticker( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `choose_sticker` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="choose_sticker", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def find_location( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `find_location` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="find_location", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_video_note( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `record_video_note` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="record_video_note", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_video_note( + cls, + chat_id: Union[int, str], + bot: Optional[Bot] = None, + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + """Create instance of the sender with `upload_video_note` action""" + return cls( + bot=bot, + chat_id=chat_id, + action="upload_video_note", + interval=interval, + initial_sleep=initial_sleep, + ) + + +class ChatActionMiddleware(BaseMiddleware): + """ + Helps to automatically use chat action sender for all message handlers + """ + + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: Dict[str, Any], + ) -> Any: + if not isinstance(event, Message): + return await handler(event, data) + bot = data["bot"] + + chat_action = get_flag(data, "chat_action") or "typing" + kwargs = {} + if isinstance(chat_action, dict): + if initial_sleep := chat_action.get("initial_sleep"): + kwargs["initial_sleep"] = initial_sleep + if interval := chat_action.get("interval"): + kwargs["interval"] = interval + if action := chat_action.get("action"): + kwargs["action"] = action + elif isinstance(chat_action, bool): + kwargs["action"] = "typing" + else: + kwargs["action"] = chat_action + async with ChatActionSender(bot=bot, chat_id=event.chat.id, **kwargs): + return await handler(event, data) diff --git a/aiogram/utils/i18n/context.py b/aiogram/utils/i18n/context.py index 13de4d9c..245fee34 100644 --- a/aiogram/utils/i18n/context.py +++ b/aiogram/utils/i18n/context.py @@ -16,7 +16,7 @@ def gettext(*args: Any, **kwargs: Any) -> str: def lazy_gettext(*args: Any, **kwargs: Any) -> LazyProxy: - return LazyProxy(gettext, *args, **kwargs) + return LazyProxy(gettext, *args, **kwargs, enable_cache=False) ngettext = gettext diff --git a/aiogram/utils/i18n/core.py b/aiogram/utils/i18n/core.py index d564fdb3..830ead54 100644 --- a/aiogram/utils/i18n/core.py +++ b/aiogram/utils/i18n/core.py @@ -118,4 +118,6 @@ class I18n(ContextInstanceMixin["I18n"]): def lazy_gettext( self, singular: str, plural: Optional[str] = None, n: int = 1, locale: Optional[str] = None ) -> LazyProxy: - return LazyProxy(self.gettext, singular=singular, plural=plural, n=n, locale=locale) + return LazyProxy( + self.gettext, singular=singular, plural=plural, n=n, locale=locale, enable_cache=False + ) diff --git a/aiogram/utils/i18n/middleware.py b/aiogram/utils/i18n/middleware.py index 2f6a73c2..dabbfae8 100644 --- a/aiogram/utils/i18n/middleware.py +++ b/aiogram/utils/i18n/middleware.py @@ -2,10 +2,14 @@ from abc import ABC, abstractmethod from typing import Any, Awaitable, Callable, Dict, Optional, Set, cast try: - from babel import Locale + from babel import Locale, UnknownLocaleError except ImportError: # pragma: no cover Locale = None + class UnknownLocaleError(Exception): # type: ignore + pass + + from aiogram import BaseMiddleware, Router from aiogram.dispatcher.fsm.context import FSMContext from aiogram.types import TelegramObject, User @@ -116,7 +120,11 @@ class SimpleI18nMiddleware(I18nMiddleware): event_from_user: Optional[User] = data.get("event_from_user", None) if event_from_user is None: return self.i18n.default_locale - locale = Locale.parse(event_from_user.language_code, sep="-") + try: + locale = Locale.parse(event_from_user.language_code, sep="-") + except UnknownLocaleError: + return self.i18n.default_locale + if locale.language not in self.i18n.available_locales: return self.i18n.default_locale return cast(str, locale.language) diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 80f5afe9..86b3ed84 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -1,7 +1,7 @@ from __future__ import annotations import contextvars -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generic, Optional, TypeVar, cast, overload +from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, TypeVar, cast, overload if TYPE_CHECKING: from typing_extensions import Literal @@ -38,7 +38,7 @@ ContextInstance = TypeVar("ContextInstance") class ContextInstanceMixin(Generic[ContextInstance]): - __context_instance: ClassVar[contextvars.ContextVar[ContextInstance]] + __context_instance: contextvars.ContextVar[ContextInstance] def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__() diff --git a/docs/dispatcher/filters/chat_member_updated.rst b/docs/dispatcher/filters/chat_member_updated.rst new file mode 100644 index 00000000..d02313f4 --- /dev/null +++ b/docs/dispatcher/filters/chat_member_updated.rst @@ -0,0 +1,103 @@ +================= +ChatMemberUpdated +================= + +.. autoclass:: aiogram.dispatcher.filters.chat_member_updated.ChatMemberUpdatedFilter + :members: + :member-order: bysource + :undoc-members: False + +You can import from :code:`aiogram.dispatcher.filters` all available +variants of `statuses`_, `status groups`_ or `transitions`_: + +Statuses +======== + ++-------------------------+--------------------------------------+ +| name | Description | ++=========================+======================================+ +| :code:`CREATOR` | Chat owner | ++-------------------------+--------------------------------------+ +| :code:`ADMINISTRATOR` | Chat administrator | ++-------------------------+--------------------------------------+ +| :code:`MEMBER` | Member of the chat | ++-------------------------+--------------------------------------+ +| :code:`RESTRICTED` | Restricted user (can be not member) | ++-------------------------+--------------------------------------+ +| :code:`LEFT` | Isn't member of the chat | ++-------------------------+--------------------------------------+ +| :code:`KICKED` | Kicked member by administrators | ++-------------------------+--------------------------------------+ + +Statuses can be extended with `is_member` flag by prefixing with +:code:`+` (for :code:`is_member == True)` or :code:`-` (for :code:`is_member == False`) symbol, +like :code:`+RESTRICTED` or :code:`-RESTRICTED` + +Status groups +============= + +The particular statuses can be combined via bitwise :code:`or` operator, like :code:`CREATOR | ADMINISTRATOR` + ++-------------------------+-----------------------------------------------------------------------------------+ +| name | Description | ++=========================+===================================================================================+ +| :code:`IS_MEMBER` | Combination of :code:`(CREATOR | ADMINISTRATOR | MEMBER | +RESTRICTED)` statuses. | ++-------------------------+-----------------------------------------------------------------------------------+ +| :code:`IS_ADMIN` | Combination of :code:`(CREATOR | ADMINISTRATOR)` statuses. | ++-------------------------+-----------------------------------------------------------------------------------+ +| :code:`IS_NOT_MEMBER` | Combination of :code:`(LEFT | KICKED | -RESTRICTED)` statuses. | ++-------------------------+-----------------------------------------------------------------------------------+ + +Transitions +=========== + +Transitions can be defined via bitwise shift operators :code:`>>` and :code:`<<`. +Old chat member status should be defined in the left side for :code:`>>` operator (right side for :code:`<<`) +and new status should be specified on the right side for :code:`>>` operator (left side for :code:`<<`) + +The direction of transition can be changed via bitwise inversion operator: :code:`~JOIN_TRANSITION` +will produce swap of old and new statuses. + ++-----------------------------+-----------------------------------------------------------------------+ +| name | Description | ++=============================+=======================================================================+ +| :code:`JOIN_TRANSITION` | Means status changed from :code:`IS_NOT_MEMBER` to :code:`IS_MEMBER` | +| | (:code:`IS_NOT_MEMBER >> IS_MEMBER`) | ++-----------------------------+-----------------------------------------------------------------------+ +| :code:`LEAVE_TRANSITION` | Means status changed from :code:`IS_MEMBER` to :code:`IS_NOT_MEMBER` | +| | (:code:`~JOIN_TRANSITION`) | ++-----------------------------+-----------------------------------------------------------------------+ +| :code:`PROMOTED_TRANSITION` | Means status changed from | +| | :code:`(MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR` | +| | (:code:`(MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR`) | ++-----------------------------+-----------------------------------------------------------------------+ + +.. note:: + + Note that if you define the status unions (via :code:`|`) you will need to add brackets for the statement + before use shift operator in due to operator priorities. + +Usage +===== + +Handle user leave or join events + +.. code-block:: python + + from aiogram.dispatcher.filters import IS_MEMBER, IS_NOT_MEMBER + + @router.chat_member(chat_member_updated=IS_MEMBER >> IS_NOT_MEMBER) + async def on_user_leave(event: ChatMemberUpdated): ... + + @router.chat_member(chat_member_updated=IS_NOT_MEMBER >> IS_MEMBER) + async def on_user_join(event: ChatMemberUpdated): ... + +Or construct your own terms via using pre-defined set of statuses and transitions. + +Allowed handlers +================ + +Allowed update types for this filter: + +- `my_chat_member` +- `chat_member` diff --git a/docs/dispatcher/filters/index.rst b/docs/dispatcher/filters/index.rst index a94636e6..106e0b0c 100644 --- a/docs/dispatcher/filters/index.rst +++ b/docs/dispatcher/filters/index.rst @@ -18,6 +18,7 @@ Here is list of builtin filters: command content_types text + chat_member_updated exception magic_filters magic_data @@ -83,7 +84,7 @@ Bound Filters with only default arguments will be automatically applied with def to each handler in the router and nested routers to which this filter is bound. For example, although we do not specify :code:`chat_type` in the handler filters, -but since the filter has a default value, the filter will be applied to the handler +but since the filter has a default value, the filter will be applied to the handler with a default value :code:`private`: .. code-block:: python diff --git a/docs/dispatcher/flags.rst b/docs/dispatcher/flags.rst new file mode 100644 index 00000000..f4e5e118 --- /dev/null +++ b/docs/dispatcher/flags.rst @@ -0,0 +1,89 @@ +===== +Flags +===== + +Flags is a markers for handlers that can be used in `middlewares <#use-in-middlewares>`_ +or special `utilities <#use-in-utilities>`_ to make classification of the handlers. + +Flags can be added to the handler via `decorators <#via-decorators>`_, +`handlers registration <#via-handler-registration-method>`_ or +`filters `_. + +Via decorators +============== + +For example mark handler with `chat_action` flag + +.. code-block:: python + + from aiogram import flags + + @flags.chat_action + async def my_handler(message: Message) + +Or just for rate-limit or something else + +.. code-block:: python + + from aiogram import flags + + @flags.rate_limit(rate=2, key="something") + async def my_handler(message: Message) + +Via handler registration method +=============================== + +.. code-block:: python + + @router.message(..., flags={'chat_action': 'typing', 'rate_limit': {'rate': 5}}) + +Via filters +=========== + +.. code-block:: python + + class Command(BaseFilter): + ... + + def update_handler_flags(self, flags: Dict[str, Any]) -> None: + commands = flags.setdefault("commands", []) + commands.append(self) + + + +Use in middlewares +================== + +.. automodule:: aiogram.dispatcher.flags.getter + :members: + +Example in middlewares +---------------------- + +.. code-block:: python + + async def my_middleware(handler, event, data): + typing = get_flag(data, "typing") # Check that handler marked with `typing` flag + if not typing: + return await handler(event, data) + + async with ChatActionSender.typing(chat_id=event.chat.id): + return await handler(event, data) + +Use in utilities +================ + +For example you can collect all registered commands with handler description and then it can be used for generating commands help + +.. code-block:: python + + def collect_commands(router: Router) -> Generator[Tuple[Command, str], None, None]: + for handler in router.message.handlers: + if "commands" not in handler.flags: # ignore all handler without commands + continue + # the Command filter adds the flag with list of commands attached to the handler + for command in handler.flags["commands"]: + yield command, handler.callback.__doc__ or "" + # Recursively extract commands from nested routers + for sub_router in router.sub_routers: + yield from collect_commands(sub_router) diff --git a/docs/dispatcher/index.rst b/docs/dispatcher/index.rst index 935522b2..c684ba36 100644 --- a/docs/dispatcher/index.rst +++ b/docs/dispatcher/index.rst @@ -24,3 +24,4 @@ Dispatcher is subclass of router and should be always is root router. filters/index middlewares finite_state_machine/index + flags diff --git a/docs/utils/chat_action.rst b/docs/utils/chat_action.rst new file mode 100644 index 00000000..4a911bfd --- /dev/null +++ b/docs/utils/chat_action.rst @@ -0,0 +1,56 @@ +================== +Chat action sender +================== + +Sender +====== + +.. autoclass:: aiogram.utils.chat_action.ChatActionSender + :members: __init__,running,typing,upload_photo,record_video,upload_video,record_voice,upload_voice,upload_document,choose_sticker,find_location,record_video_note,upload_video_note + +Usage +----- + +.. code-block:: python + + async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id): + # Do something... + # Perform some long calculations + await message.answer(result) + + +Middleware +========== + +.. autoclass:: aiogram.utils.chat_action.ChatActionMiddleware + + +Usage +----- + +Before usa should be registered for the `message` event + +.. code-block:: python + + .message.middleware(ChatActionMiddleware()) + +After this action all handlers which works longer than `initial_sleep` will produce the '`typing`' chat action. + +Also sender can be customized via flags feature for particular handler. + +Change only action type: + +.. code-block:: python + + @router.message(...) + @flags.chat_action("sticker") + async def my_handler(message: Message): ... + + +Change sender configuration: + +.. code-block:: python + + @router.message(...) + @flags.chat_action(initial_sleep=2, action="upload_document", interval=3) + async def my_handler(message: Message): ... diff --git a/docs/utils/index.rst b/docs/utils/index.rst index 424ac0d9..9390e8ac 100644 --- a/docs/utils/index.rst +++ b/docs/utils/index.rst @@ -6,3 +6,4 @@ Utils i18n keyboard + chat_action diff --git a/poetry.lock b/poetry.lock index 3ed1df7c..22656370 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "aiofiles" -version = "0.7.0" +version = "0.8.0" description = "File support for asyncio." category = "main" optional = false @@ -8,7 +8,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "aiohttp" -version = "3.8.0" +version = "3.8.1" description = "Async http client/server framework (asyncio)" category = "main" optional = false @@ -28,7 +28,7 @@ speedups = ["aiodns", "brotli", "cchardet"] [[package]] name = "aiohttp-socks" -version = "0.5.5" +version = "0.7.1" description = "Proxy connector for aiohttp" category = "main" optional = true @@ -37,11 +37,11 @@ python-versions = "*" [package.dependencies] aiohttp = ">=2.3.2" attrs = ">=19.2.0" -python-socks = {version = ">=1.0.1", extras = ["asyncio"]} +python-socks = {version = ">=2.0.0,<3.0.0", extras = ["asyncio"]} [[package]] name = "aioredis" -version = "2.0.0" +version = "2.0.1" description = "asyncio (PEP 3156) Redis support" category = "main" optional = true @@ -83,7 +83,7 @@ python-versions = "*" [[package]] name = "aresponses" -version = "2.1.4" +version = "2.1.5" description = "Asyncio response mocking. Similar to the responses library used for 'requests'" category = "dev" optional = false @@ -93,6 +93,20 @@ python-versions = ">=3.6" aiohttp = ">=3.1.0,<4.0.0" pytest-asyncio = "*" +[[package]] +name = "asttokens" +version = "2.0.5" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + [[package]] name = "async-timeout" version = "4.0.0" @@ -182,29 +196,24 @@ lxml = ["lxml"] [[package]] name = "black" -version = "21.10b0" +version = "22.1.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -regex = ">=2020.1.8" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +tomli = ">=1.1.0" +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -212,7 +221,7 @@ name = "certifi" version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" -optional = true +optional = false python-versions = "*" [[package]] @@ -266,11 +275,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.1.1" +version = "6.3.1" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] @@ -299,6 +311,14 @@ category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "executing" +version = "0.8.2" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "filelock" version = "3.3.2" @@ -313,16 +333,16 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co [[package]] name = "flake8" -version = "3.9.2" +version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "flake8-html" @@ -348,7 +368,7 @@ python-versions = ">=3.6" [[package]] name = "furo" -version = "2021.10.9" +version = "2022.2.14.1" description = "A clean customisable Sphinx documentation theme." category = "main" optional = true @@ -356,11 +376,12 @@ python-versions = ">=3.6" [package.dependencies] beautifulsoup4 = "*" +pygments = ">=2.7,<3.0" sphinx = ">=4.0,<5.0" [package.extras] -doc = ["myst-parser", "sphinx-copybutton", "sphinx-design", "sphinx-inline-tabs"] test = ["pytest", "pytest-cov", "pytest-xdist"] +doc = ["myst-parser", "sphinx-copybutton", "sphinx-design", "sphinx-inline-tabs"] [[package]] name = "identify" @@ -426,15 +447,16 @@ python-versions = "*" [[package]] name = "ipython" -version = "7.29.0" +version = "8.0.1" description = "IPython: Productive Interactive Computing" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] appnope = {version = "*", markers = "sys_platform == \"darwin\""} backcall = "*" +black = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" jedi = ">=0.16" @@ -443,10 +465,11 @@ pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} pickleshare = "*" prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" pygments = "*" -traitlets = ">=4.2" +stack-data = "*" +traitlets = ">=5" [package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] +all = ["Sphinx (>=1.3)", "curio", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.19)", "pandas", "pygments", "pytest", "pytest-asyncio", "qtconsole", "testpath", "trio"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -454,11 +477,12 @@ nbformat = ["nbformat"] notebook = ["notebook", "ipywidgets"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] +test = ["pytest", "pytest-asyncio", "testpath", "pygments"] +test_extra = ["pytest", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pygments", "trio"] [[package]] name = "isort" -version = "5.10.0" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -513,7 +537,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] name = "magic-filter" -version = "1.0.4" +version = "1.0.5" description = "This package provides magic filter based on dynamic attribute getter" category = "main" optional = false @@ -578,20 +602,20 @@ python-versions = ">=3.6" [[package]] name = "mypy" -version = "0.910" +version = "0.931" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typing-extensions = ">=3.7.4" +mypy-extensions = ">=0.4.3" +tomli = ">=1.1.0" +typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] [[package]] name = "mypy-extensions" @@ -611,14 +635,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.9" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "parso" @@ -685,7 +709,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.15.0" +version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -718,6 +742,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +tests = ["pytest"] + [[package]] name = "py" version = "1.11.0" @@ -728,15 +763,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.8.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pydantic" -version = "1.8.2" +version = "1.9.0" description = "Data validation and settings management using python 3.6 type hinting" category = "main" optional = false @@ -751,7 +786,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "2.3.1" +version = "2.4.0" description = "passive checker of Python programs" category = "dev" optional = false @@ -759,7 +794,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.10.0" +version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -767,18 +802,18 @@ python-versions = ">=3.5" [[package]] name = "pymdown-extensions" -version = "8.2" +version = "9.2" description = "Extension pack for Python Markdown." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] Markdown = ">=3.2" [[package]] name = "pyparsing" -version = "3.0.5" +version = "3.0.7" description = "Python parsing module" category = "main" optional = false @@ -789,7 +824,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.0.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -803,49 +838,52 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-aiohttp" -version = "0.3.0" -description = "pytest plugin for aiohttp support" +version = "1.0.4" +description = "Pytest plugin for aiohttp support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] -aiohttp = ">=2.3.5" -pytest = "*" +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] [[package]] name = "pytest-asyncio" -version = "0.15.1" -description = "Pytest support for asyncio." +version = "0.18.1" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] [[package]] name = "pytest-cov" -version = "2.12.1" +version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -coverage = ">=5.2.1" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" -toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] @@ -886,11 +924,11 @@ pytest = ">=2.9.0" [[package]] name = "pytest-mock" -version = "3.6.1" +version = "3.7.0" description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] pytest = ">=5.0" @@ -900,7 +938,7 @@ dev = ["pre-commit", "tox", "pytest-asyncio"] [[package]] name = "pytest-mypy" -version = "0.8.1" +version = "0.9.1" description = "Mypy static type checker plugin for Pytest" category = "dev" optional = false @@ -913,11 +951,14 @@ mypy = [ {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""}, {version = ">=0.780", markers = "python_version >= \"3.9\""}, ] -pytest = ">=3.5" +pytest = [ + {version = ">=6.2", markers = "python_version >= \"3.10\""}, + {version = ">=4.6", markers = "python_version >= \"3.6\" and python_version < \"3.10\""}, +] [[package]] name = "python-socks" -version = "2.0.0" +version = "2.0.3" description = "Core proxy (SOCKS4, SOCKS5, HTTP tunneling) functionality for Python" category = "main" optional = true @@ -948,14 +989,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "regex" -version = "2021.11.2" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "requests" version = "2.26.0" @@ -974,6 +1007,36 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "sentry-sdk" +version = "1.5.5" +description = "Python client for Sentry (https://sentry.io)" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.10.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +flask = ["flask (>=0.11)", "blinker (>=1.1)"] +httpx = ["httpx (>=0.16.0)"] +pure_eval = ["pure-eval", "executing", "asttokens"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +tornado = ["tornado (>=5)"] + [[package]] name = "six" version = "1.16.0" @@ -1047,7 +1110,7 @@ test = ["pytest", "pytest-cov"] [[package]] name = "sphinx-copybutton" -version = "0.4.0" +version = "0.5.0" description = "Add a copy button to each of your code cells." category = "main" optional = true @@ -1058,7 +1121,7 @@ sphinx = ">=1.8" [package.extras] code_style = ["pre-commit (==2.12.1)"] -rtd = ["sphinx", "ipython", "sphinx-book-theme"] +rtd = ["sphinx", "ipython", "myst-nb", "sphinx-book-theme"] [[package]] name = "sphinx-intl" @@ -1176,11 +1239,27 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "stack-data" +version = "0.2.0" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +asttokens = "*" +executing = "*" +pure-eval = "*" + +[package.extras] +tests = ["pytest", "typeguard", "pygments", "littleutils", "cython"] + [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -1188,7 +1267,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "1.2.2" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1202,7 +1281,7 @@ python-versions = ">= 3.5" [[package]] name = "towncrier" -version = "21.3.0" +version = "21.9.0" description = "Building newsfiles for your project." category = "main" optional = true @@ -1213,7 +1292,7 @@ click = "*" click-default-group = "*" incremental = "*" jinja2 = "*" -toml = "*" +tomli = {version = "*", markers = "python_version >= \"3.6\""} [package.extras] dev = ["packaging"] @@ -1231,18 +1310,18 @@ test = ["pytest"] [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] @@ -1324,94 +1403,94 @@ redis = ["aioredis"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "27d602728b6dab256d184fbd44953030676edf320652ad828c6e1b4beaa80f8b" +content-hash = "1f20a68c7d58bc8113a73a2cef1cf9e0027fc0a69507e3ac0a81d158e33b3acc" [metadata.files] aiofiles = [ - {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, - {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, + {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, + {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, ] aiohttp = [ - {file = "aiohttp-3.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6"}, - {file = "aiohttp-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe"}, - {file = "aiohttp-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990"}, - {file = "aiohttp-3.8.0-cp310-cp310-win32.whl", hash = "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8"}, - {file = "aiohttp-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847"}, - {file = "aiohttp-3.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934"}, - {file = "aiohttp-3.8.0-cp36-cp36m-win32.whl", hash = "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d"}, - {file = "aiohttp-3.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321"}, - {file = "aiohttp-3.8.0-cp37-cp37m-win32.whl", hash = "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f"}, - {file = "aiohttp-3.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661"}, - {file = "aiohttp-3.8.0-cp38-cp38-win32.whl", hash = "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722"}, - {file = "aiohttp-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e"}, - {file = "aiohttp-3.8.0-cp39-cp39-win32.whl", hash = "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3"}, - {file = "aiohttp-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919"}, - {file = "aiohttp-3.8.0.tar.gz", hash = "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] aiohttp-socks = [ - {file = "aiohttp_socks-0.5.5-py3-none-any.whl", hash = "sha256:faaa25ed4dc34440ca888d23e089420f3b1918dc4ecf062c3fd9474827ad6a39"}, - {file = "aiohttp_socks-0.5.5.tar.gz", hash = "sha256:2eb2059756bde34c55bb429541cbf2eba3fd53e36ac80875b461221e2858b04a"}, + {file = "aiohttp_socks-0.7.1-py3-none-any.whl", hash = "sha256:94bcff5ef73611c6c6231c2ffc1be4af1599abec90dbd2fdbbd63233ec2fb0ff"}, + {file = "aiohttp_socks-0.7.1.tar.gz", hash = "sha256:2215cac4891ef3fa14b7d600ed343ed0f0a670c23b10e4142aa862b3db20341a"}, ] aioredis = [ - {file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"}, - {file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"}, + {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, + {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, ] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, @@ -1426,8 +1505,12 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] aresponses = [ - {file = "aresponses-2.1.4-py3-none-any.whl", hash = "sha256:2a5a100c9b39e559bf55c26cc837a8ce64ab160ee086afa01ee9c4ef07f245db"}, - {file = "aresponses-2.1.4.tar.gz", hash = "sha256:39674af90700f1bfe2c7c9049cd8116f5c10d34d2e2427fd744b88d9e8644c94"}, + {file = "aresponses-2.1.5-py3-none-any.whl", hash = "sha256:06161209a39880aaf8ec3c67ab76d677aaea41944672e6a3e23a4464544879b1"}, + {file = "aresponses-2.1.5.tar.gz", hash = "sha256:16535e5d24302eab194e15edd18b9e126e1fb70cd84049e63eb6b15c89e16936"}, +] +asttokens = [ + {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, + {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, ] async-timeout = [ {file = "async-timeout-4.0.0.tar.gz", hash = "sha256:7d87a4e8adba8ededb52e579ce6bc8276985888913620c935094c2276fd83382"}, @@ -1462,8 +1545,29 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] black = [ - {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, - {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, + {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, + {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, + {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, + {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, + {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, + {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, + {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, + {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, + {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, + {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, + {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, + {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, + {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, + {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, + {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, + {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, + {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -1489,52 +1593,47 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7"}, - {file = "coverage-6.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a"}, - {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef"}, - {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336"}, - {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6"}, - {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26"}, - {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9"}, - {file = "coverage-6.1.1-cp310-cp310-win32.whl", hash = "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026"}, - {file = "coverage-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9"}, - {file = "coverage-6.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d"}, - {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e"}, - {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8"}, - {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67"}, - {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f"}, - {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a"}, - {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489"}, - {file = "coverage-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003"}, - {file = "coverage-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b"}, - {file = "coverage-6.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717"}, - {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533"}, - {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33"}, - {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271"}, - {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2"}, - {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce"}, - {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b"}, - {file = "coverage-6.1.1-cp37-cp37m-win32.whl", hash = "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685"}, - {file = "coverage-6.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926"}, - {file = "coverage-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8"}, - {file = "coverage-6.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e"}, - {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a"}, - {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271"}, - {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9"}, - {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4"}, - {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad"}, - {file = "coverage-6.1.1-cp38-cp38-win32.whl", hash = "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b"}, - {file = "coverage-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54"}, - {file = "coverage-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201"}, - {file = "coverage-6.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6"}, - {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9"}, - {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a"}, - {file = "coverage-6.1.1-cp39-cp39-win32.whl", hash = "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0"}, - {file = "coverage-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b"}, - {file = "coverage-6.1.1-pp36-none-any.whl", hash = "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f"}, - {file = "coverage-6.1.1-pp37-none-any.whl", hash = "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07"}, - {file = "coverage-6.1.1-pp38-none-any.whl", hash = "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5"}, - {file = "coverage-6.1.1.tar.gz", hash = "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, + {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, + {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, + {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, + {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, + {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, + {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, + {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, + {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, + {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, + {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, + {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] decorator = [ {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, @@ -1548,13 +1647,17 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] +executing = [ + {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"}, + {file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"}, +] filelock = [ {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, ] flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] flake8-html = [ {file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"}, @@ -1635,8 +1738,8 @@ frozenlist = [ {file = "frozenlist-1.2.0.tar.gz", hash = "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de"}, ] furo = [ - {file = "furo-2021.10.9-py3-none-any.whl", hash = "sha256:8da4448836e72db59033cd8d70dbdae96b0523c4424d806216febd38253870e8"}, - {file = "furo-2021.10.9.tar.gz", hash = "sha256:2baa42a22ede3e6e95c6182ab364bac2ec15b79bc7f9ca7fb09fd9aec4b3540c"}, + {file = "furo-2022.2.14.1-py3-none-any.whl", hash = "sha256:d7cb8126034637212332350ec8490cb95732d36506b024318a58cee2e7de0fda"}, + {file = "furo-2022.2.14.1.tar.gz", hash = "sha256:1af3a3053e594666e27eefd347b84beae5d74d6d20f6294cc47777d46f5761a7"}, ] identify = [ {file = "identify-2.3.4-py2.py3-none-any.whl", hash = "sha256:4de55a93e0ba72bf917c840b3794eb1055a67272a1732351c557c88ec42011b1"}, @@ -1663,12 +1766,12 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] ipython = [ - {file = "ipython-7.29.0-py3-none-any.whl", hash = "sha256:a658beaf856ce46bc453366d5dc6b2ddc6c481efd3540cb28aa3943819caac9f"}, - {file = "ipython-7.29.0.tar.gz", hash = "sha256:4f69d7423a5a1972f6347ff233e38bbf4df6a150ef20fbb00c635442ac3060aa"}, + {file = "ipython-8.0.1-py3-none-any.whl", hash = "sha256:c503a0dd6ccac9c8c260b211f2dd4479c042b49636b097cc9a0d55fe62dff64c"}, + {file = "ipython-8.0.1.tar.gz", hash = "sha256:ab564d4521ea8ceaac26c3a2c6e5ddbca15c8848fd5a5cc325f960da88d42974"}, ] isort = [ - {file = "isort-5.10.0-py3-none-any.whl", hash = "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f"}, - {file = "isort-5.10.0.tar.gz", hash = "sha256:e52ff6d38012b131628cf0f26c51e7bd3a7c81592eefe3ac71411e692f1b9345"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, @@ -1682,8 +1785,8 @@ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] magic-filter = [ - {file = "magic-filter-1.0.4.tar.gz", hash = "sha256:2b77de98bfef16a990c22b2a68a904b4a99913b656d48cf877367da8ae5f5c84"}, - {file = "magic_filter-1.0.4-py3-none-any.whl", hash = "sha256:daf869025b9490c4438f1c2d3f4f9bcb8295fa0ffa7fec643bc20486f519f349"}, + {file = "magic-filter-1.0.5.tar.gz", hash = "sha256:974cf2793bb02a770f202d3179abfb600d1917f4e0c2af1727ef0edbb90cd0c2"}, + {file = "magic_filter-1.0.5-py3-none-any.whl", hash = "sha256:fa0c5f94da30d6cae1f0cec34fa526056db9f2636c099527513d529cb0299787"}, ] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, @@ -1846,29 +1949,26 @@ multidict = [ {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, ] mypy = [ - {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, - {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, - {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, - {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, - {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, - {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, - {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, - {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, - {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, - {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, - {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, - {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, - {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, - {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, - {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, - {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, - {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, - {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, - {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, - {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, - {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, - {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, - {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, + {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, + {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, + {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, + {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, + {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, + {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, + {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, + {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, + {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, + {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, + {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, + {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, + {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, + {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, + {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, + {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, + {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, + {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, + {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, + {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1879,8 +1979,8 @@ nodeenv = [ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, @@ -1907,8 +2007,8 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, - {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, ] prompt-toolkit = [ {file = "prompt_toolkit-3.0.22-py3-none-any.whl", hash = "sha256:48d85cdca8b6c4f16480c7ce03fd193666b62b0a21667ca56b4bb5ad679d1170"}, @@ -1918,69 +2018,86 @@ ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +pure-eval = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pydantic = [ - {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, - {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, - {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, - {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, - {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, - {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, - {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, - {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, - {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, - {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, - {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, - {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, - {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, - {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, ] pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pygments = [ - {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, - {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"}, - {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"}, + {file = "pymdown-extensions-9.2.tar.gz", hash = "sha256:ed8f69a18bc158f00cbf03abc536b88b6e541b7e699156501e767c48f81d8850"}, + {file = "pymdown_extensions-9.2-py3-none-any.whl", hash = "sha256:f2fa7d9317c672a419868c893c20a28fb7ed7fc60d4ec4774c35e01398ab330c"}, ] pyparsing = [ - {file = "pyparsing-3.0.5-py3-none-any.whl", hash = "sha256:4881e3d2979f27b41a3a2421b10be9cbfa7ce2baa6c7117952222f8bbea6650c"}, - {file = "pyparsing-3.0.5.tar.gz", hash = "sha256:9329d1c1b51f0f76371c4ded42c5ec4cc0be18456b22193e0570c2da98ed288b"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, ] pytest-aiohttp = [ - {file = "pytest-aiohttp-0.3.0.tar.gz", hash = "sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f"}, - {file = "pytest_aiohttp-0.3.0-py3-none-any.whl", hash = "sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d"}, + {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, + {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, - {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, + {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, + {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, ] pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] pytest-html = [ {file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"}, @@ -1995,16 +2112,16 @@ pytest-metadata = [ {file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"}, ] pytest-mock = [ - {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, - {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, ] pytest-mypy = [ - {file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"}, - {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"}, + {file = "pytest-mypy-0.9.1.tar.gz", hash = "sha256:9ffa3bf405c12c5c6be9e92e22bebb6ab2c91b9c32f45b0f0c93af473269ab5c"}, + {file = "pytest_mypy-0.9.1-py3-none-any.whl", hash = "sha256:a2505fcf61f1c0c51f950d4623ea8ca2daf6fb2101a5603554bad2e130202083"}, ] python-socks = [ - {file = "python-socks-2.0.0.tar.gz", hash = "sha256:7944dad882846ac73e5f79e180c841e3895ee058e16855b7e8fff24f4cd0b90b"}, - {file = "python_socks-2.0.0-py3-none-any.whl", hash = "sha256:aac65671cbd3b0eb55b20f8558c8de3894a315536aaab3ec0a7b9d46ff89c1bf"}, + {file = "python-socks-2.0.3.tar.gz", hash = "sha256:e3a9ca8e554733862ce4d8ce1d10efb480fd3a3acdafd03393943ec00c98ba8a"}, + {file = "python_socks-2.0.3-py3-none-any.whl", hash = "sha256:950723f27d2cf401e193a9e0a0d45baab848341298f5b397d27fda0c4635e9a9"}, ] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, @@ -2045,61 +2162,14 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -regex = [ - {file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"}, - {file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"}, - {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"}, - {file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"}, - {file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"}, - {file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"}, - {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"}, - {file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"}, - {file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"}, - {file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"}, - {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"}, - {file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"}, - {file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"}, - {file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"}, - {file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"}, - {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"}, - {file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"}, - {file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"}, - {file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"}, - {file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"}, - {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"}, - {file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"}, - {file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"}, - {file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"}, -] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +sentry-sdk = [ + {file = "sentry-sdk-1.5.5.tar.gz", hash = "sha256:98fd155fa5d5fec1dbabed32a1a4ae2705f1edaa5dae4e7f7b62a384ba30e759"}, + {file = "sentry_sdk-1.5.5-py2.py3-none-any.whl", hash = "sha256:3817274fba2498c8ebf6b896ee98ac916c5598706340573268c07bf2bb30d831"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -2121,8 +2191,8 @@ sphinx-autobuild = [ {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, ] sphinx-copybutton = [ - {file = "sphinx-copybutton-0.4.0.tar.gz", hash = "sha256:8daed13a87afd5013c3a9af3575cc4d5bec052075ccd3db243f895c07a689386"}, - {file = "sphinx_copybutton-0.4.0-py3-none-any.whl", hash = "sha256:4340d33c169dac6dd82dce2c83333412aa786a42dd01a81a8decac3b130dc8b0"}, + {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, + {file = "sphinx_copybutton-0.5.0-py3-none-any.whl", hash = "sha256:9684dec7434bd73f0eea58dda93f9bb879d24bff2d8b187b1f2ec08dfe7b5f48"}, ] sphinx-intl = [ {file = "sphinx-intl-2.0.1.tar.gz", hash = "sha256:b25a6ec169347909e8d983eefe2d8adecb3edc2f27760db79b965c69950638b4"}, @@ -2159,6 +2229,10 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +stack-data = [ + {file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"}, + {file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -2211,17 +2285,16 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] towncrier = [ - {file = "towncrier-21.3.0-py2.py3-none-any.whl", hash = "sha256:e6ccec65418bbcb8de5c908003e130e37fe0e9d6396cb77c1338241071edc082"}, - {file = "towncrier-21.3.0.tar.gz", hash = "sha256:6eed0bc924d72c98c000cb8a64de3bd566e5cb0d11032b73fcccf8a8f956ddfe"}, + {file = "towncrier-21.9.0-py2.py3-none-any.whl", hash = "sha256:fc5a88a2a54988e3a8ed2b60d553599da8330f65722cc607c839614ed87e0f92"}, + {file = "towncrier-21.9.0.tar.gz", hash = "sha256:9cb6f45c16e1a1eec9d0e7651165e7be60cd0ab81d13a5c96ca97a498ae87f48"}, ] traitlets = [ {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, diff --git a/pyproject.toml b/pyproject.toml index 297f02a4..160f5380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aiogram" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" description = "Modern and fully asynchronous framework for Telegram Bot API" authors = [ "Alex Root Junior ", @@ -37,53 +37,55 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8" -magic-filter = "^1.0.4" -aiohttp = "^3.8.0" -pydantic = "^1.8.2" -aiofiles = "^0.7.0" +magic-filter = "^1.0.5" +aiohttp = "^3.8.1" +pydantic = "^1.9.0" +aiofiles = "^0.8.0" # Fast uvloop = { version = "^0.16.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true } # i18n Babel = { version = "^2.9.1", optional = true } # Proxy -aiohttp-socks = { version = "^0.5.5", optional = true } +aiohttp-socks = {version = "^0.7.1", optional = true} # Redis -aioredis = { version = "^2.0.0", optional = true } +aioredis = {version = "^2.0.1", optional = true} # Docs Sphinx = { version = "^4.2.0", optional = true } sphinx-intl = { version = "^2.0.1", optional = true } sphinx-autobuild = { version = "^2021.3.14", optional = true } -sphinx-copybutton = { version = "^0.4.0", optional = true } -furo = { version = "^2021.9.8", optional = true } +sphinx-copybutton = {version = "^0.5.0", optional = true} +furo = {version = "^2022.2.14", optional = true} sphinx-prompt = { version = "^1.5.0", optional = true } Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true } -towncrier = { version = "^21.3.0", optional = true } +towncrier = {version = "^21.9.0", optional = true} pygments = { version = "^2.4", optional = true } -pymdown-extensions = { version = "^8.0", optional = true } +pymdown-extensions = {version = "^9.2", optional = true} markdown-include = { version = "^0.6", optional = true } +Pygments = {version = "^2.11.2", optional = true} [tool.poetry.dev-dependencies] -ipython = "^7.22.0" -black = "^21.4b2" -isort = "^5.8.0" -flake8 = "^3.9.1" +ipython = "^8.0.1" +black = "^22.1.0" +isort = "^5.10.1" +flake8 = "^4.0.1" flake8-html = "^0.4.1" -mypy = "^0.910" -pytest = "^6.2.3" +mypy = "^0.931" +pytest = "^7.0.1" pytest-html = "^3.1.1" -pytest-asyncio = "^0.15.1" +pytest-asyncio = "^0.18.1" pytest-lazy-fixture = "^0.6.3" -pytest-mock = "^3.6.0" -pytest-mypy = "^0.8.1" -pytest-cov = "^2.11.1" -pytest-aiohttp = "^0.3.0" -aresponses = "^2.1.4" +pytest-mock = "^3.7.0" +pytest-mypy = "^0.9.1" +pytest-cov = "^3.0.0" +pytest-aiohttp = "^1.0.4" +aresponses = "^2.1.5" asynctest = "^0.13.0" toml = "^0.10.2" -pre-commit = "^2.15.0" -packaging = "^20.3" -typing-extensions = "^3.7.4" +pre-commit = "^2.17.0" +packaging = "^21.3" +typing-extensions = "^4.1.1" +sentry-sdk = "^1.5.5" [tool.poetry.extras] diff --git a/tests/conftest.py b/tests/conftest.py index e57ec632..698ee5cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,13 @@ import pytest from _pytest.config import UsageError from aioredis.connection import parse_url as parse_redis_url -from aiogram import Bot -from aiogram.dispatcher.fsm.storage.memory import MemoryStorage -from aiogram.dispatcher.fsm.storage.redis import RedisStorage +from aiogram import Bot, Dispatcher +from aiogram.dispatcher.fsm.storage.memory import ( + DisabledEventIsolation, + MemoryStorage, + SimpleEventIsolation, +) +from aiogram.dispatcher.fsm.storage.redis import RedisEventIsolation, RedisStorage from tests.mocked_bot import MockedBot DATA_DIR = Path(__file__).parent / "data" @@ -67,6 +71,42 @@ async def memory_storage(): await storage.close() +@pytest.fixture() +@pytest.mark.redis +async def redis_isolation(redis_server): + if not redis_server: + pytest.skip("Redis is not available here") + isolation = RedisEventIsolation.from_url(redis_server) + try: + await isolation.redis.info() + except ConnectionError as e: + pytest.skip(str(e)) + try: + yield isolation + finally: + conn = await isolation.redis + await conn.flushdb() + await isolation.close() + + +@pytest.fixture() +async def lock_isolation(): + isolation = SimpleEventIsolation() + try: + yield isolation + finally: + await isolation.close() + + +@pytest.fixture() +async def disabled_isolation(): + isolation = DisabledEventIsolation() + try: + yield isolation + finally: + await isolation.close() + + @pytest.fixture() def bot(): bot = MockedBot() @@ -75,3 +115,13 @@ def bot(): yield bot finally: Bot.reset_current(token) + + +@pytest.fixture() +async def dispatcher(): + dp = Dispatcher() + await dp.emit_startup() + try: + yield dp + finally: + await dp.emit_shutdown() diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index 31e437ca..1150f073 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -76,20 +76,15 @@ class TestDispatcher: assert dp.update.handlers[0].callback == dp._listen_update assert dp.update.outer_middlewares - def test_parent_router(self): - dp = Dispatcher() + def test_parent_router(self, dispatcher: Dispatcher): with pytest.raises(RuntimeError): - dp.parent_router = Router() - assert dp.parent_router is None - dp._parent_router = Router() - assert dp.parent_router is None + dispatcher.parent_router = Router() + assert dispatcher.parent_router is None + dispatcher._parent_router = Router() + assert dispatcher.parent_router is None - @pytest.mark.parametrize("isolate_events", (True, False)) - async def test_feed_update(self, isolate_events): - dp = Dispatcher(isolate_events=isolate_events) - bot = Bot("42:TEST") - - @dp.message() + async def test_feed_update(self, dispatcher: Dispatcher, bot: MockedBot): + @dispatcher.message() async def my_handler(message: Message, **kwargs): assert "bot" in kwargs assert isinstance(kwargs["bot"], Bot) @@ -97,7 +92,7 @@ class TestDispatcher: return message.text results_count = 0 - result = await dp.feed_update( + result = await dispatcher.feed_update( bot=bot, update=Update( update_id=42, diff --git a/tests/test_dispatcher/test_filters/test_chat_member_updated.py b/tests/test_dispatcher/test_filters/test_chat_member_updated.py new file mode 100644 index 00000000..63ee1245 --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_chat_member_updated.py @@ -0,0 +1,345 @@ +from datetime import datetime + +import pytest + +from aiogram.dispatcher.filters.chat_member_updated import ( + ADMINISTRATOR, + IS_MEMBER, + JOIN_TRANSITION, + LEAVE_TRANSITION, + ChatMemberUpdatedFilter, + _MemberStatusGroupMarker, + _MemberStatusMarker, + _MemberStatusTransition, +) +from aiogram.types import Chat, ChatMember, ChatMemberUpdated, User + + +class TestMemberStatusMarker: + def test_str(self): + marker = _MemberStatusMarker("test") + assert str(marker) == "TEST" + assert str(+marker) == "+TEST" + assert str(-marker) == "-TEST" + + def test_pos(self): + marker = _MemberStatusMarker("test") + assert marker.is_member is None + + positive_marker = +marker + assert positive_marker is not marker + assert marker.is_member is None + assert positive_marker.is_member is True + + def test_neg(self): + marker = _MemberStatusMarker("test") + assert marker.is_member is None + + negative_marker = -marker + assert negative_marker is not marker + assert marker.is_member is None + assert negative_marker.is_member is False + + def test_or(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + + combination = marker1 | marker2 + assert isinstance(combination, _MemberStatusGroupMarker) + assert marker1 in combination.statuses + assert marker2 in combination.statuses + + combination2 = marker1 | marker1 + assert isinstance(combination2, _MemberStatusGroupMarker) + assert len(combination2.statuses) == 1 + + marker3 = _MemberStatusMarker("test3") + combination3 = marker3 | combination + assert isinstance(combination3, _MemberStatusGroupMarker) + assert marker3 in combination3.statuses + assert len(combination3.statuses) == 3 + assert combination3 is not combination + + with pytest.raises(TypeError): + marker1 | 42 + + def test_rshift(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + transition = marker1 >> marker2 + assert isinstance(transition, _MemberStatusTransition) + assert marker1 in transition.old.statuses + assert marker2 in transition.new.statuses + + transition2 = marker1 >> (marker2 | marker3) + assert isinstance(transition2, _MemberStatusTransition) + + with pytest.raises(TypeError): + marker1 >> 42 + + def test_lshift(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + transition = marker1 << marker2 + assert isinstance(transition, _MemberStatusTransition) + assert marker2 in transition.old.statuses + assert marker1 in transition.new.statuses + + transition2 = marker1 << (marker2 | marker3) + assert isinstance(transition2, _MemberStatusTransition) + + with pytest.raises(TypeError): + marker1 << 42 + + def test_hash(self): + marker1 = _MemberStatusMarker("test1") + marker1_1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + assert hash(marker1) != hash(marker2) + assert hash(marker1) == hash(marker1_1) + assert hash(marker1) != hash(-marker1) + + @pytest.mark.parametrize( + "name,is_member,member,result", + [ + ["test", None, ChatMember(status="member"), False], + ["test", None, ChatMember(status="test"), True], + ["test", True, ChatMember(status="test"), False], + ["test", True, ChatMember(status="test", is_member=True), True], + ["test", True, ChatMember(status="test", is_member=False), False], + ], + ) + def test_check(self, name, is_member, member, result): + marker = _MemberStatusMarker(name, is_member=is_member) + assert marker.check(member=member) == result + + +class TestMemberStatusGroupMarker: + def test_init_unique(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + + group = _MemberStatusGroupMarker(marker1, marker1, marker2, marker3) + assert len(group.statuses) == 3 + + def test_init_empty(self): + with pytest.raises(ValueError): + _MemberStatusGroupMarker() + + def test_or(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + marker4 = _MemberStatusMarker("test4") + + group1 = _MemberStatusGroupMarker(marker1, marker2) + group2 = _MemberStatusGroupMarker(marker3, marker4) + + group3 = group1 | marker3 + assert isinstance(group3, _MemberStatusGroupMarker) + assert len(group3.statuses) == 3 + + group4 = group1 | group2 + assert isinstance(group4, _MemberStatusGroupMarker) + assert len(group4.statuses) == 4 + + with pytest.raises(TypeError): + group4 | 42 + + def test_rshift(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + + group1 = _MemberStatusGroupMarker(marker1, marker2) + group2 = _MemberStatusGroupMarker(marker1, marker3) + + transition1 = group1 >> marker1 + assert isinstance(transition1, _MemberStatusTransition) + assert transition1.old is group1 + assert marker1 in transition1.new.statuses + + transition2 = group1 >> group2 + assert isinstance(transition2, _MemberStatusTransition) + + with pytest.raises(TypeError): + group1 >> 42 + + def test_lshift(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + marker3 = _MemberStatusMarker("test3") + + group1 = _MemberStatusGroupMarker(marker1, marker2) + group2 = _MemberStatusGroupMarker(marker1, marker3) + + transition1 = group1 << marker1 + assert isinstance(transition1, _MemberStatusTransition) + assert transition1.new is group1 + assert marker1 in transition1.old.statuses + + transition2 = group1 << group2 + assert isinstance(transition2, _MemberStatusTransition) + + with pytest.raises(TypeError): + group1 << 42 + + def test_str(self): + marker1 = _MemberStatusMarker("test1") + marker1_1 = +marker1 + marker2 = _MemberStatusMarker("test2") + + group1 = marker1 | marker1 + assert str(group1) == "TEST1" + + group2 = marker1 | marker2 + assert str(group2) == "(TEST1 | TEST2)" + + group3 = marker1 | marker1_1 + assert str(group3) == "(+TEST1 | TEST1)" + + @pytest.mark.parametrize( + "status,result", + [ + ["test", False], + ["test1", True], + ["test2", True], + ], + ) + def test_check(self, status, result): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + group = marker1 | marker2 + + assert group.check(member=ChatMember(status=status)) is result + + +class TestMemberStatusTransition: + def test_invert(self): + marker1 = _MemberStatusMarker("test1") + marker2 = _MemberStatusMarker("test2") + + transition1 = marker1 >> marker2 + transition2 = ~transition1 + + assert transition1 is not transition2 + assert transition1.old == transition2.new + assert transition1.new == transition2.old + + assert str(transition1) == "TEST1 >> TEST2" + assert str(transition2) == "TEST2 >> TEST1" + + @pytest.mark.parametrize( + "transition,old,new,result", + [ + [JOIN_TRANSITION, ChatMember(status="left"), ChatMember(status="member"), True], + [ + JOIN_TRANSITION, + ChatMember(status="restricted", is_member=True), + ChatMember(status="member"), + False, + ], + [ + JOIN_TRANSITION, + ChatMember(status="restricted", is_member=False), + ChatMember(status="member"), + True, + ], + [ + JOIN_TRANSITION, + ChatMember(status="member"), + ChatMember(status="restricted", is_member=False), + False, + ], + [ + LEAVE_TRANSITION, + ChatMember(status="member"), + ChatMember(status="restricted", is_member=False), + True, + ], + ], + ) + def test_check(self, transition, old, new, result): + assert transition.check(old=old, new=new) == result + + +class TestChatMemberUpdatedStatusFilter: + @pytest.mark.asyncio + @pytest.mark.parametrize( + "transition,old,new,result", + [ + [JOIN_TRANSITION, ChatMember(status="left"), ChatMember(status="member"), True], + [ + JOIN_TRANSITION, + ChatMember(status="restricted", is_member=True), + ChatMember(status="member"), + False, + ], + [ + JOIN_TRANSITION, + ChatMember(status="restricted", is_member=False), + ChatMember(status="member"), + True, + ], + [ + JOIN_TRANSITION, + ChatMember(status="member"), + ChatMember(status="restricted", is_member=False), + False, + ], + [ + LEAVE_TRANSITION, + ChatMember(status="member"), + ChatMember(status="restricted", is_member=False), + True, + ], + [ + ADMINISTRATOR, + ChatMember(status="member"), + ChatMember(status="administrator"), + True, + ], + [ + IS_MEMBER, + ChatMember(status="restricted", is_member=False), + ChatMember(status="member"), + True, + ], + ], + ) + async def test_call(self, transition, old, new, result): + updated_filter = ChatMemberUpdatedFilter(member_status_changed=transition) + user = User(id=42, first_name="Test", is_bot=False) + update = { + "user": user, + "until_date": datetime.now(), + "is_anonymous": False, + "can_be_edited": True, + "can_manage_chat": True, + "can_delete_messages": True, + "can_manage_voice_chats": True, + "can_restrict_members": True, + "can_promote_members": True, + "can_change_info": True, + "can_invite_users": True, + "can_post_messages": True, + "can_edit_messages": True, + "can_pin_messages": True, + "can_send_messages": True, + "can_send_media_messages": True, + "can_send_polls": True, + "can_send_other_messages": True, + "can_add_web_page_previews": True, + } + event = ChatMemberUpdated( + chat=Chat(id=42, type="test"), + from_user=user, + old_chat_member=old.copy(update=update), + new_chat_member=new.copy(update=update), + date=datetime.now(), + ) + + assert await updated_filter(event) is result diff --git a/tests/test_dispatcher/test_flags/__init__.py b/tests/test_dispatcher/test_flags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_dispatcher/test_flags/test_decorator.py b/tests/test_dispatcher/test_flags/test_decorator.py new file mode 100644 index 00000000..6c4e40df --- /dev/null +++ b/tests/test_dispatcher/test_flags/test_decorator.py @@ -0,0 +1,66 @@ +import pytest + +from aiogram.dispatcher.flags.flag import Flag, FlagDecorator, FlagGenerator + + +@pytest.fixture(name="flag") +def flag_fixture() -> Flag: + return Flag("test", True) + + +@pytest.fixture(name="flag_decorator") +def flag_decorator_fixture(flag: Flag) -> FlagDecorator: + return FlagDecorator(flag) + + +@pytest.fixture(name="flag_generator") +def flag_flag_generator() -> FlagGenerator: + return FlagGenerator() + + +class TestFlagDecorator: + def test_with_value(self, flag_decorator: FlagDecorator): + new_decorator = flag_decorator._with_value(True) + + assert new_decorator is not flag_decorator + assert new_decorator.flag is not flag_decorator.flag + assert new_decorator.flag + + def test_call_invalid(self, flag_decorator: FlagDecorator): + with pytest.raises(ValueError): + flag_decorator(True, test=True) + + def test_call_with_function(self, flag_decorator: FlagDecorator): + def func(): + pass + + decorated = flag_decorator(func) + assert decorated is func + assert hasattr(decorated, "aiogram_flag") + + def test_call_with_arg(self, flag_decorator: FlagDecorator): + new_decorator = flag_decorator("hello") + assert new_decorator is not flag_decorator + assert new_decorator.flag.value == "hello" + + def test_call_with_kwargs(self, flag_decorator: FlagDecorator): + new_decorator = flag_decorator(test=True) + assert new_decorator is not flag_decorator + assert isinstance(new_decorator.flag.value, dict) + assert "test" in new_decorator.flag.value + + +class TestFlagGenerator: + def test_getattr(self): + generator = FlagGenerator() + assert isinstance(generator.foo, FlagDecorator) + assert isinstance(generator.bar, FlagDecorator) + + assert generator.foo is not generator.foo + assert generator.foo is not generator.bar + + def test_failed_getattr(self): + generator = FlagGenerator() + + with pytest.raises(AttributeError): + generator._something diff --git a/tests/test_dispatcher/test_flags/test_getter.py b/tests/test_dispatcher/test_flags/test_getter.py new file mode 100644 index 00000000..afe7891c --- /dev/null +++ b/tests/test_dispatcher/test_flags/test_getter.py @@ -0,0 +1,64 @@ +from unittest.mock import patch + +import pytest + +from aiogram import F +from aiogram.dispatcher.event.handler import HandlerObject +from aiogram.dispatcher.flags.getter import ( + check_flags, + extract_flags, + extract_flags_from_object, + get_flag, +) + + +class TestGetters: + def test_extract_flags_from_object(self): + def func(): + pass + + assert extract_flags_from_object(func) == {} + + func.aiogram_flag = {"test": True} + assert extract_flags_from_object(func) == func.aiogram_flag + + @pytest.mark.parametrize( + "obj,result", + [ + [None, {}], + [{}, {}], + [{"handler": None}, {}], + [{"handler": HandlerObject(lambda: True, flags={"test": True})}, {"test": True}], + ], + ) + def test_extract_flags(self, obj, result): + assert extract_flags(obj) == result + + @pytest.mark.parametrize( + "obj,name,default,result", + [ + [None, "test", None, None], + [None, "test", 42, 42], + [{}, "test", None, None], + [{}, "test", 42, 42], + [{"handler": None}, "test", None, None], + [{"handler": None}, "test", 42, 42], + [{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test", None, True], + [{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test2", None, None], + [{"handler": HandlerObject(lambda: True, flags={"test": True})}, "test2", 42, 42], + ], + ) + def test_get_flag(self, obj, name, default, result): + assert get_flag(obj, name, default=default) == result + + @pytest.mark.parametrize( + "flags,magic,result", + [ + [{}, F.test, None], + [{"test": True}, F.test, True], + [{"test": True}, F.spam, None], + ], + ) + def test_check_flag(self, flags, magic, result): + with patch("aiogram.dispatcher.flags.getter.extract_flags", return_value=flags): + assert check_flags(object(), magic) == result diff --git a/tests/test_dispatcher/test_fsm/storage/test_isolation.py b/tests/test_dispatcher/test_fsm/storage/test_isolation.py new file mode 100644 index 00000000..8b582f45 --- /dev/null +++ b/tests/test_dispatcher/test_fsm/storage/test_isolation.py @@ -0,0 +1,30 @@ +import pytest + +from aiogram.dispatcher.fsm.storage.base import BaseEventIsolation, StorageKey +from tests.mocked_bot import MockedBot + +pytestmark = pytest.mark.asyncio + + +@pytest.fixture(name="storage_key") +def create_storate_key(bot: MockedBot): + return StorageKey(chat_id=-42, user_id=42, bot_id=bot.id) + + +@pytest.mark.parametrize( + "isolation", + [ + pytest.lazy_fixture("redis_isolation"), + pytest.lazy_fixture("lock_isolation"), + pytest.lazy_fixture("disabled_isolation"), + ], +) +class TestIsolations: + async def test_lock( + self, + bot: MockedBot, + isolation: BaseEventIsolation, + storage_key: StorageKey, + ): + async with isolation.lock(bot=bot, key=storage_key): + assert True, "You are kidding me?" diff --git a/tests/test_dispatcher/test_fsm/storage/test_redis.py b/tests/test_dispatcher/test_fsm/storage/test_redis.py index dcb71c3d..5bf3170e 100644 --- a/tests/test_dispatcher/test_fsm/storage/test_redis.py +++ b/tests/test_dispatcher/test_fsm/storage/test_redis.py @@ -1,9 +1,11 @@ -from typing import Literal - import pytest from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, StorageKey -from aiogram.dispatcher.fsm.storage.redis import DefaultKeyBuilder +from aiogram.dispatcher.fsm.storage.redis import ( + DefaultKeyBuilder, + RedisEventIsolation, + RedisStorage, +) pytestmark = pytest.mark.asyncio @@ -45,3 +47,11 @@ class TestRedisDefaultKeyBuilder: ) with pytest.raises(ValueError): key_builder.build(key, FIELD) + + def test_create_isolation(self): + fake_redis = object() + storage = RedisStorage(redis=fake_redis) + isolation = storage.create_isolation() + assert isinstance(isolation, RedisEventIsolation) + assert isolation.redis is fake_redis + assert isolation.key_builder is storage.key_builder diff --git a/tests/test_dispatcher/test_fsm/storage/test_storages.py b/tests/test_dispatcher/test_fsm/storage/test_storages.py index 428f6d02..803e3059 100644 --- a/tests/test_dispatcher/test_fsm/storage/test_storages.py +++ b/tests/test_dispatcher/test_fsm/storage/test_storages.py @@ -16,11 +16,6 @@ def create_storate_key(bot: MockedBot): [pytest.lazy_fixture("redis_storage"), pytest.lazy_fixture("memory_storage")], ) class TestStorages: - async def test_lock(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey): - # TODO: ?!? - async with storage.lock(bot=bot, key=storage_key): - assert True, "You are kidding me?" - async def test_set_state(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey): assert await storage.get_state(bot=bot, key=storage_key) is None diff --git a/tests/test_dispatcher/test_webhook/test_aiohtt_server.py b/tests/test_dispatcher/test_webhook/test_aiohtt_server.py index fa9dad9c..7a044715 100644 --- a/tests/test_dispatcher/test_webhook/test_aiohtt_server.py +++ b/tests/test_dispatcher/test_webhook/test_aiohtt_server.py @@ -1,4 +1,6 @@ +import asyncio import time +from asyncio import Event from dataclasses import dataclass from typing import Any, Dict @@ -19,6 +21,12 @@ from aiogram.methods import GetMe, Request from aiogram.types import Message, User from tests.mocked_bot import MockedBot +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock # type: ignore + from unittest.mock import patch + class TestAiohttpServer: def test_setup_application(self): @@ -74,8 +82,11 @@ class TestSimpleRequestHandler: app = Application() dp = Dispatcher() + handler_event = Event() + @dp.message(F.text == "test") def handle_message(msg: Message): + handler_event.set() return msg.answer("PASS") handler = SimpleRequestHandler( @@ -97,8 +108,15 @@ class TestSimpleRequestHandler: assert not result handler.handle_in_background = True - resp = await self.make_reqest(client=client) - assert resp.status == 200 + with patch( + "aiogram.dispatcher.dispatcher.Dispatcher.silent_call_request", + new_callable=CoroutineMock, + ) as mocked_silent_call_request: + handler_event.clear() + resp = await self.make_reqest(client=client) + assert resp.status == 200 + await asyncio.wait_for(handler_event.wait(), timeout=1) + mocked_silent_call_request.assert_awaited() result = await resp.json() assert not result diff --git a/tests/test_utils/test_chat_action.py b/tests/test_utils/test_chat_action.py new file mode 100644 index 00000000..caf05e33 --- /dev/null +++ b/tests/test_utils/test_chat_action.py @@ -0,0 +1,129 @@ +import asyncio +import time +from datetime import datetime + +import pytest + +from aiogram import Bot, flags +from aiogram.dispatcher.event.handler import HandlerObject +from aiogram.types import Chat, Message, User +from aiogram.utils.chat_action import ChatActionMiddleware, ChatActionSender +from tests.mocked_bot import MockedBot + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock # type: ignore + from unittest.mock import patch + +pytestmarm = pytest.mark.asyncio + + +class TestChatActionSender: + async def test_wait(self, bot: Bot, loop: asyncio.BaseEventLoop): + sender = ChatActionSender.typing(bot=bot, chat_id=42) + loop.call_soon(sender._close_event.set) + start = time.monotonic() + await sender._wait(1) + assert time.monotonic() - start < 1 + + @pytest.mark.parametrize( + "action", + [ + "typing", + "upload_photo", + "record_video", + "upload_video", + "record_voice", + "upload_voice", + "upload_document", + "choose_sticker", + "find_location", + "record_video_note", + "upload_video_note", + ], + ) + @pytest.mark.parametrize("pass_bot", [True, False]) + async def test_factory(self, action: str, bot: MockedBot, pass_bot: bool): + sender_factory = getattr(ChatActionSender, action) + sender = sender_factory(chat_id=42, bot=bot if pass_bot else None) + assert isinstance(sender, ChatActionSender) + assert sender.action == action + assert sender.chat_id == 42 + assert sender.bot is bot + + async def test_worker(self, bot: Bot): + with patch( + "aiogram.client.bot.Bot.send_chat_action", + new_callable=CoroutineMock, + ) as mocked_send_chat_action: + async with ChatActionSender.typing( + bot=bot, chat_id=42, interval=0.01, initial_sleep=0 + ): + await asyncio.sleep(0.1) + assert mocked_send_chat_action.await_count > 1 + mocked_send_chat_action.assert_awaited_with(action="typing", chat_id=42) + + async def test_contextmanager(self, bot: MockedBot): + sender: ChatActionSender = ChatActionSender.typing(bot=bot, chat_id=42) + assert not sender.running + await sender._stop() # nothing + + async with sender: + assert sender.running + assert not sender._close_event.is_set() + + with pytest.raises(RuntimeError): + await sender._run() + + assert not sender.running + + +class TestChatActionMiddleware: + @pytest.mark.parametrize( + "value", + [ + None, + "sticker", + {"action": "upload_photo"}, + {"interval": 1, "initial_sleep": 0.5}, + ], + ) + async def test_call_default(self, value, bot: Bot): + async def handler(event, data): + return "OK" + + if value is None: + handler1 = flags.chat_action(handler) + else: + handler1 = flags.chat_action(value)(handler) + + middleware = ChatActionMiddleware() + with patch( + "aiogram.utils.chat_action.ChatActionSender._run", + new_callable=CoroutineMock, + ) as mocked_run, patch( + "aiogram.utils.chat_action.ChatActionSender._stop", + new_callable=CoroutineMock, + ) as mocked_stop: + data = {"handler": HandlerObject(callback=handler1), "bot": bot} + message = Message( + chat=Chat(id=42, type="private", title="Test"), + from_user=User(id=42, is_bot=False, first_name="Test"), + date=datetime.now(), + message_id=42, + ) + + result = await middleware(handler=handler1, event=None, data=data) + assert result == "OK" + mocked_run.assert_not_awaited() + mocked_stop.assert_not_awaited() + + result = await middleware( + handler=handler1, + event=message, + data=data, + ) + assert result == "OK" + mocked_run.assert_awaited() + mocked_stop.assert_awaited() diff --git a/tests/test_utils/test_i18n.py b/tests/test_utils/test_i18n.py index e8581414..31843080 100644 --- a/tests/test_utils/test_i18n.py +++ b/tests/test_utils/test_i18n.py @@ -114,6 +114,24 @@ class TestSimpleI18nMiddleware: assert middleware not in dp.update.outer_middlewares assert middleware in dp.message.outer_middlewares + async def test_get_unknown_locale(self, i18n: I18n): + dp = Dispatcher() + middleware = SimpleI18nMiddleware(i18n=i18n) + middleware.setup(router=dp) + + locale = await middleware.get_locale( + None, + { + "event_from_user": User( + id=42, + is_bot=False, + first_name="Test", + language_code="unknown", + ) + }, + ) + assert locale == i18n.default_locale + @pytest.mark.asyncio class TestConstI18nMiddleware: