Added detection of API Errors and fixed coverage

This commit is contained in:
Alex Root Junior 2021-08-01 00:34:50 +03:00
parent 4f2cc75951
commit c3844bb18f
17 changed files with 179 additions and 216 deletions

View file

@ -1,64 +0,0 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING, List, Type
from aiogram.methods import Response, TelegramMethod
from aiogram.types import TelegramObject
from aiogram.utils.exceptions.base import TelegramAPIError
from aiogram.utils.exceptions.exceptions import (
CantParseEntitiesStartTag,
CantParseEntitiesUnclosed,
CantParseEntitiesUnmatchedTags,
CantParseEntitiesUnsupportedTag,
DetailedTelegramAPIError,
)
if TYPE_CHECKING:
from aiogram.client.bot import Bot
from aiogram.client.session.base import NextRequestMiddlewareType
class RequestErrorMiddleware:
def __init__(self) -> None:
self._registry: List[Type[DetailedTelegramAPIError]] = [
CantParseEntitiesStartTag,
CantParseEntitiesUnmatchedTags,
CantParseEntitiesUnclosed,
CantParseEntitiesUnsupportedTag,
]
def mount(self, error: Type[DetailedTelegramAPIError]) -> Type[DetailedTelegramAPIError]:
if error in self:
raise ValueError(f"{error!r} is already registered")
if not hasattr(error, "patterns"):
raise ValueError(f"{error!r} has no attribute 'patterns'")
self._registry.append(error)
return error
def detect_error(self, err: TelegramAPIError) -> TelegramAPIError:
message = err.message
for variant in self._registry:
for pattern in variant.patterns:
if match := re.match(pattern, message):
return variant(
method=err.method,
message=err.message,
match=match,
)
return err
def __contains__(self, item: Type[DetailedTelegramAPIError]) -> bool:
return item in self._registry
async def __call__(
self,
bot: Bot,
method: TelegramMethod[TelegramObject],
make_request: NextRequestMiddlewareType,
) -> Response[TelegramObject]:
try:
return await make_request(bot, method)
except TelegramAPIError as e:
detected_err = self.detect_error(err=e)
raise detected_err from e

View file

@ -4,6 +4,7 @@ import abc
import datetime
import json
from functools import partial
from http import HTTPStatus
from types import TracebackType
from typing import (
TYPE_CHECKING,
@ -25,8 +26,13 @@ from aiogram.utils.helper import Default
from ...methods import Response, TelegramMethod
from ...methods.base import TelegramType
from ...types import UNSET, TelegramObject
from ...utils.exceptions.bad_request import BadRequest
from ...utils.exceptions.conflict import ConflictError
from ...utils.exceptions.network import EntityTooLarge
from ...utils.exceptions.not_found import NotFound
from ...utils.exceptions.server import RestartingTelegram, ServerError
from ...utils.exceptions.special import MigrateToChat, RetryAfter
from ..errors_middleware import RequestErrorMiddleware
from ...utils.exceptions.unauthorized import UnauthorizedError
from ..telegram import PRODUCTION, TelegramAPIServer
if TYPE_CHECKING: # pragma: no cover
@ -55,12 +61,8 @@ class BaseSession(abc.ABC):
timeout: Default[float] = Default(fget=lambda self: float(self.__class__.default_timeout))
"""Session scope request timeout"""
errors_middleware: ClassVar[RequestErrorMiddleware] = RequestErrorMiddleware()
def __init__(self) -> None:
self.middlewares: List[RequestMiddlewareType[TelegramObject]] = [
self.errors_middleware,
]
self.middlewares: List[RequestMiddlewareType[TelegramObject]] = []
def check_response(
self, method: TelegramMethod[TelegramType], status_code: int, content: str
@ -70,10 +72,11 @@ class BaseSession(abc.ABC):
"""
json_data = self.json_loads(content)
response = method.build_response(json_data)
if response.ok:
if HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED and response.ok:
return response
description = cast(str, response.description)
if parameters := response.parameters:
if parameters.retry_after:
raise RetryAfter(
@ -85,6 +88,21 @@ class BaseSession(abc.ABC):
message=description,
migrate_to_chat_id=parameters.migrate_to_chat_id,
)
if status_code == HTTPStatus.BAD_REQUEST:
raise BadRequest(method=method, message=description)
if status_code == HTTPStatus.NOT_FOUND:
raise NotFound(method=method, message=description)
if status_code == HTTPStatus.CONFLICT:
raise ConflictError(method=method, message=description)
if status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
raise UnauthorizedError(method=method, message=description)
if status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
raise EntityTooLarge(method=method, message=description)
if status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
if "restart" in description:
raise RestartingTelegram(method=method, message=description)
raise ServerError(method=method, message=description)
raise TelegramAPIError(
method=method,
message=description,

View file

@ -1,5 +1,5 @@
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
from aiogram.utils.exceptions.base import TelegramAPIError
class BadRequest(DetailedTelegramAPIError):
class BadRequest(TelegramAPIError):
pass

View file

@ -1,4 +1,4 @@
from typing import ClassVar, List, Match, Optional, TypeVar
from typing import Optional, TypeVar
from aiogram.methods import TelegramMethod
from aiogram.methods.base import TelegramType
@ -25,16 +25,3 @@ class TelegramAPIError(Exception):
if self.url:
message.append(f"(background on this error at: {self.url})")
return "\n".join(message)
class DetailedTelegramAPIError(TelegramAPIError):
patterns: ClassVar[List[str]]
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
match: Match[str],
) -> None:
super().__init__(method=method, message=message)
self.match: Match[str] = match

View file

@ -0,0 +1,5 @@
from aiogram.utils.exceptions.base import TelegramAPIError
class ConflictError(TelegramAPIError):
pass

View file

@ -1,93 +1,5 @@
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
from aiogram.utils.exceptions.base import TelegramAPIError
class BadRequest(DetailedTelegramAPIError):
class BadRequest(TelegramAPIError):
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

@ -3,3 +3,7 @@ from aiogram.utils.exceptions.base import TelegramAPIError
class NetworkError(TelegramAPIError):
pass
class EntityTooLarge(NetworkError):
url = "https://core.telegram.org/bots/api#sending-files"

View file

@ -1,5 +1,5 @@
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
from aiogram.utils.exceptions.base import TelegramAPIError
class NotFound(DetailedTelegramAPIError):
class NotFound(TelegramAPIError):
pass

View file

@ -3,3 +3,7 @@ from aiogram.utils.exceptions.base import TelegramAPIError
class ServerError(TelegramAPIError):
pass
class RestartingTelegram(ServerError):
pass

View file

@ -1,5 +1,3 @@
from typing import Optional
from aiogram.methods import TelegramMethod
from aiogram.methods.base import TelegramType
from aiogram.utils.exceptions.base import TelegramAPIError
@ -37,7 +35,7 @@ class MigrateToChat(TelegramAPIError):
super().__init__(method=method, message=message)
self.migrate_to_chat_id = migrate_to_chat_id
def render_message(self) -> Optional[str]:
def render_description(self) -> str:
description = (
f"The group has been migrated to a supergroup with id {self.migrate_to_chat_id}"
)

View file

@ -0,0 +1,5 @@
from aiogram.utils.exceptions.base import TelegramAPIError
class UnauthorizedError(TelegramAPIError):
pass

View file

@ -1,20 +0,0 @@
def mark_line(text: str, offset: int, length: int = 1) -> str:
try:
if offset > 0 and (new_line_pos := text[:offset].rindex("\n")):
text = "..." + text[:new_line_pos]
offset -= new_line_pos - 3
except ValueError:
pass
if offset > 10:
text = "..." + text[offset - 10 :]
offset = 13
mark = " " * offset
mark += "^" * length
try:
if new_line_pos := text[len(mark) :].index("\n"):
text = text[:new_line_pos].rstrip() + "..."
except ValueError:
pass
return text + "\n" + mark