Added base of FSM. Markup constructor and small refactoring

This commit is contained in:
Alex Root Junior 2021-04-19 00:58:47 +03:00
parent 20d0c979b8
commit a11323478c
27 changed files with 1243 additions and 380 deletions

View file

@ -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"

View file

@ -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<token>/<file_path>`, where :code:`<file_path>` 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)
# =============================================================================================

View file

@ -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)

View file

View file

@ -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({})

View file

@ -0,0 +1,6 @@
from enum import Enum, auto
class FSMStrategy(Enum):
CHAT = auto()
USER = auto()

View file

@ -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)

View file

@ -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"<State '{self.state or ''}'>"
__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"<StatesGroup '{self.__full_group_name__}'>"
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="*")

View file

@ -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()

View file

@ -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()

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

View file

@ -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

View file

@ -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
]
)

View file

@ -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))

View file

@ -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)

224
aiogram/utils/markup.py Normal file
View file

@ -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

View file

@ -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: ...`

View file

@ -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 <cbh-base-handler>` 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

View file

@ -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 <cbh-base-handler>` with some extensions:
- :code:`self.chat` is alias for :code:`self.event.chat`

354
poetry.lock generated
View file

@ -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"},

View file

@ -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"]

View file

@ -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")