Merge branch 'dev-3.x' into dev-3.x

This commit is contained in:
Aleksandr 2021-05-25 10:33:08 +03:00 committed by GitHub
commit 4b4c9055ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1112 additions and 839 deletions

View file

@ -19,7 +19,6 @@ jobs:
- macos-latest
- windows-latest
python-version:
- 3.7
- 3.8
- 3.9

View file

@ -9,4 +9,4 @@
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Codecov](https://img.shields.io/codecov/c/github/aiogram/aiogram?style=flat-square)](https://app.codecov.io/gh/aiogram/aiogram)
**aiogram** modern and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
**aiogram** modern and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.8 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.

View file

@ -302,7 +302,7 @@ class Bot(ContextInstanceMixin["Bot"]):
:param method:
:return:
"""
return await self.session.make_request(self, method, timeout=request_timeout)
return await self.session(self, method, timeout=request_timeout)
def __hash__(self) -> int:
"""

View file

@ -0,0 +1,64 @@
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

@ -10,7 +10,6 @@ from typing import (
Optional,
Tuple,
Type,
TypeVar,
Union,
cast,
)
@ -19,12 +18,12 @@ from aiohttp import BasicAuth, ClientSession, FormData, TCPConnector
from aiogram.methods import Request, TelegramMethod
from ...methods.base import TelegramType
from .base import UNSET, BaseSession
if TYPE_CHECKING: # pragma: no cover
from ..bot import Bot
T = TypeVar("T")
_ProxyBasic = Union[str, Tuple[str, BasicAuth]]
_ProxyChain = Iterable[_ProxyBasic]
_ProxyType = Union[_ProxyChain, _ProxyBasic]
@ -76,6 +75,8 @@ def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"]
class AiohttpSession(BaseSession):
def __init__(self, proxy: Optional[_ProxyType] = None):
super().__init__()
self._session: Optional[ClientSession] = None
self._connector_type: Type[TCPConnector] = TCPConnector
self._connector_init: Dict[str, Any] = {}
@ -86,7 +87,7 @@ class AiohttpSession(BaseSession):
try:
self._setup_proxy_connector(proxy)
except ImportError as exc: # pragma: no cover
raise UserWarning(
raise RuntimeError(
"In order to use aiohttp client for proxy requests, install "
"https://pypi.org/project/aiohttp-socks/"
) from exc
@ -130,8 +131,8 @@ class AiohttpSession(BaseSession):
return form
async def make_request(
self, bot: Bot, call: TelegramMethod[T], timeout: Optional[int] = None
) -> T:
self, bot: Bot, call: TelegramMethod[TelegramType], timeout: Optional[int] = None
) -> TelegramType:
session = await self.create_session()
request = call.build_request(bot)
@ -141,11 +142,10 @@ class AiohttpSession(BaseSession):
async with session.post(
url, data=form, timeout=self.timeout if timeout is None else timeout
) as resp:
raw_result = await resp.json(loads=self.json_loads)
raw_result = await resp.text()
response = call.build_response(raw_result)
self.raise_for_status(response)
return cast(T, response.result)
response = self.check_response(method=call, status_code=resp.status, content=raw_result)
return cast(TelegramType, response.result)
async def stream_content(
self, url: str, timeout: int, chunk_size: int

View file

@ -3,32 +3,44 @@ from __future__ import annotations
import abc
import datetime
import json
from functools import partial
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Awaitable,
Callable,
ClassVar,
List,
Optional,
Type,
TypeVar,
Union,
cast,
)
from aiogram.utils.exceptions import TelegramAPIError
from aiogram.utils.exceptions.base import TelegramAPIError
from aiogram.utils.helper import Default
from ...methods import Response, TelegramMethod
from ...types import UNSET
from ...methods.base import TelegramType
from ...types import UNSET, TelegramObject
from ...utils.exceptions.special import MigrateToChat, RetryAfter
from ..errors_middleware import RequestErrorMiddleware
from ..telegram import PRODUCTION, TelegramAPIServer
if TYPE_CHECKING: # pragma: no cover
from ..bot import Bot
T = TypeVar("T")
_JsonLoads = Callable[..., Any]
_JsonDumps = Callable[..., str]
NextRequestMiddlewareType = Callable[
["Bot", TelegramMethod[TelegramObject]], Awaitable[Response[TelegramObject]]
]
RequestMiddlewareType = Callable[
["Bot", TelegramMethod[TelegramType], NextRequestMiddlewareType],
Awaitable[Response[TelegramType]],
]
class BaseSession(abc.ABC):
@ -43,16 +55,40 @@ class BaseSession(abc.ABC):
timeout: Default[float] = Default(fget=lambda self: float(self.__class__.default_timeout))
"""Session scope request timeout"""
@classmethod
def raise_for_status(cls, response: Response[T]) -> None:
errors_middleware: ClassVar[RequestErrorMiddleware] = RequestErrorMiddleware()
def __init__(self) -> None:
self.middlewares: List[RequestMiddlewareType[TelegramObject]] = [
self.errors_middleware,
]
def check_response(
self, method: TelegramMethod[TelegramType], status_code: int, content: str
) -> Response[TelegramType]:
"""
Check response status
:param response: Response instance
"""
json_data = self.json_loads(content)
response = method.build_response(json_data)
if response.ok:
return
raise TelegramAPIError(response.description)
return response
description = cast(str, response.description)
if parameters := response.parameters:
if parameters.retry_after:
raise RetryAfter(
method=method, message=description, retry_after=parameters.retry_after
)
if parameters.migrate_to_chat_id:
raise MigrateToChat(
method=method,
message=description,
migrate_to_chat_id=parameters.migrate_to_chat_id,
)
raise TelegramAPIError(
method=method,
message=description,
)
@abc.abstractmethod
async def close(self) -> None: # pragma: no cover
@ -63,8 +99,8 @@ class BaseSession(abc.ABC):
@abc.abstractmethod
async def make_request(
self, bot: Bot, method: TelegramMethod[T], timeout: Optional[int] = UNSET
) -> T: # pragma: no cover
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
) -> TelegramType: # pragma: no cover
"""
Make request to Telegram Bot API
@ -111,6 +147,20 @@ class BaseSession(abc.ABC):
return {k: self.clean_json(v) for k, v in value.items() if v is not None}
return value
def middleware(
self, middleware: RequestMiddlewareType[TelegramObject]
) -> RequestMiddlewareType[TelegramObject]:
self.middlewares.append(middleware)
return middleware
async def __call__(
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
) -> TelegramType:
middleware = partial(self.make_request, timeout=timeout)
for m in reversed(self.middlewares):
middleware = partial(m, make_request=middleware) # type: ignore
return await middleware(bot, method)
async def __aenter__(self) -> BaseSession:
return self

View file

@ -8,9 +8,9 @@ from typing import Any, AsyncGenerator, Dict, Optional, Union, cast
from .. import loggers
from ..client.bot import Bot
from ..methods import TelegramMethod
from ..methods import GetUpdates, TelegramMethod
from ..types import TelegramObject, Update, User
from ..utils.exceptions import TelegramAPIError
from ..utils.exceptions.base import TelegramAPIError
from .event.bases import UNHANDLED, SkipHandler
from .event.telegram import TelegramEventObserver
from .fsm.context import FSMContext
@ -119,16 +119,22 @@ class Dispatcher(Router):
return await self.feed_update(bot=bot, update=parsed_update, **kwargs)
@classmethod
async def _listen_updates(cls, bot: Bot) -> AsyncGenerator[Update, None]:
async def _listen_updates(
cls, bot: Bot, polling_timeout: int = 30
) -> AsyncGenerator[Update, None]:
"""
Infinity updates reader
"""
update_id: Optional[int] = None
get_updates = GetUpdates(timeout=polling_timeout)
kwargs = {}
if bot.session.timeout:
kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout)
while True:
# TODO: Skip restarting telegram error
for update in await bot.get_updates(offset=update_id):
updates = await bot(get_updates, **kwargs)
for update in updates:
yield update
update_id = update.update_id + 1
get_updates.offset = update.update_id + 1
async def _listen_update(self, update: Update, **kwargs: Any) -> Any:
"""
@ -249,7 +255,7 @@ class Dispatcher(Router):
)
return True # because update was processed but unsuccessful
async def _polling(self, bot: Bot, **kwargs: Any) -> None:
async def _polling(self, bot: Bot, polling_timeout: int = 30, **kwargs: Any) -> None:
"""
Internal polling process
@ -257,7 +263,7 @@ class Dispatcher(Router):
:param kwargs:
:return:
"""
async for update in self._listen_updates(bot):
async for update in self._listen_updates(bot, polling_timeout=polling_timeout):
await self._process_update(bot=bot, update=update, **kwargs)
async def _feed_webhook_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any:
@ -336,7 +342,7 @@ class Dispatcher(Router):
return None
async def start_polling(self, *bots: Bot, **kwargs: Any) -> None:
async def start_polling(self, *bots: Bot, polling_timeout: int = 10, **kwargs: Any) -> None:
"""
Polling runner
@ -356,7 +362,9 @@ class Dispatcher(Router):
loggers.dispatcher.info(
"Run polling for bot @%s id=%d - %r", user.username, bot.id, user.full_name
)
coro_list.append(self._polling(bot=bot, **kwargs))
coro_list.append(
self._polling(bot=bot, polling_timeout=polling_timeout, **kwargs)
)
await asyncio.gather(*coro_list)
finally:
for bot in bots: # Close sessions
@ -364,16 +372,19 @@ class Dispatcher(Router):
loggers.dispatcher.info("Polling stopped")
await self.emit_shutdown(**workflow_data)
def run_polling(self, *bots: Bot, **kwargs: Any) -> None:
def run_polling(self, *bots: Bot, polling_timeout: int = 30, **kwargs: Any) -> None:
"""
Run many bots with polling
:param bots:
:param kwargs:
:param bots: Bot instances
:param polling_timeout: Poling timeout
:param kwargs: contextual data
:return:
"""
try:
return asyncio.run(self.start_polling(*bots, **kwargs))
return asyncio.run(
self.start_polling(*bots, **kwargs, polling_timeout=polling_timeout)
)
except (KeyboardInterrupt, SystemExit): # pragma: no cover
# Allow to graceful shutdown
pass

View file

@ -150,7 +150,7 @@ class TelegramEventObserver:
return UNHANDLED
def __call__(
self, *args: FilterType, **bound_filters: BaseFilter
self, *args: FilterType, **bound_filters: Any
) -> Callable[[CallbackType], CallbackType]:
"""
Decorator for registering event handlers

View file

@ -0,0 +1,113 @@
from __future__ import annotations
from decimal import Decimal
from enum import Enum
from fractions import Fraction
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, TypeVar, Union
from uuid import UUID
from magic_filter import MagicFilter
from pydantic import BaseModel
from aiogram.dispatcher.filters import BaseFilter
from aiogram.types import CallbackQuery
T = TypeVar("T", bound="CallbackData")
MAX_CALLBACK_LENGTH: int = 64
class CallbackDataException(Exception):
pass
class CallbackData(BaseModel):
if TYPE_CHECKING: # pragma: no cover
sep: str
prefix: str
def __init_subclass__(cls, **kwargs: Any) -> None:
if "prefix" not in kwargs:
raise ValueError(
f"prefix required, usage example: "
f"`class {cls.__name__}(CallbackData, prefix='my_callback'): ...`"
)
cls.sep = kwargs.pop("sep", ":")
cls.prefix = kwargs.pop("prefix")
if cls.sep in cls.prefix:
raise ValueError(
f"Separator symbol {cls.sep!r} can not be used inside prefix {cls.prefix!r}"
)
def _encode_value(self, key: str, value: Any) -> str:
if value is None:
return ""
if isinstance(value, Enum):
return str(value.value)
if isinstance(value, (int, str, float, Decimal, Fraction, UUID)):
return str(value)
raise ValueError(
f"Attribute {key}={value!r} of type {type(value).__name__!r}"
f" can not be packed to callback data"
)
def pack(self) -> str:
result = [self.prefix]
for key, value in self.dict().items():
encoded = self._encode_value(key, value)
if self.sep in encoded:
raise ValueError(
f"Separator symbol {self.sep!r} can not be used in value {key}={encoded!r}"
)
result.append(encoded)
callback_data = self.sep.join(result)
if len(callback_data.encode()) > MAX_CALLBACK_LENGTH:
raise ValueError(
f"Resulted callback data is too long! len({callback_data!r}.encode()) > {MAX_CALLBACK_LENGTH}"
)
return callback_data
@classmethod
def unpack(cls: Type[T], value: str) -> T:
prefix, *parts = value.split(cls.sep)
names = cls.__fields__.keys()
if len(parts) != len(names):
raise TypeError(
f"Callback data {cls.__name__!r} takes {len(names)} arguments but {len(parts)} were given"
)
if prefix != cls.prefix:
raise ValueError(f"Bad prefix ({prefix!r} != {cls.prefix!r})")
payload = {}
for k, v in zip(names, parts): # type: str, Optional[str]
if field := cls.__fields__.get(k):
if v == "" and not field.required:
v = None
payload[k] = v
return cls(**payload)
@classmethod
def filter(cls, rule: MagicFilter) -> CallbackQueryFilter:
return CallbackQueryFilter(callback_data=cls, rule=rule)
class Config:
use_enum_values = True
class CallbackQueryFilter(BaseFilter):
callback_data: Type[CallbackData]
rule: MagicFilter
async def __call__(self, query: CallbackQuery) -> Union[bool, Dict[str, Any]]:
if not isinstance(query, CallbackQuery) or not query.data:
return False
try:
callback_data = self.callback_data.unpack(query.data)
except (TypeError, ValueError):
return False
if self.rule.resolve(callback_data):
return {"callback_data": callback_data}
return False
class Config:
arbitrary_types_allowed = True

View file

@ -28,9 +28,9 @@ class FSMContextMiddleware(BaseMiddleware[Update]):
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)
if self.isolate_events:
async with self.storage.lock(chat_id=context.chat_id, user_id=context.user_id):
return await handler(event, data)
return await handler(event, data)
def resolve_event_context(self, data: Dict[str, Any]) -> Optional[FSMContext]:

View file

@ -10,7 +10,9 @@ StateType = Optional[Union[str, State]]
class BaseStorage(ABC):
@abstractmethod
@asynccontextmanager
async def lock(self) -> AsyncGenerator[None, None]: # pragma: no cover
async def lock(
self, chat_id: int, user_id: int
) -> AsyncGenerator[None, None]: # pragma: no cover
yield None
@abstractmethod

View file

@ -12,6 +12,7 @@ from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType
class MemoryStorageRecord:
data: Dict[str, Any] = field(default_factory=dict)
state: Optional[str] = None
lock: Lock = field(default_factory=Lock)
class MemoryStorage(BaseStorage):
@ -19,11 +20,10 @@ class MemoryStorage(BaseStorage):
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:
async def lock(self, chat_id: int, user_id: int) -> AsyncGenerator[None, None]:
async with self.storage[chat_id][user_id].lock:
yield None
async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None:

View file

@ -12,7 +12,7 @@ from ..types import UNSET, InputFile, ResponseParameters
if TYPE_CHECKING: # pragma: no cover
from ..client.bot import Bot
T = TypeVar("T")
TelegramType = TypeVar("TelegramType", bound=Any)
class Request(BaseModel):
@ -31,14 +31,15 @@ class Request(BaseModel):
}
class Response(ResponseParameters, GenericModel, Generic[T]):
class Response(GenericModel, Generic[TelegramType]):
ok: bool
result: Optional[T] = None
result: Optional[TelegramType] = None
description: Optional[str] = None
error_code: Optional[int] = None
parameters: Optional[ResponseParameters] = None
class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
class TelegramMethod(abc.ABC, BaseModel, Generic[TelegramType]):
class Config(BaseConfig):
# use_enum_values = True
extra = Extra.allow
@ -76,14 +77,14 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
return super().dict(exclude=exclude, **kwargs)
def build_response(self, data: Dict[str, Any]) -> Response[T]:
def build_response(self, data: Dict[str, Any]) -> Response[TelegramType]:
# noinspection PyTypeChecker
return Response[self.__returning__](**data) # type: ignore
async def emit(self, bot: Bot) -> T:
async def emit(self, bot: Bot) -> TelegramType:
return await bot(self)
def __await__(self) -> Generator[Any, None, T]:
def __await__(self) -> Generator[Any, None, TelegramType]:
from aiogram.client.bot import Bot
bot = Bot.get_current(no_error=False)

View file

@ -12,6 +12,9 @@ from .base import UNSET, TelegramObject
if TYPE_CHECKING: # pragma: no cover
from ..methods import (
CopyMessage,
DeleteMessage,
EditMessageCaption,
EditMessageText,
SendAnimation,
SendAudio,
SendContact,
@ -1714,6 +1717,49 @@ class Message(TelegramObject):
reply_markup=reply_markup,
)
def edit_text(
self,
text: str,
parse_mode: Optional[str] = UNSET,
entities: Optional[List[MessageEntity]] = None,
disable_web_page_preview: Optional[bool] = None,
reply_markup: Optional[InlineKeyboardMarkup] = None,
) -> EditMessageText:
from ..methods import EditMessageText
return EditMessageText(
chat_id=self.chat.id,
message_id=self.message_id,
text=text,
parse_mode=parse_mode,
entities=entities,
disable_web_page_preview=disable_web_page_preview,
reply_markup=reply_markup,
)
def edit_caption(
self,
caption: str,
parse_mode: Optional[str] = UNSET,
caption_entities: Optional[List[MessageEntity]] = None,
reply_markup: Optional[InlineKeyboardMarkup] = None,
) -> EditMessageCaption:
from ..methods import EditMessageCaption
return EditMessageCaption(
chat_id=self.chat.id,
message_id=self.message_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
reply_markup=reply_markup,
)
def delete(self) -> DeleteMessage:
from ..methods import DeleteMessage
return DeleteMessage(chat_id=self.chat.id, message_id=self.message_id)
def get_url(self, force_private: bool = False) -> Optional[str]:
"""
Returns message URL. Cannot be used in private (one-to-one) chats.

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

View file

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

View file

@ -0,0 +1,40 @@
from typing import ClassVar, List, Match, Optional, TypeVar
from aiogram.methods import TelegramMethod
from aiogram.methods.base import TelegramType
ErrorType = TypeVar("ErrorType")
class TelegramAPIError(Exception):
url: Optional[str] = None
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
) -> None:
self.method = method
self.message = message
def render_description(self) -> str:
return self.message
def __str__(self) -> str:
message = [self.render_description()]
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

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

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

View file

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

View file

View file

@ -0,0 +1,46 @@
from typing import Optional
from aiogram.methods import TelegramMethod
from aiogram.methods.base import TelegramType
from aiogram.utils.exceptions.base import TelegramAPIError
class RetryAfter(TelegramAPIError):
url = "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this"
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
retry_after: int,
) -> None:
super().__init__(method=method, message=message)
self.retry_after = retry_after
def render_description(self) -> str:
description = f"Flood control exceeded on method {type(self.method).__name__!r}"
if chat_id := getattr(self.method, "chat_id", None):
description += f" in chat {chat_id}"
description += f". Retry in {self.retry_after} seconds."
return description
class MigrateToChat(TelegramAPIError):
url = "https://core.telegram.org/bots/api#responseparameters"
def __init__(
self,
method: TelegramMethod[TelegramType],
message: str,
migrate_to_chat_id: int,
) -> None:
super().__init__(method=method, message=message)
self.migrate_to_chat_id = migrate_to_chat_id
def render_message(self) -> Optional[str]:
description = (
f"The group has been migrated to a supergroup with id {self.migrate_to_chat_id}"
)
if chat_id := getattr(self.method, "chat_id", None):
description += f" from {chat_id}"
return description

View file

View file

@ -0,0 +1,20 @@
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

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)

View file

@ -21,4 +21,4 @@ comment:
require_changes: no
branches:
- dev-3.x
after_n_builds: 8
after_n_builds: 6

View file

@ -1,39 +1,46 @@
import logging
from typing import Any
from aiogram import Bot, Dispatcher, types
from aiogram.dispatcher.handler import MessageHandler
from aiogram.types import Message
TOKEN = "42:TOKEN"
dp = Dispatcher()
logger = logging.getLogger(__name__)
@dp.message(commands=["start"])
class MyHandler(MessageHandler):
@dp.message(commands={"start"})
async def command_start_handler(message: Message) -> None:
"""
This handler receive messages with `/start` command
Usage of Class-based handlers
"""
async def handle(self) -> Any:
await self.event.answer(f"<b>Hello, {self.from_user.full_name}!</b>")
# Most of event objects has an aliases for API methods to be called in event context
# For example if you want to answer to incoming message you can use `message.answer(...)` alias
# and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage` method automatically
# or call API method directly via Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
@dp.message(content_types=[types.ContentType.ANY])
async def echo_handler(message: types.Message, bot: Bot) -> Any:
@dp.message()
async def echo_handler(message: types.Message) -> Any:
"""
Handler will forward received message back to the sender
Usage of Function-based handlers
By default message handler will handle all message types (like text, photo, sticker and etc.)
"""
await bot.forward_message(
from_chat_id=message.chat.id, chat_id=message.chat.id, message_id=message.message_id
)
try:
# Send copy of the received message
await message.send_copy(chat_id=message.chat.id)
except TypeError:
# But not all the types is supported to be copied so need to handle it
await message.answer("Nice try!")
def main() -> None:
# Initialize Bot instance with an default parse mode which will be passed to all API calls
bot = Bot(TOKEN, parse_mode="HTML")
# And the run events dispatching
dp.run_polling(bot)

View file

@ -0,0 +1,111 @@
import asyncio
import logging
import sys
from os import getenv
from aiogram import Bot, Dispatcher, F
from aiogram.dispatcher.filters import Command
from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.state import State, StatesGroup
from aiogram.types import Message, ReplyKeyboardRemove, ReplyKeyboardMarkup, KeyboardButton
from aiogram.utils.markdown import hbold
from aiogram.utils.markup import KeyboardConstructor
GENDERS = ["Male", "Female", "Helicopter", "Other"]
dp = Dispatcher()
# States
class Form(StatesGroup):
name = State() # Will be represented in storage as 'Form:name'
age = State() # Will be represented in storage as 'Form:age'
gender = State() # Will be represented in storage as 'Form:gender'
@dp.message(Command(commands=["start"]))
async def cmd_start(message: Message, state: FSMContext):
"""
Conversation's entry point
"""
# Set state
await state.set_state(Form.name)
await message.answer("Hi there! What's your name?")
@dp.message(Command(commands=["cancel"]))
@dp.message(F.text.lower() == "cancel")
async def cancel_handler(message: Message, state: FSMContext):
"""
Allow user to cancel any action
"""
current_state = await state.get_state()
if current_state is None:
return
logging.info("Cancelling state %r", current_state)
# Cancel state and inform user about it
await state.clear()
# And remove keyboard (just in case)
await message.answer("Cancelled.", reply_markup=ReplyKeyboardRemove())
@dp.message(Form.name)
async def process_name(message: Message, state: FSMContext):
"""
Process user name
"""
await state.update_data(name=message.text)
await state.set_state(Form.age)
await message.answer("How old are you?")
# Check age. Age gotta be digit
@dp.message(Form.age, ~F.text.isdigit())
async def process_age_invalid(message: Message):
"""
If age is invalid
"""
return await message.answer("Age gotta be a number.\nHow old are you? (digits only)")
@dp.message(Form.age)
async def process_age(message: Message, state: FSMContext):
# Update state and data
await state.set_state(Form.gender)
await state.update_data(age=int(message.text))
# Configure ReplyKeyboardMarkup
constructor = KeyboardConstructor(KeyboardButton)
constructor.add(*(KeyboardButton(text=text) for text in GENDERS)).adjust(2)
markup = ReplyKeyboardMarkup(
resize_keyboard=True, selective=True, keyboard=constructor.export()
)
await message.reply("What is your gender?", reply_markup=markup)
@dp.message(Form.gender)
async def process_gender(message: Message, state: FSMContext):
data = await state.update_data(gender=message.text)
await state.clear()
# And send message
await message.answer(
(
f'Hi, nice to meet you, {hbold(data["name"])}\n'
f'Age: {hbold(data["age"])}\n'
f'Gender: {hbold(data["gender"])}\n'
),
reply_markup=ReplyKeyboardRemove(),
)
async def main():
bot = Bot(token=getenv("TELEGRAM_TOKEN"), parse_mode="HTML")
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())

329
poetry.lock generated
View file

@ -108,17 +108,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "20.3.0"
version = "21.2.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "babel"
@ -156,7 +156,7 @@ lxml = ["lxml"]
[[package]]
name = "black"
version = "21.4b2"
version = "21.5b1"
description = "The uncompromising code formatter."
category = "dev"
optional = false
@ -169,17 +169,15 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.8.1,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""}
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors"]
python2 = ["typed-ast (>=1.4.2)"]
[[package]]
name = "cfgv"
version = "3.2.0"
version = "3.3.0"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
@ -195,11 +193,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "click"
version = "7.1.2"
version = "8.0.1"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
@ -217,12 +218,15 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.dependencies]
toml = {version = "*", optional = true, markers = "extra == \"toml\""}
[package.extras]
toml = ["toml"]
[[package]]
name = "decorator"
version = "5.0.7"
version = "5.0.9"
description = "Decorators for Humans"
category = "dev"
optional = false
@ -254,14 +258,13 @@ python-versions = "*"
[[package]]
name = "flake8"
version = "3.9.1"
version = "3.9.2"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.7.0,<2.8.0"
pyflakes = ">=2.3.0,<2.4.0"
@ -298,7 +301,7 @@ test = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]]
name = "identify"
version = "2.2.3"
version = "2.2.5"
description = "File identification library for Python"
category = "dev"
optional = false
@ -325,14 +328,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "4.0.0"
version = "4.0.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
@ -349,7 +351,7 @@ python-versions = "*"
[[package]]
name = "ipython"
version = "7.22.0"
version = "7.23.1"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
@ -361,6 +363,7 @@ backcall = "*"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
@ -416,17 +419,17 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
[[package]]
name = "jinja2"
version = "2.11.3"
version = "3.0.1"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=0.23"
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=0.8)"]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "livereload"
@ -456,9 +459,6 @@ category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage", "pyyaml"]
@ -475,11 +475,22 @@ markdown = "*"
[[package]]
name = "markupsafe"
version = "1.1.1"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
python-versions = ">=3.6"
[[package]]
name = "matplotlib-inline"
version = "0.1.2"
description = "Inline Matplotlib backend for Jupyter"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
traitlets = "*"
[[package]]
name = "mccabe"
@ -587,15 +598,12 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "pre-commit"
version = "2.12.1"
version = "2.13.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@ -604,7 +612,6 @@ python-versions = ">=3.6.1"
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
@ -647,7 +654,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydantic"
version = "1.8.1"
version = "1.8.2"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
@ -670,7 +677,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.8.1"
version = "2.9.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false
@ -678,7 +685,7 @@ python-versions = ">=3.5"
[[package]]
name = "pymdown-extensions"
version = "8.1.1"
version = "8.2"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
@ -697,7 +704,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pytest"
version = "6.2.3"
version = "6.2.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@ -707,7 +714,6 @@ python-versions = ">=3.6"
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"
@ -733,14 +739,14 @@ testing = ["coverage", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-cov"
version = "2.11.1"
version = "2.12.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
coverage = ">=5.2.1"
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
@ -771,7 +777,7 @@ pytest = ">=2.9.0"
[[package]]
name = "pytest-mock"
version = "3.6.0"
version = "3.6.1"
description = "Thin-wrapper around the mock package for easier use with pytest"
category = "dev"
optional = false
@ -795,7 +801,6 @@ python-versions = ">=3.5"
attrs = ">=19.0"
filelock = ">=3.0"
mypy = [
{version = ">=0.500", markers = "python_version < \"3.8\""},
{version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""},
{version = ">=0.780", markers = "python_version >= \"3.9\""},
]
@ -855,7 +860,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "six"
version = "1.15.0"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
@ -1008,11 +1013,11 @@ test = ["pytest"]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "1.0.3"
version = "2.0.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
@ -1043,7 +1048,7 @@ test = ["pytest"]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "1.1.4"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "main"
optional = false
@ -1093,7 +1098,7 @@ python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
version = "3.10.0.0"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "main"
optional = false
@ -1114,7 +1119,7 @@ test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2
[[package]]
name = "virtualenv"
version = "20.4.3"
version = "20.4.7"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@ -1124,7 +1129,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
appdirs = ">=1.4.3,<2"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
six = ">=1.9.0,<2"
[package.extras]
@ -1150,7 +1154,6 @@ python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]]
name = "zipp"
@ -1171,8 +1174,8 @@ proxy = ["aiohttp-socks"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "9a787135a6d8ed2a395c07246db424e9961281cf7e0dc00fc08507da3081143e"
python-versions = "^3.8"
content-hash = "2fcd44a8937b3ea48196c8eba8ceb0533281af34c884103bcc5b4f5f16b817d5"
[metadata.files]
aiofiles = [
@ -1254,8 +1257,8 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
babel = [
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
@ -1271,20 +1274,20 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
]
black = [
{file = "black-21.4b2-py3-none-any.whl", hash = "sha256:bff7067d8bc25eb21dcfdbc8c72f2baafd9ec6de4663241a52fb904b304d391f"},
{file = "black-21.4b2.tar.gz", hash = "sha256:fc9bcf3b482b05c1f35f6a882c079dc01b9c7795827532f4cc43c0ec88067bbc"},
{file = "black-21.5b1-py3-none-any.whl", hash = "sha256:8a60071a0043876a4ae96e6c69bd3a127dad2c1ca7c8083573eb82f92705d008"},
{file = "black-21.5b1.tar.gz", hash = "sha256:23695358dbcb3deafe7f0a3ad89feee5999a46be5fec21f4f1d108be0bcdb3b1"},
]
cfgv = [
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
{file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"},
{file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@ -1345,8 +1348,8 @@ coverage = [
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
]
decorator = [
{file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
{file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
{file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"},
{file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"},
]
distlib = [
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
@ -1361,8 +1364,8 @@ filelock = [
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
]
flake8 = [
{file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"},
{file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"},
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
flake8-html = [
{file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"},
@ -1373,8 +1376,8 @@ furo = [
{file = "furo-2020.12.30b24.tar.gz", hash = "sha256:30171899c9c06d692a778e6daf6cb2e5cbb05efc6006e1692e5e776007dc8a8c"},
]
identify = [
{file = "identify-2.2.3-py2.py3-none-any.whl", hash = "sha256:398cb92a7599da0b433c65301a1b62b9b1f4bb8248719b84736af6c0b22289d6"},
{file = "identify-2.2.3.tar.gz", hash = "sha256:4537474817e0bbb8cea3e5b7504b7de6d44e3f169a90846cbc6adb0fc8294502"},
{file = "identify-2.2.5-py2.py3-none-any.whl", hash = "sha256:9c3ab58543c03bd794a1735e4552ef6dec49ec32053278130d525f0982447d47"},
{file = "identify-2.2.5.tar.gz", hash = "sha256:bc1705694253763a3160b943316867792ec00ba7a0ee40b46e20aebaf4e0c46a"},
]
idna = [
{file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"},
@ -1385,16 +1388,16 @@ imagesize = [
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
importlib-metadata = [
{file = "importlib_metadata-4.0.0-py3-none-any.whl", hash = "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef"},
{file = "importlib_metadata-4.0.0.tar.gz", hash = "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9"},
{file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"},
{file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipython = [
{file = "ipython-7.22.0-py3-none-any.whl", hash = "sha256:c0ce02dfaa5f854809ab7413c601c4543846d9da81010258ecdab299b542d199"},
{file = "ipython-7.22.0.tar.gz", hash = "sha256:9c900332d4c5a6de534b4befeeb7de44ad0cc42e8327fa41b7685abde58cec74"},
{file = "ipython-7.23.1-py3-none-any.whl", hash = "sha256:f78c6a3972dde1cc9e4041cbf4de583546314ba52d3c97208e5b6b2221a9cb7d"},
{file = "ipython-7.23.1.tar.gz", hash = "sha256:714810a5c74f512b69d5f3b944c86e592cee0a5fb9c728e582f074610f6cf038"},
]
ipython-genutils = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@ -1409,8 +1412,8 @@ jedi = [
{file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
]
jinja2 = [
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
]
livereload = [
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
@ -1427,58 +1430,44 @@ markdown-include = [
{file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
{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"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
matplotlib-inline = [
{file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"},
{file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
@ -1580,8 +1569,8 @@ pluggy = [
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
pre-commit = [
{file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"},
{file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"},
{file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"},
{file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"},
]
prompt-toolkit = [
{file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"},
@ -1600,56 +1589,56 @@ pycodestyle = [
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
]
pydantic = [
{file = "pydantic-1.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850"},
{file = "pydantic-1.8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"},
{file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e"},
{file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771"},
{file = "pydantic-1.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f"},
{file = "pydantic-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d"},
{file = "pydantic-1.8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f"},
{file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8"},
{file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4"},
{file = "pydantic-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a"},
{file = "pydantic-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683"},
{file = "pydantic-1.8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2"},
{file = "pydantic-1.8.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125"},
{file = "pydantic-1.8.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0"},
{file = "pydantic-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99"},
{file = "pydantic-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9"},
{file = "pydantic-1.8.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c"},
{file = "pydantic-1.8.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e"},
{file = "pydantic-1.8.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f"},
{file = "pydantic-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58"},
{file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"},
{file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"},
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
pyflakes = [
{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.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
]
pymdown-extensions = [
{file = "pymdown-extensions-8.1.1.tar.gz", hash = "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735"},
{file = "pymdown_extensions-8.1.1-py3-none-any.whl", hash = "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b"},
{file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
{file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"},
{file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"},
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
]
pytest-asyncio = [
{file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"},
{file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"},
]
pytest-cov = [
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
{file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
{file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"},
{file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"},
]
pytest-html = [
{file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"},
@ -1660,8 +1649,8 @@ pytest-metadata = [
{file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"},
]
pytest-mock = [
{file = "pytest-mock-3.6.0.tar.gz", hash = "sha256:f7c3d42d6287f4e45846c8231c31902b6fa2bea98735af413a43da4cf5b727f1"},
{file = "pytest_mock-3.6.0-py3-none-any.whl", hash = "sha256:952139a535b5b48ac0bb2f90b5dd36b67c7e1ba92601f3a8012678c4bd7f0bcc"},
{file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"},
{file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"},
]
pytest-mypy = [
{file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"},
@ -1754,8 +1743,8 @@ requests = [
{file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
snowballstemmer = [
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
@ -1798,8 +1787,8 @@ sphinxcontrib-devhelp = [
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
]
sphinxcontrib-htmlhelp = [
{file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
{file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
{file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
{file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
]
sphinxcontrib-jsmath = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
@ -1810,8 +1799,8 @@ sphinxcontrib-qthelp = [
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
]
sphinxcontrib-serializinghtml = [
{file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
{file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
{file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
@ -1897,9 +1886,9 @@ typed-ast = [
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
]
uvloop = [
{file = "uvloop-0.15.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69"},
@ -1914,8 +1903,8 @@ uvloop = [
{file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"},
]
virtualenv = [
{file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"},
{file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"},
{file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"},
{file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},

View file

@ -23,7 +23,6 @@ classifiers = [
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Libraries :: Application Frameworks",
@ -32,7 +31,7 @@ classifiers = [
]
[tool.poetry.dependencies]
python = "^3.7"
python = "^3.8"
aiohttp = "^3.7.4"
pydantic = "^1.8.1"
Babel = "^2.9.1"

View file

@ -4,17 +4,17 @@ from typing import TYPE_CHECKING, AsyncGenerator, Deque, Optional, Type
from aiogram import Bot
from aiogram.client.session.base import BaseSession
from aiogram.methods import TelegramMethod
from aiogram.methods.base import Request, Response, T
from aiogram.types import UNSET
from aiogram.methods.base import Request, Response, TelegramType
from aiogram.types import UNSET, ResponseParameters
class MockedSession(BaseSession):
def __init__(self):
super(MockedSession, self).__init__()
self.responses: Deque[Response[T]] = deque()
self.responses: Deque[Response[TelegramType]] = deque()
self.requests: Deque[Request] = deque()
def add_result(self, response: Response[T]) -> Response[T]:
def add_result(self, response: Response[TelegramType]) -> Response[TelegramType]:
self.responses.append(response)
return response
@ -25,11 +25,13 @@ class MockedSession(BaseSession):
pass
async def make_request(
self, bot: Bot, method: TelegramMethod[T], timeout: Optional[int] = UNSET
) -> T:
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
) -> TelegramType:
self.requests.append(method.build_request(bot))
response: Response[T] = self.responses.pop()
self.raise_for_status(response)
response: Response[TelegramType] = self.responses.pop()
self.check_response(
method=method, status_code=response.error_code, content=response.json()
)
return response.result # type: ignore
async def stream_content(
@ -47,21 +49,23 @@ class MockedBot(Bot):
def add_result_for(
self,
method: Type[TelegramMethod[T]],
method: Type[TelegramMethod[TelegramType]],
ok: bool,
result: T = None,
result: TelegramType = None,
description: Optional[str] = None,
error_code: Optional[int] = None,
migrate_to_chat_id: Optional[int] = None,
retry_after: Optional[int] = None,
) -> Response[T]:
) -> Response[TelegramType]:
response = Response[method.__returning__]( # type: ignore
ok=ok,
result=result,
description=description,
error_code=error_code,
migrate_to_chat_id=migrate_to_chat_id,
retry_after=retry_after,
parameters=ResponseParameters(
migrate_to_chat_id=migrate_to_chat_id,
retry_after=retry_after,
),
)
self.session.add_result(response)
return response

View file

@ -172,14 +172,10 @@ class TestAiohttpSession:
return Request(method="method", data={})
call = TestMethod()
with patch(
"aiogram.client.session.base.BaseSession.raise_for_status"
) as patched_raise_for_status:
result = await session.make_request(bot, call)
assert isinstance(result, int)
assert result == 42
assert patched_raise_for_status.called_once()
result = await session.make_request(bot, call)
assert isinstance(result, int)
assert result == 42
@pytest.mark.asyncio
async def test_stream_content(self, aresponses: ResponsesMockServer):

View file

@ -4,9 +4,9 @@ from typing import AsyncContextManager, AsyncGenerator, Optional
import pytest
from aiogram.client.session.base import BaseSession, T
from aiogram.client.session.base import BaseSession, TelegramType
from aiogram.client.telegram import PRODUCTION, TelegramAPIServer
from aiogram.methods import GetMe, Response, TelegramMethod
from aiogram.methods import DeleteMessage, GetMe, Response, TelegramMethod
from aiogram.types import UNSET
try:
@ -20,7 +20,7 @@ class CustomSession(BaseSession):
async def close(self):
pass
async def make_request(self, token: str, method: TelegramMethod[T], timeout: Optional[int] = UNSET) -> None: # type: ignore
async def make_request(self, token: str, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET) -> None: # type: ignore
assert isinstance(token, str)
assert isinstance(method, TelegramMethod)
@ -135,12 +135,20 @@ class TestBaseSession:
assert session.clean_json(42) == 42
def test_raise_for_status(self):
def check_response(self):
session = CustomSession()
session.raise_for_status(Response[bool](ok=True, result=True))
session.check_response(
method=DeleteMessage(chat_id=42, message_id=42),
status_code=200,
content='{"ok":true,"result":true}',
)
with pytest.raises(Exception):
session.raise_for_status(Response[bool](ok=False, description="Error", error_code=400))
session.check_response(
method=DeleteMessage(chat_id=42, message_id=42),
status_code=400,
content='{"ok":false,"description":"test"}',
)
@pytest.mark.asyncio
async def test_make_request(self):

View file

@ -5,6 +5,9 @@ import pytest
from aiogram.methods import (
CopyMessage,
DeleteMessage,
EditMessageCaption,
EditMessageText,
SendAnimation,
SendAudio,
SendContact,
@ -549,3 +552,28 @@ class TestMessage:
if method:
assert isinstance(method, expected_method)
# TODO: Check additional fields
def test_edit_text(self):
message = Message(
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
)
method = message.edit_text(text="test")
assert isinstance(method, EditMessageText)
assert method.chat_id == message.chat.id
def test_edit_caption(self):
message = Message(
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
)
method = message.edit_caption(caption="test")
assert isinstance(method, EditMessageCaption)
assert method.chat_id == message.chat.id
def test_delete(self):
message = Message(
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
)
method = message.delete()
assert isinstance(method, DeleteMessage)
assert method.chat_id == message.chat.id
assert method.message_id == message.message_id

View file

@ -0,0 +1,177 @@
from decimal import Decimal
from enum import Enum, auto
from fractions import Fraction
from typing import Optional
from uuid import UUID
import pytest
from magic_filter import MagicFilter
from pydantic import ValidationError
from aiogram import F
from aiogram.dispatcher.filters.callback_data import CallbackData
from aiogram.types import CallbackQuery, User
class MyIntEnum(Enum):
FOO = auto()
class MyStringEnum(str, Enum):
FOO = "FOO"
class MyCallback(CallbackData, prefix="test"):
foo: str
bar: int
class TestCallbackData:
def test_init_subclass_prefix_required(self):
assert MyCallback.prefix == "test"
with pytest.raises(ValueError, match="prefix required.+"):
class MyInvalidCallback(CallbackData):
pass
def test_init_subclass_sep_validation(self):
assert MyCallback.sep == ":"
class MyCallback2(CallbackData, prefix="test2", sep="@"):
pass
assert MyCallback2.sep == "@"
with pytest.raises(ValueError, match="Separator symbol '@' .+ 'sp@m'"):
class MyInvalidCallback(CallbackData, prefix="sp@m", sep="@"):
pass
@pytest.mark.parametrize(
"value,success,expected",
[
[None, True, ""],
[42, True, "42"],
["test", True, "test"],
[9.99, True, "9.99"],
[Decimal("9.99"), True, "9.99"],
[Fraction("3/2"), True, "3/2"],
[
UUID("123e4567-e89b-12d3-a456-426655440000"),
True,
"123e4567-e89b-12d3-a456-426655440000",
],
[MyIntEnum.FOO, True, "1"],
[MyStringEnum.FOO, True, "FOO"],
[..., False, "..."],
[object, False, "..."],
[object(), False, "..."],
[User(id=42, is_bot=False, first_name="test"), False, "..."],
],
)
def test_encode_value(self, value, success, expected):
callback = MyCallback(foo="test", bar=42)
if success:
assert callback._encode_value("test", value) == expected
else:
with pytest.raises(ValueError):
assert callback._encode_value("test", value) == expected
def test_pack(self):
with pytest.raises(ValueError, match="Separator symbol .+"):
assert MyCallback(foo="te:st", bar=42).pack()
with pytest.raises(ValueError, match=".+is too long.+"):
assert MyCallback(foo="test" * 32, bar=42).pack()
assert MyCallback(foo="test", bar=42).pack() == "test:test:42"
def test_pack_optional(self):
class MyCallback1(CallbackData, prefix="test1"):
foo: str
bar: Optional[int] = None
assert MyCallback1(foo="spam").pack() == "test1:spam:"
assert MyCallback1(foo="spam", bar=42).pack() == "test1:spam:42"
class MyCallback2(CallbackData, prefix="test2"):
foo: Optional[str] = None
bar: int
assert MyCallback2(bar=42).pack() == "test2::42"
assert MyCallback2(foo="spam", bar=42).pack() == "test2:spam:42"
class MyCallback3(CallbackData, prefix="test3"):
foo: Optional[str] = "experiment"
bar: int
assert MyCallback3(bar=42).pack() == "test3:experiment:42"
assert MyCallback3(foo="spam", bar=42).pack() == "test3:spam:42"
def test_unpack(self):
with pytest.raises(TypeError, match=".+ takes 2 arguments but 3 were given"):
MyCallback.unpack("test:test:test:test")
with pytest.raises(ValueError, match="Bad prefix .+"):
MyCallback.unpack("spam:test:test")
assert MyCallback.unpack("test:test:42") == MyCallback(foo="test", bar=42)
def test_unpack_optional(self):
with pytest.raises(ValidationError):
assert MyCallback.unpack("test:test:")
class MyCallback1(CallbackData, prefix="test1"):
foo: str
bar: Optional[int] = None
assert MyCallback1.unpack("test1:spam:") == MyCallback1(foo="spam")
assert MyCallback1.unpack("test1:spam:42") == MyCallback1(foo="spam", bar=42)
class MyCallback2(CallbackData, prefix="test2"):
foo: Optional[str] = None
bar: int
assert MyCallback2.unpack("test2::42") == MyCallback2(bar=42)
assert MyCallback2.unpack("test2:spam:42") == MyCallback2(foo="spam", bar=42)
class MyCallback3(CallbackData, prefix="test3"):
foo: Optional[str] = "experiment"
bar: int
assert MyCallback3.unpack("test3:experiment:42") == MyCallback3(bar=42)
assert MyCallback3.unpack("test3:spam:42") == MyCallback3(foo="spam", bar=42)
def test_build_filter(self):
filter_object = MyCallback.filter(F.foo == "test")
assert isinstance(filter_object.rule, MagicFilter)
assert filter_object.callback_data is MyCallback
class TestCallbackDataFilter:
@pytest.mark.parametrize(
"query,rule,result",
[
["test", F.foo == "test", False],
["test:spam:42", F.foo == "test", False],
["test:test:42", F.foo == "test", {"callback_data": MyCallback(foo="test", bar=42)}],
["test:test:", F.foo == "test", False],
],
)
@pytest.mark.asyncio
async def test_call(self, query, rule, result):
callback_query = CallbackQuery(
id="1",
from_user=User(id=42, is_bot=False, first_name="test"),
data=query,
chat_instance="test",
)
filter_object = MyCallback.filter(rule)
assert await filter_object(callback_query) == result
@pytest.mark.asyncio
async def test_invalid_call(self):
filter_object = MyCallback.filter(F.test)
assert not await filter_object(User(id=42, is_bot=False, first_name="test"))