diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 72f2a21c..dc8c4913 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,3 +1,5 @@ +from magic_filter import MagicFilter + from .client import session from .client.bot import Bot from .dispatcher import filters, handler @@ -10,8 +12,9 @@ try: _uvloop.install() except ImportError: # pragma: no cover - _uvloop = None + _uvloop = None # type: ignore +F = MagicFilter() __all__ = ( "__api_version__", @@ -25,6 +28,7 @@ __all__ = ( "BaseMiddleware", "filters", "handler", + "F", ) __version__ = "3.0.0-alpha.6" diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index b22028c8..5db8feaa 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -145,7 +145,10 @@ T = TypeVar("T") class Bot(ContextInstanceMixin["Bot"]): def __init__( - self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None, + self, + token: str, + session: Optional[BaseSession] = None, + parse_mode: Optional[str] = None, ) -> None: """ Bot class @@ -349,7 +352,10 @@ class Bot(ContextInstanceMixin["Bot"]): :return: An Array of Update objects is returned. """ call = GetUpdates( - offset=offset, limit=limit, timeout=timeout, allowed_updates=allowed_updates, + offset=offset, + limit=limit, + timeout=timeout, + allowed_updates=allowed_updates, ) return await self(call, request_timeout=request_timeout) @@ -398,7 +404,9 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def delete_webhook( - self, drop_pending_updates: Optional[bool] = None, request_timeout: Optional[int] = None, + self, + drop_pending_updates: Optional[bool] = None, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to remove webhook integration if you decide to switch back to :class:`aiogram.methods.get_updates.GetUpdates`. Returns :code:`True` on success. @@ -409,10 +417,15 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = DeleteWebhook(drop_pending_updates=drop_pending_updates,) + call = DeleteWebhook( + drop_pending_updates=drop_pending_updates, + ) return await self(call, request_timeout=request_timeout) - async def get_webhook_info(self, request_timeout: Optional[int] = None,) -> WebhookInfo: + async def get_webhook_info( + self, + request_timeout: Optional[int] = None, + ) -> WebhookInfo: """ Use this method to get current webhook status. Requires no parameters. On success, returns a :class:`aiogram.types.webhook_info.WebhookInfo` object. If the bot is using :class:`aiogram.methods.get_updates.GetUpdates`, will return an object with the *url* field empty. @@ -430,7 +443,10 @@ class Bot(ContextInstanceMixin["Bot"]): # Source: https://core.telegram.org/bots/api#available-methods # ============================================================================================= - async def get_me(self, request_timeout: Optional[int] = None,) -> User: + async def get_me( + self, + request_timeout: Optional[int] = None, + ) -> User: """ A simple method for testing your bot's auth token. Requires no parameters. Returns basic information about the bot in form of a :class:`aiogram.types.user.User` object. @@ -442,7 +458,10 @@ class Bot(ContextInstanceMixin["Bot"]): call = GetMe() return await self(call, request_timeout=request_timeout) - async def log_out(self, request_timeout: Optional[int] = None,) -> bool: + async def log_out( + self, + request_timeout: Optional[int] = None, + ) -> bool: """ Use this method to log out from the cloud Bot API server before launching the bot locally. You **must** log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates. After a successful call, you can immediately log in on a local server, but will not be able to log in back to the cloud Bot API server for 10 minutes. Returns :code:`True` on success. Requires no parameters. @@ -454,7 +473,10 @@ class Bot(ContextInstanceMixin["Bot"]): call = LogOut() return await self(call, request_timeout=request_timeout) - async def close(self, request_timeout: Optional[int] = None,) -> bool: + async def close( + self, + request_timeout: Optional[int] = None, + ) -> bool: """ Use this method to close the bot instance before moving it from one local server to another. You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart. The method will return error 429 in the first 10 minutes after the bot is launched. Returns :code:`True` on success. Requires no parameters. @@ -1315,7 +1337,10 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def send_chat_action( - self, chat_id: Union[int, str], action: str, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + action: str, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns :code:`True` on success. @@ -1331,7 +1356,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SendChatAction(chat_id=chat_id, action=action,) + call = SendChatAction( + chat_id=chat_id, + action=action, + ) return await self(call, request_timeout=request_timeout) async def get_user_profile_photos( @@ -1352,10 +1380,18 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns a UserProfilePhotos object. """ - call = GetUserProfilePhotos(user_id=user_id, offset=offset, limit=limit,) + call = GetUserProfilePhotos( + user_id=user_id, + offset=offset, + limit=limit, + ) return await self(call, request_timeout=request_timeout) - async def get_file(self, file_id: str, request_timeout: Optional[int] = None,) -> File: + async def get_file( + self, + file_id: str, + request_timeout: Optional[int] = None, + ) -> File: """ Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a :class:`aiogram.types.file.File` object is returned. The file can then be downloaded via the link :code:`https://api.telegram.org/file/bot/`, where :code:`` is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling :class:`aiogram.methods.get_file.GetFile` again. **Note:** This function may not preserve the original file name and MIME type. You should save the file's MIME type and name (if available) when the File object is received. @@ -1366,7 +1402,9 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: On success, a File object is returned. """ - call = GetFile(file_id=file_id,) + call = GetFile( + file_id=file_id, + ) return await self(call, request_timeout=request_timeout) async def kick_chat_member( @@ -1417,7 +1455,11 @@ class Bot(ContextInstanceMixin["Bot"]): :return: The user will not return to the group or channel automatically, but will be able to join via link, etc. Returns True on success. """ - call = UnbanChatMember(chat_id=chat_id, user_id=user_id, only_if_banned=only_if_banned,) + call = UnbanChatMember( + chat_id=chat_id, + user_id=user_id, + only_if_banned=only_if_banned, + ) return await self(call, request_timeout=request_timeout) async def restrict_chat_member( @@ -1441,7 +1483,10 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Returns True on success. """ call = RestrictChatMember( - chat_id=chat_id, user_id=user_id, permissions=permissions, until_date=until_date, + chat_id=chat_id, + user_id=user_id, + permissions=permissions, + until_date=until_date, ) return await self(call, request_timeout=request_timeout) @@ -1519,7 +1564,9 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Returns True on success. """ call = SetChatAdministratorCustomTitle( - chat_id=chat_id, user_id=user_id, custom_title=custom_title, + chat_id=chat_id, + user_id=user_id, + custom_title=custom_title, ) return await self(call, request_timeout=request_timeout) @@ -1539,11 +1586,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetChatPermissions(chat_id=chat_id, permissions=permissions,) + call = SetChatPermissions( + chat_id=chat_id, + permissions=permissions, + ) return await self(call, request_timeout=request_timeout) async def export_chat_invite_link( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> str: """ Use this method to generate a new primary invite link for a chat; any previously generated primary link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as *String* on success. @@ -1556,7 +1608,9 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns the new invite link as String on success. """ - call = ExportChatInviteLink(chat_id=chat_id,) + call = ExportChatInviteLink( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def create_chat_invite_link( @@ -1578,7 +1632,9 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Returns the new invite link as ChatInviteLink object. """ call = CreateChatInviteLink( - chat_id=chat_id, expire_date=expire_date, member_limit=member_limit, + chat_id=chat_id, + expire_date=expire_date, + member_limit=member_limit, ) return await self(call, request_timeout=request_timeout) @@ -1611,7 +1667,10 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def revoke_chat_invite_link( - self, chat_id: Union[int, str], invite_link: str, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + invite_link: str, + request_timeout: Optional[int] = None, ) -> ChatInviteLink: """ Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as :class:`aiogram.types.chat_invite_link.ChatInviteLink` object. @@ -1623,11 +1682,17 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns the revoked invite link as ChatInviteLink object. """ - call = RevokeChatInviteLink(chat_id=chat_id, invite_link=invite_link,) + call = RevokeChatInviteLink( + chat_id=chat_id, + invite_link=invite_link, + ) return await self(call, request_timeout=request_timeout) async def set_chat_photo( - self, chat_id: Union[int, str], photo: InputFile, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + photo: InputFile, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns :code:`True` on success. @@ -1639,11 +1704,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetChatPhoto(chat_id=chat_id, photo=photo,) + call = SetChatPhoto( + chat_id=chat_id, + photo=photo, + ) return await self(call, request_timeout=request_timeout) async def delete_chat_photo( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns :code:`True` on success. @@ -1654,11 +1724,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = DeleteChatPhoto(chat_id=chat_id,) + call = DeleteChatPhoto( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def set_chat_title( - self, chat_id: Union[int, str], title: str, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + title: str, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns :code:`True` on success. @@ -1670,7 +1745,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetChatTitle(chat_id=chat_id, title=title,) + call = SetChatTitle( + chat_id=chat_id, + title=title, + ) return await self(call, request_timeout=request_timeout) async def set_chat_description( @@ -1689,7 +1767,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetChatDescription(chat_id=chat_id, description=description,) + call = SetChatDescription( + chat_id=chat_id, + description=description, + ) return await self(call, request_timeout=request_timeout) async def pin_chat_message( @@ -1711,7 +1792,9 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Returns True on success. """ call = PinChatMessage( - chat_id=chat_id, message_id=message_id, disable_notification=disable_notification, + chat_id=chat_id, + message_id=message_id, + disable_notification=disable_notification, ) return await self(call, request_timeout=request_timeout) @@ -1731,11 +1814,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = UnpinChatMessage(chat_id=chat_id, message_id=message_id,) + call = UnpinChatMessage( + chat_id=chat_id, + message_id=message_id, + ) return await self(call, request_timeout=request_timeout) async def unpin_all_chat_messages( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to clear the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns :code:`True` on success. @@ -1746,11 +1834,15 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = UnpinAllChatMessages(chat_id=chat_id,) + call = UnpinAllChatMessages( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def leave_chat( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> bool: """ Use this method for your bot to leave a group, supergroup or channel. Returns :code:`True` on success. @@ -1761,11 +1853,15 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = LeaveChat(chat_id=chat_id,) + call = LeaveChat( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def get_chat( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> Chat: """ Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a :class:`aiogram.types.chat.Chat` object on success. @@ -1776,11 +1872,15 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns a Chat object on success. """ - call = GetChat(chat_id=chat_id,) + call = GetChat( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def get_chat_administrators( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> List[ChatMember]: """ Use this method to get a list of administrators in a chat. On success, returns an Array of :class:`aiogram.types.chat_member.ChatMember` objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. @@ -1794,11 +1894,15 @@ class Bot(ContextInstanceMixin["Bot"]): supergroup and no administrators were appointed, only the creator will be returned. """ - call = GetChatAdministrators(chat_id=chat_id,) + call = GetChatAdministrators( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def get_chat_members_count( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> int: """ Use this method to get the number of members in a chat. Returns *Int* on success. @@ -1809,11 +1913,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns Int on success. """ - call = GetChatMembersCount(chat_id=chat_id,) + call = GetChatMembersCount( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def get_chat_member( - self, chat_id: Union[int, str], user_id: int, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + user_id: int, + request_timeout: Optional[int] = None, ) -> ChatMember: """ Use this method to get information about a member of a chat. Returns a :class:`aiogram.types.chat_member.ChatMember` object on success. @@ -1825,7 +1934,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns a ChatMember object on success. """ - call = GetChatMember(chat_id=chat_id, user_id=user_id,) + call = GetChatMember( + chat_id=chat_id, + user_id=user_id, + ) return await self(call, request_timeout=request_timeout) async def set_chat_sticker_set( @@ -1845,11 +1957,16 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. """ - call = SetChatStickerSet(chat_id=chat_id, sticker_set_name=sticker_set_name,) + call = SetChatStickerSet( + chat_id=chat_id, + sticker_set_name=sticker_set_name, + ) return await self(call, request_timeout=request_timeout) async def delete_chat_sticker_set( - self, chat_id: Union[int, str], request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field *can_set_sticker_set* optionally returned in :class:`aiogram.methods.get_chat.GetChat` requests to check if the bot can use this method. Returns :code:`True` on success. @@ -1861,7 +1978,9 @@ class Bot(ContextInstanceMixin["Bot"]): :return: Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. """ - call = DeleteChatStickerSet(chat_id=chat_id,) + call = DeleteChatStickerSet( + chat_id=chat_id, + ) return await self(call, request_timeout=request_timeout) async def answer_callback_query( @@ -1898,7 +2017,9 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def set_my_commands( - self, commands: List[BotCommand], request_timeout: Optional[int] = None, + self, + commands: List[BotCommand], + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to change the list of the bot's commands. Returns :code:`True` on success. @@ -1909,10 +2030,15 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetMyCommands(commands=commands,) + call = SetMyCommands( + commands=commands, + ) return await self(call, request_timeout=request_timeout) - async def get_my_commands(self, request_timeout: Optional[int] = None,) -> List[BotCommand]: + async def get_my_commands( + self, + request_timeout: Optional[int] = None, + ) -> List[BotCommand]: """ Use this method to get the current list of the bot's commands. Requires no parameters. Returns Array of :class:`aiogram.types.bot_command.BotCommand` on success. @@ -2087,11 +2213,18 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: On success, the stopped Poll with the final results is returned. """ - call = StopPoll(chat_id=chat_id, message_id=message_id, reply_markup=reply_markup,) + call = StopPoll( + chat_id=chat_id, + message_id=message_id, + reply_markup=reply_markup, + ) return await self(call, request_timeout=request_timeout) async def delete_message( - self, chat_id: Union[int, str], message_id: int, request_timeout: Optional[int] = None, + self, + chat_id: Union[int, str], + message_id: int, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to delete a message, including service messages, with the following limitations: @@ -2119,7 +2252,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = DeleteMessage(chat_id=chat_id, message_id=message_id,) + call = DeleteMessage( + chat_id=chat_id, + message_id=message_id, + ) return await self(call, request_timeout=request_timeout) # ============================================================================================= @@ -2164,7 +2300,9 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def get_sticker_set( - self, name: str, request_timeout: Optional[int] = None, + self, + name: str, + request_timeout: Optional[int] = None, ) -> StickerSet: """ Use this method to get a sticker set. On success, a :class:`aiogram.types.sticker_set.StickerSet` object is returned. @@ -2175,11 +2313,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: On success, a StickerSet object is returned. """ - call = GetStickerSet(name=name,) + call = GetStickerSet( + name=name, + ) return await self(call, request_timeout=request_timeout) async def upload_sticker_file( - self, user_id: int, png_sticker: InputFile, request_timeout: Optional[int] = None, + self, + user_id: int, + png_sticker: InputFile, + request_timeout: Optional[int] = None, ) -> File: """ Use this method to upload a .PNG file with a sticker for later use in *createNewStickerSet* and *addStickerToSet* methods (can be used multiple times). Returns the uploaded :class:`aiogram.types.file.File` on success. @@ -2191,7 +2334,10 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns the uploaded File on success. """ - call = UploadStickerFile(user_id=user_id, png_sticker=png_sticker,) + call = UploadStickerFile( + user_id=user_id, + png_sticker=png_sticker, + ) return await self(call, request_timeout=request_timeout) async def create_new_sticker_set( @@ -2269,7 +2415,10 @@ class Bot(ContextInstanceMixin["Bot"]): return await self(call, request_timeout=request_timeout) async def set_sticker_position_in_set( - self, sticker: str, position: int, request_timeout: Optional[int] = None, + self, + sticker: str, + position: int, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to move a sticker in a set created by the bot to a specific position. Returns :code:`True` on success. @@ -2281,11 +2430,16 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetStickerPositionInSet(sticker=sticker, position=position,) + call = SetStickerPositionInSet( + sticker=sticker, + position=position, + ) return await self(call, request_timeout=request_timeout) async def delete_sticker_from_set( - self, sticker: str, request_timeout: Optional[int] = None, + self, + sticker: str, + request_timeout: Optional[int] = None, ) -> bool: """ Use this method to delete a sticker from a set created by the bot. Returns :code:`True` on success. @@ -2296,7 +2450,9 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = DeleteStickerFromSet(sticker=sticker,) + call = DeleteStickerFromSet( + sticker=sticker, + ) return await self(call, request_timeout=request_timeout) async def set_sticker_set_thumb( @@ -2317,7 +2473,11 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - call = SetStickerSetThumb(name=name, user_id=user_id, thumb=thumb,) + call = SetStickerSetThumb( + name=name, + user_id=user_id, + thumb=thumb, + ) return await self(call, request_timeout=request_timeout) # ============================================================================================= @@ -2504,7 +2664,9 @@ class Bot(ContextInstanceMixin["Bot"]): :return: On success, True is returned. """ call = AnswerPreCheckoutQuery( - pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message, + pre_checkout_query_id=pre_checkout_query_id, + ok=ok, + error_message=error_message, ) return await self(call, request_timeout=request_timeout) @@ -2532,7 +2694,10 @@ class Bot(ContextInstanceMixin["Bot"]): fixed (the contents of the field for which you returned the error must change). Returns True on success. """ - call = SetPassportDataErrors(user_id=user_id, errors=errors,) + call = SetPassportDataErrors( + user_id=user_id, + errors=errors, + ) return await self(call, request_timeout=request_timeout) # ============================================================================================= diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 713b7ae0..e7d71741 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -13,6 +13,11 @@ from ..types import TelegramObject, Update, User from ..utils.exceptions import TelegramAPIError from .event.bases import UNHANDLED, SkipHandler from .event.telegram import TelegramEventObserver +from .fsm.context import FSMContext +from .fsm.engine import FSMStrategy +from .fsm.middleware import FSMContextMiddleware +from .fsm.storage.base import BaseStorage +from .fsm.storage.memory import MemoryStorage from .middlewares.error import ErrorsMiddleware from .middlewares.user_context import UserContextMiddleware from .router import Router @@ -23,15 +28,36 @@ class Dispatcher(Router): Root router """ - def __init__(self, **kwargs: Any) -> None: + def __init__( + self, + storage: Optional[BaseStorage] = None, + fsm_strategy: FSMStrategy = FSMStrategy.USER, + isolate_events: bool = True, + **kwargs: Any, + ) -> None: super(Dispatcher, self).__init__(**kwargs) - self.update = TelegramEventObserver(router=self, event_name="update") - self.observers["update"] = self.update - + # Telegram API provides originally only one event type - Update + # For making easily interactions with events here is registered handler which helps + # to separate Update to different event types like Message, CallbackQuery and etc. + self.update = self.observers["update"] = TelegramEventObserver( + router=self, event_name="update" + ) self.update.register(self._listen_update) - self.update.outer_middleware(UserContextMiddleware()) + + # Error handlers should works is out of all other functions and be registered before all other 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, + ) + self.update.outer_middleware(self.fsm) self._running_lock = Lock() @@ -353,3 +379,6 @@ class Dispatcher(Router): except (KeyboardInterrupt, SystemExit): # pragma: no cover # Allow to graceful shutdown pass + + def current_state(self, user_id: int, chat_id: int) -> FSMContext: + return self.fsm.get_context(user_id=user_id, chat_id=chat_id) diff --git a/aiogram/dispatcher/fsm/__init__.py b/aiogram/dispatcher/fsm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/fsm/context.py b/aiogram/dispatcher/fsm/context.py new file mode 100644 index 00000000..78ed480b --- /dev/null +++ b/aiogram/dispatcher/fsm/context.py @@ -0,0 +1,35 @@ +from typing import Any, Dict, Optional + +from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType + + +class FSMContext: + def __init__(self, storage: BaseStorage, chat_id: int, user_id: int) -> None: + self.storage = storage + self.chat_id = chat_id + self.user_id = user_id + + async def set_state(self, state: StateType = None) -> None: + await self.storage.set_state(chat_id=self.chat_id, user_id=self.user_id, state=state) + + async def get_state(self) -> Optional[str]: + return await self.storage.get_state(chat_id=self.chat_id, user_id=self.user_id) + + async def set_data(self, data: Dict[str, Any]) -> None: + await self.storage.set_data(chat_id=self.chat_id, user_id=self.user_id, data=data) + + async def get_data(self) -> Dict[str, Any]: + return await self.storage.get_data(chat_id=self.chat_id, user_id=self.user_id) + + async def update_data( + self, data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> Dict[str, Any]: + if data: + kwargs.update(data) + return await self.storage.update_data( + chat_id=self.chat_id, user_id=self.user_id, data=kwargs + ) + + async def clear(self) -> None: + await self.set_state(state=None) + await self.set_data({}) diff --git a/aiogram/dispatcher/fsm/engine.py b/aiogram/dispatcher/fsm/engine.py new file mode 100644 index 00000000..bb9f83d5 --- /dev/null +++ b/aiogram/dispatcher/fsm/engine.py @@ -0,0 +1,6 @@ +from enum import Enum, auto + + +class FSMStrategy(Enum): + CHAT = auto() + USER = auto() diff --git a/aiogram/dispatcher/fsm/middleware.py b/aiogram/dispatcher/fsm/middleware.py new file mode 100644 index 00000000..161976df --- /dev/null +++ b/aiogram/dispatcher/fsm/middleware.py @@ -0,0 +1,52 @@ +from typing import Any, Awaitable, Callable, Dict, Optional + +from aiogram.dispatcher.fsm.context import FSMContext +from aiogram.dispatcher.fsm.engine import FSMStrategy +from aiogram.dispatcher.fsm.storage.base import BaseStorage +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.types import Update + + +class FSMContextMiddleware(BaseMiddleware[Update]): + def __init__( + self, + storage: BaseStorage, + strategy: FSMStrategy = FSMStrategy.USER, + isolate_events: bool = True, + ) -> None: + self.storage = storage + self.strategy = strategy + self.isolate_events = isolate_events + + async def __call__( + self, + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any], + ) -> Any: + context = self._resolve_context(data) + 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(): + return await handler(event, data) + return await handler(event, data) + + def _resolve_context(self, data: Dict[str, Any]) -> Optional[FSMContext]: + user = data.get("event_from_user") + chat = data.get("event_chat") + user_id = chat.id if chat else None + chat_id = user.id if user else None + + if chat_id is None: + chat_id = user_id + if self.strategy == FSMStrategy.CHAT: + user_id = chat_id + + if chat_id is not None and user_id is not None: + return self.get_context(chat_id=chat_id, user_id=user_id) + return None + + def get_context(self, chat_id: int, user_id: int) -> FSMContext: + return FSMContext(storage=self.storage, chat_id=chat_id, user_id=user_id) diff --git a/aiogram/dispatcher/fsm/state.py b/aiogram/dispatcher/fsm/state.py new file mode 100644 index 00000000..c0814589 --- /dev/null +++ b/aiogram/dispatcher/fsm/state.py @@ -0,0 +1,139 @@ +import inspect +from typing import Any, Optional, Tuple, Type, no_type_check + +from ...types import TelegramObject + + +class State: + """ + State object + """ + + def __init__(self, state: Optional[str] = None, group_name: Optional[str] = None) -> None: + self._state = state + self._group_name = group_name + self._group: Optional[Type[StatesGroup]] = None + + @property + def group(self) -> "Type[StatesGroup]": + if not self._group: + raise RuntimeError("This state is not in any group.") + return self._group + + @property + def state(self) -> Optional[str]: + if self._state is None or self._state == "*": + return self._state + + if self._group_name is None and self._group: + group = self._group.__full_group_name__ + elif self._group_name: + group = self._group_name + else: + group = "@" + + return f"{group}:{self._state}" + + def set_parent(self, group: "Type[StatesGroup]") -> None: + if not issubclass(group, StatesGroup): + raise ValueError("Group must be subclass of StatesGroup") + self._group = group + + def __set_name__(self, owner: "Type[StatesGroup]", name: str) -> None: + if self._state is None: + self._state = name + self.set_parent(owner) + + def __str__(self) -> str: + return f"" + + __repr__ = __str__ + + def __call__(self, event: TelegramObject, raw_state: Optional[str] = None) -> bool: + if self.state == "*": + return True + return raw_state == self.state + + +class StatesGroupMeta(type): + __parent__: "Optional[Type[StatesGroup]]" + __childs__: "Tuple[Type[StatesGroup], ...]" + __states__: Tuple[State, ...] + __state_names__: Tuple[str, ...] + + @no_type_check + def __new__(mcs, name, bases, namespace, **kwargs): + cls = super(StatesGroupMeta, mcs).__new__(mcs, name, bases, namespace) + + states = [] + childs = [] + + for name, arg in namespace.items(): + if isinstance(arg, State): + states.append(arg) + elif inspect.isclass(arg) and issubclass(arg, StatesGroup): + childs.append(arg) + arg.__parent__ = cls + + cls.__parent__ = None + cls.__childs__ = tuple(childs) + cls.__states__ = tuple(states) + cls.__state_names__ = tuple(state.state for state in states) + + return cls + + @property + def __full_group_name__(cls) -> str: + if cls.__parent__: + return ".".join((cls.__parent__.__full_group_name__, cls.__name__)) + return cls.__name__ + + @property + def __all_childs__(cls) -> Tuple[Type["StatesGroup"], ...]: + result = cls.__childs__ + for child in cls.__childs__: + result += child.__childs__ + return result + + @property + def __all_states__(cls) -> Tuple[State, ...]: + result = cls.__states__ + for group in cls.__childs__: + result += group.__all_states__ + return result + + @property + def __states_names__(cls) -> Tuple[str, ...]: + return tuple(state.state for state in cls.__states__ if state.state) + + @property + def __all_states_names__(cls) -> Tuple[str, ...]: + return tuple(state.state for state in cls.__all_states__ if state.state) + + @no_type_check + def get_root(cls) -> "StatesGroup": + if cls.__parent__ is None: + return cls + return cls.__parent__.get_root() + + def __contains__(cls, item: Any) -> bool: + if isinstance(item, str): + return item in cls.__all_states_names__ + if isinstance(item, State): + return item in cls.__all_states__ + if isinstance(item, StatesGroup): + return item in cls.__all_childs__ + return False + + def __str__(self) -> str: + return f"" + + +class StatesGroup(metaclass=StatesGroupMeta): + pass + # def __call__(cls, event: TelegramObject, raw_state: Optional[str] = None) -> bool: + # return raw_state in cls.__all_states_names__ + + +default_state = State() +any_state = State(state="*") diff --git a/aiogram/dispatcher/fsm/storage/__init__.py b/aiogram/dispatcher/fsm/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/fsm/storage/base.py b/aiogram/dispatcher/fsm/storage/base.py new file mode 100644 index 00000000..29cce9ed --- /dev/null +++ b/aiogram/dispatcher/fsm/storage/base.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod +from contextlib import asynccontextmanager +from typing import Any, AsyncGenerator, Dict, Optional, Union + +from aiogram.dispatcher.fsm.state import State + +StateType = Optional[Union[str, State]] + + +class BaseStorage(ABC): + @abstractmethod + @asynccontextmanager + async def lock(self) -> AsyncGenerator[None, None]: + yield None + + @abstractmethod + async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None: + pass + + @abstractmethod + async def get_state(self, chat_id: int, user_id: int) -> Optional[str]: + pass + + @abstractmethod + async def set_data(self, chat_id: int, user_id: int, data: Dict[str, Any]) -> None: + pass + + @abstractmethod + async def get_data(self, chat_id: int, user_id: int) -> Dict[str, Any]: + pass + + async def update_data( + self, chat_id: int, user_id: int, data: Dict[str, Any] + ) -> Dict[str, Any]: + current_data = await self.get_data(chat_id=chat_id, user_id=user_id) + current_data.update(data) + await self.set_data(chat_id=chat_id, user_id=user_id, data=current_data) + return current_data.copy() diff --git a/aiogram/dispatcher/fsm/storage/memory.py b/aiogram/dispatcher/fsm/storage/memory.py new file mode 100644 index 00000000..46b6d60b --- /dev/null +++ b/aiogram/dispatcher/fsm/storage/memory.py @@ -0,0 +1,39 @@ +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 aiogram.dispatcher.fsm.state import State +from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType + + +@dataclass +class MemoryStorageRecord: + data: Dict[str, Any] = field(default_factory=dict) + state: Optional[str] = None + + +class MemoryStorage(BaseStorage): + def __init__(self) -> None: + self.storage: DefaultDict[int, DefaultDict[int, MemoryStorageRecord]] = defaultdict( + lambda: defaultdict(MemoryStorageRecord) + ) + self._lock = Lock() + + @asynccontextmanager + async def lock(self) -> AsyncGenerator[None, None]: + async with self._lock: + yield None + + async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None: + self.storage[chat_id][user_id].state = state.state if isinstance(state, State) else state + + async def get_state(self, chat_id: int, user_id: int) -> Optional[str]: + return self.storage[chat_id][user_id].state + + async def set_data(self, chat_id: int, user_id: int, data: Dict[str, Any]) -> None: + self.storage[chat_id][user_id].data = data.copy() + + async def get_data(self, chat_id: int, user_id: int) -> Dict[str, Any]: + return self.storage[chat_id][user_id].data.copy() diff --git a/aiogram/dispatcher/handler/__init__.py b/aiogram/dispatcher/handler/__init__.py index ee6ccc98..7388a80c 100644 --- a/aiogram/dispatcher/handler/__init__.py +++ b/aiogram/dispatcher/handler/__init__.py @@ -1,5 +1,6 @@ from .base import BaseHandler, BaseHandlerMixin from .callback_query import CallbackQueryHandler +from .chat_member import ChatMemberUpdated from .chosen_inline_result import ChosenInlineResultHandler from .error import ErrorHandler from .inline_query import InlineQueryHandler @@ -7,7 +8,6 @@ from .message import MessageHandler, MessageHandlerCommandMixin from .poll import PollHandler from .pre_checkout_query import PreCheckoutQueryHandler from .shipping_query import ShippingQueryHandler -from .chat_member import ChatMemberUpdated __all__ = ( "BaseHandler", diff --git a/aiogram/dispatcher/handler/callback_query.py b/aiogram/dispatcher/handler/callback_query.py index 8f129b21..690cdfea 100644 --- a/aiogram/dispatcher/handler/callback_query.py +++ b/aiogram/dispatcher/handler/callback_query.py @@ -7,17 +7,37 @@ from aiogram.types import CallbackQuery, Message, User class CallbackQueryHandler(BaseHandler[CallbackQuery], ABC): """ - Base class for callback query handlers + There is base class for callback query handlers. + + Example: + .. code-block:: python + + from aiogram.handlers import CallbackQueryHandler + + ... + + @router.callback_query() + class MyHandler(CallbackQueryHandler): + async def handle(self) -> Any: ... """ @property def from_user(self) -> User: + """ + Is alias for `event.from_user` + """ return self.event.from_user @property def message(self) -> Optional[Message]: + """ + Is alias for `event.message` + """ return self.event.message @property def callback_data(self) -> Optional[str]: + """ + Is alias for `event.data` + """ return self.event.data diff --git a/aiogram/dispatcher/handler/chat_member.py b/aiogram/dispatcher/handler/chat_member.py new file mode 100644 index 00000000..92793c98 --- /dev/null +++ b/aiogram/dispatcher/handler/chat_member.py @@ -0,0 +1,14 @@ +from abc import ABC + +from aiogram.dispatcher.handler import BaseHandler +from aiogram.types import ChatMemberUpdated, User + + +class ChatMemberHandler(BaseHandler[ChatMemberUpdated], ABC): + """ + Base class for chat member updated events + """ + + @property + def from_user(self) -> User: + return self.event.from_user diff --git a/aiogram/dispatcher/middlewares/user_context.py b/aiogram/dispatcher/middlewares/user_context.py index 1434eec4..c7b0a459 100644 --- a/aiogram/dispatcher/middlewares/user_context.py +++ b/aiogram/dispatcher/middlewares/user_context.py @@ -14,6 +14,10 @@ class UserContextMiddleware(BaseMiddleware[Update]): ) -> Any: chat, user = self.resolve_event_context(event=event) with self.context(chat=chat, user=user): + if user is not None: + data["event_from_user"] = user + if chat is not None: + data["event_chat"] = chat return await handler(event, data) @contextmanager diff --git a/aiogram/utils/help/__init__.py b/aiogram/utils/help/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/utils/help/engine.py b/aiogram/utils/help/engine.py new file mode 100644 index 00000000..c91c8899 --- /dev/null +++ b/aiogram/utils/help/engine.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod +from collections import Generator +from typing import Dict, List + +from aiogram.utils.help.record import CommandRecord + + +class BaseHelpBackend(ABC): + @abstractmethod + def add(self, record: CommandRecord) -> None: + pass + + @abstractmethod + def search(self, value: str) -> CommandRecord: + pass + + @abstractmethod + def all(self) -> Generator[CommandRecord, None, None]: + pass + + def __getitem__(self, item: str) -> CommandRecord: + return self.search(item) + + def __iter__(self) -> Generator[CommandRecord, None, None]: + return self.all() + + +class MappingBackend(BaseHelpBackend): + def __init__(self, search_empty_prefix: bool = True) -> None: + self._records: List[CommandRecord] = [] + self._mapping: Dict[str, CommandRecord] = {} + self.search_empty_prefix = search_empty_prefix + + def search(self, value: str) -> CommandRecord: + return self._mapping[value] + + def add(self, record: CommandRecord) -> None: + new_records = {} + for key in record.as_keys(with_empty_prefix=self.search_empty_prefix): + if key in self._mapping: + raise ValueError(f"Key '{key}' is already indexed") + new_records[key] = record + self._mapping.update(new_records) + self._records.append(record) + self._records.sort(key=lambda rec: (rec.priority, rec.commands[0])) + + def all(self) -> Generator[CommandRecord, None, None]: + yield from self._records diff --git a/aiogram/utils/help/manager.py b/aiogram/utils/help/manager.py new file mode 100644 index 00000000..b9aefb0e --- /dev/null +++ b/aiogram/utils/help/manager.py @@ -0,0 +1,113 @@ +from typing import Any, Optional, Tuple + +from aiogram import Bot, Router +from aiogram.dispatcher.filters import Command, CommandObject +from aiogram.types import BotCommand, Message +from aiogram.utils.help.engine import BaseHelpBackend, MappingBackend +from aiogram.utils.help.record import DEFAULT_PREFIXES, CommandRecord +from aiogram.utils.help.render import BaseHelpRenderer, SimpleRenderer + + +class HelpManager: + def __init__( + self, + backend: Optional[BaseHelpBackend] = None, + renderer: Optional[BaseHelpRenderer] = None, + ) -> None: + if backend is None: + backend = MappingBackend() + if renderer is None: + renderer = SimpleRenderer() + self._backend = backend + self._renderer = renderer + + def add( + self, + *commands: str, + help: str, + description: Optional[str] = None, + prefix: str = DEFAULT_PREFIXES, + ignore_case: bool = False, + ignore_mention: bool = False, + priority: int = 0, + ) -> CommandRecord: + record = CommandRecord( + commands=commands, + help=help, + description=description, + prefix=prefix, + ignore_case=ignore_case, + ignore_mention=ignore_mention, + priority=priority, + ) + self._backend.add(record) + return record + + def command( + self, + *commands: str, + help: str, + description: Optional[str] = None, + prefix: str = DEFAULT_PREFIXES, + ignore_case: bool = False, + ignore_mention: bool = False, + priority: int = 0, + ) -> Command: + record = self.add( + *commands, + help=help, + description=description, + prefix=prefix, + ignore_case=ignore_case, + ignore_mention=ignore_mention, + priority=priority, + ) + return record.as_filter() + + def mount_help( + self, + router: Router, + *commands: str, + prefix: str = "/", + help: str = "Help", + description: str = "Show help for the commands\n" + "Also you can use '/help command' for get help for specific command", + as_reply: bool = True, + filters: Tuple[Any, ...] = (), + **kw_filters: Any, + ) -> Any: + if not commands: + commands = ("help",) + help_filter = self.command(*commands, prefix=prefix, help=help, description=description) + + async def handle(message: Message, command: CommandObject, **kwargs: Any) -> Any: + return await self._handle_help( + message=message, command=command, as_reply=as_reply, **kwargs + ) + + return router.message.register(handle, help_filter, *filters, **kw_filters) + + async def _handle_help( + self, + message: Message, + bot: Bot, + command: CommandObject, + as_reply: bool = True, + **kwargs: Any, + ) -> Any: + lines = self._renderer.render(backend=self._backend, command=command, **kwargs) + text = "\n".join(line or "" for line in lines) + return await bot.send_message( + chat_id=message.chat.id, + text=text, + reply_to_message_id=message.message_id if as_reply else None, + ) + + async def set_bot_commands(self, bot: Bot) -> bool: + return await bot.set_my_commands( + commands=[ + BotCommand(command=record.commands[0], description=record.help) + for record in self._backend + if "/" in record.prefix + ] + ) diff --git a/aiogram/utils/help/record.py b/aiogram/utils/help/record.py new file mode 100644 index 00000000..beb00468 --- /dev/null +++ b/aiogram/utils/help/record.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +from itertools import product +from typing import Generator, Optional, Sequence + +from aiogram.dispatcher.filters import Command + +DEFAULT_PREFIXES = "/" + + +@dataclass +class CommandRecord: + commands: Sequence[str] + help: str + description: Optional[str] = None + prefix: str = DEFAULT_PREFIXES + ignore_case: bool = False + ignore_mention: bool = False + priority: int = 0 + + def as_filter(self) -> Command: + return Command(commands=self.commands, commands_prefix=self.prefix) + + def as_keys(self, with_empty_prefix: bool = False) -> Generator[str, None, None]: + for command in self.commands: + yield command + for prefix in self.prefix: + yield f"{prefix}{command}" + + def as_command(self) -> str: + return f"{self.prefix[0]}{self.commands[0]}" + + def as_aliases(self) -> str: + return ", ".join(f"{p}{c}" for c, p in product(self.commands, self.prefix)) diff --git a/aiogram/utils/help/render.py b/aiogram/utils/help/render.py new file mode 100644 index 00000000..8b15d0af --- /dev/null +++ b/aiogram/utils/help/render.py @@ -0,0 +1,64 @@ +from abc import ABC, abstractmethod +from typing import Any, Generator, Optional + +from aiogram.dispatcher.filters import CommandObject +from aiogram.utils.help.engine import BaseHelpBackend + + +class BaseHelpRenderer(ABC): + @abstractmethod + def render( + self, backend: BaseHelpBackend, command: CommandObject, **kwargs: Any + ) -> Generator[Optional[str], None, None]: + pass + + +class SimpleRenderer(BaseHelpRenderer): + def __init__( + self, + help_title: str = "Commands list:", + help_footer: str = "", + aliases_line: str = "Aliases", + command_title: str = "Help for command:", + unknown_command: str = "Command not found", + ): + self.help_title = help_title + self.help_footer = help_footer + self.aliases_line = aliases_line + self.command_title = command_title + self.unknown_command = unknown_command + + def render_help(self, backend: BaseHelpBackend) -> Generator[Optional[str], None, None]: + yield self.help_title + + for command in backend: + yield f"{command.prefix[0]}{command.commands[0]} - {command.help}" + + if self.help_footer: + yield None + yield self.help_footer + + def render_command_help( + self, backend: BaseHelpBackend, target: str + ) -> Generator[Optional[str], None, None]: + try: + record = backend[target] + except KeyError: + yield f"{self.command_title} {target}" + yield self.unknown_command + return + + yield f"{self.command_title} {record.as_command()}" + if len(record.commands) > 1 or len(record.prefix) > 1: + yield f"{self.aliases_line}: {record.as_aliases()}" + yield record.help + yield None + yield record.description + + def render( + self, backend: BaseHelpBackend, command: CommandObject, **kwargs: Any + ) -> Generator[Optional[str], None, None]: + if command.args: + yield from self.render_command_help(backend=backend, target=command.args) + else: + yield from self.render_help(backend=backend) diff --git a/aiogram/utils/markup.py b/aiogram/utils/markup.py new file mode 100644 index 00000000..32169104 --- /dev/null +++ b/aiogram/utils/markup.py @@ -0,0 +1,224 @@ +from itertools import chain +from itertools import cycle as repeat_all +from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar + +from aiogram.types import InlineKeyboardButton, KeyboardButton + +ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton) +T = TypeVar("T") +MAX_WIDTH = 8 +MIN_WIDTH = 1 +MAX_BUTTONS = 100 + + +class MarkupConstructor(Generic[ButtonType]): + def __init__( + self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None + ) -> None: + if not issubclass(button_type, (InlineKeyboardButton, KeyboardButton)): + raise ValueError(f"Button type {button_type} are not allowed here") + self._button_type: Type[ButtonType] = button_type + if markup: + self._validate_markup(markup) + else: + markup = [] + self._markup: List[List[ButtonType]] = markup + + @property + def buttons(self) -> Generator[ButtonType, None, None]: + """ + Get flatten set of all buttons + + :return: + """ + yield from chain.from_iterable(self.export()) + + def _validate_button(self, button: ButtonType) -> bool: + """ + Check that button item has correct type + + :param button: + :return: + """ + allowed = self._button_type + if not isinstance(button, allowed): + raise ValueError( + f"{button!r} should be type {allowed.__name__!r} not {type(button).__name__!r}" + ) + return True + + def _validate_buttons(self, *buttons: ButtonType) -> bool: + """ + Check that all passed button has correct type + + :param buttons: + :return: + """ + return all(map(self._validate_button, buttons)) + + def _validate_row(self, row: List[ButtonType]) -> bool: + """ + Check that row of buttons are correct + Row can be only list of allowed button types and has length 0 <= n <= 8 + + :param row: + :return: + """ + if not isinstance(row, list): + raise ValueError( + f"Row {row!r} should be type 'List[{self._button_type.__name__}]' not type {type(row).__name__}" + ) + if len(row) > MAX_WIDTH: + raise ValueError(f"Row {row!r} is too long (MAX_WIDTH={MAX_WIDTH})") + self._validate_buttons(*row) + return True + + def _validate_markup(self, markup: List[List[ButtonType]]) -> bool: + """ + Check that passed markup has correct data structure + Markup is list of lists of buttons + + :param markup: + :return: + """ + count = 0 + if not isinstance(markup, list): + raise ValueError( + f"Markup should be type 'List[List[{self._button_type.__name__}]]' not type {type(markup).__name__!r}" + ) + for row in markup: + self._validate_row(row) + count += len(row) + if count > MAX_BUTTONS: + raise ValueError(f"Too much buttons detected Max allowed count - {MAX_BUTTONS}") + return True + + def _validate_size(self, size: Any) -> int: + """ + Validate that passed size is legit + + :param size: + :return: + """ + if not isinstance(size, int): + raise ValueError("Only int sizes are allowed") + if size not in range(MIN_WIDTH, MAX_WIDTH + 1): + raise ValueError(f"Row size {size} are not allowed") + return size + + def copy(self: "MarkupConstructor[ButtonType]") -> "MarkupConstructor[ButtonType]": + """ + Make full copy of current constructor with markup + + :return: + """ + return self.__class__(self._button_type, markup=self.export()) + + def export(self) -> List[List[ButtonType]]: + """ + Export configured markup as list of lists of buttons + + .. code-block:: python + + >>> constructor = MarkupConstructor(button_type=InlineKeyboardButton) + >>> ... # Add buttons to constructor + >>> markup = InlineKeyboardMarkup(inline_keyboard=constructor.export()) + + :return: + """ + return self._markup.copy() + + def add(self, *buttons: ButtonType) -> "MarkupConstructor[ButtonType]": + """ + Add one or many buttons to markup. + + :param buttons: + :return: + """ + self._validate_buttons(*buttons) + markup = self.export() + + # Try to add new buttons to the end of last row if it possible + if markup and len(markup[-1]) < MAX_WIDTH: + last_row = markup[-1] + pos = MAX_WIDTH - len(last_row) + head, buttons = buttons[:pos], buttons[pos:] + last_row.extend(head) + + # Separate buttons to exclusive rows with max possible row width + while buttons: + row, buttons = buttons[:MAX_WIDTH], buttons[MAX_WIDTH:] + markup.append(list(row)) + + self._markup = markup + return self + + def row(self, *buttons: ButtonType, width: int = MAX_WIDTH) -> "MarkupConstructor[ButtonType]": + """ + Add row to markup + + When too much buttons is passed it will be separated to many rows + + :param buttons: + :param width: + :return: + """ + self._validate_size(width) + self._validate_buttons(*buttons) + self._markup.extend( + list(buttons[pos : pos + width]) for pos in range(0, len(buttons), width) + ) + return self + + def adjust(self, *sizes: int, repeat: bool = False) -> "MarkupConstructor[ButtonType]": + """ + Adjust previously added buttons to specific row sizes. + + By default when the sum of passed sizes is lower than buttons count the last + one size will be used for tail of the markup. + If repeat=True is passed - all sizes will be cycled when available more buttons count than all sizes + + :param sizes: + :param repeat: + :return: + """ + if not sizes: + sizes = (MAX_WIDTH,) + + validated_sizes = map(self._validate_size, sizes) + sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes) + size = next(sizes_iter) + + markup = [] + row: List[ButtonType] = [] + for button in self.buttons: + if len(row) >= size: + markup.append(row) + size = next(sizes_iter) + row = [] + row.append(button) + if row: + markup.append(row) + self._markup = markup + return self + + def button(self, **kwargs: Any) -> "MarkupConstructor[ButtonType]": + button = self._button_type(**kwargs) + return self.add(button) + + +def repeat_last(items: Iterable[T]) -> Generator[T, None, None]: + items_iter = iter(items) + try: + value = next(items_iter) + except StopIteration: + return + yield value + finished = False + while True: + if not finished: + try: + value = next(items_iter) + except StopIteration: + finished = True + yield value diff --git a/docs2/dispatcher/class_based_handlers/base.rst b/docs2/dispatcher/class_based_handlers/base.rst index 3c3e910d..0d478224 100644 --- a/docs2/dispatcher/class_based_handlers/base.rst +++ b/docs2/dispatcher/class_based_handlers/base.rst @@ -6,7 +6,7 @@ BaseHandler Base handler is generic abstract class and should be used in all other class-based handlers. -Import: :code:`from aiogram.hanler import BaseHandler` +Import: :code:`from aiogram.handler import BaseHandler` By default you will need to override only method :code:`async def handle(self) -> Any: ...` diff --git a/docs2/dispatcher/class_based_handlers/callback_query.rst b/docs2/dispatcher/class_based_handlers/callback_query.rst index 292f111d..a8cdf152 100644 --- a/docs2/dispatcher/class_based_handlers/callback_query.rst +++ b/docs2/dispatcher/class_based_handlers/callback_query.rst @@ -1,27 +1,9 @@ -==================== +#################### CallbackQueryHandler -==================== - -There is base class for callback query handlers. - -Simple usage -============ -.. code-block:: python - - from aiogram.handlers import CallbackQueryHandler - - ... - - @router.callback_query() - class MyHandler(CallbackQueryHandler): - async def handle(self) -> Any: ... +#################### -Extension -========= - -This base handler is subclass of :ref:`BaseHandler ` with some extensions: - -- :code:`self.from_user` is alias for :code:`self.event.from_user` -- :code:`self.message` is alias for :code:`self.event.message` -- :code:`self.callback_data` is alias for :code:`self.event.data` +.. automodule:: aiogram.dispatcher.handler.callback_query + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs2/dispatcher/class_based_handlers/chat_member.rst b/docs2/dispatcher/class_based_handlers/chat_member.rst new file mode 100644 index 00000000..9df18a0c --- /dev/null +++ b/docs2/dispatcher/class_based_handlers/chat_member.rst @@ -0,0 +1,28 @@ +================= +ChatMemberHandler +================= + +There is base class for chat member updated events. + +Simple usage +============ + +.. code-block:: python + + from aiogram.handlers import ChatMemberHandler + + ... + + @router.chat_member() + @router.my_chat_member() + class MyHandler(ChatMemberHandler): + async def handle(self) -> Any: ... + + +Extension +========= + +This base handler is subclass of :ref:`BaseHandler ` with some extensions: + +- :code:`self.chat` is alias for :code:`self.event.chat` + diff --git a/poetry.lock b/poetry.lock index d91a7a5f..437271d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -261,17 +261,17 @@ python-versions = "*" [[package]] name = "flake8" -version = "3.8.4" +version = "3.9.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.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-html" @@ -303,14 +303,6 @@ sphinx = ">=3.0,<4.0" doc = ["myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] test = ["pytest", "pytest-cov", "pytest-xdist"] -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "identify" version = "1.5.13" @@ -403,7 +395,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.7.0" +version = "5.8.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -443,14 +435,6 @@ MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] -[[package]] -name = "joblib" -version = "1.0.0" -description = "Lightweight pipelining with Python functions" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "livereload" version = "2.6.3" @@ -463,36 +447,6 @@ python-versions = "*" six = "*" tornado = {version = "*", markers = "python_version > \"2.7\""} -[[package]] -name = "lunr" -version = "0.5.8" -description = "A Python implementation of Lunr.js" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -future = ">=0.16.0" -nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} -six = ">=1.11.0" - -[package.extras] -languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] - -[[package]] -name = "lxml" -version = "4.6.2" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] -source = ["Cython (>=0.29.7)"] - [[package]] name = "magic-filter" version = "0.1.2" @@ -542,57 +496,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "mkautodoc" -version = "0.1.0" -description = "AutoDoc for MarkDown" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mkdocs" -version = "1.1.2" -description = "Project documentation with Markdown." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -click = ">=3.3" -Jinja2 = ">=2.10.1" -livereload = ">=2.5.1" -lunr = {version = "0.5.8", extras = ["languages"]} -Markdown = ">=3.2.1" -PyYAML = ">=3.10" -tornado = ">=5.0" - -[[package]] -name = "mkdocs-material" -version = "6.2.5" -description = "A Material Design theme for MkDocs" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -markdown = ">=3.2" -mkdocs = ">=1.1" -mkdocs-material-extensions = ">=1.0" -Pygments = ">=2.4" -pymdown-extensions = ">=7.0" - -[[package]] -name = "mkdocs-material-extensions" -version = "1.0.1" -description = "Extension pack for Python Markdown." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -mkdocs-material = ">=5.0.0" - [[package]] name = "multidict" version = "5.1.0" @@ -603,7 +506,7 @@ python-versions = ">=3.6" [[package]] name = "mypy" -version = "0.800" +version = "0.812" description = "Optional static typing for Python" category = "dev" optional = false @@ -625,28 +528,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "nltk" -version = "3.5" -description = "Natural Language Toolkit" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -click = "*" -joblib = "*" -regex = "*" -tqdm = "*" - -[package.extras] -all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"] -corenlp = ["requests"] -machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - [[package]] name = "nodeenv" version = "1.5.0" @@ -765,7 +646,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycodestyle" -version = "2.6.0" +version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false @@ -786,7 +667,7 @@ typing_extensions = ["typing-extensions (>=3.7.2)"] [[package]] name = "pyflakes" -version = "2.2.0" +version = "2.3.1" description = "passive checker of Python programs" category = "dev" optional = false @@ -821,7 +702,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.2.1" +version = "6.2.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -909,7 +790,7 @@ dev = ["pre-commit", "tox", "pytest-asyncio"] [[package]] name = "pytest-mypy" -version = "0.8.0" +version = "0.8.1" description = "Mypy static type checker plugin for Pytest" category = "dev" optional = false @@ -927,7 +808,7 @@ pytest = ">=3.5" [[package]] name = "python-socks" -version = "1.2.0" +version = "1.2.4" description = "Core proxy (SOCKS4, SOCKS5, HTTP tunneling) functionality for Python" category = "main" optional = false @@ -1199,18 +1080,6 @@ category = "main" optional = false python-versions = ">= 3.5" -[[package]] -name = "tqdm" -version = "4.56.0" -description = "Fast, Extensible Progress Meter" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -telegram = ["requests"] - [[package]] name = "traitlets" version = "5.0.5" @@ -1256,11 +1125,16 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvloop" -version = "0.14.0" +version = "0.15.2" description = "Fast implementation of asyncio event loop on top of libuv" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" + +[package.extras] +dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)", "aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] +test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] [[package]] name = "virtualenv" @@ -1322,7 +1196,7 @@ proxy = ["aiohttp-socks"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "ee4cbf4fb0a62ec777bec179dad21e7cea1ced466ab2a5424f54f8764f2955d3" +content-hash = "eafca0c03b9e34dc7878f0e9adace48dfeca600208b988946153ddfa0f3bddaf" [metadata.files] aiofiles = [ @@ -1511,8 +1385,8 @@ filelock = [ {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] flake8 = [ - {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, - {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, + {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"}, + {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"}, ] flake8-html = [ {file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"}, @@ -1522,9 +1396,6 @@ furo = [ {file = "furo-2020.12.30b24-py3-none-any.whl", hash = "sha256:251dadee4dee96dddf2dc9b5243b88212e16923f53397bf12bc98574918bda41"}, {file = "furo-2020.12.30b24.tar.gz", hash = "sha256:30171899c9c06d692a778e6daf6cb2e5cbb05efc6006e1692e5e776007dc8a8c"}, ] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] identify = [ {file = "identify-1.5.13-py2.py3-none-any.whl", hash = "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4"}, {file = "identify-1.5.13.tar.gz", hash = "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66"}, @@ -1554,8 +1425,8 @@ ipython-genutils = [ {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] isort = [ - {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, - {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, ] jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, @@ -1565,56 +1436,9 @@ jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] -joblib = [ - {file = "joblib-1.0.0-py3-none-any.whl", hash = "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f"}, - {file = "joblib-1.0.0.tar.gz", hash = "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"}, -] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] -lunr = [ - {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, - {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, -] -lxml = [ - {file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"}, - {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"}, - {file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"}, - {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"}, - {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"}, - {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"}, - {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"}, - {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"}, - {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"}, - {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"}, - {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"}, - {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"}, - {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"}, - {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"}, - {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"}, - {file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"}, - {file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"}, - {file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"}, -] magic-filter = [ {file = "magic-filter-0.1.2.tar.gz", hash = "sha256:dfd1a778493083ac1355791d1716c3beb6629598739f2c2ec078815952282c1d"}, {file = "magic_filter-0.1.2-py3-none-any.whl", hash = "sha256:16d0c96584f0660fd7fa94b6cd16f92383616208a32568bf8f95a57fc1a69e9d"}, @@ -1645,41 +1469,45 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -mkautodoc = [ - {file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"}, -] -mkdocs = [ - {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, - {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, -] -mkdocs-material = [ - {file = "mkdocs-material-6.2.5.tar.gz", hash = "sha256:26900f51e177eb7dcfc8140ffe33c71b22ffa2920271e093679f0670b78e7e8b"}, - {file = "mkdocs_material-6.2.5-py2.py3-none-any.whl", hash = "sha256:04574cbaeb12671da66cd58904d6066dd269239f4a1bdb819c2c6e1ac9d9947a"}, -] -mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, - {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, -] multidict = [ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, @@ -1720,36 +1548,33 @@ multidict = [ {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, ] mypy = [ - {file = "mypy-0.800-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1c84c65ff6d69fb42958ece5b1255394714e0aac4df5ffe151bc4fe19c7600a"}, - {file = "mypy-0.800-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:947126195bfe4709c360e89b40114c6746ae248f04d379dca6f6ab677aa07641"}, - {file = "mypy-0.800-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b95068a3ce3b50332c40e31a955653be245666a4bc7819d3c8898aa9fb9ea496"}, - {file = "mypy-0.800-cp35-cp35m-win_amd64.whl", hash = "sha256:ca7ad5aed210841f1e77f5f2f7d725b62c78fa77519312042c719ed2ab937876"}, - {file = "mypy-0.800-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e32b7b282c4ed4e378bba8b8dfa08e1cfa6f6574067ef22f86bee5b1039de0c9"}, - {file = "mypy-0.800-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e497a544391f733eca922fdcb326d19e894789cd4ff61d48b4b195776476c5cf"}, - {file = "mypy-0.800-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:5615785d3e2f4f03ab7697983d82c4b98af5c321614f51b8f1034eb9ebe48363"}, - {file = "mypy-0.800-cp36-cp36m-win_amd64.whl", hash = "sha256:2b216eacca0ec0ee124af9429bfd858d5619a0725ee5f88057e6e076f9eb1a7b"}, - {file = "mypy-0.800-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e3b8432f8df19e3c11235c4563a7250666dc9aa7cdda58d21b4177b20256ca9f"}, - {file = "mypy-0.800-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d16c54b0dffb861dc6318a8730952265876d90c5101085a4bc56913e8521ba19"}, - {file = "mypy-0.800-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d2fc8beb99cd88f2d7e20d69131353053fbecea17904ee6f0348759302c52fa"}, - {file = "mypy-0.800-cp37-cp37m-win_amd64.whl", hash = "sha256:aa9d4901f3ee1a986a3a79fe079ffbf7f999478c281376f48faa31daaa814e86"}, - {file = "mypy-0.800-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:319ee5c248a7c3f94477f92a729b7ab06bf8a6d04447ef3aa8c9ba2aa47c6dcf"}, - {file = "mypy-0.800-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:74f5aa50d0866bc6fb8e213441c41e466c86678c800700b87b012ed11c0a13e0"}, - {file = "mypy-0.800-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a301da58d566aca05f8f449403c710c50a9860782148332322decf73a603280b"}, - {file = "mypy-0.800-cp38-cp38-win_amd64.whl", hash = "sha256:b9150db14a48a8fa114189bfe49baccdff89da8c6639c2717750c7ae62316738"}, - {file = "mypy-0.800-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5fdf935a46aa20aa937f2478480ebf4be9186e98e49cc3843af9a5795a49a25"}, - {file = "mypy-0.800-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6f8425fecd2ba6007e526209bb985ce7f49ed0d2ac1cc1a44f243380a06a84fb"}, - {file = "mypy-0.800-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5ff616787122774f510caeb7b980542a7cc2222be3f00837a304ea85cd56e488"}, - {file = "mypy-0.800-cp39-cp39-win_amd64.whl", hash = "sha256:90b6f46dc2181d74f80617deca611925d7e63007cf416397358aa42efb593e07"}, - {file = "mypy-0.800-py3-none-any.whl", hash = "sha256:3e0c159a7853e3521e3f582adb1f3eac66d0b0639d434278e2867af3a8c62653"}, - {file = "mypy-0.800.tar.gz", hash = "sha256:e0202e37756ed09daf4b0ba64ad2c245d357659e014c3f51d8cd0681ba66940a"}, + {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, + {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, + {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, + {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, + {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, + {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, + {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, + {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, + {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, + {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, + {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, + {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, + {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, + {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, + {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, + {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, + {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, + {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, + {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, + {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, + {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, + {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -nltk = [ - {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, -] nodeenv = [ {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, @@ -1795,8 +1620,8 @@ py = [ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pydantic = [ {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, @@ -1823,8 +1648,8 @@ pydantic = [ {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, ] pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, @@ -1839,8 +1664,8 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, - {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, + {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, + {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, @@ -1863,12 +1688,12 @@ pytest-mock = [ {file = "pytest_mock-3.5.1-py3-none-any.whl", hash = "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e"}, ] pytest-mypy = [ - {file = "pytest-mypy-0.8.0.tar.gz", hash = "sha256:63d418a4fea7d598ac40b659723c00804d16a251d90a5cfbca213eeba5aaf01c"}, - {file = "pytest_mypy-0.8.0-py3-none-any.whl", hash = "sha256:8d2112972c1debf087943f48963a0daf04f3424840aea0cf437cc97053b1b0ef"}, + {file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"}, + {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"}, ] python-socks = [ - {file = "python-socks-1.2.0.tar.gz", hash = "sha256:3054a8afa984a35144198e00fed1144eeae3287cc231ac7db3908d32ab642cd4"}, - {file = "python_socks-1.2.0-py3-none-any.whl", hash = "sha256:26e45b29e18ab7a28ad646e82d3e47a32fe13942b0b1c75ae3f6fe9e5c03efcb"}, + {file = "python-socks-1.2.4.tar.gz", hash = "sha256:7d0ef2578cead9f762b71317d25a6c118fabaf79535555e75b3e102f5158ddd8"}, + {file = "python_socks-1.2.4-py3-none-any.whl", hash = "sha256:9f12e8fe78629b87543fad0e4ea0ccf103a4fad6a7872c5d0ecb36d9903fa548"}, ] pytz = [ {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, @@ -1881,18 +1706,26 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, @@ -2051,10 +1884,6 @@ tornado = [ {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] -tqdm = [ - {file = "tqdm-4.56.0-py2.py3-none-any.whl", hash = "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a"}, - {file = "tqdm-4.56.0.tar.gz", hash = "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65"}, -] traitlets = [ {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, @@ -2101,15 +1930,16 @@ urllib3 = [ {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, ] uvloop = [ - {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, - {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, - {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, - {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, - {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, - {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, - {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, - {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, - {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, + {file = "uvloop-0.15.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69"}, + {file = "uvloop-0.15.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"}, + {file = "uvloop-0.15.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d"}, + {file = "uvloop-0.15.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c"}, + {file = "uvloop-0.15.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47"}, + {file = "uvloop-0.15.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c"}, + {file = "uvloop-0.15.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc"}, + {file = "uvloop-0.15.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760"}, + {file = "uvloop-0.15.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c"}, + {file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"}, ] virtualenv = [ {file = "virtualenv-20.4.0-py2.py3-none-any.whl", hash = "sha256:227a8fed626f2f20a6cdb0870054989f82dd27b2560a911935ba905a2a5e0034"}, diff --git a/pyproject.toml b/pyproject.toml index 0c7a8df6..be434fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ aiohttp = "^3.6" pydantic = "^1.5" Babel = "^2.7" aiofiles = "^0.6.0" -uvloop = { version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true } +uvloop = "^0.15.2" async_lru = "^1.0" aiohttp-socks = { version = "^0.5.5", optional = true } typing-extensions = { version = "^3.7.4", python = "<3.8" } @@ -51,28 +51,26 @@ sphinx-prompt = { version = "^1.3.0", optional = true } Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true } [tool.poetry.dev-dependencies] -uvloop = { version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'" } -pytest = "^6.1" -pytest-html = "^3.1" +aiohttp-socks = "^0.5" +ipython = "^7.10" +uvloop = { version = "^0.15.2", markers = "sys_platform == 'darwin' or sys_platform == 'linux'" } +black = "^20.8b1" +isort = "^5.8.0" +flake8 = "^3.9.1" +flake8-html = "^0.4.1" +mypy = "^0.812" +pytest = "^6.2.3" +pytest-html = "^3.1.1" pytest-asyncio = "^0.14.0" -pytest-mypy = "^0.8" -pytest-mock = "^3.3" -pytest-cov = "^2.8" -aresponses = "^2.0" -asynctest = { version = "^0.13.0", python = "<3.8" } -isort = "^5.6" -flake8 = "^3.7" -flake8-html = "^0.4.0" -mypy = "^0.800" -mkdocs = "^1.0" -mkdocs-material = "^6.1" -mkautodoc = "^0.1.0" +pytest-mypy = "^0.8.1" +pytest-mock = "^3.5.1" +pytest-cov = "^2.11.1" +aresponses = "^2.1.4" +asynctest = "^0.13.0" +toml = "^0.10.2" pygments = "^2.4" pymdown-extensions = "^8.0" -lxml = "^4.4" -ipython = "^7.10" markdown-include = "^0.6" -aiohttp-socks = "^0.5" pre-commit = "^2.3.0" packaging = "^20.3" typing-extensions = "^3.7.4" @@ -83,8 +81,6 @@ sphinx-copybutton = "^0.3.1" furo = "^2020.11.15-beta.17" sphinx-prompt = "^1.3.0" Sphinx-Substitution-Extensions = "^2020.9.30" -black = "^20.8b1" -toml = "^0.10.2" [tool.poetry.extras] fast = ["uvloop"] diff --git a/tests/test_dispatcher/test_filters/test_content_types.py b/tests/test_dispatcher/test_filters/test_content_types.py index a009acfa..63eb207e 100644 --- a/tests/test_dispatcher/test_filters/test_content_types.py +++ b/tests/test_dispatcher/test_filters/test_content_types.py @@ -23,7 +23,7 @@ class TestContentTypesFilter: def test_validator_empty_list(self): filter_ = ContentTypesFilter(content_types=[]) - assert filter_.content_types == ["text"] + assert filter_.content_types == [] def test_convert_to_list(self): filter_ = ContentTypesFilter(content_types="text")