Added more aliases, refactor CallbackData factory, added base exceptions classification mechanism

This commit is contained in:
Alex Root Junior 2021-05-25 00:56:44 +03:00
parent 9451a085d1
commit f022b4441c
18 changed files with 364 additions and 664 deletions

View file

@ -1,563 +0,0 @@
"""
- TelegramAPIError
- ValidationError
- Throttled
- BadRequest
- MessageError
- MessageNotModified
- MessageToForwardNotFound
- MessageToDeleteNotFound
- MessageIdentifierNotSpecified
- MessageTextIsEmpty
- MessageCantBeEdited
- MessageCantBeDeleted
- MessageToEditNotFound
- MessageToReplyNotFound
- ToMuchMessages
- PollError
- PollCantBeStopped
- PollHasAlreadyClosed
- PollsCantBeSentToPrivateChats
- PollSizeError
- PollMustHaveMoreOptions
- PollCantHaveMoreOptions
- PollsOptionsLengthTooLong
- PollOptionsMustBeNonEmpty
- PollQuestionMustBeNonEmpty
- MessageWithPollNotFound (with MessageError)
- MessageIsNotAPoll (with MessageError)
- ObjectExpectedAsReplyMarkup
- InlineKeyboardExpected
- ChatNotFound
- ChatDescriptionIsNotModified
- InvalidQueryID
- InvalidPeerID
- InvalidHTTPUrlContent
- ButtonURLInvalid
- URLHostIsEmpty
- StartParamInvalid
- ButtonDataInvalid
- WrongFileIdentifier
- GroupDeactivated
- BadWebhook
- WebhookRequireHTTPS
- BadWebhookPort
- BadWebhookAddrInfo
- BadWebhookNoAddressAssociatedWithHostname
- NotFound
- MethodNotKnown
- PhotoAsInputFileRequired
- InvalidStickersSet
- NoStickerInRequest
- ChatAdminRequired
- NeedAdministratorRightsInTheChannel
- MethodNotAvailableInPrivateChats
- CantDemoteChatCreator
- CantRestrictSelf
- NotEnoughRightsToRestrict
- PhotoDimensions
- UnavailableMembers
- TypeOfFileMismatch
- WrongRemoteFileIdSpecified
- PaymentProviderInvalid
- CurrencyTotalAmountInvalid
- CantParseUrl
- UnsupportedUrlProtocol
- CantParseEntities
- ResultIdDuplicate
- ConflictError
- TerminatedByOtherGetUpdates
- CantGetUpdates
- Unauthorized
- BotKicked
- BotBlocked
- UserDeactivated
- CantInitiateConversation
- CantTalkWithBots
- NetworkError
- RetryAfter
- MigrateToChat
- RestartingTelegram
- AIOGramWarning
- TimeoutWarning
"""
class TelegramAPIError(Exception):
pass
# _PREFIXES = ["error: ", "[error]: ", "bad request: ", "conflict: ", "not found: "]
#
# def _clean_message(text):
# for prefix in _PREFIXES:
# if text.startswith(prefix):
# text = text[len(prefix) :]
# return (text[0].upper() + text[1:]).strip()
#
#
#
# class _MatchErrorMixin:
# match = ""
# text = None
#
# __subclasses = []
#
# def __init_subclass__(cls, **kwargs):
# super(_MatchErrorMixin, cls).__init_subclass__(**kwargs)
# # cls.match = cls.match.lower() if cls.match else ''
# if not hasattr(cls, f"_{cls.__name__}__group"):
# cls.__subclasses.append(cls)
#
# @classmethod
# def check(cls, message) -> bool:
# """
# Compare pattern with message
#
# :param message: always must be in lowercase
# :return: bool
# """
# return cls.match.lower() in message
#
# @classmethod
# def detect(cls, description):
# description = description.lower()
# for err in cls.__subclasses:
# if err is cls:
# continue
# if err.check(description):
# raise err(cls.text or description)
# raise cls(description)
#
#
# class AIOGramWarning(Warning):
# pass
#
#
# class TimeoutWarning(AIOGramWarning):
# pass
#
#
# class FSMStorageWarning(AIOGramWarning):
# pass
#
#
# class ValidationError(TelegramAPIError):
# pass
#
#
# class BadRequest(TelegramAPIError, _MatchErrorMixin):
# __group = True
#
#
# class MessageError(BadRequest):
# __group = True
#
#
# class MessageNotModified(MessageError):
# """
# Will be raised when you try to set new text is equals to current text.
# """
#
# match = "message is not modified"
#
#
# class MessageToForwardNotFound(MessageError):
# """
# Will be raised when you try to forward very old or deleted or unknown message.
# """
#
# match = "message to forward not found"
#
#
# class MessageToDeleteNotFound(MessageError):
# """
# Will be raised when you try to delete very old or deleted or unknown message.
# """
#
# match = "message to delete not found"
#
#
# class MessageToReplyNotFound(MessageError):
# """
# Will be raised when you try to reply to very old or deleted or unknown message.
# """
#
# match = "message to reply not found"
#
#
# class MessageIdentifierNotSpecified(MessageError):
# match = "message identifier is not specified"
#
#
# class MessageTextIsEmpty(MessageError):
# match = "Message text is empty"
#
#
# class MessageCantBeEdited(MessageError):
# match = "message can't be edited"
#
#
# class MessageCantBeDeleted(MessageError):
# match = "message can't be deleted"
#
#
# class MessageToEditNotFound(MessageError):
# match = "message to edit not found"
#
#
# class MessageIsTooLong(MessageError):
# match = "message is too long"
#
#
# class ToMuchMessages(MessageError):
# """
# Will be raised when you try to send media group with more than 10 items.
# """
#
# match = "Too much messages to send as an album"
#
#
# class ObjectExpectedAsReplyMarkup(BadRequest):
# match = "object expected as reply markup"
#
#
# class InlineKeyboardExpected(BadRequest):
# match = "inline keyboard expected"
#
#
# class PollError(BadRequest):
# __group = True
#
#
# class PollCantBeStopped(PollError):
# match = "poll can't be stopped"
#
#
# class PollHasAlreadyBeenClosed(PollError):
# match = "poll has already been closed"
#
#
# class PollsCantBeSentToPrivateChats(PollError):
# match = "polls can't be sent to private chats"
#
#
# class PollSizeError(PollError):
# __group = True
#
#
# class PollMustHaveMoreOptions(PollSizeError):
# match = "poll must have at least 2 option"
#
#
# class PollCantHaveMoreOptions(PollSizeError):
# match = "poll can't have more than 10 options"
#
#
# class PollOptionsMustBeNonEmpty(PollSizeError):
# match = "poll options must be non-empty"
#
#
# class PollQuestionMustBeNonEmpty(PollSizeError):
# match = "poll question must be non-empty"
#
#
# class PollOptionsLengthTooLong(PollSizeError):
# match = "poll options length must not exceed 100"
#
#
# class PollQuestionLengthTooLong(PollSizeError):
# match = "poll question length must not exceed 255"
#
#
# class MessageWithPollNotFound(PollError, MessageError):
# """
# Will be raised when you try to stop poll with message without poll
# """
#
# match = "message with poll to stop not found"
#
#
# class MessageIsNotAPoll(PollError, MessageError):
# """
# Will be raised when you try to stop poll with message without poll
# """
#
# match = "message is not a poll"
#
#
# class ChatNotFound(BadRequest):
# match = "chat not found"
#
#
# class ChatIdIsEmpty(BadRequest):
# match = "chat_id is empty"
#
#
# class InvalidUserId(BadRequest):
# match = "user_id_invalid"
# text = "Invalid user id"
#
#
# class ChatDescriptionIsNotModified(BadRequest):
# match = "chat description is not modified"
#
#
# class InvalidQueryID(BadRequest):
# match = "query is too old and response timeout expired or query id is invalid"
#
#
# class InvalidPeerID(BadRequest):
# match = "PEER_ID_INVALID"
# text = "Invalid peer ID"
#
#
# class InvalidHTTPUrlContent(BadRequest):
# match = "Failed to get HTTP URL content"
#
#
# class ButtonURLInvalid(BadRequest):
# match = "BUTTON_URL_INVALID"
# text = "Button URL invalid"
#
#
# class URLHostIsEmpty(BadRequest):
# match = "URL host is empty"
#
#
# class StartParamInvalid(BadRequest):
# match = "START_PARAM_INVALID"
# text = "Start param invalid"
#
#
# class ButtonDataInvalid(BadRequest):
# match = "BUTTON_DATA_INVALID"
# text = "Button data invalid"
#
#
# class WrongFileIdentifier(BadRequest):
# match = "wrong file identifier/HTTP URL specified"
#
#
# class GroupDeactivated(BadRequest):
# match = "group is deactivated"
#
#
# class PhotoAsInputFileRequired(BadRequest):
# """
# Will be raised when you try to set chat photo from file ID.
# """
#
# match = "Photo should be uploaded as an InputFile"
#
#
# class InvalidStickersSet(BadRequest):
# match = "STICKERSET_INVALID"
# text = "Stickers set is invalid"
#
#
# class NoStickerInRequest(BadRequest):
# match = "there is no sticker in the request"
#
#
# class ChatAdminRequired(BadRequest):
# match = "CHAT_ADMIN_REQUIRED"
# text = "Admin permissions is required!"
#
#
# class NeedAdministratorRightsInTheChannel(BadRequest):
# match = "need administrator rights in the channel chat"
# text = "Admin permissions is required!"
#
#
# class NotEnoughRightsToPinMessage(BadRequest):
# match = "not enough rights to pin a message"
#
#
# class MethodNotAvailableInPrivateChats(BadRequest):
# match = "method is available only for supergroups and channel"
#
#
# class CantDemoteChatCreator(BadRequest):
# match = "can't demote chat creator"
#
#
# class CantRestrictSelf(BadRequest):
# match = "can't restrict self"
# text = "Admin can't restrict self."
#
#
# class NotEnoughRightsToRestrict(BadRequest):
# match = "not enough rights to restrict/unrestrict chat member"
#
#
# class PhotoDimensions(BadRequest):
# match = "PHOTO_INVALID_DIMENSIONS"
# text = "Invalid photo dimensions"
#
#
# class UnavailableMembers(BadRequest):
# match = "supergroup members are unavailable"
#
#
# class TypeOfFileMismatch(BadRequest):
# match = "type of file mismatch"
#
#
# class WrongRemoteFileIdSpecified(BadRequest):
# match = "wrong remote file id specified"
#
#
# class PaymentProviderInvalid(BadRequest):
# match = "PAYMENT_PROVIDER_INVALID"
# text = "payment provider invalid"
#
#
# class CurrencyTotalAmountInvalid(BadRequest):
# match = "currency_total_amount_invalid"
# text = "currency total amount invalid"
#
#
# class BadWebhook(BadRequest):
# __group = True
#
#
# class WebhookRequireHTTPS(BadWebhook):
# match = "HTTPS url must be provided for webhook"
# text = "bad webhook: " + match
#
#
# class BadWebhookPort(BadWebhook):
# match = "Webhook can be set up only on ports 80, 88, 443 or 8443"
# text = "bad webhook: " + match
#
#
# class BadWebhookAddrInfo(BadWebhook):
# match = "getaddrinfo: Temporary failure in name resolution"
# text = "bad webhook: " + match
#
#
# class BadWebhookNoAddressAssociatedWithHostname(BadWebhook):
# match = "failed to resolve host: no address associated with hostname"
#
#
# class CantParseUrl(BadRequest):
# match = "can't parse URL"
#
#
# class UnsupportedUrlProtocol(BadRequest):
# match = "unsupported URL protocol"
#
#
# class CantParseEntities(BadRequest):
# match = "can't parse entities"
#
#
# class ResultIdDuplicate(BadRequest):
# match = "result_id_duplicate"
# text = "Result ID duplicate"
#
#
# class BotDomainInvalid(BadRequest):
# match = "bot_domain_invalid"
# text = "Invalid bot domain"
#
#
# class NotFound(TelegramAPIError, _MatchErrorMixin):
# __group = True
#
#
# class MethodNotKnown(NotFound):
# match = "method not found"
#
#
# class ConflictError(TelegramAPIError, _MatchErrorMixin):
# __group = True
#
#
# class TerminatedByOtherGetUpdates(ConflictError):
# match = "terminated by other getUpdates request"
# text = (
# "Terminated by other getUpdates request; "
# "Make sure that only one bot instance is running"
# )
#
#
# class CantGetUpdates(ConflictError):
# match = "can't use getUpdates method while webhook is active"
#
#
# class Unauthorized(TelegramAPIError, _MatchErrorMixin):
# __group = True
#
#
# class BotKicked(Unauthorized):
# match = "bot was kicked from a chat"
#
#
# class BotBlocked(Unauthorized):
# match = "bot was blocked by the user"
#
#
# class UserDeactivated(Unauthorized):
# match = "user is deactivated"
#
#
# class CantInitiateConversation(Unauthorized):
# match = "bot can't initiate conversation with a user"
#
#
# class CantTalkWithBots(Unauthorized):
# match = "bot can't send messages to bots"
#
#
# class NetworkError(TelegramAPIError):
# pass
#
#
# class RestartingTelegram(TelegramAPIError):
# def __init__(self):
# super(RestartingTelegram, self).__init__(
# "The Telegram Bot API service is restarting. Wait few second."
# )
#
#
# class RetryAfter(TelegramAPIError):
# def __init__(self, retry_after):
# super(RetryAfter, self).__init__(
# f"Flood control exceeded. Retry in {retry_after} seconds."
# )
# self.timeout = retry_after
#
#
# class MigrateToChat(TelegramAPIError):
# def __init__(self, chat_id):
# super(MigrateToChat, self).__init__(
# f"The group has been migrated to a supergroup. New id: {chat_id}."
# )
# self.migrate_to_chat_id = chat_id
#
#
# class Throttled(TelegramAPIError):
# def __init__(self, **kwargs):
# from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT
#
# self.key = kwargs.pop(KEY, "<None>")
# self.called_at = kwargs.pop(LAST_CALL, time.time())
# self.rate = kwargs.pop(RATE_LIMIT, None)
# self.result = kwargs.pop(RESULT, False)
# self.exceeded_count = kwargs.pop(EXCEEDED_COUNT, 0)
# self.delta = kwargs.pop(DELTA, 0)
# self.user = kwargs.pop("user", None)
# self.chat = kwargs.pop("chat", None)
#
# def __str__(self):
# return (
# f"Rate limit exceeded! (Limit: {self.rate} s, "
# f"exceeded: {self.exceeded_count}, "
# f"time delta: {round(self.delta, 3)} s)"
# )

View file

@ -0,0 +1,93 @@
from textwrap import indent
from typing import Match
from aiogram.methods.base import TelegramMethod, TelegramType
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
from aiogram.utils.exceptions.util import mark_line
class BadRequest(DetailedTelegramAPIError):
pass
class CantParseEntities(BadRequest):
pass
class CantParseEntitiesStartTag(CantParseEntities):
patterns = [
"Bad Request: can't parse entities: Can't find end tag corresponding to start tag (?P<tag>.+)"
]
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
match: Match[str],
) -> None:
super().__init__(method=method, message=message, match=match)
self.tag: str = match.group("tag")
class CantParseEntitiesUnmatchedTags(CantParseEntities):
patterns = [
r'Bad Request: can\'t parse entities: Unmatched end tag at byte offset (?P<offset>\d), expected "</(?P<expected>\w+)>", found "</(?P<found>\w+)>"'
]
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
match: Match[str],
) -> None:
super().__init__(method=method, message=message, match=match)
self.offset: int = int(match.group("offset"))
self.expected: str = match.group("expected")
self.found: str = match.group("found")
class CantParseEntitiesUnclosed(CantParseEntities):
patterns = [
"Bad Request: can't parse entities: Unclosed start tag at byte offset (?P<offset>.+)"
]
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
match: Match[str],
) -> None:
super().__init__(method=method, message=message, match=match)
self.offset: int = int(match.group("offset"))
def __str__(self) -> str:
message = [self.message]
text = getattr(self.method, "text", None) or getattr(self.method, "caption", None)
if text:
message.extend(["Example:", indent(mark_line(text, self.offset), prefix=" ")])
return "\n".join(message)
class CantParseEntitiesUnsupportedTag(CantParseEntities):
patterns = [
r'Bad Request: can\'t parse entities: Unsupported start tag "(?P<tag>.+)" at byte offset (?P<offset>\d+)'
]
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
match: Match[str],
) -> None:
super().__init__(method=method, message=message, match=match)
self.offset = int(match.group("offset"))
self.tag = match.group("tag")
def __str__(self) -> str:
message = [self.message]
text = getattr(self.method, "text", None) or getattr(self.method, "caption", None)
if text:
message.extend(
["Example:", indent(mark_line(text, self.offset, len(self.tag)), prefix=" ")]
)
return "\n".join(message)

View file

@ -1,8 +1,16 @@
from __future__ import annotations
from itertools import chain
from itertools import cycle as repeat_all
from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar
from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar, Union
from aiogram.types import InlineKeyboardButton, KeyboardButton
from aiogram.dispatcher.filters.callback_data import CallbackData
from aiogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
ReplyKeyboardMarkup,
)
ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton)
T = TypeVar("T")
@ -11,7 +19,7 @@ MIN_WIDTH = 1
MAX_BUTTONS = 100
class MarkupConstructor(Generic[ButtonType]):
class KeyboardConstructor(Generic[ButtonType]):
def __init__(
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
) -> None:
@ -106,7 +114,7 @@ class MarkupConstructor(Generic[ButtonType]):
raise ValueError(f"Row size {size} are not allowed")
return size
def copy(self: "MarkupConstructor[ButtonType]") -> "MarkupConstructor[ButtonType]":
def copy(self: "KeyboardConstructor[ButtonType]") -> "KeyboardConstructor[ButtonType]":
"""
Make full copy of current constructor with markup
@ -120,7 +128,7 @@ class MarkupConstructor(Generic[ButtonType]):
.. code-block:: python
>>> constructor = MarkupConstructor(button_type=InlineKeyboardButton)
>>> constructor = KeyboardConstructor(button_type=InlineKeyboardButton)
>>> ... # Add buttons to constructor
>>> markup = InlineKeyboardMarkup(inline_keyboard=constructor.export())
@ -128,7 +136,7 @@ class MarkupConstructor(Generic[ButtonType]):
"""
return self._markup.copy()
def add(self, *buttons: ButtonType) -> "MarkupConstructor[ButtonType]":
def add(self, *buttons: ButtonType) -> "KeyboardConstructor[ButtonType]":
"""
Add one or many buttons to markup.
@ -153,7 +161,9 @@ class MarkupConstructor(Generic[ButtonType]):
self._markup = markup
return self
def row(self, *buttons: ButtonType, width: int = MAX_WIDTH) -> "MarkupConstructor[ButtonType]":
def row(
self, *buttons: ButtonType, width: int = MAX_WIDTH
) -> "KeyboardConstructor[ButtonType]":
"""
Add row to markup
@ -170,7 +180,7 @@ class MarkupConstructor(Generic[ButtonType]):
)
return self
def adjust(self, *sizes: int, repeat: bool = False) -> "MarkupConstructor[ButtonType]":
def adjust(self, *sizes: int, repeat: bool = False) -> "KeyboardConstructor[ButtonType]":
"""
Adjust previously added buttons to specific row sizes.
@ -202,10 +212,17 @@ class MarkupConstructor(Generic[ButtonType]):
self._markup = markup
return self
def button(self, **kwargs: Any) -> "MarkupConstructor[ButtonType]":
def button(self, **kwargs: Any) -> "KeyboardConstructor[ButtonType]":
if isinstance(callback_data := kwargs.get("callback_data", None), CallbackData):
kwargs["callback_data"] = callback_data.pack()
button = self._button_type(**kwargs)
return self.add(button)
def as_markup(self, **kwargs: Any) -> Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]:
if self._button_type is ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(keyboard=self.export(), **kwargs)
return InlineKeyboardMarkup(inline_keyboard=self.export())
def repeat_last(items: Iterable[T]) -> Generator[T, None, None]:
items_iter = iter(items)