From fac69e52b7f0d38f06d28434322f3cb69460104d Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 18 Mar 2020 17:04:11 +0300 Subject: [PATCH 01/18] :poop: First iteration --- aiogram/api/client/base.py | 21 +- aiogram/api/client/bot.py | 4 +- aiogram/api/client/session/aiohttp.py | 8 +- aiogram/api/client/session/base.py | 24 +- aiogram/api/methods/base.py | 21 +- aiogram/api/types/base.py | 2 +- aiogram/api/types/input_file.py | 14 +- aiogram/api/types/message.py | 2 +- aiogram/api/types/user.py | 2 +- aiogram/dispatcher/dispatcher.py | 12 +- aiogram/dispatcher/event/handler.py | 28 +- aiogram/dispatcher/filters/__init__.py | 8 +- aiogram/dispatcher/filters/base.py | 30 +- aiogram/dispatcher/filters/command.py | 2 +- aiogram/dispatcher/filters/text.py | 2 +- aiogram/dispatcher/handler/base.py | 20 +- aiogram/dispatcher/handler/message.py | 8 +- aiogram/dispatcher/router.py | 12 +- aiogram/utils/helper.py | 70 +++-- aiogram/utils/mixins.py | 91 +++--- mypy.ini | 19 +- poetry.lock | 260 +++++++++++------- pyproject.toml | 2 +- .../test_session/test_base_session.py | 3 +- tests/test_utils/test_mixins.py | 35 +-- 25 files changed, 427 insertions(+), 273 deletions(-) diff --git a/aiogram/api/client/base.py b/aiogram/api/client/base.py index 4d8b7453..c4f7aff3 100644 --- a/aiogram/api/client/base.py +++ b/aiogram/api/client/base.py @@ -1,9 +1,17 @@ from __future__ import annotations from contextlib import asynccontextmanager -from typing import Any, Optional, TypeVar +from typing import ( + Any, + AsyncIterator, + Optional, + TypeVar, +) -from ...utils.mixins import ContextInstanceMixin, DataMixin +from ...utils.mixins import ( + ContextInstance, + ContextInstanceMixin, +) from ...utils.token import extract_bot_id, validate_token from ..methods import TelegramMethod from .session.aiohttp import AiohttpSession @@ -12,13 +20,13 @@ from .session.base import BaseSession T = TypeVar("T") -class BaseBot(ContextInstanceMixin, DataMixin): +class BaseBot(ContextInstanceMixin[ContextInstance]): """ Base class for bots """ def __init__( - self, token: str, session: BaseSession = None, parse_mode: Optional[str] = None + self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None ) -> None: validate_token(token) @@ -54,14 +62,15 @@ class BaseBot(ContextInstanceMixin, DataMixin): await self.session.close() @asynccontextmanager - async def context(self, auto_close: bool = True): + async def context(self, auto_close: bool = True) -> AsyncIterator["BaseBot[ContextInstance]"]: """ Generate bot context :param auto_close: :return: """ - token = self.set_current(self) + # TODO: because set_current expects Bot, not BaseBot — this check fails + token = self.set_current(self) # type: ignore try: yield self finally: diff --git a/aiogram/api/client/bot.py b/aiogram/api/client/bot.py index 23586fc2..51058f1d 100644 --- a/aiogram/api/client/bot.py +++ b/aiogram/api/client/bot.py @@ -101,12 +101,12 @@ from ..types import ( from .base import BaseBot -class Bot(BaseBot): +class Bot(BaseBot["Bot"]): """ Class where located all API methods """ - @alru_cache() + @alru_cache() # type: ignore async def me(self) -> User: return await self.get_me() diff --git a/aiogram/api/client/session/aiohttp.py b/aiogram/api/client/session/aiohttp.py index 06a1c99d..b123bcfc 100644 --- a/aiogram/api/client/session/aiohttp.py +++ b/aiogram/api/client/session/aiohttp.py @@ -15,8 +15,8 @@ class AiohttpSession(BaseSession): def __init__( self, api: TelegramAPIServer = PRODUCTION, - json_loads: Optional[Callable] = None, - json_dumps: Optional[Callable] = None, + json_loads: Optional[Callable[..., str]] = None, + json_dumps: Optional[Callable[..., str]] = None, ): super(AiohttpSession, self).__init__(api=api, json_loads=json_loads, json_dumps=json_dumps) self._session: Optional[ClientSession] = None @@ -27,11 +27,11 @@ class AiohttpSession(BaseSession): return self._session - async def close(self): + async def close(self) -> None: if self._session is not None and not self._session.closed: await self._session.close() - def build_form_data(self, request: Request): + def build_form_data(self, request: Request) -> FormData: form = FormData(quote_fields=False) for key, value in request.data.items(): if value is None: diff --git a/aiogram/api/client/session/base.py b/aiogram/api/client/session/base.py index f31dd451..b7eeabd3 100644 --- a/aiogram/api/client/session/base.py +++ b/aiogram/api/client/session/base.py @@ -3,7 +3,16 @@ from __future__ import annotations import abc import datetime import json -from typing import Any, AsyncGenerator, Callable, Optional, TypeVar, Union +from types import TracebackType +from typing import ( + Any, + AsyncGenerator, + Callable, + Optional, + Type, + TypeVar, + Union, +) from aiogram.utils.exceptions import TelegramAPIError @@ -17,8 +26,8 @@ class BaseSession(abc.ABC): def __init__( self, api: Optional[TelegramAPIServer] = None, - json_loads: Optional[Callable[[Any], Any]] = None, - json_dumps: Optional[Callable[[Any], Any]] = None, + json_loads: Optional[Callable[..., str]] = None, + json_dumps: Optional[Callable[..., str]] = None, ) -> None: if api is None: api = PRODUCTION @@ -37,7 +46,7 @@ class BaseSession(abc.ABC): raise TelegramAPIError(response.description) @abc.abstractmethod - async def close(self): # pragma: no cover + async def close(self) -> None: # pragma: no cover pass @abc.abstractmethod @@ -73,5 +82,10 @@ class BaseSession(abc.ABC): async def __aenter__(self) -> BaseSession: return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: await self.close() diff --git a/aiogram/api/methods/base.py b/aiogram/api/methods/base.py index c1077701..e7fa1f2c 100644 --- a/aiogram/api/methods/base.py +++ b/aiogram/api/methods/base.py @@ -2,7 +2,16 @@ from __future__ import annotations import abc import secrets -from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, Union +from typing import ( + Generator, + TYPE_CHECKING, + Any, + Dict, + Generic, + Optional, + TypeVar, + Union, +) from pydantic import BaseConfig, BaseModel, Extra from pydantic.generics import GenericModel @@ -24,7 +33,7 @@ class Request(BaseModel): class Config(BaseConfig): arbitrary_types_allowed = True - def render_webhook_request(self): + def render_webhook_request(self) -> Dict[str, Any]: return { "method": self.method, **{key: value for key, value in self.data.items() if value is not None}, @@ -48,7 +57,7 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]): @property @abc.abstractmethod - def __returning__(self) -> Type: # pragma: no cover + def __returning__(self) -> type: # pragma: no cover pass @abc.abstractmethod @@ -62,14 +71,14 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]): async def emit(self, bot: Bot) -> T: return await bot(self) - def __await__(self): + def __await__(self) -> Generator[Any, None, T]: from aiogram.api.client.bot import Bot bot = Bot.get_current(no_error=False) return self.emit(bot).__await__() -def prepare_file(name: str, value: Any, data: Dict[str, Any], files: Dict[str, Any]): +def prepare_file(name: str, value: Any, data: Dict[str, Any], files: Dict[str, Any]) -> None: if not value: return if name == "thumb": @@ -101,7 +110,7 @@ def prepare_media_file(data: Dict[str, Any], files: Dict[str, InputFile]) -> Non and isinstance(data["media"]["media"], InputFile) ): tag = secrets.token_urlsafe(10) - files[tag] = data["media"].pop("media") # type: ignore + files[tag] = data["media"].pop("media") data["media"]["media"] = f"attach://{tag}" diff --git a/aiogram/api/types/base.py b/aiogram/api/types/base.py index 057c4b3d..8c098202 100644 --- a/aiogram/api/types/base.py +++ b/aiogram/api/types/base.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from aiogram.utils.mixins import ContextInstanceMixin -class TelegramObject(ContextInstanceMixin, BaseModel): +class TelegramObject(ContextInstanceMixin["TelegramObject"], BaseModel): class Config: use_enum_values = True orm_mode = True diff --git a/aiogram/api/types/input_file.py b/aiogram/api/types/input_file.py index d6ec7d81..27b452cb 100644 --- a/aiogram/api/types/input_file.py +++ b/aiogram/api/types/input_file.py @@ -4,7 +4,13 @@ import io import os from abc import ABC, abstractmethod from pathlib import Path -from typing import AsyncGenerator, Optional, Union +from typing import ( + AsyncGenerator, + AsyncIterator, + Iterator, + Optional, + Union, +) import aiofiles as aiofiles @@ -24,14 +30,14 @@ class InputFile(ABC): self.chunk_size = chunk_size @classmethod - def __get_validators__(cls): - yield + def __get_validators__(cls) -> Iterator[None]: + yield None @abstractmethod async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]: # pragma: no cover yield b"" - async def __aiter__(self): + async def __aiter__(self) -> AsyncIterator[bytes]: async for chunk in self.read(self.chunk_size): yield chunk diff --git a/aiogram/api/types/message.py b/aiogram/api/types/message.py index e2eeb478..5af79d25 100644 --- a/aiogram/api/types/message.py +++ b/aiogram/api/types/message.py @@ -181,7 +181,7 @@ class Message(TelegramObject): buttons.""" @property - def content_type(self): + def content_type(self) -> str: if self.text: return ContentType.TEXT if self.audio: diff --git a/aiogram/api/types/user.py b/aiogram/api/types/user.py index f46d3f2b..e3a9eee1 100644 --- a/aiogram/api/types/user.py +++ b/aiogram/api/types/user.py @@ -32,7 +32,7 @@ class User(TelegramObject): """True, if the bot supports inline queries. Returned only in getMe.""" @property - def full_name(self): + def full_name(self) -> str: if self.last_name: return f"{self.first_name} {self.last_name}" return self.first_name diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index ef707a6a..06cce1b1 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import contextvars import warnings @@ -96,7 +98,7 @@ class Dispatcher(Router): update_id = update.update_id + 1 @classmethod - async def _silent_call_request(cls, bot: Bot, result: TelegramMethod) -> None: + async def _silent_call_request(cls, bot: Bot, result: TelegramMethod[Any]) -> None: """ Simulate answer into WebHook @@ -172,7 +174,7 @@ class Dispatcher(Router): raise async def feed_webhook_update( - self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs + self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs: Any ) -> Optional[Dict[str, Any]]: if not isinstance(update, Update): # Allow to use raw updates update = Update(**update) @@ -181,18 +183,18 @@ class Dispatcher(Router): loop = asyncio.get_running_loop() waiter = loop.create_future() - def release_waiter(*args: Any): + def release_waiter(*args: Any) -> None: if not waiter.done(): waiter.set_result(None) timeout_handle = loop.call_later(_timeout, release_waiter) - process_updates: Future = asyncio.ensure_future( + process_updates: Future[Any] = asyncio.ensure_future( self._feed_webhook_update(bot=bot, update=update, **kwargs) ) process_updates.add_done_callback(release_waiter, context=ctx) - def process_response(task: Future): + def process_response(task: Future[Any]) -> None: warnings.warn( f"Detected slow response into webhook.\n" f"Telegram is waiting for response only first 60 seconds and then re-send update.\n" diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index 52e8c0da..fa24f259 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -1,7 +1,16 @@ import inspect from dataclasses import dataclass, field from functools import partial -from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + List, + Optional, + Tuple, + Union, +) from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.handler.base import BaseHandler @@ -19,21 +28,22 @@ class CallableMixin: awaitable: bool = field(init=False) spec: inspect.FullArgSpec = field(init=False) - def __post_init__(self): + def __post_init__(self) -> None: callback = self.callback self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback) while hasattr(callback, "__wrapped__"): # Try to resolve decorated callbacks - callback = callback.__wrapped__ + callback = callback.__wrapped__ # type: ignore self.spec = inspect.getfullargspec(callback) - def _prepare_kwargs(self, kwargs): + def _prepare_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: if self.spec.varkw: return kwargs return {k: v for k, v in kwargs.items() if k in self.spec.args} - async def call(self, *args, **kwargs): - wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) + async def call(self, *args: Any, **kwargs: Any) -> Any: + # TODO: what we should do if callback is BaseHandler? + wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) # type: ignore if self.awaitable: return await wrapped() return wrapped() @@ -49,10 +59,10 @@ class HandlerObject(CallableMixin): callback: HandlerType filters: Optional[List[FilterObject]] = None - def __post_init__(self): + def __post_init__(self) -> None: super(HandlerObject, self).__post_init__() - - if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler): + # TODO: by types callback must be Callable or BaseHandler, not Type[BaseHandler] + if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler): # type: ignore self.awaitable = True async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]: diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 21fb80c3..3baca36f 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,4 +1,8 @@ -from typing import Dict, Tuple, Union +from typing import ( + Dict, + Tuple, + Type, +) from .base import BaseFilter from .command import Command, CommandObject @@ -14,7 +18,7 @@ __all__ = ( "ContentTypesFilter", ) -BUILTIN_FILTERS: Dict[str, Union[Tuple[BaseFilter], Tuple]] = { +BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { "update": (), "message": (Text, Command, ContentTypesFilter), "edited_message": (Text, Command, ContentTypesFilter), diff --git a/aiogram/dispatcher/filters/base.py b/aiogram/dispatcher/filters/base.py index c0a6c377..8d226720 100644 --- a/aiogram/dispatcher/filters/base.py +++ b/aiogram/dispatcher/filters/base.py @@ -1,22 +1,26 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Union +from typing import ( + Awaitable, + Callable, + Any, + Dict, + Union, +) from pydantic import BaseModel +async def _call_for_override(*args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: + pass + + class BaseFilter(ABC, BaseModel): - if TYPE_CHECKING: # pragma: no cover - # This checking type-hint is needed because mypy checks validity of overrides and raises: - # error: Signature of "__call__" incompatible with supertype "BaseFilter" [override] - # https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override + # This little hack with typehint is needed because mypy checks validity of overrides and raises: + # error: Signature of "__call__" incompatible with supertype "BaseFilter" [override] + # https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override + __call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]] = _call_for_override + abstractmethod(__call__) - pass - else: # pragma: no cover - - @abstractmethod - async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: - pass - - def __await__(self): # pragma: no cover + def __await__(self): # type: ignore # pragma: no cover # Is needed only for inspection and this method is never be called return self.__call__ diff --git a/aiogram/dispatcher/filters/command.py b/aiogram/dispatcher/filters/command.py index 0fa49b30..7d0c05f8 100644 --- a/aiogram/dispatcher/filters/command.py +++ b/aiogram/dispatcher/filters/command.py @@ -10,7 +10,7 @@ from aiogram import Bot from aiogram.api.types import Message from aiogram.dispatcher.filters import BaseFilter -CommandPatterType = Union[str, re.Pattern] # type: ignore +CommandPatterType = Union[str, re.Pattern] class Command(BaseFilter): diff --git a/aiogram/dispatcher/filters/text.py b/aiogram/dispatcher/filters/text.py index 8a94cd7b..8ab3822d 100644 --- a/aiogram/dispatcher/filters/text.py +++ b/aiogram/dispatcher/filters/text.py @@ -80,7 +80,7 @@ class Text(BaseFilter): # Impossible because the validator prevents this situation return False # pragma: no cover - def prepare_text(self, text: str): + def prepare_text(self, text: str) -> str: if self.text_ignore_case: return str(text).lower() else: diff --git a/aiogram/dispatcher/handler/base.py b/aiogram/dispatcher/handler/base.py index d4ce4c53..21bc248b 100644 --- a/aiogram/dispatcher/handler/base.py +++ b/aiogram/dispatcher/handler/base.py @@ -1,5 +1,13 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Generic, TypeVar +from typing import ( + Optional, + TYPE_CHECKING, + Any, + Dict, + Generic, + TypeVar, + cast, +) from aiogram import Bot from aiogram.api.types import Update @@ -23,18 +31,20 @@ class BaseHandler(BaseHandlerMixin[T], ABC): self.data: Dict[str, Any] = kwargs @property - def bot(self) -> Bot: + def bot(self) -> Optional[Bot]: if "bot" in self.data: - return self.data["bot"] + # TODO: remove cast + return cast(Bot, self.data["bot"]) return Bot.get_current() @property def update(self) -> Update: - return self.data["update"] + # TODO: remove cast + return cast(Update, self.data["update"]) @abstractmethod async def handle(self) -> Any: # pragma: no cover pass - def __await__(self): + def __await__(self) -> Any: return self.handle().__await__() diff --git a/aiogram/dispatcher/handler/message.py b/aiogram/dispatcher/handler/message.py index d881a75b..47ad54f3 100644 --- a/aiogram/dispatcher/handler/message.py +++ b/aiogram/dispatcher/handler/message.py @@ -1,5 +1,8 @@ from abc import ABC -from typing import Optional +from typing import ( + Optional, + cast, +) from aiogram.api.types import Chat, Message, User from aiogram.dispatcher.filters import CommandObject @@ -24,5 +27,6 @@ class MessageHandlerCommandMixin(BaseHandlerMixin[Message]): @property def command(self) -> Optional[CommandObject]: if "command" in self.data: - return self.data["command"] + # TODO: remove cast + return cast(CommandObject, self.data["command"]) return None diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 3bf5dd27..44a47255 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -3,7 +3,12 @@ from __future__ import annotations import warnings from typing import Any, Dict, List, Optional, Union -from ..api.types import Chat, Update, User +from ..api.types import ( + Chat, + TelegramObject, + Update, + User, +) from ..utils.imports import import_module from ..utils.warnings import CodeHasNoEffect from .event.observer import EventObserver, SkipHandler, TelegramEventObserver @@ -151,6 +156,7 @@ class Router: chat: Optional[Chat] = None from_user: Optional[User] = None + event: TelegramObject if update.message: update_type = "message" from_user = update.message.from_user @@ -211,7 +217,7 @@ class Router: raise SkipHandler - async def emit_startup(self, *args, **kwargs) -> None: + async def emit_startup(self, *args: Any, **kwargs: Any) -> None: """ Recursively call startup callbacks @@ -225,7 +231,7 @@ class Router: for router in self.sub_routers: await router.emit_startup(*args, **kwargs) - async def emit_shutdown(self, *args, **kwargs) -> None: + async def emit_shutdown(self, *args: Any, **kwargs: Any) -> None: """ Recursively call shutdown callbacks to graceful shutdown diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index fbb1524b..87ecfe04 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -13,7 +13,15 @@ Example: >>> print(MyHelper.all()) <<< ['barItem', 'bazItem', 'fooItem', 'lorem'] """ -from typing import List +from typing import ( + Any, + Callable, + Iterable, + List, + Optional, + Union, + cast, +) PROPS_KEYS_ATTR_NAME = "_props_keys" @@ -22,12 +30,12 @@ class Helper: mode = "" @classmethod - def all(cls): + def all(cls) -> List[Any]: """ Get all consts :return: list """ - result = [] + result: List[Any] = [] for name in dir(cls): if not name.isupper(): continue @@ -49,7 +57,7 @@ class HelperMode(Helper): lowercase = "lowercase" @classmethod - def all(cls): + def all(cls) -> List[str]: return [ cls.SCREAMING_SNAKE_CASE, cls.lowerCamelCase, @@ -59,7 +67,7 @@ class HelperMode(Helper): ] @classmethod - def _screaming_snake_case(cls, text): + def _screaming_snake_case(cls, text: str) -> str: """ Transform text to SCREAMING_SNAKE_CASE @@ -77,7 +85,7 @@ class HelperMode(Helper): return result @classmethod - def _snake_case(cls, text): + def _snake_case(cls, text: str) -> str: """ Transform text to snake case (Based on SCREAMING_SNAKE_CASE) @@ -89,7 +97,7 @@ class HelperMode(Helper): return cls._screaming_snake_case(text).lower() @classmethod - def _camel_case(cls, text, first_upper=False): + def _camel_case(cls, text: str, first_upper: bool = False) -> str: """ Transform text to camelCase or CamelCase @@ -113,7 +121,7 @@ class HelperMode(Helper): return result @classmethod - def apply(cls, text, mode): + def apply(cls, text: str, mode: Union[str, Callable[[str], str]]) -> str: """ Apply mode for text @@ -136,7 +144,20 @@ class HelperMode(Helper): return text -class Item: +class _BaseItem: + def __init__(self, value: Optional[str] = None): + self._value = cast(str, value) + + def __set_name__(self, owner: Any, name: str) -> None: + if not name.isupper(): + raise NameError("Name for helper item must be in uppercase!") + if not self._value: + # TODO: а если не имеет? + if hasattr(owner, "mode"): + self._value = HelperMode.apply(name, getattr(owner, "mode")) + + +class Item(_BaseItem): """ Helper item @@ -144,34 +165,24 @@ class Item: it will be automatically generated based on a variable's name """ - def __init__(self, value=None): - self._value = value - - def __get__(self, instance, owner): + def __get__(self, instance: Any, owner: Any) -> str: return self._value - def __set_name__(self, owner, name): - if not name.isupper(): - raise NameError("Name for helper item must be in uppercase!") - if not self._value: - if hasattr(owner, "mode"): - self._value = HelperMode.apply(name, getattr(owner, "mode")) - -class ListItem(Item): +class ListItem(_BaseItem): """ This item is always a list You can use &, | and + operators for that. """ - def add(self, other): # pragma: no cover + def add(self, other: "ListItem") -> "ListItem": # pragma: no cover return self + other - def __get__(self, instance, owner): + def __get__(self, instance: Any, owner: Any) -> "ItemsList": return ItemsList(self._value) - def __getitem__(self, item): # pragma: no cover + def __getitem__(self, item: Any) -> Any: # pragma: no cover # Only for IDE. This method is never be called. return self._value @@ -179,17 +190,17 @@ class ListItem(Item): __iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add -class ItemsList(list): +class ItemsList(List[str]): """ Patch for default list This class provides +, &, |, +=, &=, |= operators for extending the list """ - def __init__(self, *seq): + def __init__(self, *seq: Any): super(ItemsList, self).__init__(map(str, seq)) - def add(self, other): + def add(self, other: Iterable[str]) -> "ItemsList": self.extend(other) return self @@ -197,7 +208,7 @@ class ItemsList(list): class OrderedHelperMeta(type): - def __new__(mcs, name, bases, namespace, **kwargs): + def __new__(mcs, name: Any, bases: Any, namespace: Any, **kwargs: Any) -> "OrderedHelperMeta": cls = super().__new__(mcs, name, bases, namespace) props_keys = [] @@ -209,7 +220,8 @@ class OrderedHelperMeta(type): setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys) - return cls + # ref: https://gitter.im/python/typing?at=5da98cc5fa637359fc9cbfe1 + return cast(OrderedHelperMeta, cls) class OrderedHelper(metaclass=OrderedHelperMeta): diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index ca17b8d8..719cfaed 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -1,51 +1,64 @@ +from __future__ import annotations import contextvars -from typing import Type, TypeVar +from typing import ( + Any, + ClassVar, + Generic, + Optional, + TypeVar, + cast, + overload, +) -__all__ = ("DataMixin", "ContextInstanceMixin") +__all__ = ("ContextInstanceMixin",) + +from typing_extensions import Literal -class DataMixin: - @property - def data(self): - data = getattr(self, "_data", None) - if data is None: - data = {} - setattr(self, "_data", data) - return data - - def __getitem__(self, item): - return self.data[item] - - def __setitem__(self, key, value): - self.data[key] = value - - def __delitem__(self, key): - del self.data[key] - - def __contains__(self, item): - return item in self.data - - def get(self, key, default=None): - return self.data.get(key, default) +ContextInstance = TypeVar("ContextInstance") -T = TypeVar("T") +class ContextInstanceMixin(Generic[ContextInstance]): + __context_instance: ClassVar[contextvars.ContextVar[ContextInstance]] - -class ContextInstanceMixin: - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__() cls.__context_instance = contextvars.ContextVar(f"instance_{cls.__name__}") - return cls + + @overload + @classmethod + def get_current(cls) -> Optional[ContextInstance]: + ... + + @overload # noqa: F811, it's overload, not redefinition + @classmethod + def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: + ... + + @overload # noqa: F811, it's overload, not redefinition + @classmethod + def get_current(cls, no_error: Literal[False]) -> ContextInstance: + ... + + @classmethod # noqa: F811, it's overload, not redefinition + def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: + # on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any] + cls.__context_instance = cast( + contextvars.ContextVar[ContextInstance], cls.__context_instance + ) + + try: + current: Optional[ContextInstance] = cls.__context_instance.get() + except LookupError: + if no_error: + current = None + else: + raise + + return current @classmethod - def get_current(cls: Type[T], no_error=True) -> T: - if no_error: - return cls.__context_instance.get(None) - return cls.__context_instance.get() - - @classmethod - def set_current(cls: Type[T], value: T) -> contextvars.Token: + def set_current(cls, value: ContextInstance) -> contextvars.Token[ContextInstance]: if not isinstance(value, cls): raise TypeError( f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}" @@ -53,5 +66,5 @@ class ContextInstanceMixin: return cls.__context_instance.set(value) @classmethod - def reset_current(cls: Type[T], token: contextvars.Token): + def reset_current(cls, token: contextvars.Token[ContextInstance]) -> None: cls.__context_instance.reset(token) diff --git a/mypy.ini b/mypy.ini index d4a3381e..7cbf0ca4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,21 @@ [mypy] -# plugins = pydantic.mypy - +;plugins = pydantic.mypy +;python_version = 3.8 +warn_unused_configs = True +disallow_subclassing_any = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_return_any = True +follow_imports_for_stubs = True +namespace_packages = True +show_absolute_path = True ignore_missing_imports = True show_error_context = True show_error_codes = True diff --git a/poetry.lock b/poetry.lock index f1088435..f20ab5b1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -153,8 +153,8 @@ category = "dev" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.1" [[package]] category = "dev" @@ -171,7 +171,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.0.3" +version = "5.0.4" [package.extras] toml = ["toml"] @@ -182,7 +182,7 @@ description = "Decorators for Humans" name = "decorator" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.4.1" +version = "4.4.2" [[package]] category = "dev" @@ -219,6 +219,14 @@ flake8 = ">=3.3.0" jinja2 = ">=2.9.0" pygments = ">=2.2.0" +[[package]] +category = "dev" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.18.2" + [[package]] category = "main" description = "Internationalized Domain Names in Applications (IDNA)" @@ -249,7 +257,7 @@ description = "IPython: Productive Interactive Computing" name = "ipython" optional = false python-versions = ">=3.6" -version = "7.12.0" +version = "7.13.0" [package.dependencies] appnope = "*" @@ -265,7 +273,7 @@ setuptools = ">=18.5" traitlets = ">=4.2" [package.extras] -all = ["ipyparallel", "requests", "notebook", "qtconsole", "ipywidgets", "pygments", "nbconvert", "testpath", "Sphinx (>=1.3)", "nbformat", "numpy (>=1.14)", "ipykernel", "nose (>=0.10.1)"] +all = ["numpy (>=1.14)", "testpath", "notebook", "nose (>=0.10.1)", "nbconvert", "requests", "ipywidgets", "qtconsole", "ipyparallel", "Sphinx (>=1.3)", "pygments", "nbformat", "ipykernel"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -338,6 +346,25 @@ version = "2.6.1" six = "*" tornado = "*" +[[package]] +category = "dev" +description = "A Python implementation of Lunr.js" +name = "lunr" +optional = false +python-versions = "*" +version = "0.5.6" + +[package.dependencies] +future = ">=0.16.0" +six = ">=1.11.0" + +[package.dependencies.nltk] +optional = true +version = ">=3.2.5" + +[package.extras] +languages = ["nltk (>=3.2.5)"] + [[package]] category = "dev" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." @@ -406,17 +433,21 @@ category = "dev" description = "Project documentation with Markdown." name = "mkdocs" optional = false -python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.0.4" +python-versions = ">=3.5" +version = "1.1" [package.dependencies] -Jinja2 = ">=2.7.1" -Markdown = ">=2.3.1" +Jinja2 = ">=2.10.1" +Markdown = ">=3.2.1" PyYAML = ">=3.10" click = ">=3.3" livereload = ">=2.5.1" tornado = ">=5.0" +[package.dependencies.lunr] +extras = ["languages"] +version = "0.5.6" + [[package]] category = "dev" description = "A Material Design theme for MkDocs" @@ -453,7 +484,7 @@ description = "Optional static typing for Python" name = "mypy" optional = false python-versions = ">=3.5" -version = "0.761" +version = "0.770" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" @@ -471,13 +502,32 @@ optional = false python-versions = "*" version = "0.4.3" +[[package]] +category = "dev" +description = "Natural Language Toolkit" +name = "nltk" +optional = false +python-versions = "*" +version = "3.4.5" + +[package.dependencies] +six = "*" + +[package.extras] +all = ["pyparsing", "scikit-learn", "python-crfsuite", "matplotlib", "scipy", "gensim", "requests", "twython", "numpy"] +corenlp = ["requests"] +machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + [[package]] category = "dev" description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.1" +version = "20.3" [package.dependencies] pyparsing = ">=2.0.2" @@ -489,7 +539,7 @@ description = "A Python Parser" name = "parso" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.6.1" +version = "0.6.2" [package.extras] testing = ["docopt", "pytest (>=3.0.7)"] @@ -543,8 +593,8 @@ category = "dev" description = "Library for building powerful interactive command lines in Python" name = "prompt-toolkit" optional = false -python-versions = ">=3.6" -version = "3.0.3" +python-versions = ">=3.6.1" +version = "3.0.4" [package.dependencies] wcwidth = "*" @@ -600,8 +650,8 @@ category = "dev" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.5.2" +python-versions = ">=3.5" +version = "2.6.1" [[package]] category = "dev" @@ -628,7 +678,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.3.5" +version = "5.4.1" [package.dependencies] atomicwrites = ">=1.0" @@ -683,7 +733,7 @@ description = "pytest plugin for generating HTML reports" name = "pytest-html" optional = false python-versions = ">=3.6" -version = "2.0.1" +version = "2.1.0" [package.dependencies] pytest = ">=5.0" @@ -781,7 +831,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib name = "tornado" optional = false python-versions = ">= 3.5" -version = "6.0.3" +version = "6.0.4" [[package]] category = "dev" @@ -851,7 +901,7 @@ marker = "python_version >= \"3.5\" and python_version < \"3.8\" or python_versi name = "zipp" optional = false python-versions = ">=3.6" -version = "3.0.0" +version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -861,7 +911,7 @@ testing = ["jaraco.itertools", "func-timeout"] fast = ["uvloop"] [metadata] -content-hash = "2eb50b5b57d0fac4780f1eb3f92ff129d891fd346e0c00856c1a56c58feffb03" +content-hash = "bce1bc7bf9eb949283094490a084d484a3d2f7b0d992ea3a4ea1e75401f6e2da" python-versions = "^3.7" [metadata.files] @@ -931,49 +981,49 @@ chardet = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] click = [ - {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, - {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, + {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, + {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, ] colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] coverage = [ - {file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"}, - {file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"}, - {file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"}, - {file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"}, - {file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"}, - {file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"}, - {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"}, - {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"}, - {file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"}, - {file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"}, - {file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"}, - {file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"}, - {file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"}, - {file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"}, - {file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"}, - {file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"}, - {file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"}, - {file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"}, - {file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"}, - {file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"}, - {file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"}, - {file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"}, - {file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"}, - {file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"}, - {file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"}, - {file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"}, - {file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"}, - {file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"}, - {file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"}, - {file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"}, - {file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"}, + {file = "coverage-5.0.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307"}, + {file = "coverage-5.0.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8"}, + {file = "coverage-5.0.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31"}, + {file = "coverage-5.0.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441"}, + {file = "coverage-5.0.4-cp27-cp27m-win32.whl", hash = "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac"}, + {file = "coverage-5.0.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435"}, + {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037"}, + {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a"}, + {file = "coverage-5.0.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5"}, + {file = "coverage-5.0.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30"}, + {file = "coverage-5.0.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7"}, + {file = "coverage-5.0.4-cp35-cp35m-win32.whl", hash = "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de"}, + {file = "coverage-5.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1"}, + {file = "coverage-5.0.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"}, + {file = "coverage-5.0.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0"}, + {file = "coverage-5.0.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd"}, + {file = "coverage-5.0.4-cp36-cp36m-win32.whl", hash = "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0"}, + {file = "coverage-5.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b"}, + {file = "coverage-5.0.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78"}, + {file = "coverage-5.0.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6"}, + {file = "coverage-5.0.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014"}, + {file = "coverage-5.0.4-cp37-cp37m-win32.whl", hash = "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732"}, + {file = "coverage-5.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006"}, + {file = "coverage-5.0.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2"}, + {file = "coverage-5.0.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe"}, + {file = "coverage-5.0.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9"}, + {file = "coverage-5.0.4-cp38-cp38-win32.whl", hash = "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1"}, + {file = "coverage-5.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0"}, + {file = "coverage-5.0.4-cp39-cp39-win32.whl", hash = "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7"}, + {file = "coverage-5.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892"}, + {file = "coverage-5.0.4.tar.gz", hash = "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823"}, ] decorator = [ - {file = "decorator-4.4.1-py2.py3-none-any.whl", hash = "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"}, - {file = "decorator-4.4.1.tar.gz", hash = "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce"}, + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, ] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, @@ -987,6 +1037,9 @@ flake8-html = [ {file = "flake8-html-0.4.0.tar.gz", hash = "sha256:44bec37f142e97c4a5b2cf10efe24ed253617a9736878851a594d4763011e742"}, {file = "flake8_html-0.4.0-py2.py3-none-any.whl", hash = "sha256:f372cd599ba9a374943eaa75a9cce30408cf4c0cc2251bc5194e6b0d3fc2bc3a"}, ] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] idna = [ {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, @@ -996,8 +1049,8 @@ importlib-metadata = [ {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, ] ipython = [ - {file = "ipython-7.12.0-py3-none-any.whl", hash = "sha256:f6689108b1734501d3b59c84427259fd5ac5141afe2e846cfa8598eb811886c9"}, - {file = "ipython-7.12.0.tar.gz", hash = "sha256:d9459e7237e2e5858738ff9c3e26504b79899b58a6d49e574d352493d80684c6"}, + {file = "ipython-7.13.0-py3-none-any.whl", hash = "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333"}, + {file = "ipython-7.13.0.tar.gz", hash = "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1019,6 +1072,10 @@ livereload = [ {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, ] +lunr = [ + {file = "lunr-0.5.6-py2.py3-none-any.whl", hash = "sha256:1208622930c915a07e6f8e8640474357826bad48534c0f57969b6fca9bffc88e"}, + {file = "lunr-0.5.6.tar.gz", hash = "sha256:7be69d7186f65784a4f2adf81e5c58efd6a9921aa95966babcb1f2f2ada75c20"}, +] lxml = [ {file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"}, {file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"}, @@ -1083,6 +1140,11 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {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-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ @@ -1093,8 +1155,8 @@ mkautodoc = [ {file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"}, ] mkdocs = [ - {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, - {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, + {file = "mkdocs-1.1-py2.py3-none-any.whl", hash = "sha256:1e385a70aea8a9dedb731aea4fd5f3704b2074801c4f96f06b2920999babda8a"}, + {file = "mkdocs-1.1.tar.gz", hash = "sha256:9243291392f59e20b655e4e46210233453faf97787c2cf72176510e868143174"}, ] mkdocs-material = [ {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, @@ -1124,32 +1186,36 @@ multidict = [ {file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"}, ] mypy = [ - {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, - {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, - {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, - {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, - {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, - {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, - {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, - {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, - {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, - {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, - {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, - {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, - {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, - {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, + {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"}, + {file = "mypy-0.770-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754"}, + {file = "mypy-0.770-cp35-cp35m-win_amd64.whl", hash = "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65"}, + {file = "mypy-0.770-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce"}, + {file = "mypy-0.770-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761"}, + {file = "mypy-0.770-cp36-cp36m-win_amd64.whl", hash = "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2"}, + {file = "mypy-0.770-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8"}, + {file = "mypy-0.770-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913"}, + {file = "mypy-0.770-cp37-cp37m-win_amd64.whl", hash = "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9"}, + {file = "mypy-0.770-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1"}, + {file = "mypy-0.770-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27"}, + {file = "mypy-0.770-cp38-cp38-win_amd64.whl", hash = "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"}, + {file = "mypy-0.770-py3-none-any.whl", hash = "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164"}, + {file = "mypy-0.770.tar.gz", hash = "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nltk = [ + {file = "nltk-3.4.5.win32.exe", hash = "sha256:a08bdb4b8a1c13de16743068d9eb61c8c71c2e5d642e8e08205c528035843f82"}, + {file = "nltk-3.4.5.zip", hash = "sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94"}, +] packaging = [ - {file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"}, - {file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"}, + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, ] parso = [ - {file = "parso-0.6.1-py2.py3-none-any.whl", hash = "sha256:951af01f61e6dccd04159042a0706a31ad437864ec6e25d0d7a96a9fbb9b0095"}, - {file = "parso-0.6.1.tar.gz", hash = "sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57"}, + {file = "parso-0.6.2-py2.py3-none-any.whl", hash = "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995"}, + {file = "parso-0.6.2.tar.gz", hash = "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157"}, ] pathspec = [ {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, @@ -1168,8 +1234,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, - {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, + {file = "prompt_toolkit-3.0.4-py3-none-any.whl", hash = "sha256:859e1b205b6cf6a51fa57fa34202e45365cf58f8338f0ee9f4e84a4165b37d5b"}, + {file = "prompt_toolkit-3.0.4.tar.gz", hash = "sha256:ebe6b1b08c888b84c50d7f93dee21a09af39860144ff6130aadbd61ae8d29783"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, @@ -1204,8 +1270,8 @@ pyflakes = [ {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, ] pygments = [ - {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, - {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, ] pymdown-extensions = [ {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"}, @@ -1216,8 +1282,8 @@ pyparsing = [ {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, ] pytest = [ - {file = "pytest-5.3.5-py3-none-any.whl", hash = "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"}, - {file = "pytest-5.3.5.tar.gz", hash = "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d"}, + {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"}, + {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.10.0.tar.gz", hash = "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf"}, @@ -1228,8 +1294,8 @@ pytest-cov = [ {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, ] pytest-html = [ - {file = "pytest-html-2.0.1.tar.gz", hash = "sha256:933da7a5e71e5eace9e475441ed88a684f20f6198aa36516cb947ac05edd9921"}, - {file = "pytest_html-2.0.1-py2.py3-none-any.whl", hash = "sha256:bc40553ca2a1835479c2caf7d48604502cd66d0c5db58ddbca53d74946ee71bd"}, + {file = "pytest-html-2.1.0.tar.gz", hash = "sha256:8645a8616c8ed7414678e0aeebc3b2fd7d44268773ef5e7289289ad8632c9e91"}, + {file = "pytest_html-2.1.0-py2.py3-none-any.whl", hash = "sha256:0317a0a589db59c26091ab6068b3edac8d9bc1a8bb9727ade48f806797346956"}, ] pytest-metadata = [ {file = "pytest-metadata-1.8.0.tar.gz", hash = "sha256:2071a59285de40d7541fde1eb9f1ddea1c9db165882df82781367471238b66ba"}, @@ -1293,13 +1359,15 @@ toml = [ {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, ] tornado = [ - {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, - {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, - {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, - {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, - {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, - {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, - {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, @@ -1368,6 +1436,6 @@ yarl = [ {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, ] zipp = [ - {file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"}, - {file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"}, + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/pyproject.toml b/pyproject.toml index 119fe577..4ee980ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ black = {version = "^19.0", allow-prereleases = true} isort = "^4.3" flake8 = "^3.7" flake8-html = "^0.4.0" -mypy = "^0.761" +mypy = "^0.770" mkdocs = "^1.0" mkdocs-material = "^4.6" mkautodoc = "^0.1.0" diff --git a/tests/test_api/test_client/test_session/test_base_session.py b/tests/test_api/test_client/test_session/test_base_session.py index cc565a42..422acd3b 100644 --- a/tests/test_api/test_client/test_session/test_base_session.py +++ b/tests/test_api/test_client/test_session/test_base_session.py @@ -6,7 +6,6 @@ import pytest from aiogram.api.client.session.base import BaseSession, T from aiogram.api.client.telegram import PRODUCTION, TelegramAPIServer from aiogram.api.methods import GetMe, Response, TelegramMethod -from aiogram.utils.mixins import DataMixin try: from asynctest import CoroutineMock, patch @@ -31,7 +30,7 @@ class CustomSession(BaseSession): yield b"\f" * 10 -class TestBaseSession(DataMixin): +class TestBaseSession(): def test_init_api(self): session = CustomSession() assert session.api == PRODUCTION diff --git a/tests/test_utils/test_mixins.py b/tests/test_utils/test_mixins.py index 606d6aaa..e7b77727 100644 --- a/tests/test_utils/test_mixins.py +++ b/tests/test_utils/test_mixins.py @@ -1,42 +1,11 @@ import pytest -from aiogram.utils.mixins import ContextInstanceMixin, DataMixin +from aiogram.utils.mixins import ContextInstanceMixin - -class DataObject(DataMixin): +class ContextObject(ContextInstanceMixin['ContextObject']): pass -class ContextObject(ContextInstanceMixin): - pass - - -class TestDataMixin: - def test_store_value(self): - obj = DataObject() - obj["foo"] = 42 - - assert "foo" in obj - assert obj["foo"] == 42 - assert len(obj.data) == 1 - - def test_remove_value(self): - obj = DataObject() - obj["foo"] = 42 - del obj["foo"] - - assert "key" not in obj - assert len(obj.data) == 0 - - def test_getter(self): - obj = DataObject() - obj["foo"] = 42 - - assert obj.get("foo") == 42 - assert obj.get("bar") is None - assert obj.get("baz", "test") == "test" - - class TestContextInstanceMixin: def test_empty(self): obj = ContextObject() From a823e275a7e946c35ba2edf0aa20cf5e607b6bf8 Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 25 Mar 2020 15:35:32 +0300 Subject: [PATCH 02/18] Merge BaseBot to Bot class --- aiogram/api/client/base.py | 98 ------------------- aiogram/api/client/bot.py | 98 ++++++++++++++++++- aiogram/dispatcher/event/handler.py | 7 +- aiogram/dispatcher/handler/base.py | 6 +- tests/test_api/test_client/test_base_bot.py | 18 ++-- .../test_dispatcher/test_handler/test_base.py | 4 +- 6 files changed, 111 insertions(+), 120 deletions(-) delete mode 100644 aiogram/api/client/base.py diff --git a/aiogram/api/client/base.py b/aiogram/api/client/base.py deleted file mode 100644 index c4f7aff3..00000000 --- a/aiogram/api/client/base.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -from contextlib import asynccontextmanager -from typing import ( - Any, - AsyncIterator, - Optional, - TypeVar, -) - -from ...utils.mixins import ( - ContextInstance, - ContextInstanceMixin, -) -from ...utils.token import extract_bot_id, validate_token -from ..methods import TelegramMethod -from .session.aiohttp import AiohttpSession -from .session.base import BaseSession - -T = TypeVar("T") - - -class BaseBot(ContextInstanceMixin[ContextInstance]): - """ - Base class for bots - """ - - def __init__( - self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None - ) -> None: - validate_token(token) - - if session is None: - session = AiohttpSession() - - self.session = session - self.parse_mode = parse_mode - self.__token = token - - @property - def id(self) -> int: - """ - Get bot ID from token - - :return: - """ - return extract_bot_id(self.__token) - - async def __call__(self, method: TelegramMethod[T]) -> T: - """ - Call API method - - :param method: - :return: - """ - return await self.session.make_request(self.__token, method) - - async def close(self) -> None: - """ - Close bot session - """ - await self.session.close() - - @asynccontextmanager - async def context(self, auto_close: bool = True) -> AsyncIterator["BaseBot[ContextInstance]"]: - """ - Generate bot context - - :param auto_close: - :return: - """ - # TODO: because set_current expects Bot, not BaseBot — this check fails - token = self.set_current(self) # type: ignore - try: - yield self - finally: - if auto_close: - await self.close() - self.reset_current(token) - - def __hash__(self) -> int: - """ - Get hash for the token - - :return: - """ - return hash(self.__token) - - def __eq__(self, other: Any) -> bool: - """ - Compare current bot with another bot instance - - :param other: - :return: - """ - if not isinstance(other, BaseBot): - return False - return hash(self) == hash(other) diff --git a/aiogram/api/client/bot.py b/aiogram/api/client/bot.py index 51058f1d..5e58b4f8 100644 --- a/aiogram/api/client/bot.py +++ b/aiogram/api/client/bot.py @@ -1,8 +1,20 @@ +from __future__ import annotations + import datetime -from typing import List, Optional, Union +from contextlib import asynccontextmanager +from typing import ( + List, + Optional, + Union, + TypeVar, + AsyncIterator, + Any, +) from async_lru import alru_cache +from .session.aiohttp import AiohttpSession +from .session.base import BaseSession from ..methods import ( AddStickerToSet, AnswerCallbackQuery, @@ -70,6 +82,7 @@ from ..methods import ( UnbanChatMember, UnpinChatMessage, UploadStickerFile, + TelegramMethod, ) from ..types import ( Chat, @@ -98,14 +111,93 @@ from ..types import ( UserProfilePhotos, WebhookInfo, ) -from .base import BaseBot +from ...utils.mixins import ( + ContextInstanceMixin, +) +from ...utils.token import ( + validate_token, + extract_bot_id, +) + +T = TypeVar("T") -class Bot(BaseBot["Bot"]): +class Bot(ContextInstanceMixin["Bot"]): """ Class where located all API methods """ + def __init__( + self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None + ) -> None: + validate_token(token) + + if session is None: + session = AiohttpSession() + + self.session = session + self.parse_mode = parse_mode + self.__token = token + + @property + def id(self) -> int: + """ + Get bot ID from token + + :return: + """ + return extract_bot_id(self.__token) + + async def __call__(self, method: TelegramMethod[T]) -> T: + """ + Call API method + + :param method: + :return: + """ + return await self.session.make_request(self.__token, method) + + async def close(self) -> None: + """ + Close bot session + """ + await self.session.close() + + @asynccontextmanager + async def context(self, auto_close: bool = True) -> AsyncIterator[Bot]: + """ + Generate bot context + + :param auto_close: + :return: + """ + token = self.set_current(self) + try: + yield self + finally: + if auto_close: + await self.close() + self.reset_current(token) + + def __hash__(self) -> int: + """ + Get hash for the token + + :return: + """ + return hash(self.__token) + + def __eq__(self, other: Any) -> bool: + """ + Compare current bot with another bot instance + + :param other: + :return: + """ + if not isinstance(other, Bot): + return False + return hash(self) == hash(other) + @alru_cache() # type: ignore async def me(self) -> User: return await self.get_me() diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index fa24f259..5df6f28d 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -10,6 +10,7 @@ from typing import ( Optional, Tuple, Union, + Type, ) from aiogram.dispatcher.filters.base import BaseFilter @@ -19,7 +20,7 @@ CallbackType = Callable[[Any], Awaitable[Any]] SyncFilter = Callable[[Any], Any] AsyncFilter = Callable[[Any], Awaitable[Any]] FilterType = Union[SyncFilter, AsyncFilter, BaseFilter] -HandlerType = Union[FilterType, BaseHandler] +HandlerType = Union[FilterType, Type[BaseHandler]] @dataclass @@ -42,8 +43,7 @@ class CallableMixin: return {k: v for k, v in kwargs.items() if k in self.spec.args} async def call(self, *args: Any, **kwargs: Any) -> Any: - # TODO: what we should do if callback is BaseHandler? - wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) # type: ignore + wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) if self.awaitable: return await wrapped() return wrapped() @@ -61,7 +61,6 @@ class HandlerObject(CallableMixin): def __post_init__(self) -> None: super(HandlerObject, self).__post_init__() - # TODO: by types callback must be Callable or BaseHandler, not Type[BaseHandler] if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler): # type: ignore self.awaitable = True diff --git a/aiogram/dispatcher/handler/base.py b/aiogram/dispatcher/handler/base.py index 21bc248b..45d3f28e 100644 --- a/aiogram/dispatcher/handler/base.py +++ b/aiogram/dispatcher/handler/base.py @@ -31,15 +31,13 @@ class BaseHandler(BaseHandlerMixin[T], ABC): self.data: Dict[str, Any] = kwargs @property - def bot(self) -> Optional[Bot]: + def bot(self) -> Bot: if "bot" in self.data: - # TODO: remove cast return cast(Bot, self.data["bot"]) - return Bot.get_current() + return Bot.get_current(no_error=False) @property def update(self) -> Update: - # TODO: remove cast return cast(Update, self.data["update"]) @abstractmethod diff --git a/tests/test_api/test_client/test_base_bot.py b/tests/test_api/test_client/test_base_bot.py index 652f0918..a254bfaf 100644 --- a/tests/test_api/test_client/test_base_bot.py +++ b/tests/test_api/test_client/test_base_bot.py @@ -1,6 +1,6 @@ import pytest -from aiogram.api.client.base import BaseBot +from aiogram import Bot from aiogram.api.client.session.aiohttp import AiohttpSession from aiogram.api.methods import GetMe @@ -12,22 +12,22 @@ except ImportError: class TestBaseBot: def test_init(self): - base_bot = BaseBot("42:TEST") + base_bot = Bot("42:TEST") assert isinstance(base_bot.session, AiohttpSession) assert base_bot.id == 42 def test_hashable(self): - base_bot = BaseBot("42:TEST") + base_bot = Bot("42:TEST") assert hash(base_bot) == hash("42:TEST") def test_equals(self): - base_bot = BaseBot("42:TEST") - assert base_bot == BaseBot("42:TEST") + base_bot = Bot("42:TEST") + assert base_bot == Bot("42:TEST") assert base_bot != "42:TEST" @pytest.mark.asyncio async def test_emit(self): - base_bot = BaseBot("42:TEST") + base_bot = Bot("42:TEST") method = GetMe() @@ -40,7 +40,7 @@ class TestBaseBot: @pytest.mark.asyncio async def test_close(self): - base_bot = BaseBot("42:TEST", session=AiohttpSession()) + base_bot = Bot("42:TEST", session=AiohttpSession()) await base_bot.session.create_session() with patch( @@ -55,10 +55,10 @@ class TestBaseBot: with patch( "aiogram.api.client.session.aiohttp.AiohttpSession.close", new_callable=CoroutineMock ) as mocked_close: - async with BaseBot("42:TEST", session=AiohttpSession()).context( + async with Bot("42:TEST", session=AiohttpSession()).context( auto_close=close ) as bot: - assert isinstance(bot, BaseBot) + assert isinstance(bot, Bot) if close: mocked_close.assert_awaited() else: diff --git a/tests/test_dispatcher/test_handler/test_base.py b/tests/test_dispatcher/test_handler/test_base.py index 5e8ef0f3..21063b62 100644 --- a/tests/test_dispatcher/test_handler/test_base.py +++ b/tests/test_dispatcher/test_handler/test_base.py @@ -23,7 +23,6 @@ class TestBaseClassBasedHandler: assert handler.event == event assert handler.data["key"] == 42 - assert hasattr(handler, "bot") assert not hasattr(handler, "filters") assert await handler == 42 @@ -33,7 +32,8 @@ class TestBaseClassBasedHandler: handler = MyHandler(event=event, key=42) bot = Bot("42:TEST") - assert handler.bot is None + with pytest.raises(LookupError): + handler.bot Bot.set_current(bot) assert handler.bot == bot From 7db1572fd3ac348e5d88666c87fa99e792a9e04e Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 25 Mar 2020 15:49:43 +0300 Subject: [PATCH 03/18] Return DataMixin --- aiogram/utils/mixins.py | 28 ++++++++++++++++++++++++- tests/test_utils/test_mixins.py | 36 ++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 719cfaed..2972e1b7 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -8,13 +8,39 @@ from typing import ( TypeVar, cast, overload, + Dict, ) -__all__ = ("ContextInstanceMixin",) +__all__ = ("ContextInstanceMixin", "DataMixin") from typing_extensions import Literal +class DataMixin: + @property + def data(self) -> Dict[str, Any]: + data: Optional[Dict[str, Any]] = getattr(self, "_data", None) + if data is None: + data = {} + setattr(self, "_data", data) + return data + + def __getitem__(self, key: str) -> Any: + return self.data[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.data[key] = value + + def __delitem__(self, key: str) -> None: + del self.data[key] + + def __contains__(self, key: str) -> bool: + return key in self.data + + def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]: + return self.data.get(key, default) + + ContextInstance = TypeVar("ContextInstance") diff --git a/tests/test_utils/test_mixins.py b/tests/test_utils/test_mixins.py index e7b77727..fe96b3e1 100644 --- a/tests/test_utils/test_mixins.py +++ b/tests/test_utils/test_mixins.py @@ -1,11 +1,45 @@ import pytest -from aiogram.utils.mixins import ContextInstanceMixin +from aiogram.utils.mixins import ( + ContextInstanceMixin, + DataMixin, +) + class ContextObject(ContextInstanceMixin['ContextObject']): pass +class DataObject(DataMixin): + pass + + +class TestDataMixin: + def test_store_value(self): + obj = DataObject() + obj["foo"] = 42 + + assert "foo" in obj + assert obj["foo"] == 42 + assert len(obj.data) == 1 + + def test_remove_value(self): + obj = DataObject() + obj["foo"] = 42 + del obj["foo"] + + assert "key" not in obj + assert len(obj.data) == 0 + + def test_getter(self): + obj = DataObject() + obj["foo"] = 42 + + assert obj.get("foo") == 42 + assert obj.get("bar") is None + assert obj.get("baz", "test") == "test" + + class TestContextInstanceMixin: def test_empty(self): obj = ContextObject() From 23c632b37b4618ca454234bdd986dedae8668853 Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 25 Mar 2020 15:57:25 +0300 Subject: [PATCH 04/18] Fix style errors --- aiogram/api/client/bot.py | 4 +--- aiogram/dispatcher/handler/base.py | 1 - aiogram/dispatcher/handler/message.py | 1 - aiogram/utils/mixins.py | 8 ++++---- tests/test_api/test_client/test_base_bot.py | 4 +--- .../test_client/test_session/test_base_session.py | 2 +- tests/test_utils/test_mixins.py | 2 +- 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/aiogram/api/client/bot.py b/aiogram/api/client/bot.py index 5e58b4f8..00ec955d 100644 --- a/aiogram/api/client/bot.py +++ b/aiogram/api/client/bot.py @@ -111,9 +111,7 @@ from ..types import ( UserProfilePhotos, WebhookInfo, ) -from ...utils.mixins import ( - ContextInstanceMixin, -) +from ...utils.mixins import ContextInstanceMixin from ...utils.token import ( validate_token, extract_bot_id, diff --git a/aiogram/dispatcher/handler/base.py b/aiogram/dispatcher/handler/base.py index 45d3f28e..bf8f9785 100644 --- a/aiogram/dispatcher/handler/base.py +++ b/aiogram/dispatcher/handler/base.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod from typing import ( - Optional, TYPE_CHECKING, Any, Dict, diff --git a/aiogram/dispatcher/handler/message.py b/aiogram/dispatcher/handler/message.py index 47ad54f3..7e7e0ab6 100644 --- a/aiogram/dispatcher/handler/message.py +++ b/aiogram/dispatcher/handler/message.py @@ -27,6 +27,5 @@ class MessageHandlerCommandMixin(BaseHandlerMixin[Message]): @property def command(self) -> Optional[CommandObject]: if "command" in self.data: - # TODO: remove cast return cast(CommandObject, self.data["command"]) return None diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 2972e1b7..897502ab 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -58,16 +58,16 @@ class ContextInstanceMixin(Generic[ContextInstance]): @overload # noqa: F811, it's overload, not redefinition @classmethod - def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: + def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: # noqa: F811 ... - @overload # noqa: F811, it's overload, not redefinition + @overload # noqa: F811, it's overload, not redefinition @classmethod - def get_current(cls, no_error: Literal[False]) -> ContextInstance: + def get_current(cls, no_error: Literal[False]) -> ContextInstance: # noqa: F811 ... @classmethod # noqa: F811, it's overload, not redefinition - def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: + def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: # noqa: F811 # on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any] cls.__context_instance = cast( contextvars.ContextVar[ContextInstance], cls.__context_instance diff --git a/tests/test_api/test_client/test_base_bot.py b/tests/test_api/test_client/test_base_bot.py index a254bfaf..91c9f7c8 100644 --- a/tests/test_api/test_client/test_base_bot.py +++ b/tests/test_api/test_client/test_base_bot.py @@ -55,9 +55,7 @@ class TestBaseBot: with patch( "aiogram.api.client.session.aiohttp.AiohttpSession.close", new_callable=CoroutineMock ) as mocked_close: - async with Bot("42:TEST", session=AiohttpSession()).context( - auto_close=close - ) as bot: + async with Bot("42:TEST", session=AiohttpSession()).context(auto_close=close) as bot: assert isinstance(bot, Bot) if close: mocked_close.assert_awaited() diff --git a/tests/test_api/test_client/test_session/test_base_session.py b/tests/test_api/test_client/test_session/test_base_session.py index 422acd3b..4c86f9da 100644 --- a/tests/test_api/test_client/test_session/test_base_session.py +++ b/tests/test_api/test_client/test_session/test_base_session.py @@ -30,7 +30,7 @@ class CustomSession(BaseSession): yield b"\f" * 10 -class TestBaseSession(): +class TestBaseSession: def test_init_api(self): session = CustomSession() assert session.api == PRODUCTION diff --git a/tests/test_utils/test_mixins.py b/tests/test_utils/test_mixins.py index fe96b3e1..1f4805bd 100644 --- a/tests/test_utils/test_mixins.py +++ b/tests/test_utils/test_mixins.py @@ -6,7 +6,7 @@ from aiogram.utils.mixins import ( ) -class ContextObject(ContextInstanceMixin['ContextObject']): +class ContextObject(ContextInstanceMixin["ContextObject"]): pass From 756412d784cd0c94e32051cd3edb2f9476c00b40 Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 25 Mar 2020 16:19:48 +0300 Subject: [PATCH 05/18] Turn off mypy on tests folder --- .github/workflows/tests.yml | 2 +- Makefile | 4 ++-- mypy.ini | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c030f5d..8710add4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: - name: Lint code run: | poetry run flake8 aiogram test - poetry run mypy aiogram tests + poetry run mypy aiogram - name: Run tests run: | diff --git a/Makefile b/Makefile index d4007f91..1a4696e2 100644 --- a/Makefile +++ b/Makefile @@ -86,11 +86,11 @@ flake8-report: .PHONY: mypy mypy: - $(py) mypy aiogram tests + $(py) mypy aiogram .PHONY: mypy-report mypy-report: - $(py) mypy aiogram tests --html-report $(reports_dir)/typechecking + $(py) mypy aiogram --html-report $(reports_dir)/typechecking .PHONY: lint lint: isort black flake8 mypy diff --git a/mypy.ini b/mypy.ini index 7cbf0ca4..afe61218 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,10 @@ [mypy] ;plugins = pydantic.mypy -;python_version = 3.8 +python_version = 3.7 +show_error_codes = True +show_error_context = True +pretty = True +ignore_missing_imports = False warn_unused_configs = True disallow_subclassing_any = True disallow_any_generics = True @@ -16,7 +20,12 @@ warn_return_any = True follow_imports_for_stubs = True namespace_packages = True show_absolute_path = True + +[mypy-aiofiles] +ignore_missing_imports = True + +[mypy-async_lru] +ignore_missing_imports = True + +[mypy-uvloop] ignore_missing_imports = True -show_error_context = True -show_error_codes = True -pretty = True From 45cfa5b3c98c605ae77fa1785d135e5d782adc40 Mon Sep 17 00:00:00 2001 From: Boger Date: Wed, 25 Mar 2020 16:28:24 +0300 Subject: [PATCH 06/18] Fix coverage with `no cover` for unreachable code --- aiogram/dispatcher/filters/base.py | 2 +- aiogram/utils/mixins.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/dispatcher/filters/base.py b/aiogram/dispatcher/filters/base.py index 8d226720..af712c70 100644 --- a/aiogram/dispatcher/filters/base.py +++ b/aiogram/dispatcher/filters/base.py @@ -10,7 +10,7 @@ from typing import ( from pydantic import BaseModel -async def _call_for_override(*args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: +async def _call_for_override(*args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: # pragma: no cover pass diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 897502ab..8676ea33 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -53,21 +53,21 @@ class ContextInstanceMixin(Generic[ContextInstance]): @overload @classmethod - def get_current(cls) -> Optional[ContextInstance]: + def get_current(cls) -> Optional[ContextInstance]: # pragma: no cover ... @overload # noqa: F811, it's overload, not redefinition @classmethod - def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: # noqa: F811 + def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 ... @overload # noqa: F811, it's overload, not redefinition @classmethod - def get_current(cls, no_error: Literal[False]) -> ContextInstance: # noqa: F811 + def get_current(cls, no_error: Literal[False]) -> ContextInstance: # pragma: no cover # noqa: F811 ... @classmethod # noqa: F811, it's overload, not redefinition - def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: # noqa: F811 + def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 # on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any] cls.__context_instance = cast( contextvars.ContextVar[ContextInstance], cls.__context_instance From 4298406bd20d2c48aa77c198e175ede54eb2b091 Mon Sep 17 00:00:00 2001 From: Boger Date: Sat, 28 Mar 2020 18:47:42 +0300 Subject: [PATCH 07/18] Add check for owner class in Item --- aiogram/utils/helper.py | 9 +++++---- tests/test_utils/test_helper.py | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 87ecfe04..11e67ab4 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -13,6 +13,7 @@ Example: >>> print(MyHelper.all()) <<< ['barItem', 'bazItem', 'fooItem', 'lorem'] """ +import inspect from typing import ( Any, Callable, @@ -152,9 +153,9 @@ class _BaseItem: if not name.isupper(): raise NameError("Name for helper item must be in uppercase!") if not self._value: - # TODO: а если не имеет? - if hasattr(owner, "mode"): - self._value = HelperMode.apply(name, getattr(owner, "mode")) + if not inspect.isclass(owner) or not issubclass(owner, Helper): + raise RuntimeError("Instances of Item can be used only as Helper attributes") + self._value = HelperMode.apply(name, owner.mode) class Item(_BaseItem): @@ -224,7 +225,7 @@ class OrderedHelperMeta(type): return cast(OrderedHelperMeta, cls) -class OrderedHelper(metaclass=OrderedHelperMeta): +class OrderedHelper(Helper, metaclass=OrderedHelperMeta): mode = "" @classmethod diff --git a/tests/test_utils/test_helper.py b/tests/test_utils/test_helper.py index ae07ce97..b468dc6b 100644 --- a/tests/test_utils/test_helper.py +++ b/tests/test_utils/test_helper.py @@ -40,6 +40,11 @@ class TestHelper: class MyHelper(Helper): kaboom = Item() + def test_not_a_helper_subclass(self): + with pytest.raises(RuntimeError): + + class NotAHelperSubclass: + A = Item() class TestHelperMode: def test_helper_mode_all(self): From 880e93570037c32490cc741e5f124691059e84d6 Mon Sep 17 00:00:00 2001 From: Boger Date: Sat, 28 Mar 2020 19:43:35 +0300 Subject: [PATCH 08/18] Return old hack, because new break mypy plugins :face_with_rolling_eyes: --- aiogram/dispatcher/filters/base.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/aiogram/dispatcher/filters/base.py b/aiogram/dispatcher/filters/base.py index af712c70..a71c484a 100644 --- a/aiogram/dispatcher/filters/base.py +++ b/aiogram/dispatcher/filters/base.py @@ -1,26 +1,28 @@ from abc import ABC, abstractmethod from typing import ( - Awaitable, - Callable, + TYPE_CHECKING, Any, Dict, Union, + Callable, + Awaitable, ) from pydantic import BaseModel -async def _call_for_override(*args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: # pragma: no cover - pass - - class BaseFilter(ABC, BaseModel): - # This little hack with typehint is needed because mypy checks validity of overrides and raises: - # error: Signature of "__call__" incompatible with supertype "BaseFilter" [override] - # https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override - __call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]] = _call_for_override - abstractmethod(__call__) + if TYPE_CHECKING: # pragma: no cover + # This checking type-hint is needed because mypy checks validity of overrides and raises: + # error: Signature of "__call__" incompatible with supertype "BaseFilter" [override] + # https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override + __call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]] + else: # pragma: no cover - def __await__(self): # type: ignore # pragma: no cover + @abstractmethod + async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: + pass + + def __await__(self): # type: ignore # pragma: no cover # Is needed only for inspection and this method is never be called return self.__call__ From 33003f20266dd16339dd912c8484f460009ae849 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 11 Apr 2020 20:15:03 +0300 Subject: [PATCH 09/18] Add support of Bot API 4.7. Bump version --- aiogram/__init__.py | 4 +- aiogram/api/client/bot.py | 209 +++++++++++++----- aiogram/api/client/session/base.py | 10 +- aiogram/api/methods/__init__.py | 8 + aiogram/api/methods/add_sticker_to_set.py | 14 +- aiogram/api/methods/base.py | 11 +- aiogram/api/methods/create_new_sticker_set.py | 20 +- aiogram/api/methods/delete_message.py | 1 + aiogram/api/methods/edit_message_caption.py | 2 +- aiogram/api/methods/edit_message_text.py | 2 +- aiogram/api/methods/get_my_commands.py | 20 ++ aiogram/api/methods/send_animation.py | 3 +- aiogram/api/methods/send_audio.py | 2 +- aiogram/api/methods/send_dice.py | 40 ++++ aiogram/api/methods/send_document.py | 3 +- aiogram/api/methods/send_message.py | 2 +- aiogram/api/methods/send_photo.py | 3 +- aiogram/api/methods/send_poll.py | 2 +- aiogram/api/methods/send_sticker.py | 2 +- aiogram/api/methods/send_video.py | 3 +- aiogram/api/methods/send_voice.py | 4 +- aiogram/api/methods/set_my_commands.py | 23 ++ .../methods/set_sticker_position_in_set.py | 2 +- aiogram/api/methods/set_sticker_set_thumb.py | 36 +++ aiogram/api/methods/upload_sticker_file.py | 2 +- aiogram/api/types/__init__.py | 4 + aiogram/api/types/bot_command.py | 17 ++ aiogram/api/types/dice.py | 15 ++ .../api/types/inline_query_result_audio.py | 2 +- .../types/inline_query_result_cached_audio.py | 2 +- .../inline_query_result_cached_document.py | 2 +- .../types/inline_query_result_cached_gif.py | 2 +- .../inline_query_result_cached_mpeg4_gif.py | 2 +- .../types/inline_query_result_cached_photo.py | 2 +- .../types/inline_query_result_cached_video.py | 2 +- .../types/inline_query_result_cached_voice.py | 2 +- .../api/types/inline_query_result_document.py | 2 +- aiogram/api/types/inline_query_result_gif.py | 2 +- .../types/inline_query_result_mpeg4_gif.py | 2 +- .../api/types/inline_query_result_photo.py | 2 +- .../api/types/inline_query_result_video.py | 2 +- .../api/types/inline_query_result_voice.py | 4 +- aiogram/api/types/input_file.py | 8 +- aiogram/api/types/input_media_animation.py | 2 +- aiogram/api/types/input_media_audio.py | 2 +- aiogram/api/types/input_media_document.py | 2 +- aiogram/api/types/input_media_photo.py | 2 +- aiogram/api/types/input_media_video.py | 2 +- aiogram/api/types/message.py | 8 +- aiogram/api/types/sticker.py | 2 +- aiogram/api/types/sticker_set.py | 5 +- aiogram/dispatcher/event/handler.py | 12 +- aiogram/dispatcher/filters/__init__.py | 6 +- aiogram/dispatcher/filters/base.py | 9 +- aiogram/dispatcher/handler/base.py | 9 +- aiogram/dispatcher/handler/message.py | 5 +- aiogram/dispatcher/router.py | 7 +- aiogram/utils/helper.py | 10 +- aiogram/utils/mixins.py | 34 ++- docs/_api_version.md | 2 +- docs/api/methods/add_sticker_to_set.md | 9 +- docs/api/methods/answer_callback_query.md | 4 +- docs/api/methods/answer_inline_query.md | 4 +- docs/api/methods/answer_pre_checkout_query.md | 4 +- docs/api/methods/answer_shipping_query.md | 4 +- docs/api/methods/create_new_sticker_set.md | 9 +- docs/api/methods/delete_chat_photo.md | 4 +- docs/api/methods/delete_chat_sticker_set.md | 4 +- docs/api/methods/delete_message.md | 6 +- docs/api/methods/delete_sticker_from_set.md | 4 +- docs/api/methods/delete_webhook.md | 4 +- docs/api/methods/edit_message_caption.md | 6 +- .../api/methods/edit_message_live_location.md | 4 +- docs/api/methods/edit_message_media.md | 4 +- docs/api/methods/edit_message_reply_markup.md | 4 +- docs/api/methods/edit_message_text.md | 6 +- docs/api/methods/export_chat_invite_link.md | 4 +- docs/api/methods/forward_message.md | 4 +- docs/api/methods/get_chat.md | 3 +- docs/api/methods/get_chat_administrators.md | 3 +- docs/api/methods/get_chat_member.md | 3 +- docs/api/methods/get_chat_members_count.md | 3 +- docs/api/methods/get_file.md | 3 +- docs/api/methods/get_game_high_scores.md | 3 +- docs/api/methods/get_me.md | 3 +- docs/api/methods/get_my_commands.md | 48 ++++ docs/api/methods/get_sticker_set.md | 3 +- docs/api/methods/get_updates.md | 3 +- docs/api/methods/get_user_profile_photos.md | 3 +- docs/api/methods/get_webhook_info.md | 3 +- docs/api/methods/index.md | 5 +- docs/api/methods/kick_chat_member.md | 4 +- docs/api/methods/leave_chat.md | 4 +- docs/api/methods/pin_chat_message.md | 4 +- docs/api/methods/promote_chat_member.md | 4 +- docs/api/methods/restrict_chat_member.md | 4 +- docs/api/methods/send_animation.md | 6 +- docs/api/methods/send_audio.md | 6 +- docs/api/methods/send_chat_action.md | 4 +- docs/api/methods/send_contact.md | 4 +- docs/api/methods/send_dice.md | 64 ++++++ docs/api/methods/send_document.md | 6 +- docs/api/methods/send_game.md | 4 +- docs/api/methods/send_invoice.md | 4 +- docs/api/methods/send_location.md | 4 +- docs/api/methods/send_media_group.md | 4 +- docs/api/methods/send_message.md | 6 +- docs/api/methods/send_photo.md | 6 +- docs/api/methods/send_poll.md | 4 +- docs/api/methods/send_sticker.md | 6 +- docs/api/methods/send_venue.md | 4 +- docs/api/methods/send_video.md | 6 +- docs/api/methods/send_video_note.md | 4 +- docs/api/methods/send_voice.md | 8 +- .../set_chat_administrator_custom_title.md | 4 +- docs/api/methods/set_chat_description.md | 4 +- docs/api/methods/set_chat_permissions.md | 4 +- docs/api/methods/set_chat_photo.md | 3 +- docs/api/methods/set_chat_sticker_set.md | 4 +- docs/api/methods/set_chat_title.md | 4 +- docs/api/methods/set_game_score.md | 4 +- docs/api/methods/set_my_commands.md | 57 +++++ docs/api/methods/set_passport_data_errors.md | 4 +- .../methods/set_sticker_position_in_set.md | 6 +- docs/api/methods/set_sticker_set_thumb.md | 60 +++++ docs/api/methods/set_webhook.md | 4 +- .../api/methods/stop_message_live_location.md | 4 +- docs/api/methods/stop_poll.md | 4 +- docs/api/methods/unban_chat_member.md | 4 +- docs/api/methods/unpin_chat_message.md | 4 +- docs/api/methods/upload_sticker_file.md | 5 +- docs/api/types/bot_command.md | 25 +++ docs/api/types/dice.md | 24 ++ docs/api/types/index.md | 3 +- docs/api/types/inline_query.md | 2 +- docs/api/types/inline_query_result_audio.md | 2 +- .../types/inline_query_result_cached_audio.md | 2 +- .../inline_query_result_cached_document.md | 2 +- .../types/inline_query_result_cached_gif.md | 2 +- .../inline_query_result_cached_mpeg4_gif.md | 2 +- .../types/inline_query_result_cached_photo.md | 2 +- .../types/inline_query_result_cached_video.md | 2 +- .../types/inline_query_result_cached_voice.md | 2 +- .../api/types/inline_query_result_document.md | 2 +- docs/api/types/inline_query_result_gif.md | 2 +- .../types/inline_query_result_mpeg4_gif.md | 2 +- docs/api/types/inline_query_result_photo.md | 2 +- docs/api/types/inline_query_result_video.md | 2 +- docs/api/types/inline_query_result_voice.md | 4 +- docs/api/types/input_media_animation.md | 2 +- docs/api/types/input_media_audio.md | 2 +- docs/api/types/input_media_document.md | 2 +- docs/api/types/input_media_photo.md | 2 +- docs/api/types/input_media_video.md | 2 +- docs/api/types/keyboard_button.md | 4 +- docs/api/types/message.md | 8 +- docs/api/types/sticker.md | 2 +- docs/api/types/sticker_set.md | 2 + mkdocs.yml | 14 +- poetry.lock | 117 +++++----- pyproject.toml | 3 +- .../test_methods/test_get_my_commands.py | 27 +++ tests/test_api/test_methods/test_send_dice.py | 25 +++ .../test_methods/test_set_my_commands.py | 28 +++ .../test_set_sticker_set_thumb.py | 26 +++ tests/test_utils/test_helper.py | 1 + tests/test_utils/test_mixins.py | 5 +- 167 files changed, 996 insertions(+), 504 deletions(-) create mode 100644 aiogram/api/methods/get_my_commands.py create mode 100644 aiogram/api/methods/send_dice.py create mode 100644 aiogram/api/methods/set_my_commands.py create mode 100644 aiogram/api/methods/set_sticker_set_thumb.py create mode 100644 aiogram/api/types/bot_command.py create mode 100644 aiogram/api/types/dice.py create mode 100644 docs/api/methods/get_my_commands.md create mode 100644 docs/api/methods/send_dice.md create mode 100644 docs/api/methods/set_my_commands.md create mode 100644 docs/api/methods/set_sticker_set_thumb.md create mode 100644 docs/api/types/bot_command.md create mode 100644 docs/api/types/dice.md create mode 100644 tests/test_api/test_methods/test_get_my_commands.py create mode 100644 tests/test_api/test_methods/test_send_dice.py create mode 100644 tests/test_api/test_methods/test_set_my_commands.py create mode 100644 tests/test_api/test_methods/test_set_sticker_set_thumb.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 0bc46460..8975399c 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -26,5 +26,5 @@ __all__ = ( "handler", ) -__version__ = "3.0.0a2" -__api_version__ = "4.6" +__version__ = "3.0.0a3" +__api_version__ = "4.7" diff --git a/aiogram/api/client/bot.py b/aiogram/api/client/bot.py index 00ec955d..27293d83 100644 --- a/aiogram/api/client/bot.py +++ b/aiogram/api/client/bot.py @@ -2,19 +2,12 @@ from __future__ import annotations import datetime from contextlib import asynccontextmanager -from typing import ( - List, - Optional, - Union, - TypeVar, - AsyncIterator, - Any, -) +from typing import Any, AsyncIterator, List, Optional, TypeVar, Union from async_lru import alru_cache -from .session.aiohttp import AiohttpSession -from .session.base import BaseSession +from ...utils.mixins import ContextInstanceMixin +from ...utils.token import extract_bot_id, validate_token from ..methods import ( AddStickerToSet, AnswerCallbackQuery, @@ -41,6 +34,7 @@ from ..methods import ( GetFile, GetGameHighScores, GetMe, + GetMyCommands, GetStickerSet, GetUpdates, GetUserProfilePhotos, @@ -54,6 +48,7 @@ from ..methods import ( SendAudio, SendChatAction, SendContact, + SendDice, SendDocument, SendGame, SendInvoice, @@ -74,17 +69,20 @@ from ..methods import ( SetChatStickerSet, SetChatTitle, SetGameScore, + SetMyCommands, SetPassportDataErrors, SetStickerPositionInSet, + SetStickerSetThumb, SetWebhook, StopMessageLiveLocation, StopPoll, + TelegramMethod, UnbanChatMember, UnpinChatMessage, UploadStickerFile, - TelegramMethod, ) from ..types import ( + BotCommand, Chat, ChatMember, ChatPermissions, @@ -111,18 +109,15 @@ from ..types import ( UserProfilePhotos, WebhookInfo, ) -from ...utils.mixins import ContextInstanceMixin -from ...utils.token import ( - validate_token, - extract_bot_id, -) +from .session.aiohttp import AiohttpSession +from .session.base import BaseSession T = TypeVar("T") class Bot(ContextInstanceMixin["Bot"]): """ - Class where located all API methods + Main bot class """ def __init__( @@ -146,21 +141,6 @@ class Bot(ContextInstanceMixin["Bot"]): """ return extract_bot_id(self.__token) - async def __call__(self, method: TelegramMethod[T]) -> T: - """ - Call API method - - :param method: - :return: - """ - return await self.session.make_request(self.__token, method) - - async def close(self) -> None: - """ - Close bot session - """ - await self.session.close() - @asynccontextmanager async def context(self, auto_close: bool = True) -> AsyncIterator[Bot]: """ @@ -177,6 +157,25 @@ class Bot(ContextInstanceMixin["Bot"]): await self.close() self.reset_current(token) + @alru_cache() # type: ignore + async def me(self) -> User: + return await self.get_me() + + async def close(self) -> None: + """ + Close bot session + """ + await self.session.close() + + async def __call__(self, method: TelegramMethod[T]) -> T: + """ + Call API method + + :param method: + :return: + """ + return await self.session.make_request(self.__token, method) + def __hash__(self) -> int: """ Get hash for the token @@ -196,10 +195,6 @@ class Bot(ContextInstanceMixin["Bot"]): return False return hash(self) == hash(other) - @alru_cache() # type: ignore - async def me(self) -> User: - return await self.get_me() - # ============================================================================================= # Group: Getting updates # Source: https://core.telegram.org/bots/api#getting-updates @@ -359,7 +354,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) - :param text: Text of the message to be sent + :param text: Text of the message to be sent, 1-4096 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. :param disable_web_page_preview: Disables link previews for links in this message @@ -435,7 +430,7 @@ class Bot(ContextInstanceMixin["Bot"]): get a photo from the Internet, or upload a new photo using multipart/form-data. :param caption: Photo caption (may also be used when resending photos by file_id), 0-1024 - characters + characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param disable_notification: Sends the message silently. Users will receive a notification @@ -488,7 +483,7 @@ class Bot(ContextInstanceMixin["Bot"]): exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. - :param caption: Audio caption, 0-1024 characters + :param caption: Audio caption, 0-1024 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param duration: Duration of the audio in seconds @@ -558,7 +553,7 @@ class Bot(ContextInstanceMixin["Bot"]): can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :param caption: Document caption (may also be used when resending documents by file_id), - 0-1024 characters + 0-1024 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param disable_notification: Sends the message silently. Users will receive a notification @@ -622,7 +617,7 @@ class Bot(ContextInstanceMixin["Bot"]): can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 - characters + characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param supports_streaming: Pass True, if the uploaded video is suitable for streaming @@ -690,7 +685,7 @@ class Bot(ContextInstanceMixin["Bot"]): can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . :param caption: Animation caption (may also be used when resending animation by file_id), - 0-1024 characters + 0-1024 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param disable_notification: Sends the message silently. Users will receive a notification @@ -731,7 +726,7 @@ class Bot(ContextInstanceMixin["Bot"]): ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a - playable voice message. For this to work, your audio must be in an .ogg file encoded with + playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. @@ -744,7 +739,7 @@ class Bot(ContextInstanceMixin["Bot"]): the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. - :param caption: Voice message caption, 0-1024 characters + :param caption: Voice message caption, 0-1024 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param duration: Duration of the voice message in seconds @@ -1121,6 +1116,40 @@ class Bot(ContextInstanceMixin["Bot"]): ) return await self(call) + async def send_dice( + self, + chat_id: Union[int, str], + disable_notification: Optional[bool] = None, + reply_to_message_id: Optional[int] = None, + reply_markup: Optional[ + Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] + ] = None, + ) -> Message: + """ + Use this method to send a dice, which will have a random value from 1 to 6. On success, + the sent Message is returned. (Yes, we're aware of the 'proper' singular of die. But it's + awkward, and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#senddice + + :param chat_id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param disable_notification: Sends the message silently. Users will receive a notification + with no sound. + :param reply_to_message_id: If the message is a reply, ID of the original message + :param reply_markup: Additional interface options. A JSON-serialized object for an inline + keyboard, custom reply keyboard, instructions to remove reply + keyboard or to force a reply from the user. + :return: On success, the sent Message is returned. + """ + call = SendDice( + chat_id=chat_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + ) + return await self(call) + async def send_chat_action(self, chat_id: Union[int, str], action: str,) -> bool: """ Use this method when you need to tell the user that something is happening on the bot's @@ -1631,6 +1660,31 @@ class Bot(ContextInstanceMixin["Bot"]): ) return await self(call) + async def set_my_commands(self, commands: List[BotCommand],) -> bool: + """ + Use this method to change the list of the bot's commands. Returns True on success. + + Source: https://core.telegram.org/bots/api#setmycommands + + :param commands: A JSON-serialized list of bot commands to be set as the list of the bot's + commands. At most 100 commands can be specified. + :return: Returns True on success. + """ + call = SetMyCommands(commands=commands,) + return await self(call) + + async def get_my_commands(self,) -> List[BotCommand]: + """ + Use this method to get the current list of the bot's commands. Requires no parameters. + Returns Array of BotCommand on success. + + Source: https://core.telegram.org/bots/api#getmycommands + + :return: Returns Array of BotCommand on success. + """ + call = GetMyCommands() + return await self(call) + # ============================================================================================= # Group: Updating messages # Source: https://core.telegram.org/bots/api#updating-messages @@ -1652,7 +1706,7 @@ class Bot(ContextInstanceMixin["Bot"]): Source: https://core.telegram.org/bots/api#editmessagetext - :param text: New text of the message + :param text: New text of the message, 1-4096 characters after entities parsing :param chat_id: Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) @@ -1700,7 +1754,7 @@ class Bot(ContextInstanceMixin["Bot"]): message to edit :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :param caption: New caption of the message + :param caption: New caption of the message, 0-1024 characters after entities parsing :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. :param reply_markup: A JSON-serialized object for an inline keyboard. @@ -1814,6 +1868,8 @@ class Bot(ContextInstanceMixin["Bot"]): Use this method to delete a message, including service messages, with the following limitations: - A message can only be deleted if it was sent less than 48 hours ago. + - A dice message in a private chat can only be deleted if it was sent more than 24 hours + ago. - Bots can delete outgoing messages in private chats, groups, and supergroups. - Bots can delete incoming messages in private chats. - Bots granted can_post_messages permissions can delete outgoing messages in channels. @@ -1857,7 +1913,7 @@ class Bot(ContextInstanceMixin["Bot"]): (in the format @channelusername) :param sticker: Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for - Telegram to get a .webp file from the Internet, or upload a new one using + Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. :param disable_notification: Sends the message silently. Users will receive a notification with no sound. @@ -1890,7 +1946,7 @@ class Bot(ContextInstanceMixin["Bot"]): async def upload_sticker_file(self, user_id: int, png_sticker: InputFile,) -> File: """ - Use this method to upload a .png file with a sticker for later use in createNewStickerSet + Use this method to upload a .PNG file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. @@ -1910,14 +1966,16 @@ class Bot(ContextInstanceMixin["Bot"]): user_id: int, name: str, title: str, - png_sticker: Union[InputFile, str], emojis: str, + png_sticker: Optional[Union[InputFile, str]] = None, + tgs_sticker: Optional[InputFile] = None, contains_masks: Optional[bool] = None, mask_position: Optional[MaskPosition] = None, ) -> bool: """ - Use this method to create new sticker set owned by a user. The bot will be able to edit - the created sticker set. Returns True on success. + Use this method to create a new sticker set owned by a user. The bot will be able to edit + the sticker set thus created. You must use exactly one of the fields png_sticker or + tgs_sticker. Returns True on success. Source: https://core.telegram.org/bots/api#createnewstickerset @@ -1927,13 +1985,16 @@ class Bot(ContextInstanceMixin["Bot"]): begin with a letter, can't contain consecutive underscores and must end in '_by_'. is case insensitive. 1-64 characters. :param title: Sticker set title, 1-64 characters - :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size, + :param emojis: One or more emoji corresponding to the sticker + :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. - :param emojis: One or more emoji corresponding to the sticker + :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/animated_stickers#technical-requirements + for technical requirements :param contains_masks: Pass True, if a set of mask stickers should be created :param mask_position: A JSON-serialized object for position where the mask should be placed on faces @@ -1943,8 +2004,9 @@ class Bot(ContextInstanceMixin["Bot"]): user_id=user_id, name=name, title=title, - png_sticker=png_sticker, emojis=emojis, + png_sticker=png_sticker, + tgs_sticker=tgs_sticker, contains_masks=contains_masks, mask_position=mask_position, ) @@ -1956,22 +2018,29 @@ class Bot(ContextInstanceMixin["Bot"]): name: str, png_sticker: Union[InputFile, str], emojis: str, + tgs_sticker: Optional[InputFile] = None, mask_position: Optional[MaskPosition] = None, ) -> bool: """ - Use this method to add a new sticker to a set created by the bot. Returns True on success. + Use this method to add a new sticker to a set created by the bot. You must use exactly one + of the fields png_sticker or tgs_sticker. Animated stickers can be added to animated + sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static + sticker sets can have up to 120 stickers. Returns True on success. Source: https://core.telegram.org/bots/api#addstickertoset :param user_id: User identifier of sticker set owner :param name: Sticker set name - :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size, + :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :param emojis: One or more emoji corresponding to the sticker + :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/animated_stickers#technical-requirements + for technical requirements :param mask_position: A JSON-serialized object for position where the mask should be placed on faces :return: Returns True on success. @@ -1981,13 +2050,14 @@ class Bot(ContextInstanceMixin["Bot"]): name=name, png_sticker=png_sticker, emojis=emojis, + tgs_sticker=tgs_sticker, mask_position=mask_position, ) return await self(call) async def set_sticker_position_in_set(self, sticker: str, position: int,) -> bool: """ - Use this method to move a sticker in a set created by the bot to a specific position . + Use this method to move a sticker in a set created by the bot to a specific position. Returns True on success. Source: https://core.telegram.org/bots/api#setstickerpositioninset @@ -2012,6 +2082,31 @@ class Bot(ContextInstanceMixin["Bot"]): call = DeleteStickerFromSet(sticker=sticker,) return await self(call) + async def set_sticker_set_thumb( + self, name: str, user_id: int, thumb: Optional[Union[InputFile, str]] = None, + ) -> bool: + """ + Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for + animated sticker sets only. Returns True on success. + + Source: https://core.telegram.org/bots/api#setstickersetthumb + + :param name: Sticker set name + :param user_id: User identifier of the sticker set owner + :param thumb: A PNG image with the thumbnail, must be up to 128 kilobytes in size and have + width and height exactly 100px, or a TGS animation with the thumbnail up to + 32 kilobytes in size; see + https://core.telegram.org/animated_stickers#technical-requirements for + animated sticker technical requirements. Pass a file_id as a String to send + a file that already exists on the Telegram servers, pass an HTTP URL as a + String for Telegram to get a file from the Internet, or upload a new one + using multipart/form-data.. Animated sticker set thumbnail can't be uploaded + via HTTP URL. + :return: Returns True on success. + """ + call = SetStickerSetThumb(name=name, user_id=user_id, thumb=thumb,) + return await self(call) + # ============================================================================================= # Group: Inline mode # Source: https://core.telegram.org/bots/api#inline-mode diff --git a/aiogram/api/client/session/base.py b/aiogram/api/client/session/base.py index b7eeabd3..700810f1 100644 --- a/aiogram/api/client/session/base.py +++ b/aiogram/api/client/session/base.py @@ -4,15 +4,7 @@ import abc import datetime import json from types import TracebackType -from typing import ( - Any, - AsyncGenerator, - Callable, - Optional, - Type, - TypeVar, - Union, -) +from typing import Any, AsyncGenerator, Callable, Optional, Type, TypeVar, Union from aiogram.utils.exceptions import TelegramAPIError diff --git a/aiogram/api/methods/__init__.py b/aiogram/api/methods/__init__.py index b1a1344e..52785ed0 100644 --- a/aiogram/api/methods/__init__.py +++ b/aiogram/api/methods/__init__.py @@ -24,6 +24,7 @@ from .get_chat_members_count import GetChatMembersCount from .get_file import GetFile from .get_game_high_scores import GetGameHighScores from .get_me import GetMe +from .get_my_commands import GetMyCommands from .get_sticker_set import GetStickerSet from .get_updates import GetUpdates from .get_user_profile_photos import GetUserProfilePhotos @@ -37,6 +38,7 @@ from .send_animation import SendAnimation from .send_audio import SendAudio from .send_chat_action import SendChatAction from .send_contact import SendContact +from .send_dice import SendDice from .send_document import SendDocument from .send_game import SendGame from .send_invoice import SendInvoice @@ -57,8 +59,10 @@ from .set_chat_photo import SetChatPhoto from .set_chat_sticker_set import SetChatStickerSet from .set_chat_title import SetChatTitle from .set_game_score import SetGameScore +from .set_my_commands import SetMyCommands from .set_passport_data_errors import SetPassportDataErrors from .set_sticker_position_in_set import SetStickerPositionInSet +from .set_sticker_set_thumb import SetStickerSetThumb from .set_webhook import SetWebhook from .stop_message_live_location import StopMessageLiveLocation from .stop_poll import StopPoll @@ -91,6 +95,7 @@ __all__ = ( "SendVenue", "SendContact", "SendPoll", + "SendDice", "SendChatAction", "GetUserProfilePhotos", "GetFile", @@ -115,6 +120,8 @@ __all__ = ( "SetChatStickerSet", "DeleteChatStickerSet", "AnswerCallbackQuery", + "SetMyCommands", + "GetMyCommands", "EditMessageText", "EditMessageCaption", "EditMessageMedia", @@ -128,6 +135,7 @@ __all__ = ( "AddStickerToSet", "SetStickerPositionInSet", "DeleteStickerFromSet", + "SetStickerSetThumb", "AnswerInlineQuery", "SendInvoice", "AnswerShippingQuery", diff --git a/aiogram/api/methods/add_sticker_to_set.py b/aiogram/api/methods/add_sticker_to_set.py index 57c1d2a7..517d21fc 100644 --- a/aiogram/api/methods/add_sticker_to_set.py +++ b/aiogram/api/methods/add_sticker_to_set.py @@ -6,7 +6,10 @@ from .base import Request, TelegramMethod, prepare_file class AddStickerToSet(TelegramMethod[bool]): """ - Use this method to add a new sticker to a set created by the bot. Returns True on success. + Use this method to add a new sticker to a set created by the bot. You must use exactly one of + the fields png_sticker or tgs_sticker. Animated stickers can be added to animated sticker sets + and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can + have up to 120 stickers. Returns True on success. Source: https://core.telegram.org/bots/api#addstickertoset """ @@ -18,19 +21,24 @@ class AddStickerToSet(TelegramMethod[bool]): name: str """Sticker set name""" png_sticker: Union[InputFile, str] - """Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed + """PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data.""" emojis: str """One or more emoji corresponding to the sticker""" + tgs_sticker: Optional[InputFile] = None + """TGS animation with the sticker, uploaded using multipart/form-data. See + https://core.telegram.org/animated_stickers#technical-requirements for technical + requirements""" mask_position: Optional[MaskPosition] = None """A JSON-serialized object for position where the mask should be placed on faces""" def build_request(self) -> Request: - data: Dict[str, Any] = self.dict(exclude={"png_sticker"}) + data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"}) files: Dict[str, InputFile] = {} prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker) + prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker) return Request(method="addStickerToSet", data=data, files=files) diff --git a/aiogram/api/methods/base.py b/aiogram/api/methods/base.py index e7fa1f2c..72eafa05 100644 --- a/aiogram/api/methods/base.py +++ b/aiogram/api/methods/base.py @@ -2,16 +2,7 @@ from __future__ import annotations import abc import secrets -from typing import ( - Generator, - TYPE_CHECKING, - Any, - Dict, - Generic, - Optional, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Optional, TypeVar, Union from pydantic import BaseConfig, BaseModel, Extra from pydantic.generics import GenericModel diff --git a/aiogram/api/methods/create_new_sticker_set.py b/aiogram/api/methods/create_new_sticker_set.py index 72c3f699..f495986c 100644 --- a/aiogram/api/methods/create_new_sticker_set.py +++ b/aiogram/api/methods/create_new_sticker_set.py @@ -6,8 +6,9 @@ from .base import Request, TelegramMethod, prepare_file class CreateNewStickerSet(TelegramMethod[bool]): """ - Use this method to create new sticker set owned by a user. The bot will be able to edit the - created sticker set. Returns True on success. + Use this method to create a new sticker set owned by a user. The bot will be able to edit the + sticker set thus created. You must use exactly one of the fields png_sticker or tgs_sticker. + Returns True on success. Source: https://core.telegram.org/bots/api#createnewstickerset """ @@ -23,22 +24,27 @@ class CreateNewStickerSet(TelegramMethod[bool]): case insensitive. 1-64 characters.""" title: str """Sticker set title, 1-64 characters""" - png_sticker: Union[InputFile, str] - """Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed + emojis: str + """One or more emoji corresponding to the sticker""" + png_sticker: Optional[Union[InputFile, str]] = None + """PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data.""" - emojis: str - """One or more emoji corresponding to the sticker""" + tgs_sticker: Optional[InputFile] = None + """TGS animation with the sticker, uploaded using multipart/form-data. See + https://core.telegram.org/animated_stickers#technical-requirements for technical + requirements""" contains_masks: Optional[bool] = None """Pass True, if a set of mask stickers should be created""" mask_position: Optional[MaskPosition] = None """A JSON-serialized object for position where the mask should be placed on faces""" def build_request(self) -> Request: - data: Dict[str, Any] = self.dict(exclude={"png_sticker"}) + data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"}) files: Dict[str, InputFile] = {} prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker) + prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker) return Request(method="createNewStickerSet", data=data, files=files) diff --git a/aiogram/api/methods/delete_message.py b/aiogram/api/methods/delete_message.py index 7032ad61..a38df0c0 100644 --- a/aiogram/api/methods/delete_message.py +++ b/aiogram/api/methods/delete_message.py @@ -8,6 +8,7 @@ class DeleteMessage(TelegramMethod[bool]): Use this method to delete a message, including service messages, with the following limitations: - A message can only be deleted if it was sent less than 48 hours ago. + - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. - Bots can delete outgoing messages in private chats, groups, and supergroups. - Bots can delete incoming messages in private chats. - Bots granted can_post_messages permissions can delete outgoing messages in channels. diff --git a/aiogram/api/methods/edit_message_caption.py b/aiogram/api/methods/edit_message_caption.py index f5ec110d..75b5cc69 100644 --- a/aiogram/api/methods/edit_message_caption.py +++ b/aiogram/api/methods/edit_message_caption.py @@ -22,7 +22,7 @@ class EditMessageCaption(TelegramMethod[Union[Message, bool]]): inline_message_id: Optional[str] = None """Required if chat_id and message_id are not specified. Identifier of the inline message""" caption: Optional[str] = None - """New caption of the message""" + """New caption of the message, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/edit_message_text.py b/aiogram/api/methods/edit_message_text.py index 8306ff06..5c3c4c4c 100644 --- a/aiogram/api/methods/edit_message_text.py +++ b/aiogram/api/methods/edit_message_text.py @@ -15,7 +15,7 @@ class EditMessageText(TelegramMethod[Union[Message, bool]]): __returning__ = Union[Message, bool] text: str - """New text of the message""" + """New text of the message, 1-4096 characters after entities parsing""" chat_id: Optional[Union[int, str]] = None """Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername)""" diff --git a/aiogram/api/methods/get_my_commands.py b/aiogram/api/methods/get_my_commands.py new file mode 100644 index 00000000..1e1d8f1f --- /dev/null +++ b/aiogram/api/methods/get_my_commands.py @@ -0,0 +1,20 @@ +from typing import Any, Dict, List + +from ..types import BotCommand +from .base import Request, TelegramMethod + + +class GetMyCommands(TelegramMethod[List[BotCommand]]): + """ + Use this method to get the current list of the bot's commands. Requires no parameters. Returns + Array of BotCommand on success. + + Source: https://core.telegram.org/bots/api#getmycommands + """ + + __returning__ = List[BotCommand] + + def build_request(self) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="getMyCommands", data=data) diff --git a/aiogram/api/methods/send_animation.py b/aiogram/api/methods/send_animation.py index 343019d2..27007d1b 100644 --- a/aiogram/api/methods/send_animation.py +++ b/aiogram/api/methods/send_animation.py @@ -43,7 +43,8 @@ class SendAnimation(TelegramMethod[Message]): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Animation caption (may also be used when resending animation by file_id), 0-1024 characters""" + """Animation caption (may also be used when resending animation by file_id), 0-1024 characters + after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/send_audio.py b/aiogram/api/methods/send_audio.py index fbbcd618..a67ca833 100644 --- a/aiogram/api/methods/send_audio.py +++ b/aiogram/api/methods/send_audio.py @@ -32,7 +32,7 @@ class SendAudio(TelegramMethod[Message]): Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data.""" caption: Optional[str] = None - """Audio caption, 0-1024 characters""" + """Audio caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/send_dice.py b/aiogram/api/methods/send_dice.py new file mode 100644 index 00000000..42a8b3a6 --- /dev/null +++ b/aiogram/api/methods/send_dice.py @@ -0,0 +1,40 @@ +from typing import Any, Dict, Optional, Union + +from ..types import ( + ForceReply, + InlineKeyboardMarkup, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from .base import Request, TelegramMethod + + +class SendDice(TelegramMethod[Message]): + """ + Use this method to send a dice, which will have a random value from 1 to 6. On success, the + sent Message is returned. (Yes, we're aware of the 'proper' singular of die. But it's awkward, + and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#senddice + """ + + __returning__ = Message + + chat_id: Union[int, str] + """Unique identifier for the target chat or username of the target channel (in the format + @channelusername)""" + disable_notification: Optional[bool] = None + """Sends the message silently. Users will receive a notification with no sound.""" + reply_to_message_id: Optional[int] = None + """If the message is a reply, ID of the original message""" + reply_markup: Optional[ + Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] + ] = None + """Additional interface options. A JSON-serialized object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user.""" + + def build_request(self) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="sendDice", data=data) diff --git a/aiogram/api/methods/send_document.py b/aiogram/api/methods/send_document.py index c8bca68b..3c070539 100644 --- a/aiogram/api/methods/send_document.py +++ b/aiogram/api/methods/send_document.py @@ -37,7 +37,8 @@ class SendDocument(TelegramMethod[Message]): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Document caption (may also be used when resending documents by file_id), 0-1024 characters""" + """Document caption (may also be used when resending documents by file_id), 0-1024 characters + after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/send_message.py b/aiogram/api/methods/send_message.py index 15e3c7c3..849a1f5b 100644 --- a/aiogram/api/methods/send_message.py +++ b/aiogram/api/methods/send_message.py @@ -23,7 +23,7 @@ class SendMessage(TelegramMethod[Message]): """Unique identifier for the target chat or username of the target channel (in the format @channelusername)""" text: str - """Text of the message to be sent""" + """Text of the message to be sent, 1-4096 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message.""" diff --git a/aiogram/api/methods/send_photo.py b/aiogram/api/methods/send_photo.py index d737e145..328b29b6 100644 --- a/aiogram/api/methods/send_photo.py +++ b/aiogram/api/methods/send_photo.py @@ -28,7 +28,8 @@ class SendPhoto(TelegramMethod[Message]): (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data.""" caption: Optional[str] = None - """Photo caption (may also be used when resending photos by file_id), 0-1024 characters""" + """Photo caption (may also be used when resending photos by file_id), 0-1024 characters after + entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/send_poll.py b/aiogram/api/methods/send_poll.py index ce8f26cb..8a71fe20 100644 --- a/aiogram/api/methods/send_poll.py +++ b/aiogram/api/methods/send_poll.py @@ -25,7 +25,7 @@ class SendPoll(TelegramMethod[Message]): question: str """Poll question, 1-255 characters""" options: List[str] - """List of answer options, 2-10 strings 1-100 characters each""" + """A JSON-serialized list of answer options, 2-10 strings 1-100 characters each""" is_anonymous: Optional[bool] = None """True, if the poll needs to be anonymous, defaults to True""" type: Optional[str] = None diff --git a/aiogram/api/methods/send_sticker.py b/aiogram/api/methods/send_sticker.py index 06cc5369..e110b2f8 100644 --- a/aiogram/api/methods/send_sticker.py +++ b/aiogram/api/methods/send_sticker.py @@ -26,7 +26,7 @@ class SendSticker(TelegramMethod[Message]): @channelusername)""" sticker: Union[InputFile, str] """Sticker to send. Pass a file_id as String to send a file that exists on the Telegram - servers (recommended), pass an HTTP URL as a String for Telegram to get a .webp file from + servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data.""" disable_notification: Optional[bool] = None """Sends the message silently. Users will receive a notification with no sound.""" diff --git a/aiogram/api/methods/send_video.py b/aiogram/api/methods/send_video.py index a7702c1a..07c4a3b6 100644 --- a/aiogram/api/methods/send_video.py +++ b/aiogram/api/methods/send_video.py @@ -43,7 +43,8 @@ class SendVideo(TelegramMethod[Message]): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Video caption (may also be used when resending videos by file_id), 0-1024 characters""" + """Video caption (may also be used when resending videos by file_id), 0-1024 characters after + entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/send_voice.py b/aiogram/api/methods/send_voice.py index 2829e69c..17522782 100644 --- a/aiogram/api/methods/send_voice.py +++ b/aiogram/api/methods/send_voice.py @@ -14,7 +14,7 @@ from .base import Request, TelegramMethod, prepare_file class SendVoice(TelegramMethod[Message]): """ Use this method to send audio files, if you want Telegram clients to display the file as a - playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS + playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. @@ -32,7 +32,7 @@ class SendVoice(TelegramMethod[Message]): servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data.""" caption: Optional[str] = None - """Voice message caption, 0-1024 characters""" + """Voice message caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/methods/set_my_commands.py b/aiogram/api/methods/set_my_commands.py new file mode 100644 index 00000000..97ac9bcb --- /dev/null +++ b/aiogram/api/methods/set_my_commands.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, List + +from ..types import BotCommand +from .base import Request, TelegramMethod + + +class SetMyCommands(TelegramMethod[bool]): + """ + Use this method to change the list of the bot's commands. Returns True on success. + + Source: https://core.telegram.org/bots/api#setmycommands + """ + + __returning__ = bool + + commands: List[BotCommand] + """A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most + 100 commands can be specified.""" + + def build_request(self) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="setMyCommands", data=data) diff --git a/aiogram/api/methods/set_sticker_position_in_set.py b/aiogram/api/methods/set_sticker_position_in_set.py index c5b4a8a5..378a39d3 100644 --- a/aiogram/api/methods/set_sticker_position_in_set.py +++ b/aiogram/api/methods/set_sticker_position_in_set.py @@ -5,7 +5,7 @@ from .base import Request, TelegramMethod class SetStickerPositionInSet(TelegramMethod[bool]): """ - Use this method to move a sticker in a set created by the bot to a specific position . Returns + Use this method to move a sticker in a set created by the bot to a specific position. Returns True on success. Source: https://core.telegram.org/bots/api#setstickerpositioninset diff --git a/aiogram/api/methods/set_sticker_set_thumb.py b/aiogram/api/methods/set_sticker_set_thumb.py new file mode 100644 index 00000000..5ccd3bf3 --- /dev/null +++ b/aiogram/api/methods/set_sticker_set_thumb.py @@ -0,0 +1,36 @@ +from typing import Any, Dict, Optional, Union + +from ..types import InputFile +from .base import Request, TelegramMethod, prepare_file + + +class SetStickerSetThumb(TelegramMethod[bool]): + """ + Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for + animated sticker sets only. Returns True on success. + + Source: https://core.telegram.org/bots/api#setstickersetthumb + """ + + __returning__ = bool + + name: str + """Sticker set name""" + user_id: int + """User identifier of the sticker set owner""" + thumb: Optional[Union[InputFile, str]] = None + """A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and + height exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size; see + https://core.telegram.org/animated_stickers#technical-requirements for animated sticker + technical requirements. Pass a file_id as a String to send a file that already exists on + the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the + Internet, or upload a new one using multipart/form-data.. Animated sticker set thumbnail + can't be uploaded via HTTP URL.""" + + def build_request(self) -> Request: + data: Dict[str, Any] = self.dict(exclude={"thumb"}) + + files: Dict[str, InputFile] = {} + prepare_file(data=data, files=files, name="thumb", value=self.thumb) + + return Request(method="setStickerSetThumb", data=data, files=files) diff --git a/aiogram/api/methods/upload_sticker_file.py b/aiogram/api/methods/upload_sticker_file.py index 247d42af..e9eea3f0 100644 --- a/aiogram/api/methods/upload_sticker_file.py +++ b/aiogram/api/methods/upload_sticker_file.py @@ -6,7 +6,7 @@ from .base import Request, TelegramMethod, prepare_file class UploadStickerFile(TelegramMethod[File]): """ - Use this method to upload a .png file with a sticker for later use in createNewStickerSet and + Use this method to upload a .PNG file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. Source: https://core.telegram.org/bots/api#uploadstickerfile diff --git a/aiogram/api/types/__init__.py b/aiogram/api/types/__init__.py index fc94a7a7..429b6f41 100644 --- a/aiogram/api/types/__init__.py +++ b/aiogram/api/types/__init__.py @@ -1,6 +1,7 @@ from .animation import Animation from .audio import Audio from .base import TelegramObject +from .bot_command import BotCommand from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat @@ -9,6 +10,7 @@ from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chosen_inline_result import ChosenInlineResult from .contact import Contact +from .dice import Dice from .document import Document from .encrypted_credentials import EncryptedCredentials from .encrypted_passport_element import EncryptedPassportElement @@ -121,6 +123,7 @@ __all__ = ( "PollOption", "PollAnswer", "Poll", + "Dice", "UserProfilePhotos", "File", "ReplyKeyboardMarkup", @@ -135,6 +138,7 @@ __all__ = ( "ChatPhoto", "ChatMember", "ChatPermissions", + "BotCommand", "ResponseParameters", "InputMedia", "InputMediaPhoto", diff --git a/aiogram/api/types/bot_command.py b/aiogram/api/types/bot_command.py new file mode 100644 index 00000000..96ef0a31 --- /dev/null +++ b/aiogram/api/types/bot_command.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .base import MutableTelegramObject + + +class BotCommand(MutableTelegramObject): + """ + This object represents a bot command. + + Source: https://core.telegram.org/bots/api#botcommand + """ + + command: str + """Text of the command, 1-32 characters. Can contain only lowercase English letters, digits + and underscores.""" + description: str + """Description of the command, 3-256 characters.""" diff --git a/aiogram/api/types/dice.py b/aiogram/api/types/dice.py new file mode 100644 index 00000000..3b1436c6 --- /dev/null +++ b/aiogram/api/types/dice.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from .base import TelegramObject + + +class Dice(TelegramObject): + """ + This object represents a dice with random value from 1 to 6. (Yes, we're aware of the 'proper' + singular of die. But it's awkward, and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#dice + """ + + value: int + """Value of the dice, 1-6""" diff --git a/aiogram/api/types/inline_query_result_audio.py b/aiogram/api/types/inline_query_result_audio.py index 0290ee7e..c47104ba 100644 --- a/aiogram/api/types/inline_query_result_audio.py +++ b/aiogram/api/types/inline_query_result_audio.py @@ -31,7 +31,7 @@ class InlineQueryResultAudio(InlineQueryResult): title: str """Title""" caption: Optional[str] = None - """Caption, 0-1024 characters""" + """Caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_audio.py b/aiogram/api/types/inline_query_result_cached_audio.py index 8f6fe23f..426ea462 100644 --- a/aiogram/api/types/inline_query_result_cached_audio.py +++ b/aiogram/api/types/inline_query_result_cached_audio.py @@ -29,7 +29,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult): audio_file_id: str """A valid file identifier for the audio file""" caption: Optional[str] = None - """Caption, 0-1024 characters""" + """Caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_document.py b/aiogram/api/types/inline_query_result_cached_document.py index 860dbcc8..27bcd27e 100644 --- a/aiogram/api/types/inline_query_result_cached_document.py +++ b/aiogram/api/types/inline_query_result_cached_document.py @@ -33,7 +33,7 @@ class InlineQueryResultCachedDocument(InlineQueryResult): description: Optional[str] = None """Short description of the result""" caption: Optional[str] = None - """Caption of the document to be sent, 0-1024 characters""" + """Caption of the document to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_gif.py b/aiogram/api/types/inline_query_result_cached_gif.py index f237c5fb..2b85bcd9 100644 --- a/aiogram/api/types/inline_query_result_cached_gif.py +++ b/aiogram/api/types/inline_query_result_cached_gif.py @@ -29,7 +29,7 @@ class InlineQueryResultCachedGif(InlineQueryResult): title: Optional[str] = None """Title for the result""" caption: Optional[str] = None - """Caption of the GIF file to be sent, 0-1024 characters""" + """Caption of the GIF file to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_mpeg4_gif.py b/aiogram/api/types/inline_query_result_cached_mpeg4_gif.py index 0b770121..5611e114 100644 --- a/aiogram/api/types/inline_query_result_cached_mpeg4_gif.py +++ b/aiogram/api/types/inline_query_result_cached_mpeg4_gif.py @@ -30,7 +30,7 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): title: Optional[str] = None """Title for the result""" caption: Optional[str] = None - """Caption of the MPEG-4 file to be sent, 0-1024 characters""" + """Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_photo.py b/aiogram/api/types/inline_query_result_cached_photo.py index 1c163285..da865bed 100644 --- a/aiogram/api/types/inline_query_result_cached_photo.py +++ b/aiogram/api/types/inline_query_result_cached_photo.py @@ -31,7 +31,7 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): description: Optional[str] = None """Short description of the result""" caption: Optional[str] = None - """Caption of the photo to be sent, 0-1024 characters""" + """Caption of the photo to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_video.py b/aiogram/api/types/inline_query_result_cached_video.py index 3114e9e2..db71f4f2 100644 --- a/aiogram/api/types/inline_query_result_cached_video.py +++ b/aiogram/api/types/inline_query_result_cached_video.py @@ -31,7 +31,7 @@ class InlineQueryResultCachedVideo(InlineQueryResult): description: Optional[str] = None """Short description of the result""" caption: Optional[str] = None - """Caption of the video to be sent, 0-1024 characters""" + """Caption of the video to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_cached_voice.py b/aiogram/api/types/inline_query_result_cached_voice.py index 4ba72004..9d65b6fd 100644 --- a/aiogram/api/types/inline_query_result_cached_voice.py +++ b/aiogram/api/types/inline_query_result_cached_voice.py @@ -31,7 +31,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult): title: str """Voice message title""" caption: Optional[str] = None - """Caption, 0-1024 characters""" + """Caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_document.py b/aiogram/api/types/inline_query_result_document.py index 97fb5209..625b4675 100644 --- a/aiogram/api/types/inline_query_result_document.py +++ b/aiogram/api/types/inline_query_result_document.py @@ -34,7 +34,7 @@ class InlineQueryResultDocument(InlineQueryResult): mime_type: str """Mime type of the content of the file, either 'application/pdf' or 'application/zip'""" caption: Optional[str] = None - """Caption of the document to be sent, 0-1024 characters""" + """Caption of the document to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_gif.py b/aiogram/api/types/inline_query_result_gif.py index 3f4abfb4..ffaa7a66 100644 --- a/aiogram/api/types/inline_query_result_gif.py +++ b/aiogram/api/types/inline_query_result_gif.py @@ -37,7 +37,7 @@ class InlineQueryResultGif(InlineQueryResult): title: Optional[str] = None """Title for the result""" caption: Optional[str] = None - """Caption of the GIF file to be sent, 0-1024 characters""" + """Caption of the GIF file to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_mpeg4_gif.py b/aiogram/api/types/inline_query_result_mpeg4_gif.py index 8edaf79b..59d1ef01 100644 --- a/aiogram/api/types/inline_query_result_mpeg4_gif.py +++ b/aiogram/api/types/inline_query_result_mpeg4_gif.py @@ -38,7 +38,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): title: Optional[str] = None """Title for the result""" caption: Optional[str] = None - """Caption of the MPEG-4 file to be sent, 0-1024 characters""" + """Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_photo.py b/aiogram/api/types/inline_query_result_photo.py index 0a08bdc1..7ee7e4f5 100644 --- a/aiogram/api/types/inline_query_result_photo.py +++ b/aiogram/api/types/inline_query_result_photo.py @@ -37,7 +37,7 @@ class InlineQueryResultPhoto(InlineQueryResult): description: Optional[str] = None """Short description of the result""" caption: Optional[str] = None - """Caption of the photo to be sent, 0-1024 characters""" + """Caption of the photo to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_video.py b/aiogram/api/types/inline_query_result_video.py index ecc3663a..a0010fea 100644 --- a/aiogram/api/types/inline_query_result_video.py +++ b/aiogram/api/types/inline_query_result_video.py @@ -35,7 +35,7 @@ class InlineQueryResultVideo(InlineQueryResult): title: str """Title for the result""" caption: Optional[str] = None - """Caption of the video to be sent, 0-1024 characters""" + """Caption of the video to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/inline_query_result_voice.py b/aiogram/api/types/inline_query_result_voice.py index d04ca805..b4b10f4f 100644 --- a/aiogram/api/types/inline_query_result_voice.py +++ b/aiogram/api/types/inline_query_result_voice.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: # pragma: no cover class InlineQueryResultVoice(InlineQueryResult): """ - Represents a link to a voice recording in an .ogg container encoded with OPUS. By default, + Represents a link to a voice recording in an .OGG container encoded with OPUS. By default, this voice recording will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message. @@ -32,7 +32,7 @@ class InlineQueryResultVoice(InlineQueryResult): title: str """Recording title""" caption: Optional[str] = None - """Caption, 0-1024 characters""" + """Caption, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/input_file.py b/aiogram/api/types/input_file.py index 27b452cb..0b6c1442 100644 --- a/aiogram/api/types/input_file.py +++ b/aiogram/api/types/input_file.py @@ -4,13 +4,7 @@ import io import os from abc import ABC, abstractmethod from pathlib import Path -from typing import ( - AsyncGenerator, - AsyncIterator, - Iterator, - Optional, - Union, -) +from typing import AsyncGenerator, AsyncIterator, Iterator, Optional, Union import aiofiles as aiofiles diff --git a/aiogram/api/types/input_media_animation.py b/aiogram/api/types/input_media_animation.py index 5e4439b4..de994e85 100644 --- a/aiogram/api/types/input_media_animation.py +++ b/aiogram/api/types/input_media_animation.py @@ -32,7 +32,7 @@ class InputMediaAnimation(InputMedia): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Caption of the animation to be sent, 0-1024 characters""" + """Caption of the animation to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/input_media_audio.py b/aiogram/api/types/input_media_audio.py index 73bba3be..9d700101 100644 --- a/aiogram/api/types/input_media_audio.py +++ b/aiogram/api/types/input_media_audio.py @@ -32,7 +32,7 @@ class InputMediaAudio(InputMedia): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Caption of the audio to be sent, 0-1024 characters""" + """Caption of the audio to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/input_media_document.py b/aiogram/api/types/input_media_document.py index 1c9d2b31..5f95b233 100644 --- a/aiogram/api/types/input_media_document.py +++ b/aiogram/api/types/input_media_document.py @@ -32,7 +32,7 @@ class InputMediaDocument(InputMedia): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Caption of the document to be sent, 0-1024 characters""" + """Caption of the document to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/input_media_photo.py b/aiogram/api/types/input_media_photo.py index 60b9227c..7698d95d 100644 --- a/aiogram/api/types/input_media_photo.py +++ b/aiogram/api/types/input_media_photo.py @@ -25,7 +25,7 @@ class InputMediaPhoto(InputMedia): 'attach://' to upload a new one using multipart/form-data under name.""" caption: Optional[str] = None - """Caption of the photo to be sent, 0-1024 characters""" + """Caption of the photo to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/input_media_video.py b/aiogram/api/types/input_media_video.py index 4537fa80..a0cdd46f 100644 --- a/aiogram/api/types/input_media_video.py +++ b/aiogram/api/types/input_media_video.py @@ -32,7 +32,7 @@ class InputMediaVideo(InputMedia): file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under .""" caption: Optional[str] = None - """Caption of the video to be sent, 0-1024 characters""" + """Caption of the video to be sent, 0-1024 characters after entities parsing""" parse_mode: Optional[str] = None """Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.""" diff --git a/aiogram/api/types/message.py b/aiogram/api/types/message.py index 5af79d25..f4b9e655 100644 --- a/aiogram/api/types/message.py +++ b/aiogram/api/types/message.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: # pragma: no cover from .audio import Audio from .chat import Chat from .contact import Contact + from .dice import Dice from .document import Document from .force_reply import ForceReply from .game import Game @@ -95,7 +96,7 @@ class Message(TelegramObject): author_signature: Optional[str] = None """Signature of the post author for messages in channels""" text: Optional[str] = None - """For text messages, the actual UTF-8 text of the message, 0-4096 characters.""" + """For text messages, the actual UTF-8 text of the message, 0-4096 characters""" entities: Optional[List[MessageEntity]] = None """For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text""" @@ -131,6 +132,8 @@ class Message(TelegramObject): """Message is a venue, information about the venue""" poll: Optional[Poll] = None """Message is a native poll, information about the poll""" + dice: Optional[Dice] = None + """Message is a dice with random value from 1 to 6""" new_chat_members: Optional[List[User]] = None """New members that were added to the group or supergroup and information about them (the bot itself may be one of these members)""" @@ -1479,7 +1482,8 @@ class ContentType(helper.Helper): DELETE_CHAT_PHOTO = helper.Item() # delete_chat_photo GROUP_CHAT_CREATED = helper.Item() # group_chat_created PASSPORT_DATA = helper.Item() # passport_data - POLL = helper.Item() + POLL = helper.Item() # poll + DICE = helper.Item() # dice UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any diff --git a/aiogram/api/types/sticker.py b/aiogram/api/types/sticker.py index 1743de6b..23d22bfc 100644 --- a/aiogram/api/types/sticker.py +++ b/aiogram/api/types/sticker.py @@ -28,7 +28,7 @@ class Sticker(TelegramObject): is_animated: bool """True, if the sticker is animated""" thumb: Optional[PhotoSize] = None - """Sticker thumbnail in the .webp or .jpg format""" + """Sticker thumbnail in the .WEBP or .JPG format""" emoji: Optional[str] = None """Emoji associated with the sticker""" set_name: Optional[str] = None diff --git a/aiogram/api/types/sticker_set.py b/aiogram/api/types/sticker_set.py index cec9b3be..2b9e7ab1 100644 --- a/aiogram/api/types/sticker_set.py +++ b/aiogram/api/types/sticker_set.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from .base import TelegramObject if TYPE_CHECKING: # pragma: no cover + from .photo_size import PhotoSize from .sticker import Sticker @@ -25,3 +26,5 @@ class StickerSet(TelegramObject): """True, if the sticker set contains masks""" stickers: List[Sticker] """List of all set stickers""" + thumb: Optional[PhotoSize] = None + """Sticker set thumbnail in the .WEBP or .TGS format""" diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index 5df6f28d..98bd5070 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -1,17 +1,7 @@ import inspect from dataclasses import dataclass, field from functools import partial -from typing import ( - Any, - Awaitable, - Callable, - Dict, - List, - Optional, - Tuple, - Union, - Type, -) +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.handler.base import BaseHandler diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 3baca36f..25db7020 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,8 +1,4 @@ -from typing import ( - Dict, - Tuple, - Type, -) +from typing import Dict, Tuple, Type from .base import BaseFilter from .command import Command, CommandObject diff --git a/aiogram/dispatcher/filters/base.py b/aiogram/dispatcher/filters/base.py index a71c484a..04631dfc 100644 --- a/aiogram/dispatcher/filters/base.py +++ b/aiogram/dispatcher/filters/base.py @@ -1,12 +1,5 @@ from abc import ABC, abstractmethod -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Union, - Callable, - Awaitable, -) +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Union from pydantic import BaseModel diff --git a/aiogram/dispatcher/handler/base.py b/aiogram/dispatcher/handler/base.py index bf8f9785..e4000d1a 100644 --- a/aiogram/dispatcher/handler/base.py +++ b/aiogram/dispatcher/handler/base.py @@ -1,12 +1,5 @@ from abc import ABC, abstractmethod -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generic, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Dict, Generic, TypeVar, cast from aiogram import Bot from aiogram.api.types import Update diff --git a/aiogram/dispatcher/handler/message.py b/aiogram/dispatcher/handler/message.py index 7e7e0ab6..a25daddc 100644 --- a/aiogram/dispatcher/handler/message.py +++ b/aiogram/dispatcher/handler/message.py @@ -1,8 +1,5 @@ from abc import ABC -from typing import ( - Optional, - cast, -) +from typing import Optional, cast from aiogram.api.types import Chat, Message, User from aiogram.dispatcher.filters import CommandObject diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 44a47255..0b5cde3c 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -3,12 +3,7 @@ from __future__ import annotations import warnings from typing import Any, Dict, List, Optional, Union -from ..api.types import ( - Chat, - TelegramObject, - Update, - User, -) +from ..api.types import Chat, TelegramObject, Update, User from ..utils.imports import import_module from ..utils.warnings import CodeHasNoEffect from .event.observer import EventObserver, SkipHandler, TelegramEventObserver diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 11e67ab4..e582a4f0 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -14,15 +14,7 @@ Example: <<< ['barItem', 'bazItem', 'fooItem', 'lorem'] """ import inspect -from typing import ( - Any, - Callable, - Iterable, - List, - Optional, - Union, - cast, -) +from typing import Any, Callable, Iterable, List, Optional, Union, cast PROPS_KEYS_ATTR_NAME = "_props_keys" diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 8676ea33..f2def5d6 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -1,20 +1,12 @@ from __future__ import annotations -import contextvars -from typing import ( - Any, - ClassVar, - Generic, - Optional, - TypeVar, - cast, - overload, - Dict, -) -__all__ = ("ContextInstanceMixin", "DataMixin") +import contextvars +from typing import Any, ClassVar, Dict, Generic, Optional, TypeVar, cast, overload from typing_extensions import Literal +__all__ = ("ContextInstanceMixin", "DataMixin") + class DataMixin: @property @@ -56,18 +48,24 @@ class ContextInstanceMixin(Generic[ContextInstance]): def get_current(cls) -> Optional[ContextInstance]: # pragma: no cover ... - @overload # noqa: F811, it's overload, not redefinition + @overload @classmethod - def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 + def get_current( # noqa: F811 + cls, no_error: Literal[True] + ) -> Optional[ContextInstance]: # pragma: no cover ... - @overload # noqa: F811, it's overload, not redefinition + @overload @classmethod - def get_current(cls, no_error: Literal[False]) -> ContextInstance: # pragma: no cover # noqa: F811 + def get_current( # noqa: F811 + cls, no_error: Literal[False] + ) -> ContextInstance: # pragma: no cover ... - @classmethod # noqa: F811, it's overload, not redefinition - def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 + @classmethod + def get_current( # noqa: F811 + cls, no_error: bool = True + ) -> Optional[ContextInstance]: # pragma: no cover # on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any] cls.__context_instance = cast( contextvars.ContextVar[ContextInstance], cls.__context_instance diff --git a/docs/_api_version.md b/docs/_api_version.md index b3d791d7..4f8c6396 100644 --- a/docs/_api_version.md +++ b/docs/_api_version.md @@ -1 +1 @@ -4.6 +4.7 diff --git a/docs/api/methods/add_sticker_to_set.md b/docs/api/methods/add_sticker_to_set.md index b98b85d5..910b072d 100644 --- a/docs/api/methods/add_sticker_to_set.md +++ b/docs/api/methods/add_sticker_to_set.md @@ -2,7 +2,7 @@ ## Description -Use this method to add a new sticker to a set created by the bot. Returns True on success. +Use this method to add a new sticker to a set created by the bot. You must use exactly one of the fields png_sticker or tgs_sticker. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns True on success. ## Arguments @@ -11,8 +11,9 @@ Use this method to add a new sticker to a set created by the bot. Returns True o | - | - | - | | `user_id` | `#!python3 int` | User identifier of sticker set owner | | `name` | `#!python3 str` | Sticker set name | -| `png_sticker` | `#!python3 Union[InputFile, str]` | Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | +| `png_sticker` | `#!python3 Union[InputFile, str]` | PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | | `emojis` | `#!python3 str` | One or more emoji corresponding to the sticker | +| `tgs_sticker` | `#!python3 Optional[InputFile]` | Optional. TGS animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements | | `mask_position` | `#!python3 Optional[MaskPosition]` | Optional. A JSON-serialized object for position where the mask should be placed on faces | @@ -26,8 +27,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.add_sticker_to_set(...) @@ -56,7 +56,6 @@ return AddStickerToSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#addstickertoset) diff --git a/docs/api/methods/answer_callback_query.md b/docs/api/methods/answer_callback_query.md index 14fe48ff..202ecb49 100644 --- a/docs/api/methods/answer_callback_query.md +++ b/docs/api/methods/answer_callback_query.md @@ -28,8 +28,7 @@ Description: On success, True is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.answer_callback_query(...) @@ -58,7 +57,6 @@ return AnswerCallbackQuery(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#answercallbackquery) diff --git a/docs/api/methods/answer_inline_query.md b/docs/api/methods/answer_inline_query.md index 819544b1..88add63d 100644 --- a/docs/api/methods/answer_inline_query.md +++ b/docs/api/methods/answer_inline_query.md @@ -30,8 +30,7 @@ Description: On success, True is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.answer_inline_query(...) @@ -60,7 +59,6 @@ return AnswerInlineQuery(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#answerinlinequery) diff --git a/docs/api/methods/answer_pre_checkout_query.md b/docs/api/methods/answer_pre_checkout_query.md index cbc8cbcd..e836fe96 100644 --- a/docs/api/methods/answer_pre_checkout_query.md +++ b/docs/api/methods/answer_pre_checkout_query.md @@ -24,8 +24,7 @@ Description: On success, True is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.answer_pre_checkout_query(...) @@ -54,7 +53,6 @@ return AnswerPreCheckoutQuery(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#answerprecheckoutquery) diff --git a/docs/api/methods/answer_shipping_query.md b/docs/api/methods/answer_shipping_query.md index ddf25f0f..6ac4948b 100644 --- a/docs/api/methods/answer_shipping_query.md +++ b/docs/api/methods/answer_shipping_query.md @@ -25,8 +25,7 @@ Description: On success, True is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.answer_shipping_query(...) @@ -55,7 +54,6 @@ return AnswerShippingQuery(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#answershippingquery) diff --git a/docs/api/methods/create_new_sticker_set.md b/docs/api/methods/create_new_sticker_set.md index 8e9ada88..f1f36a38 100644 --- a/docs/api/methods/create_new_sticker_set.md +++ b/docs/api/methods/create_new_sticker_set.md @@ -2,7 +2,7 @@ ## Description -Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. Returns True on success. +Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You must use exactly one of the fields png_sticker or tgs_sticker. Returns True on success. ## Arguments @@ -12,8 +12,9 @@ Use this method to create new sticker set owned by a user. The bot will be able | `user_id` | `#!python3 int` | User identifier of created sticker set owner | | `name` | `#!python3 str` | Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in '_by_'. is case insensitive. 1-64 characters. | | `title` | `#!python3 str` | Sticker set title, 1-64 characters | -| `png_sticker` | `#!python3 Union[InputFile, str]` | Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | | `emojis` | `#!python3 str` | One or more emoji corresponding to the sticker | +| `png_sticker` | `#!python3 Optional[Union[InputFile, str]]` | Optional. PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | +| `tgs_sticker` | `#!python3 Optional[InputFile]` | Optional. TGS animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements | | `contains_masks` | `#!python3 Optional[bool]` | Optional. Pass True, if a set of mask stickers should be created | | `mask_position` | `#!python3 Optional[MaskPosition]` | Optional. A JSON-serialized object for position where the mask should be placed on faces | @@ -28,8 +29,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.create_new_sticker_set(...) @@ -58,7 +58,6 @@ return CreateNewStickerSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#createnewstickerset) diff --git a/docs/api/methods/delete_chat_photo.md b/docs/api/methods/delete_chat_photo.md index cfe0b7d4..44d93dcb 100644 --- a/docs/api/methods/delete_chat_photo.md +++ b/docs/api/methods/delete_chat_photo.md @@ -22,8 +22,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.delete_chat_photo(...) @@ -52,7 +51,6 @@ return DeleteChatPhoto(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#deletechatphoto) diff --git a/docs/api/methods/delete_chat_sticker_set.md b/docs/api/methods/delete_chat_sticker_set.md index 083f4bdd..9946efaf 100644 --- a/docs/api/methods/delete_chat_sticker_set.md +++ b/docs/api/methods/delete_chat_sticker_set.md @@ -22,8 +22,7 @@ Description: Use the field can_set_sticker_set optionally returned in getChat re ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.delete_chat_sticker_set(...) @@ -52,7 +51,6 @@ return DeleteChatStickerSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#deletechatstickerset) diff --git a/docs/api/methods/delete_message.md b/docs/api/methods/delete_message.md index fc50f66e..a6f42e08 100644 --- a/docs/api/methods/delete_message.md +++ b/docs/api/methods/delete_message.md @@ -6,6 +6,8 @@ Use this method to delete a message, including service messages, with the follow - A message can only be deleted if it was sent less than 48 hours ago. +- A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. + - Bots can delete outgoing messages in private chats, groups, and supergroups. - Bots can delete incoming messages in private chats. @@ -37,8 +39,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.delete_message(...) @@ -67,7 +68,6 @@ return DeleteMessage(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#deletemessage) diff --git a/docs/api/methods/delete_sticker_from_set.md b/docs/api/methods/delete_sticker_from_set.md index 4b6dcfd3..7414d9d6 100644 --- a/docs/api/methods/delete_sticker_from_set.md +++ b/docs/api/methods/delete_sticker_from_set.md @@ -22,8 +22,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.delete_sticker_from_set(...) @@ -52,7 +51,6 @@ return DeleteStickerFromSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#deletestickerfromset) diff --git a/docs/api/methods/delete_webhook.md b/docs/api/methods/delete_webhook.md index 888b227e..93b51ea4 100644 --- a/docs/api/methods/delete_webhook.md +++ b/docs/api/methods/delete_webhook.md @@ -16,8 +16,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.delete_webhook(...) @@ -46,7 +45,6 @@ return DeleteWebhook(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#deletewebhook) diff --git a/docs/api/methods/edit_message_caption.md b/docs/api/methods/edit_message_caption.md index cdaecdae..7f2c6556 100644 --- a/docs/api/methods/edit_message_caption.md +++ b/docs/api/methods/edit_message_caption.md @@ -12,7 +12,7 @@ Use this method to edit captions of messages. On success, if edited message is s | `chat_id` | `#!python3 Optional[Union[int, str]]` | Optional. Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `message_id` | `#!python3 Optional[int]` | Optional. Required if inline_message_id is not specified. Identifier of the message to edit | | `inline_message_id` | `#!python3 Optional[str]` | Optional. Required if chat_id and message_id are not specified. Identifier of the inline message | -| `caption` | `#!python3 Optional[str]` | Optional. New caption of the message | +| `caption` | `#!python3 Optional[str]` | Optional. New caption of the message, 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python3 Optional[InlineKeyboardMarkup]` | Optional. A JSON-serialized object for an inline keyboard. | @@ -27,8 +27,7 @@ Description: On success, if edited message is sent by the bot, the edited Messag ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.edit_message_caption(...) @@ -57,7 +56,6 @@ return EditMessageCaption(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#editmessagecaption) diff --git a/docs/api/methods/edit_message_live_location.md b/docs/api/methods/edit_message_live_location.md index e3a9ff39..93be46c4 100644 --- a/docs/api/methods/edit_message_live_location.md +++ b/docs/api/methods/edit_message_live_location.md @@ -27,8 +27,7 @@ Description: On success, if the edited message was sent by the bot, the edited M ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.edit_message_live_location(...) @@ -57,7 +56,6 @@ return EditMessageLiveLocation(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#editmessagelivelocation) diff --git a/docs/api/methods/edit_message_media.md b/docs/api/methods/edit_message_media.md index 5d25baa5..63927ea3 100644 --- a/docs/api/methods/edit_message_media.md +++ b/docs/api/methods/edit_message_media.md @@ -26,8 +26,7 @@ Description: On success, if the edited message was sent by the bot, the edited M ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.edit_message_media(...) @@ -56,7 +55,6 @@ return EditMessageMedia(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#editmessagemedia) diff --git a/docs/api/methods/edit_message_reply_markup.md b/docs/api/methods/edit_message_reply_markup.md index efbda5aa..d5a4693a 100644 --- a/docs/api/methods/edit_message_reply_markup.md +++ b/docs/api/methods/edit_message_reply_markup.md @@ -25,8 +25,7 @@ Description: On success, if edited message is sent by the bot, the edited Messag ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.edit_message_reply_markup(...) @@ -55,7 +54,6 @@ return EditMessageReplyMarkup(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#editmessagereplymarkup) diff --git a/docs/api/methods/edit_message_text.md b/docs/api/methods/edit_message_text.md index 6fa574c9..8931a835 100644 --- a/docs/api/methods/edit_message_text.md +++ b/docs/api/methods/edit_message_text.md @@ -9,7 +9,7 @@ Use this method to edit text and game messages. On success, if edited message is | Name | Type | Description | | - | - | - | -| `text` | `#!python3 str` | New text of the message | +| `text` | `#!python3 str` | New text of the message, 1-4096 characters after entities parsing | | `chat_id` | `#!python3 Optional[Union[int, str]]` | Optional. Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `message_id` | `#!python3 Optional[int]` | Optional. Required if inline_message_id is not specified. Identifier of the message to edit | | `inline_message_id` | `#!python3 Optional[str]` | Optional. Required if chat_id and message_id are not specified. Identifier of the inline message | @@ -28,8 +28,7 @@ Description: On success, if edited message is sent by the bot, the edited Messag ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.edit_message_text(...) @@ -58,7 +57,6 @@ return EditMessageText(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#editmessagetext) diff --git a/docs/api/methods/export_chat_invite_link.md b/docs/api/methods/export_chat_invite_link.md index 8fc0d888..cfa43df6 100644 --- a/docs/api/methods/export_chat_invite_link.md +++ b/docs/api/methods/export_chat_invite_link.md @@ -24,8 +24,7 @@ Description: Returns the new invite link as String on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: str = await bot.export_chat_invite_link(...) @@ -54,7 +53,6 @@ return ExportChatInviteLink(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#exportchatinvitelink) diff --git a/docs/api/methods/forward_message.md b/docs/api/methods/forward_message.md index 755d4b43..6ac41bee 100644 --- a/docs/api/methods/forward_message.md +++ b/docs/api/methods/forward_message.md @@ -25,8 +25,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.forward_message(...) @@ -55,7 +54,6 @@ return ForwardMessage(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#forwardmessage) diff --git a/docs/api/methods/get_chat.md b/docs/api/methods/get_chat.md index c216afa2..f5388c0d 100644 --- a/docs/api/methods/get_chat.md +++ b/docs/api/methods/get_chat.md @@ -22,8 +22,7 @@ Description: Returns a Chat object on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: Chat = await bot.get_chat(...) diff --git a/docs/api/methods/get_chat_administrators.md b/docs/api/methods/get_chat_administrators.md index d7047f7e..646d0e09 100644 --- a/docs/api/methods/get_chat_administrators.md +++ b/docs/api/methods/get_chat_administrators.md @@ -22,8 +22,7 @@ Description: On success, returns an Array of ChatMember objects that contains in ## Usage - -### As bot method bot +### As bot method ```python3 result: List[ChatMember] = await bot.get_chat_administrators(...) diff --git a/docs/api/methods/get_chat_member.md b/docs/api/methods/get_chat_member.md index 2198e1fe..91d96299 100644 --- a/docs/api/methods/get_chat_member.md +++ b/docs/api/methods/get_chat_member.md @@ -23,8 +23,7 @@ Description: Returns a ChatMember object on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: ChatMember = await bot.get_chat_member(...) diff --git a/docs/api/methods/get_chat_members_count.md b/docs/api/methods/get_chat_members_count.md index e1cb3312..e97f7142 100644 --- a/docs/api/methods/get_chat_members_count.md +++ b/docs/api/methods/get_chat_members_count.md @@ -22,8 +22,7 @@ Description: Returns Int on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: int = await bot.get_chat_members_count(...) diff --git a/docs/api/methods/get_file.md b/docs/api/methods/get_file.md index c449135b..145211d8 100644 --- a/docs/api/methods/get_file.md +++ b/docs/api/methods/get_file.md @@ -24,8 +24,7 @@ Description: On success, a File object is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: File = await bot.get_file(...) diff --git a/docs/api/methods/get_game_high_scores.md b/docs/api/methods/get_game_high_scores.md index b97514c6..0b0390d1 100644 --- a/docs/api/methods/get_game_high_scores.md +++ b/docs/api/methods/get_game_high_scores.md @@ -27,8 +27,7 @@ Description: Will return the score of the specified user and several of his neig ## Usage - -### As bot method bot +### As bot method ```python3 result: List[GameHighScore] = await bot.get_game_high_scores(...) diff --git a/docs/api/methods/get_me.md b/docs/api/methods/get_me.md index b2994f25..12fe6f8b 100644 --- a/docs/api/methods/get_me.md +++ b/docs/api/methods/get_me.md @@ -16,8 +16,7 @@ Description: Returns basic information about the bot in form of a User object. ## Usage - -### As bot method bot +### As bot method ```python3 result: User = await bot.get_me(...) diff --git a/docs/api/methods/get_my_commands.md b/docs/api/methods/get_my_commands.md new file mode 100644 index 00000000..0508e8aa --- /dev/null +++ b/docs/api/methods/get_my_commands.md @@ -0,0 +1,48 @@ +# getMyCommands + +## Description + +Use this method to get the current list of the bot's commands. Requires no parameters. Returns Array of BotCommand on success. + + + + +## Response + +Type: `#!python3 List[BotCommand]` + +Description: Returns Array of BotCommand on success. + + +## Usage + +### As bot method + +```python3 +result: List[BotCommand] = await bot.get_my_commands(...) +``` + +### Method as object + +Imports: + +- `from aiogram.methods import GetMyCommands` +- `from aiogram.api.methods import GetMyCommands` +- `from aiogram.api.methods.get_my_commands import GetMyCommands` + +#### In handlers with current bot +```python3 +result: List[BotCommand] = await GetMyCommands(...) +``` + +#### With specific bot +```python3 +result: List[BotCommand] = await bot(GetMyCommands(...)) +``` + + + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#getmycommands) +- [aiogram.types.BotCommand](../types/bot_command.md) diff --git a/docs/api/methods/get_sticker_set.md b/docs/api/methods/get_sticker_set.md index 983f1331..0d5efd9c 100644 --- a/docs/api/methods/get_sticker_set.md +++ b/docs/api/methods/get_sticker_set.md @@ -22,8 +22,7 @@ Description: On success, a StickerSet object is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: StickerSet = await bot.get_sticker_set(...) diff --git a/docs/api/methods/get_updates.md b/docs/api/methods/get_updates.md index 22576520..e7850ed4 100644 --- a/docs/api/methods/get_updates.md +++ b/docs/api/methods/get_updates.md @@ -31,8 +31,7 @@ Description: An Array of Update objects is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: List[Update] = await bot.get_updates(...) diff --git a/docs/api/methods/get_user_profile_photos.md b/docs/api/methods/get_user_profile_photos.md index 0b69b59d..a02a2a99 100644 --- a/docs/api/methods/get_user_profile_photos.md +++ b/docs/api/methods/get_user_profile_photos.md @@ -24,8 +24,7 @@ Description: Returns a UserProfilePhotos object. ## Usage - -### As bot method bot +### As bot method ```python3 result: UserProfilePhotos = await bot.get_user_profile_photos(...) diff --git a/docs/api/methods/get_webhook_info.md b/docs/api/methods/get_webhook_info.md index 2428a6af..2a35184e 100644 --- a/docs/api/methods/get_webhook_info.md +++ b/docs/api/methods/get_webhook_info.md @@ -16,8 +16,7 @@ Description: On success, returns a WebhookInfo object. If the bot is using getUp ## Usage - -### As bot method bot +### As bot method ```python3 result: WebhookInfo = await bot.get_webhook_info(...) diff --git a/docs/api/methods/index.md b/docs/api/methods/index.md index 58bfd230..7b2467fc 100644 --- a/docs/api/methods/index.md +++ b/docs/api/methods/index.md @@ -2,7 +2,6 @@ Here is list of all available API methods: - - Getting updates - [getUpdates](get_updates.md) - [setWebhook](set_webhook.md) @@ -26,6 +25,7 @@ Here is list of all available API methods: - [sendVenue](send_venue.md) - [sendContact](send_contact.md) - [sendPoll](send_poll.md) + - [sendDice](send_dice.md) - [sendChatAction](send_chat_action.md) - [getUserProfilePhotos](get_user_profile_photos.md) - [getFile](get_file.md) @@ -50,6 +50,8 @@ Here is list of all available API methods: - [setChatStickerSet](set_chat_sticker_set.md) - [deleteChatStickerSet](delete_chat_sticker_set.md) - [answerCallbackQuery](answer_callback_query.md) + - [setMyCommands](set_my_commands.md) + - [getMyCommands](get_my_commands.md) - Updating messages - [editMessageText](edit_message_text.md) - [editMessageCaption](edit_message_caption.md) @@ -65,6 +67,7 @@ Here is list of all available API methods: - [addStickerToSet](add_sticker_to_set.md) - [setStickerPositionInSet](set_sticker_position_in_set.md) - [deleteStickerFromSet](delete_sticker_from_set.md) + - [setStickerSetThumb](set_sticker_set_thumb.md) - Inline mode - [answerInlineQuery](answer_inline_query.md) - Payments diff --git a/docs/api/methods/kick_chat_member.md b/docs/api/methods/kick_chat_member.md index 0a475e03..d58e5664 100644 --- a/docs/api/methods/kick_chat_member.md +++ b/docs/api/methods/kick_chat_member.md @@ -24,8 +24,7 @@ Description: In the case of supergroups and channels, the user will not be able ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.kick_chat_member(...) @@ -54,7 +53,6 @@ return KickChatMember(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#kickchatmember) diff --git a/docs/api/methods/leave_chat.md b/docs/api/methods/leave_chat.md index 284c72f1..102b673f 100644 --- a/docs/api/methods/leave_chat.md +++ b/docs/api/methods/leave_chat.md @@ -22,8 +22,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.leave_chat(...) @@ -52,7 +51,6 @@ return LeaveChat(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#leavechat) diff --git a/docs/api/methods/pin_chat_message.md b/docs/api/methods/pin_chat_message.md index 1d9fd53e..27b97337 100644 --- a/docs/api/methods/pin_chat_message.md +++ b/docs/api/methods/pin_chat_message.md @@ -24,8 +24,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.pin_chat_message(...) @@ -54,7 +53,6 @@ return PinChatMessage(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#pinchatmessage) diff --git a/docs/api/methods/promote_chat_member.md b/docs/api/methods/promote_chat_member.md index e2fd6202..eae50d26 100644 --- a/docs/api/methods/promote_chat_member.md +++ b/docs/api/methods/promote_chat_member.md @@ -31,8 +31,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.promote_chat_member(...) @@ -61,7 +60,6 @@ return PromoteChatMember(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#promotechatmember) diff --git a/docs/api/methods/restrict_chat_member.md b/docs/api/methods/restrict_chat_member.md index fa438eb9..50ed84fa 100644 --- a/docs/api/methods/restrict_chat_member.md +++ b/docs/api/methods/restrict_chat_member.md @@ -25,8 +25,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.restrict_chat_member(...) @@ -55,7 +54,6 @@ return RestrictChatMember(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#restrictchatmember) diff --git a/docs/api/methods/send_animation.md b/docs/api/methods/send_animation.md index 3ea76968..b383ca61 100644 --- a/docs/api/methods/send_animation.md +++ b/docs/api/methods/send_animation.md @@ -15,7 +15,7 @@ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without s | `width` | `#!python3 Optional[int]` | Optional. Animation width | | `height` | `#!python3 Optional[int]` | Optional. Animation height | | `thumb` | `#!python3 Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python3 Optional[str]` | Optional. Animation caption (may also be used when resending animation by file_id), 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Animation caption (may also be used when resending animation by file_id), 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | | `reply_to_message_id` | `#!python3 Optional[int]` | Optional. If the message is a reply, ID of the original message | @@ -32,8 +32,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_animation(...) @@ -62,7 +61,6 @@ return SendAnimation(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendanimation) diff --git a/docs/api/methods/send_audio.md b/docs/api/methods/send_audio.md index 1aadc59d..dede2bc1 100644 --- a/docs/api/methods/send_audio.md +++ b/docs/api/methods/send_audio.md @@ -13,7 +13,7 @@ For sending voice messages, use the sendVoice method instead. | - | - | - | | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `audio` | `#!python3 Union[InputFile, str]` | Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. | -| `caption` | `#!python3 Optional[str]` | Optional. Audio caption, 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Audio caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `duration` | `#!python3 Optional[int]` | Optional. Duration of the audio in seconds | | `performer` | `#!python3 Optional[str]` | Optional. Performer | @@ -34,8 +34,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_audio(...) @@ -64,7 +63,6 @@ return SendAudio(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendaudio) diff --git a/docs/api/methods/send_chat_action.md b/docs/api/methods/send_chat_action.md index 59e23a75..b85847f6 100644 --- a/docs/api/methods/send_chat_action.md +++ b/docs/api/methods/send_chat_action.md @@ -27,8 +27,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.send_chat_action(...) @@ -57,7 +56,6 @@ return SendChatAction(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendchataction) diff --git a/docs/api/methods/send_contact.md b/docs/api/methods/send_contact.md index b9de4bcf..fbe0e670 100644 --- a/docs/api/methods/send_contact.md +++ b/docs/api/methods/send_contact.md @@ -29,8 +29,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_contact(...) @@ -59,7 +58,6 @@ return SendContact(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendcontact) diff --git a/docs/api/methods/send_dice.md b/docs/api/methods/send_dice.md new file mode 100644 index 00000000..86180563 --- /dev/null +++ b/docs/api/methods/send_dice.md @@ -0,0 +1,64 @@ +# sendDice + +## Description + +Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. (Yes, we're aware of the 'proper' singular of die. But it's awkward, and we decided to help it change. One dice at a time!) + + +## Arguments + +| Name | Type | Description | +| - | - | - | +| `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | +| `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | +| `reply_to_message_id` | `#!python3 Optional[int]` | Optional. If the message is a reply, ID of the original message | +| `reply_markup` | `#!python3 Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply]]` | Optional. Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. | + + + +## Response + +Type: `#!python3 Message` + +Description: On success, the sent Message is returned. + + +## Usage + +### As bot method + +```python3 +result: Message = await bot.send_dice(...) +``` + +### Method as object + +Imports: + +- `from aiogram.methods import SendDice` +- `from aiogram.api.methods import SendDice` +- `from aiogram.api.methods.send_dice import SendDice` + +#### In handlers with current bot +```python3 +result: Message = await SendDice(...) +``` + +#### With specific bot +```python3 +result: Message = await bot(SendDice(...)) +``` +#### As reply into Webhook in handler +```python3 +return SendDice(...) +``` + + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#senddice) +- [aiogram.types.ForceReply](../types/force_reply.md) +- [aiogram.types.InlineKeyboardMarkup](../types/inline_keyboard_markup.md) +- [aiogram.types.Message](../types/message.md) +- [aiogram.types.ReplyKeyboardMarkup](../types/reply_keyboard_markup.md) +- [aiogram.types.ReplyKeyboardRemove](../types/reply_keyboard_remove.md) diff --git a/docs/api/methods/send_document.md b/docs/api/methods/send_document.md index 9b0134b8..99a869c4 100644 --- a/docs/api/methods/send_document.md +++ b/docs/api/methods/send_document.md @@ -12,7 +12,7 @@ Use this method to send general files. On success, the sent Message is returned. | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `document` | `#!python3 Union[InputFile, str]` | File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | | `thumb` | `#!python3 Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python3 Optional[str]` | Optional. Document caption (may also be used when resending documents by file_id), 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | | `reply_to_message_id` | `#!python3 Optional[int]` | Optional. If the message is a reply, ID of the original message | @@ -29,8 +29,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_document(...) @@ -59,7 +58,6 @@ return SendDocument(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#senddocument) diff --git a/docs/api/methods/send_game.md b/docs/api/methods/send_game.md index 47b66405..15daff9b 100644 --- a/docs/api/methods/send_game.md +++ b/docs/api/methods/send_game.md @@ -26,8 +26,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_game(...) @@ -56,7 +55,6 @@ return SendGame(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendgame) diff --git a/docs/api/methods/send_invoice.md b/docs/api/methods/send_invoice.md index 9eeab6ab..9620b2ca 100644 --- a/docs/api/methods/send_invoice.md +++ b/docs/api/methods/send_invoice.md @@ -44,8 +44,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_invoice(...) @@ -74,7 +73,6 @@ return SendInvoice(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendinvoice) diff --git a/docs/api/methods/send_location.md b/docs/api/methods/send_location.md index 26b6d72b..98cbe531 100644 --- a/docs/api/methods/send_location.md +++ b/docs/api/methods/send_location.md @@ -28,8 +28,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_location(...) @@ -58,7 +57,6 @@ return SendLocation(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendlocation) diff --git a/docs/api/methods/send_media_group.md b/docs/api/methods/send_media_group.md index 522a849b..b15ccacc 100644 --- a/docs/api/methods/send_media_group.md +++ b/docs/api/methods/send_media_group.md @@ -25,8 +25,7 @@ Description: On success, an array of the sent Messages is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: List[Message] = await bot.send_media_group(...) @@ -55,7 +54,6 @@ return SendMediaGroup(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendmediagroup) diff --git a/docs/api/methods/send_message.md b/docs/api/methods/send_message.md index bb1137df..67b740ba 100644 --- a/docs/api/methods/send_message.md +++ b/docs/api/methods/send_message.md @@ -10,7 +10,7 @@ Use this method to send text messages. On success, the sent Message is returned. | Name | Type | Description | | - | - | - | | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | -| `text` | `#!python3 str` | Text of the message to be sent | +| `text` | `#!python3 str` | Text of the message to be sent, 1-4096 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. | | `disable_web_page_preview` | `#!python3 Optional[bool]` | Optional. Disables link previews for links in this message | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | @@ -28,8 +28,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_message(...) @@ -58,7 +57,6 @@ return SendMessage(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendmessage) diff --git a/docs/api/methods/send_photo.md b/docs/api/methods/send_photo.md index e52f8bf8..d2667a85 100644 --- a/docs/api/methods/send_photo.md +++ b/docs/api/methods/send_photo.md @@ -11,7 +11,7 @@ Use this method to send photos. On success, the sent Message is returned. | - | - | - | | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `photo` | `#!python3 Union[InputFile, str]` | Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. | -| `caption` | `#!python3 Optional[str]` | Optional. Photo caption (may also be used when resending photos by file_id), 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | | `reply_to_message_id` | `#!python3 Optional[int]` | Optional. If the message is a reply, ID of the original message | @@ -28,8 +28,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_photo(...) @@ -58,7 +57,6 @@ return SendPhoto(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendphoto) diff --git a/docs/api/methods/send_poll.md b/docs/api/methods/send_poll.md index af14ded1..175dc2eb 100644 --- a/docs/api/methods/send_poll.md +++ b/docs/api/methods/send_poll.md @@ -32,8 +32,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_poll(...) @@ -62,7 +61,6 @@ return SendPoll(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendpoll) diff --git a/docs/api/methods/send_sticker.md b/docs/api/methods/send_sticker.md index 2195256d..a48fd62f 100644 --- a/docs/api/methods/send_sticker.md +++ b/docs/api/methods/send_sticker.md @@ -10,7 +10,7 @@ Use this method to send static .WEBP or animated .TGS stickers. On success, the | Name | Type | Description | | - | - | - | | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | -| `sticker` | `#!python3 Union[InputFile, str]` | Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .webp file from the Internet, or upload a new one using multipart/form-data. | +| `sticker` | `#!python3 Union[InputFile, str]` | Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | | `reply_to_message_id` | `#!python3 Optional[int]` | Optional. If the message is a reply, ID of the original message | | `reply_markup` | `#!python3 Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply]]` | Optional. Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. | @@ -26,8 +26,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_sticker(...) @@ -56,7 +55,6 @@ return SendSticker(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendsticker) diff --git a/docs/api/methods/send_venue.md b/docs/api/methods/send_venue.md index 95509b55..8dcf3c69 100644 --- a/docs/api/methods/send_venue.md +++ b/docs/api/methods/send_venue.md @@ -31,8 +31,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_venue(...) @@ -61,7 +60,6 @@ return SendVenue(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendvenue) diff --git a/docs/api/methods/send_video.md b/docs/api/methods/send_video.md index 1cd459f8..87077d72 100644 --- a/docs/api/methods/send_video.md +++ b/docs/api/methods/send_video.md @@ -15,7 +15,7 @@ Use this method to send video files, Telegram clients support mp4 videos (other | `width` | `#!python3 Optional[int]` | Optional. Video width | | `height` | `#!python3 Optional[int]` | Optional. Video height | | `thumb` | `#!python3 Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python3 Optional[str]` | Optional. Video caption (may also be used when resending videos by file_id), 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `supports_streaming` | `#!python3 Optional[bool]` | Optional. Pass True, if the uploaded video is suitable for streaming | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | @@ -33,8 +33,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_video(...) @@ -63,7 +62,6 @@ return SendVideo(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendvideo) diff --git a/docs/api/methods/send_video_note.md b/docs/api/methods/send_video_note.md index d2e60ff9..6aac0df3 100644 --- a/docs/api/methods/send_video_note.md +++ b/docs/api/methods/send_video_note.md @@ -29,8 +29,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_video_note(...) @@ -59,7 +58,6 @@ return SendVideoNote(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendvideonote) diff --git a/docs/api/methods/send_voice.md b/docs/api/methods/send_voice.md index f71790fd..becd34f7 100644 --- a/docs/api/methods/send_voice.md +++ b/docs/api/methods/send_voice.md @@ -2,7 +2,7 @@ ## Description -Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. +Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. ## Arguments @@ -11,7 +11,7 @@ Use this method to send audio files, if you want Telegram clients to display the | - | - | - | | `chat_id` | `#!python3 Union[int, str]` | Unique identifier for the target chat or username of the target channel (in the format @channelusername) | | `voice` | `#!python3 Union[InputFile, str]` | Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. | -| `caption` | `#!python3 Optional[str]` | Optional. Voice message caption, 0-1024 characters | +| `caption` | `#!python3 Optional[str]` | Optional. Voice message caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python3 Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `duration` | `#!python3 Optional[int]` | Optional. Duration of the voice message in seconds | | `disable_notification` | `#!python3 Optional[bool]` | Optional. Sends the message silently. Users will receive a notification with no sound. | @@ -29,8 +29,7 @@ Description: On success, the sent Message is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Message = await bot.send_voice(...) @@ -59,7 +58,6 @@ return SendVoice(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#sendvoice) diff --git a/docs/api/methods/set_chat_administrator_custom_title.md b/docs/api/methods/set_chat_administrator_custom_title.md index dd301d2f..f58ae5bf 100644 --- a/docs/api/methods/set_chat_administrator_custom_title.md +++ b/docs/api/methods/set_chat_administrator_custom_title.md @@ -24,8 +24,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_administrator_custom_title(...) @@ -54,7 +53,6 @@ return SetChatAdministratorCustomTitle(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setchatadministratorcustomtitle) diff --git a/docs/api/methods/set_chat_description.md b/docs/api/methods/set_chat_description.md index ce555f3f..9750f139 100644 --- a/docs/api/methods/set_chat_description.md +++ b/docs/api/methods/set_chat_description.md @@ -23,8 +23,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_description(...) @@ -53,7 +52,6 @@ return SetChatDescription(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setchatdescription) diff --git a/docs/api/methods/set_chat_permissions.md b/docs/api/methods/set_chat_permissions.md index 3f975e51..2c04f480 100644 --- a/docs/api/methods/set_chat_permissions.md +++ b/docs/api/methods/set_chat_permissions.md @@ -23,8 +23,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_permissions(...) @@ -53,7 +52,6 @@ return SetChatPermissions(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setchatpermissions) diff --git a/docs/api/methods/set_chat_photo.md b/docs/api/methods/set_chat_photo.md index 8d17dc21..657c9025 100644 --- a/docs/api/methods/set_chat_photo.md +++ b/docs/api/methods/set_chat_photo.md @@ -23,8 +23,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_photo(...) diff --git a/docs/api/methods/set_chat_sticker_set.md b/docs/api/methods/set_chat_sticker_set.md index dda34637..89841af2 100644 --- a/docs/api/methods/set_chat_sticker_set.md +++ b/docs/api/methods/set_chat_sticker_set.md @@ -23,8 +23,7 @@ Description: Use the field can_set_sticker_set optionally returned in getChat re ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_sticker_set(...) @@ -53,7 +52,6 @@ return SetChatStickerSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setchatstickerset) diff --git a/docs/api/methods/set_chat_title.md b/docs/api/methods/set_chat_title.md index d4162ce5..90b32c3b 100644 --- a/docs/api/methods/set_chat_title.md +++ b/docs/api/methods/set_chat_title.md @@ -23,8 +23,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_chat_title(...) @@ -53,7 +52,6 @@ return SetChatTitle(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setchattitle) diff --git a/docs/api/methods/set_game_score.md b/docs/api/methods/set_game_score.md index ff09795c..80401159 100644 --- a/docs/api/methods/set_game_score.md +++ b/docs/api/methods/set_game_score.md @@ -28,8 +28,7 @@ Description: On success, if the message was sent by the bot, returns the edited ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.set_game_score(...) @@ -58,7 +57,6 @@ return SetGameScore(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setgamescore) diff --git a/docs/api/methods/set_my_commands.md b/docs/api/methods/set_my_commands.md new file mode 100644 index 00000000..6f84ddf4 --- /dev/null +++ b/docs/api/methods/set_my_commands.md @@ -0,0 +1,57 @@ +# setMyCommands + +## Description + +Use this method to change the list of the bot's commands. Returns True on success. + + +## Arguments + +| Name | Type | Description | +| - | - | - | +| `commands` | `#!python3 List[BotCommand]` | A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified. | + + + +## Response + +Type: `#!python3 bool` + +Description: Returns True on success. + + +## Usage + +### As bot method + +```python3 +result: bool = await bot.set_my_commands(...) +``` + +### Method as object + +Imports: + +- `from aiogram.methods import SetMyCommands` +- `from aiogram.api.methods import SetMyCommands` +- `from aiogram.api.methods.set_my_commands import SetMyCommands` + +#### In handlers with current bot +```python3 +result: bool = await SetMyCommands(...) +``` + +#### With specific bot +```python3 +result: bool = await bot(SetMyCommands(...)) +``` +#### As reply into Webhook in handler +```python3 +return SetMyCommands(...) +``` + + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#setmycommands) +- [aiogram.types.BotCommand](../types/bot_command.md) diff --git a/docs/api/methods/set_passport_data_errors.md b/docs/api/methods/set_passport_data_errors.md index e00d2e89..3cc82934 100644 --- a/docs/api/methods/set_passport_data_errors.md +++ b/docs/api/methods/set_passport_data_errors.md @@ -25,8 +25,7 @@ Description: The user will not be able to re-submit their Passport to you until ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_passport_data_errors(...) @@ -55,7 +54,6 @@ return SetPassportDataErrors(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setpassportdataerrors) diff --git a/docs/api/methods/set_sticker_position_in_set.md b/docs/api/methods/set_sticker_position_in_set.md index 7a67dab9..9e0a094a 100644 --- a/docs/api/methods/set_sticker_position_in_set.md +++ b/docs/api/methods/set_sticker_position_in_set.md @@ -2,7 +2,7 @@ ## Description -Use this method to move a sticker in a set created by the bot to a specific position . Returns True on success. +Use this method to move a sticker in a set created by the bot to a specific position. Returns True on success. ## Arguments @@ -23,8 +23,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_sticker_position_in_set(...) @@ -53,7 +52,6 @@ return SetStickerPositionInSet(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setstickerpositioninset) diff --git a/docs/api/methods/set_sticker_set_thumb.md b/docs/api/methods/set_sticker_set_thumb.md new file mode 100644 index 00000000..ffc2b1d5 --- /dev/null +++ b/docs/api/methods/set_sticker_set_thumb.md @@ -0,0 +1,60 @@ +# setStickerSetThumb + +## Description + +Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns True on success. + + +## Arguments + +| Name | Type | Description | +| - | - | - | +| `name` | `#!python3 str` | Sticker set name | +| `user_id` | `#!python3 int` | User identifier of the sticker set owner | +| `thumb` | `#!python3 Optional[Union[InputFile, str]]` | Optional. A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data.. Animated sticker set thumbnail can't be uploaded via HTTP URL. | + + + +## Response + +Type: `#!python3 bool` + +Description: Returns True on success. + + +## Usage + +### As bot method + +```python3 +result: bool = await bot.set_sticker_set_thumb(...) +``` + +### Method as object + +Imports: + +- `from aiogram.methods import SetStickerSetThumb` +- `from aiogram.api.methods import SetStickerSetThumb` +- `from aiogram.api.methods.set_sticker_set_thumb import SetStickerSetThumb` + +#### In handlers with current bot +```python3 +result: bool = await SetStickerSetThumb(...) +``` + +#### With specific bot +```python3 +result: bool = await bot(SetStickerSetThumb(...)) +``` +#### As reply into Webhook in handler +```python3 +return SetStickerSetThumb(...) +``` + + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#setstickersetthumb) +- [aiogram.types.InputFile](../types/input_file.md) +- [How to upload file?](../sending_files.md) diff --git a/docs/api/methods/set_webhook.md b/docs/api/methods/set_webhook.md index 4af9a71b..95e82a86 100644 --- a/docs/api/methods/set_webhook.md +++ b/docs/api/methods/set_webhook.md @@ -37,8 +37,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.set_webhook(...) @@ -67,7 +66,6 @@ return SetWebhook(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#setwebhook) diff --git a/docs/api/methods/stop_message_live_location.md b/docs/api/methods/stop_message_live_location.md index 32dbaa19..76748df3 100644 --- a/docs/api/methods/stop_message_live_location.md +++ b/docs/api/methods/stop_message_live_location.md @@ -25,8 +25,7 @@ Description: On success, if the message was sent by the bot, the sent Message is ## Usage - -### As bot method bot +### As bot method ```python3 result: Union[Message, bool] = await bot.stop_message_live_location(...) @@ -55,7 +54,6 @@ return StopMessageLiveLocation(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#stopmessagelivelocation) diff --git a/docs/api/methods/stop_poll.md b/docs/api/methods/stop_poll.md index 797da7ec..a251ee73 100644 --- a/docs/api/methods/stop_poll.md +++ b/docs/api/methods/stop_poll.md @@ -24,8 +24,7 @@ Description: On success, the stopped Poll with the final results is returned. ## Usage - -### As bot method bot +### As bot method ```python3 result: Poll = await bot.stop_poll(...) @@ -54,7 +53,6 @@ return StopPoll(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#stoppoll) diff --git a/docs/api/methods/unban_chat_member.md b/docs/api/methods/unban_chat_member.md index 2c18ed5e..1620ccf7 100644 --- a/docs/api/methods/unban_chat_member.md +++ b/docs/api/methods/unban_chat_member.md @@ -23,8 +23,7 @@ Description: The user will not return to the group or channel automatically, but ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.unban_chat_member(...) @@ -53,7 +52,6 @@ return UnbanChatMember(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#unbanchatmember) diff --git a/docs/api/methods/unpin_chat_message.md b/docs/api/methods/unpin_chat_message.md index b49bf991..fc60f74e 100644 --- a/docs/api/methods/unpin_chat_message.md +++ b/docs/api/methods/unpin_chat_message.md @@ -22,8 +22,7 @@ Description: Returns True on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: bool = await bot.unpin_chat_message(...) @@ -52,7 +51,6 @@ return UnpinChatMessage(...) ``` - ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#unpinchatmessage) diff --git a/docs/api/methods/upload_sticker_file.md b/docs/api/methods/upload_sticker_file.md index b14b250e..8777df2e 100644 --- a/docs/api/methods/upload_sticker_file.md +++ b/docs/api/methods/upload_sticker_file.md @@ -2,7 +2,7 @@ ## Description -Use this method to upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. +Use this method to upload a .PNG file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. ## Arguments @@ -23,8 +23,7 @@ Description: Returns the uploaded File on success. ## Usage - -### As bot method bot +### As bot method ```python3 result: File = await bot.upload_sticker_file(...) diff --git a/docs/api/types/bot_command.md b/docs/api/types/bot_command.md new file mode 100644 index 00000000..383161da --- /dev/null +++ b/docs/api/types/bot_command.md @@ -0,0 +1,25 @@ +# BotCommand + +## Description + +This object represents a bot command. + + +## Attributes + +| Name | Type | Description | +| - | - | - | +| `command` | `#!python str` | Text of the command, 1-32 characters. Can contain only lowercase English letters, digits and underscores. | +| `description` | `#!python str` | Description of the command, 3-256 characters. | + + + +## Location + +- `from aiogram.types import BotCommand` +- `from aiogram.api.types import BotCommand` +- `from aiogram.api.types.bot_command import BotCommand` + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#botcommand) diff --git a/docs/api/types/dice.md b/docs/api/types/dice.md new file mode 100644 index 00000000..1eb938f8 --- /dev/null +++ b/docs/api/types/dice.md @@ -0,0 +1,24 @@ +# Dice + +## Description + +This object represents a dice with random value from 1 to 6. (Yes, we're aware of the 'proper' singular of die. But it's awkward, and we decided to help it change. One dice at a time!) + + +## Attributes + +| Name | Type | Description | +| - | - | - | +| `value` | `#!python int` | Value of the dice, 1-6 | + + + +## Location + +- `from aiogram.types import Dice` +- `from aiogram.api.types import Dice` +- `from aiogram.api.types.dice import Dice` + +## Related pages: + +- [Official documentation](https://core.telegram.org/bots/api#dice) diff --git a/docs/api/types/index.md b/docs/api/types/index.md index 3b43ad2f..c0916cfe 100644 --- a/docs/api/types/index.md +++ b/docs/api/types/index.md @@ -2,7 +2,6 @@ Here is list of all available API types: - - Getting updates - [Update](update.md) - [WebhookInfo](webhook_info.md) @@ -24,6 +23,7 @@ Here is list of all available API types: - [PollOption](poll_option.md) - [PollAnswer](poll_answer.md) - [Poll](poll.md) + - [Dice](dice.md) - [UserProfilePhotos](user_profile_photos.md) - [File](file.md) - [ReplyKeyboardMarkup](reply_keyboard_markup.md) @@ -38,6 +38,7 @@ Here is list of all available API types: - [ChatPhoto](chat_photo.md) - [ChatMember](chat_member.md) - [ChatPermissions](chat_permissions.md) + - [BotCommand](bot_command.md) - [ResponseParameters](response_parameters.md) - [InputMedia](input_media.md) - [InputMediaPhoto](input_media_photo.md) diff --git a/docs/api/types/inline_query.md b/docs/api/types/inline_query.md index 583d6a60..cd3c7545 100644 --- a/docs/api/types/inline_query.md +++ b/docs/api/types/inline_query.md @@ -11,7 +11,7 @@ This object represents an incoming inline query. When the user sends an empty qu | - | - | - | | `id` | `#!python str` | Unique identifier for this query | | `from_user` | `#!python User` | Sender | -| `query` | `#!python str` | Text of the query (up to 512 characters) | +| `query` | `#!python str` | Text of the query (up to 256 characters) | | `offset` | `#!python str` | Offset of the results to be returned, can be controlled by the bot | | `location` | `#!python Optional[Location]` | Optional. Sender location, only for bots that request user location | diff --git a/docs/api/types/inline_query_result_audio.md b/docs/api/types/inline_query_result_audio.md index 38d1bb91..4a4257ea 100644 --- a/docs/api/types/inline_query_result_audio.md +++ b/docs/api/types/inline_query_result_audio.md @@ -15,7 +15,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `audio_url` | `#!python str` | A valid URL for the audio file | | `title` | `#!python str` | Title | -| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `performer` | `#!python Optional[str]` | Optional. Performer | | `audio_duration` | `#!python Optional[int]` | Optional. Audio duration in seconds | diff --git a/docs/api/types/inline_query_result_cached_audio.md b/docs/api/types/inline_query_result_cached_audio.md index c1110f8e..2fdbeb0e 100644 --- a/docs/api/types/inline_query_result_cached_audio.md +++ b/docs/api/types/inline_query_result_cached_audio.md @@ -14,7 +14,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `type` | `#!python str` | Type of the result, must be audio | | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `audio_file_id` | `#!python str` | A valid file identifier for the audio file | -| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the audio | diff --git a/docs/api/types/inline_query_result_cached_document.md b/docs/api/types/inline_query_result_cached_document.md index 7401235f..0fc60042 100644 --- a/docs/api/types/inline_query_result_cached_document.md +++ b/docs/api/types/inline_query_result_cached_document.md @@ -16,7 +16,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `title` | `#!python str` | Title for the result | | `document_file_id` | `#!python str` | A valid file identifier for the file | | `description` | `#!python Optional[str]` | Optional. Short description of the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the file | diff --git a/docs/api/types/inline_query_result_cached_gif.md b/docs/api/types/inline_query_result_cached_gif.md index 44c4820d..575582af 100644 --- a/docs/api/types/inline_query_result_cached_gif.md +++ b/docs/api/types/inline_query_result_cached_gif.md @@ -13,7 +13,7 @@ Represents a link to an animated GIF file stored on the Telegram servers. By def | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `gif_file_id` | `#!python str` | A valid file identifier for the GIF file | | `title` | `#!python Optional[str]` | Optional. Title for the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the GIF file to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the GIF file to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the GIF animation | diff --git a/docs/api/types/inline_query_result_cached_mpeg4_gif.md b/docs/api/types/inline_query_result_cached_mpeg4_gif.md index 0ae5bbf6..1efd7acf 100644 --- a/docs/api/types/inline_query_result_cached_mpeg4_gif.md +++ b/docs/api/types/inline_query_result_cached_mpeg4_gif.md @@ -13,7 +13,7 @@ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) st | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `mpeg4_file_id` | `#!python str` | A valid file identifier for the MP4 file | | `title` | `#!python Optional[str]` | Optional. Title for the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the video animation | diff --git a/docs/api/types/inline_query_result_cached_photo.md b/docs/api/types/inline_query_result_cached_photo.md index 1ce40aa8..e06c6a73 100644 --- a/docs/api/types/inline_query_result_cached_photo.md +++ b/docs/api/types/inline_query_result_cached_photo.md @@ -14,7 +14,7 @@ Represents a link to a photo stored on the Telegram servers. By default, this ph | `photo_file_id` | `#!python str` | A valid file identifier of the photo | | `title` | `#!python Optional[str]` | Optional. Title for the result | | `description` | `#!python Optional[str]` | Optional. Short description of the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the photo | diff --git a/docs/api/types/inline_query_result_cached_video.md b/docs/api/types/inline_query_result_cached_video.md index 6e12fbbc..c0620726 100644 --- a/docs/api/types/inline_query_result_cached_video.md +++ b/docs/api/types/inline_query_result_cached_video.md @@ -14,7 +14,7 @@ Represents a link to a video file stored on the Telegram servers. By default, th | `video_file_id` | `#!python str` | A valid file identifier for the video file | | `title` | `#!python str` | Title for the result | | `description` | `#!python Optional[str]` | Optional. Short description of the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the video | diff --git a/docs/api/types/inline_query_result_cached_voice.md b/docs/api/types/inline_query_result_cached_voice.md index aadcbd0e..28d00e6b 100644 --- a/docs/api/types/inline_query_result_cached_voice.md +++ b/docs/api/types/inline_query_result_cached_voice.md @@ -15,7 +15,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `voice_file_id` | `#!python str` | A valid file identifier for the voice message | | `title` | `#!python str` | Voice message title | -| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the voice message | diff --git a/docs/api/types/inline_query_result_document.md b/docs/api/types/inline_query_result_document.md index 05c783d7..9ebbf1a7 100644 --- a/docs/api/types/inline_query_result_document.md +++ b/docs/api/types/inline_query_result_document.md @@ -16,7 +16,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `title` | `#!python str` | Title for the result | | `document_url` | `#!python str` | A valid URL for the file | | `mime_type` | `#!python str` | Mime type of the content of the file, either 'application/pdf' or 'application/zip' | -| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `description` | `#!python Optional[str]` | Optional. Short description of the result | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | diff --git a/docs/api/types/inline_query_result_gif.md b/docs/api/types/inline_query_result_gif.md index 4365c243..59a3a6e3 100644 --- a/docs/api/types/inline_query_result_gif.md +++ b/docs/api/types/inline_query_result_gif.md @@ -17,7 +17,7 @@ Represents a link to an animated GIF file. By default, this animated GIF file wi | `gif_height` | `#!python Optional[int]` | Optional. Height of the GIF | | `gif_duration` | `#!python Optional[int]` | Optional. Duration of the GIF | | `title` | `#!python Optional[str]` | Optional. Title for the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the GIF file to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the GIF file to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the GIF animation | diff --git a/docs/api/types/inline_query_result_mpeg4_gif.md b/docs/api/types/inline_query_result_mpeg4_gif.md index 934611ab..3b03d8ef 100644 --- a/docs/api/types/inline_query_result_mpeg4_gif.md +++ b/docs/api/types/inline_query_result_mpeg4_gif.md @@ -17,7 +17,7 @@ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). B | `mpeg4_height` | `#!python Optional[int]` | Optional. Video height | | `mpeg4_duration` | `#!python Optional[int]` | Optional. Video duration | | `title` | `#!python Optional[str]` | Optional. Title for the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the video animation | diff --git a/docs/api/types/inline_query_result_photo.md b/docs/api/types/inline_query_result_photo.md index 96d71636..3c13c787 100644 --- a/docs/api/types/inline_query_result_photo.md +++ b/docs/api/types/inline_query_result_photo.md @@ -17,7 +17,7 @@ Represents a link to a photo. By default, this photo will be sent by the user wi | `photo_height` | `#!python Optional[int]` | Optional. Height of the photo | | `title` | `#!python Optional[str]` | Optional. Title for the result | | `description` | `#!python Optional[str]` | Optional. Short description of the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | | `input_message_content` | `#!python Optional[InputMessageContent]` | Optional. Content of the message to be sent instead of the photo | diff --git a/docs/api/types/inline_query_result_video.md b/docs/api/types/inline_query_result_video.md index b212f03a..3a503511 100644 --- a/docs/api/types/inline_query_result_video.md +++ b/docs/api/types/inline_query_result_video.md @@ -17,7 +17,7 @@ If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), | `mime_type` | `#!python str` | Mime type of the content of video url, 'text/html' or 'video/mp4' | | `thumb_url` | `#!python str` | URL of the thumbnail (jpeg only) for the video | | `title` | `#!python str` | Title for the result | -| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `video_width` | `#!python Optional[int]` | Optional. Video width | | `video_height` | `#!python Optional[int]` | Optional. Video height | diff --git a/docs/api/types/inline_query_result_voice.md b/docs/api/types/inline_query_result_voice.md index f7725c5a..c3907b7b 100644 --- a/docs/api/types/inline_query_result_voice.md +++ b/docs/api/types/inline_query_result_voice.md @@ -2,7 +2,7 @@ ## Description -Represents a link to a voice recording in an .ogg container encoded with OPUS. By default, this voice recording will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message. +Represents a link to a voice recording in an .OGG container encoded with OPUS. By default, this voice recording will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message. Note: This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -15,7 +15,7 @@ Note: This will only work in Telegram versions released after 9 April, 2016. Old | `id` | `#!python str` | Unique identifier for this result, 1-64 bytes | | `voice_url` | `#!python str` | A valid URL for the voice recording | | `title` | `#!python str` | Recording title | -| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `voice_duration` | `#!python Optional[int]` | Optional. Recording duration in seconds | | `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message | diff --git a/docs/api/types/input_media_animation.md b/docs/api/types/input_media_animation.md index ce0c3132..be9ebf4f 100644 --- a/docs/api/types/input_media_animation.md +++ b/docs/api/types/input_media_animation.md @@ -12,7 +12,7 @@ Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be | `type` | `#!python str` | Type of the result, must be animation | | `media` | `#!python Union[str, InputFile]` | File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass 'attach://' to upload a new one using multipart/form-data under name. | | `thumb` | `#!python Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python Optional[str]` | Optional. Caption of the animation to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the animation to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `width` | `#!python Optional[int]` | Optional. Animation width | | `height` | `#!python Optional[int]` | Optional. Animation height | diff --git a/docs/api/types/input_media_audio.md b/docs/api/types/input_media_audio.md index dac0d773..45e699f5 100644 --- a/docs/api/types/input_media_audio.md +++ b/docs/api/types/input_media_audio.md @@ -12,7 +12,7 @@ Represents an audio file to be treated as music to be sent. | `type` | `#!python str` | Type of the result, must be audio | | `media` | `#!python Union[str, InputFile]` | File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass 'attach://' to upload a new one using multipart/form-data under name. | | `thumb` | `#!python Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python Optional[str]` | Optional. Caption of the audio to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the audio to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `duration` | `#!python Optional[int]` | Optional. Duration of the audio in seconds | | `performer` | `#!python Optional[str]` | Optional. Performer of the audio | diff --git a/docs/api/types/input_media_document.md b/docs/api/types/input_media_document.md index 77877ebe..62a9d39e 100644 --- a/docs/api/types/input_media_document.md +++ b/docs/api/types/input_media_document.md @@ -12,7 +12,7 @@ Represents a general file to be sent. | `type` | `#!python str` | Type of the result, must be document | | `media` | `#!python Union[str, InputFile]` | File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass 'attach://' to upload a new one using multipart/form-data under name. | | `thumb` | `#!python Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the document to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | diff --git a/docs/api/types/input_media_photo.md b/docs/api/types/input_media_photo.md index 29c903e6..9d75feff 100644 --- a/docs/api/types/input_media_photo.md +++ b/docs/api/types/input_media_photo.md @@ -11,7 +11,7 @@ Represents a photo to be sent. | - | - | - | | `type` | `#!python str` | Type of the result, must be photo | | `media` | `#!python Union[str, InputFile]` | File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass 'attach://' to upload a new one using multipart/form-data under name. | -| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | diff --git a/docs/api/types/input_media_video.md b/docs/api/types/input_media_video.md index 5a45d03d..3779cd1c 100644 --- a/docs/api/types/input_media_video.md +++ b/docs/api/types/input_media_video.md @@ -12,7 +12,7 @@ Represents a video to be sent. | `type` | `#!python str` | Type of the result, must be video | | `media` | `#!python Union[str, InputFile]` | File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass 'attach://' to upload a new one using multipart/form-data under name. | | `thumb` | `#!python Optional[Union[InputFile, str]]` | Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass 'attach://' if the thumbnail was uploaded using multipart/form-data under . | -| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters | +| `caption` | `#!python Optional[str]` | Optional. Caption of the video to be sent, 0-1024 characters after entities parsing | | `parse_mode` | `#!python Optional[str]` | Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. | | `width` | `#!python Optional[int]` | Optional. Video width | | `height` | `#!python Optional[int]` | Optional. Video height | diff --git a/docs/api/types/keyboard_button.md b/docs/api/types/keyboard_button.md index 9177caa2..d09616e1 100644 --- a/docs/api/types/keyboard_button.md +++ b/docs/api/types/keyboard_button.md @@ -4,9 +4,9 @@ This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields request_contact, request_location, and request_poll are mutually exclusive. -Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will receive unsupported message. +Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will display unsupported message. -Note: request_poll option will only work in Telegram versions released after 23 January, 2020. Older clients will receive unsupported message. +Note: request_poll option will only work in Telegram versions released after 23 January, 2020. Older clients will display unsupported message. ## Attributes diff --git a/docs/api/types/message.md b/docs/api/types/message.md index df9b711d..c265d656 100644 --- a/docs/api/types/message.md +++ b/docs/api/types/message.md @@ -19,11 +19,11 @@ This object represents a message. | `forward_signature` | `#!python Optional[str]` | Optional. For messages forwarded from channels, signature of the post author if present | | `forward_sender_name` | `#!python Optional[str]` | Optional. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages | | `forward_date` | `#!python Optional[int]` | Optional. For forwarded messages, date the original message was sent in Unix time | -| `reply_to_message` | `#!python Optional[send_to_message.mdMessReply to message with to_messageage]` | Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply. | +| `reply_to_message` | `#!python Optional[Message]` | Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply. | | `edit_date` | `#!python Optional[int]` | Optional. Date the message was last edited in Unix time | | `media_group_id` | `#!python Optional[str]` | Optional. The unique identifier of a media message group this message belongs to | | `author_signature` | `#!python Optional[str]` | Optional. Signature of the post author for messages in channels | -| `text` | `#!python Optional[str]` | Optional. For text messages, the actual UTF-8 text of the message, 0-4096 characters. | +| `text` | `#!python Optional[str]` | Optional. For text messages, the actual UTF-8 text of the message, 0-4096 characters | | `entities` | `#!python Optional[List[MessageEntity]]` | Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text | | `caption_entities` | `#!python Optional[List[MessageEntity]]` | Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption | | `audio` | `#!python Optional[Audio]` | Optional. Message is an audio file, information about the file | @@ -40,6 +40,7 @@ This object represents a message. | `location` | `#!python Optional[Location]` | Optional. Message is a shared location, information about the location | | `venue` | `#!python Optional[Venue]` | Optional. Message is a venue, information about the venue | | `poll` | `#!python Optional[Poll]` | Optional. Message is a native poll, information about the poll | +| `dice` | `#!python Optional[Dice]` | Optional. Message is a dice with random value from 1 to 6 | | `new_chat_members` | `#!python Optional[List[User]]` | Optional. New members that were added to the group or supergroup and information about them (the bot itself may be one of these members) | | `left_chat_member` | `#!python Optional[User]` | Optional. A member was removed from the group, information about them (this member may be the bot itself) | | `new_chat_title` | `#!python Optional[str]` | Optional. A chat title was changed to this value | @@ -55,7 +56,7 @@ This object represents a message. | `successful_payment` | `#!python Optional[SuccessfulPayment]` | Optional. Message is a service message about a successful payment, information about the payment. | | `connected_website` | `#!python Optional[str]` | Optional. The domain name of the website on which the user has logged in. | | `passport_data` | `#!python Optional[PassportData]` | Optional. Telegram Passport data | -| `reply_markup` | `#!python Optional[send_markup.mdInliReply to message with markupneKeyboardMarkup]` | Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | +| `reply_markup` | `#!python Optional[InlineKeyboardMarkup]` | Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | @@ -101,6 +102,7 @@ This methods has the same specification with the API but without `chat_id` and ` - [aiogram.types.Audio](../types/audio.md) - [aiogram.types.Chat](../types/chat.md) - [aiogram.types.Contact](../types/contact.md) +- [aiogram.types.Dice](../types/dice.md) - [aiogram.types.Document](../types/document.md) - [aiogram.types.Game](../types/game.md) - [aiogram.types.InlineKeyboardMarkup](../types/inline_keyboard_markup.md) diff --git a/docs/api/types/sticker.md b/docs/api/types/sticker.md index dde51a74..2d864b66 100644 --- a/docs/api/types/sticker.md +++ b/docs/api/types/sticker.md @@ -14,7 +14,7 @@ This object represents a sticker. | `width` | `#!python int` | Sticker width | | `height` | `#!python int` | Sticker height | | `is_animated` | `#!python bool` | True, if the sticker is animated | -| `thumb` | `#!python Optional[PhotoSize]` | Optional. Sticker thumbnail in the .webp or .jpg format | +| `thumb` | `#!python Optional[PhotoSize]` | Optional. Sticker thumbnail in the .WEBP or .JPG format | | `emoji` | `#!python Optional[str]` | Optional. Emoji associated with the sticker | | `set_name` | `#!python Optional[str]` | Optional. Name of the sticker set to which the sticker belongs | | `mask_position` | `#!python Optional[MaskPosition]` | Optional. For mask stickers, the position where the mask should be placed | diff --git a/docs/api/types/sticker_set.md b/docs/api/types/sticker_set.md index 91f4909a..150f4f7a 100644 --- a/docs/api/types/sticker_set.md +++ b/docs/api/types/sticker_set.md @@ -14,6 +14,7 @@ This object represents a sticker set. | `is_animated` | `#!python bool` | True, if the sticker set contains animated stickers | | `contains_masks` | `#!python bool` | True, if the sticker set contains masks | | `stickers` | `#!python List[Sticker]` | List of all set stickers | +| `thumb` | `#!python Optional[PhotoSize]` | Optional. Sticker set thumbnail in the .WEBP or .TGS format | @@ -26,4 +27,5 @@ This object represents a sticker set. ## Related pages: - [Official documentation](https://core.telegram.org/bots/api#stickerset) +- [aiogram.types.PhotoSize](../types/photo_size.md) - [aiogram.types.Sticker](../types/sticker.md) diff --git a/mkdocs.yml b/mkdocs.yml index c5b69163..d64deb09 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,7 +42,7 @@ nav: - Bot API: - api/index.md - Methods: - - Overview: api/methods/index.md + - Available methods: api/methods/index.md - Getting updates: - api/methods/get_updates.md - api/methods/set_webhook.md @@ -66,6 +66,7 @@ nav: - api/methods/send_venue.md - api/methods/send_contact.md - api/methods/send_poll.md + - api/methods/send_dice.md - api/methods/send_chat_action.md - api/methods/get_user_profile_photos.md - api/methods/get_file.md @@ -90,6 +91,8 @@ nav: - api/methods/set_chat_sticker_set.md - api/methods/delete_chat_sticker_set.md - api/methods/answer_callback_query.md + - api/methods/set_my_commands.md + - api/methods/get_my_commands.md - Updating messages: - api/methods/edit_message_text.md - api/methods/edit_message_caption.md @@ -105,6 +108,7 @@ nav: - api/methods/add_sticker_to_set.md - api/methods/set_sticker_position_in_set.md - api/methods/delete_sticker_from_set.md + - api/methods/set_sticker_set_thumb.md - Inline mode: - api/methods/answer_inline_query.md - Payments: @@ -118,7 +122,7 @@ nav: - api/methods/set_game_score.md - api/methods/get_game_high_scores.md - Types: - - Overview: api/types/index.md + - Available types: api/types/index.md - Getting updates: - api/types/update.md - api/types/webhook_info.md @@ -137,9 +141,10 @@ nav: - api/types/contact.md - api/types/location.md - api/types/venue.md - - api/types/poll_answer.md - api/types/poll_option.md + - api/types/poll_answer.md - api/types/poll.md + - api/types/dice.md - api/types/user_profile_photos.md - api/types/file.md - api/types/reply_keyboard_markup.md @@ -154,6 +159,7 @@ nav: - api/types/chat_photo.md - api/types/chat_member.md - api/types/chat_permissions.md + - api/types/bot_command.md - api/types/response_parameters.md - api/types/input_media.md - api/types/input_media_photo.md @@ -223,7 +229,7 @@ nav: - api/types/game.md - api/types/callback_game.md - api/types/game_high_score.md - - api/sending_files.md + - api/sending_files.md - Dispatcher: - dispatcher/index.md - dispatcher/router.md diff --git a/poetry.lock b/poetry.lock index f20ab5b1..e74f26aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -242,7 +242,7 @@ marker = "python_version >= \"3.5\" and python_version < \"3.8\" or python_versi name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.0" +version = "1.6.0" [package.dependencies] zipp = ">=0.5" @@ -550,7 +550,7 @@ description = "Utility library for gitignore style pattern matching of file path name = "pathspec" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.7.0" +version = "0.8.0" [[package]] category = "dev" @@ -594,7 +594,7 @@ description = "Library for building powerful interactive command lines in Python name = "prompt-toolkit" optional = false python-versions = ">=3.6.1" -version = "3.0.4" +version = "3.0.5" [package.dependencies] wcwidth = "*" @@ -670,7 +670,7 @@ description = "Python parsing module" name = "pyparsing" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.6" +version = "2.4.7" [[package]] category = "dev" @@ -733,7 +733,7 @@ description = "pytest plugin for generating HTML reports" name = "pytest-html" optional = false python-versions = ">=3.6" -version = "2.1.0" +version = "2.1.1" [package.dependencies] pytest = ">=5.0" @@ -799,7 +799,7 @@ description = "YAML parser and emitter for Python" name = "pyyaml" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3" +version = "5.3.1" [[package]] category = "dev" @@ -807,7 +807,7 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2020.2.20" +version = "2020.4.4" [[package]] category = "dev" @@ -863,7 +863,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.1" +version = "3.7.4.2" [[package]] category = "main" @@ -880,7 +880,7 @@ description = "Measures number of Terminal column cells of wide-character codes" name = "wcwidth" optional = false python-versions = "*" -version = "0.1.8" +version = "0.1.9" [[package]] category = "main" @@ -1045,8 +1045,8 @@ idna = [ {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"}, - {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, ] ipython = [ {file = "ipython-7.13.0-py3-none-any.whl", hash = "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333"}, @@ -1140,11 +1140,6 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {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-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ @@ -1218,8 +1213,8 @@ parso = [ {file = "parso-0.6.2.tar.gz", hash = "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157"}, ] pathspec = [ - {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, - {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, @@ -1234,8 +1229,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.4-py3-none-any.whl", hash = "sha256:859e1b205b6cf6a51fa57fa34202e45365cf58f8338f0ee9f4e84a4165b37d5b"}, - {file = "prompt_toolkit-3.0.4.tar.gz", hash = "sha256:ebe6b1b08c888b84c50d7f93dee21a09af39860144ff6130aadbd61ae8d29783"}, + {file = "prompt_toolkit-3.0.5-py3-none-any.whl", hash = "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"}, + {file = "prompt_toolkit-3.0.5.tar.gz", hash = "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, @@ -1278,8 +1273,8 @@ pymdown-extensions = [ {file = "pymdown_extensions-6.3-py2.py3-none-any.whl", hash = "sha256:66fae2683c7a1dac53184f7de57f51f8dad73f9ead2f453e94e85096cb811335"}, ] pyparsing = [ - {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, - {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, + {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-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"}, @@ -1294,8 +1289,8 @@ pytest-cov = [ {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, ] pytest-html = [ - {file = "pytest-html-2.1.0.tar.gz", hash = "sha256:8645a8616c8ed7414678e0aeebc3b2fd7d44268773ef5e7289289ad8632c9e91"}, - {file = "pytest_html-2.1.0-py2.py3-none-any.whl", hash = "sha256:0317a0a589db59c26091ab6068b3edac8d9bc1a8bb9727ade48f806797346956"}, + {file = "pytest-html-2.1.1.tar.gz", hash = "sha256:6a4ac391e105e391208e3eb9bd294a60dd336447fd8e1acddff3a6de7f4e57c5"}, + {file = "pytest_html-2.1.1-py2.py3-none-any.whl", hash = "sha256:9e4817e8be8ddde62e8653c8934d0f296b605da3d2277a052f762c56a8b32df2"}, ] pytest-metadata = [ {file = "pytest-metadata-1.8.0.tar.gz", hash = "sha256:2071a59285de40d7541fde1eb9f1ddea1c9db165882df82781367471238b66ba"}, @@ -1314,40 +1309,40 @@ pytz = [ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, ] pyyaml = [ - {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, - {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, - {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, - {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, - {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, - {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, - {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, - {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, - {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, - {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, - {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] regex = [ - {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, - {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, - {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, - {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, - {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, - {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, - {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, - {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, - {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, - {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, - {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, - {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, - {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, - {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, - {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, - {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, - {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, - {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, - {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, - {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, - {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, + {file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"}, + {file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"}, + {file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"}, + {file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"}, + {file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"}, + {file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"}, + {file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"}, + {file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"}, + {file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"}, ] six = [ {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, @@ -1397,9 +1392,9 @@ typed-ast = [ {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, - {file = "typing_extensions-3.7.4.1-py3-none-any.whl", hash = "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"}, - {file = "typing_extensions-3.7.4.1.tar.gz", hash = "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"}, + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, ] uvloop = [ {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, @@ -1413,8 +1408,8 @@ uvloop = [ {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, ] wcwidth = [ - {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, - {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, ] yarl = [ {file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"}, diff --git a/pyproject.toml b/pyproject.toml index 4ee980ee..00a0d827 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aiogram" -version = "3.0.0-alpha.2" +version = "3.0.0-alpha.3" description = "Modern and fully asynchronous framework for Telegram Bot API" authors = ["Alex Root Junior "] license = "MIT" @@ -25,6 +25,7 @@ classifiers = [ "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", "Framework :: AsyncIO", "Typing :: Typed", diff --git a/tests/test_api/test_methods/test_get_my_commands.py b/tests/test_api/test_methods/test_get_my_commands.py new file mode 100644 index 00000000..0cca2287 --- /dev/null +++ b/tests/test_api/test_methods/test_get_my_commands.py @@ -0,0 +1,27 @@ +from typing import List + +import pytest + +from aiogram.api.methods import GetMyCommands, Request +from aiogram.api.types import BotCommand +from tests.mocked_bot import MockedBot + + +class TestGetMyCommands: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetMyCommands, ok=True, result=None) + + response: List[BotCommand] = await GetMyCommands() + request: Request = bot.get_request() + assert request.method == "getMyCommands" + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetMyCommands, ok=True, result=None) + + response: List[BotCommand] = await bot.get_my_commands() + request: Request = bot.get_request() + assert request.method == "getMyCommands" + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_send_dice.py b/tests/test_api/test_methods/test_send_dice.py new file mode 100644 index 00000000..1594cda4 --- /dev/null +++ b/tests/test_api/test_methods/test_send_dice.py @@ -0,0 +1,25 @@ +import pytest + +from aiogram.api.methods import Request, SendDice +from aiogram.api.types import Message +from tests.mocked_bot import MockedBot + + +class TestSendDice: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SendDice, ok=True, result=None) + + response: Message = await SendDice(chat_id=42) + request: Request = bot.get_request() + assert request.method == "sendDice" + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SendDice, ok=True, result=None) + + response: Message = await bot.send_dice(chat_id=42) + request: Request = bot.get_request() + assert request.method == "sendDice" + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_my_commands.py b/tests/test_api/test_methods/test_set_my_commands.py new file mode 100644 index 00000000..ccf9f36b --- /dev/null +++ b/tests/test_api/test_methods/test_set_my_commands.py @@ -0,0 +1,28 @@ +import pytest + +from aiogram.api.methods import Request, SetMyCommands +from aiogram.api.types import BotCommand +from tests.mocked_bot import MockedBot + + +class TestSetMyCommands: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetMyCommands, ok=True, result=None) + + response: bool = await SetMyCommands( + commands=[BotCommand(command="command", description="Bot command")], + ) + request: Request = bot.get_request() + assert request.method == "setMyCommands" + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetMyCommands, ok=True, result=None) + + response: bool = await bot.set_my_commands(commands=[],) + request: Request = bot.get_request() + assert request.method == "setMyCommands" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_sticker_set_thumb.py b/tests/test_api/test_methods/test_set_sticker_set_thumb.py new file mode 100644 index 00000000..affb49ee --- /dev/null +++ b/tests/test_api/test_methods/test_set_sticker_set_thumb.py @@ -0,0 +1,26 @@ +import pytest + +from aiogram.api.methods import Request, SetStickerSetThumb +from tests.mocked_bot import MockedBot + + +class TestSetStickerSetThumb: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetStickerSetThumb, ok=True, result=None) + + response: bool = await SetStickerSetThumb(name="test", user_id=42) + request: Request = bot.get_request() + assert request.method == "setStickerSetThumb" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetStickerSetThumb, ok=True, result=None) + + response: bool = await bot.set_sticker_set_thumb(name="test", user_id=42) + request: Request = bot.get_request() + assert request.method == "setStickerSetThumb" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_utils/test_helper.py b/tests/test_utils/test_helper.py index b468dc6b..8125ef60 100644 --- a/tests/test_utils/test_helper.py +++ b/tests/test_utils/test_helper.py @@ -46,6 +46,7 @@ class TestHelper: class NotAHelperSubclass: A = Item() + class TestHelperMode: def test_helper_mode_all(self): assert set(HelperMode.all()) == { diff --git a/tests/test_utils/test_mixins.py b/tests/test_utils/test_mixins.py index 1f4805bd..f9fbbade 100644 --- a/tests/test_utils/test_mixins.py +++ b/tests/test_utils/test_mixins.py @@ -1,9 +1,6 @@ import pytest -from aiogram.utils.mixins import ( - ContextInstanceMixin, - DataMixin, -) +from aiogram.utils.mixins import ContextInstanceMixin, DataMixin class ContextObject(ContextInstanceMixin["ContextObject"]): From 9f00a02e4d4429c9d8805358bf4501d0466a5e55 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 11 Apr 2020 20:58:57 +0300 Subject: [PATCH 10/18] "noqa: F811" in aiogram/utils/mixins.py --- aiogram/utils/mixins.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index f2def5d6..0c4834f4 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -43,29 +43,29 @@ class ContextInstanceMixin(Generic[ContextInstance]): super().__init_subclass__() cls.__context_instance = contextvars.ContextVar(f"instance_{cls.__name__}") - @overload + @overload # noqa: F811 @classmethod - def get_current(cls) -> Optional[ContextInstance]: # pragma: no cover + def get_current(cls) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 ... - @overload + @overload # noqa: F811 @classmethod def get_current( # noqa: F811 cls, no_error: Literal[True] - ) -> Optional[ContextInstance]: # pragma: no cover + ) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 ... - @overload + @overload # noqa: F811 @classmethod def get_current( # noqa: F811 cls, no_error: Literal[False] - ) -> ContextInstance: # pragma: no cover + ) -> ContextInstance: # pragma: no cover # noqa: F811 ... - @classmethod + @classmethod # noqa: F811 def get_current( # noqa: F811 cls, no_error: bool = True - ) -> Optional[ContextInstance]: # pragma: no cover + ) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811 # on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any] cls.__context_instance = cast( contextvars.ContextVar[ContextInstance], cls.__context_instance From 7449c89b04db44d8da6cd97961017d0b5f9dd000 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 11 Apr 2020 22:01:53 +0300 Subject: [PATCH 11/18] Add py.typed --- aiogram/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 aiogram/py.typed diff --git a/aiogram/py.typed b/aiogram/py.typed new file mode 100644 index 00000000..e69de29b From 82f89b9c1ddcb31eab33058f5d727ffba0bc59bb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 11 Apr 2020 22:59:37 +0300 Subject: [PATCH 12/18] Optimize events propagation by routers --- aiogram/dispatcher/router.py | 76 +++++++++++++++++++++++----- tests/test_dispatcher/test_router.py | 6 ++- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 0b5cde3c..699c82c3 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -146,8 +146,6 @@ class Router: :param kwargs: :return: """ - kwargs.update(event_update=update, event_router=self) - chat: Optional[Chat] = None from_user: Optional[User] = None @@ -196,21 +194,75 @@ class Router: update_type = "poll" event = update.poll else: + warnings.warn( + "Detected unknown update type.\n" + "Seems like Telegram Bot API was updated and you have " + "installed not latest version of aiogram framework", + RuntimeWarning, + ) raise SkipHandler - observer = self.observers[update_type] - if from_user: - User.set_current(from_user) - if chat: - Chat.set_current(chat) - async for result in observer.trigger(event, update=update, **kwargs): - return result + return await self.listen_update( + update_type=update_type, + update=update, + event=event, + from_user=from_user, + chat=chat, + **kwargs, + ) - for router in self.sub_routers: - async for result in router.update_handler.trigger(update, **kwargs): + async def listen_update( + self, + update_type: str, + update: Update, + event: TelegramObject, + from_user: Optional[User] = None, + chat: Optional[Chat] = None, + **kwargs: Any, + ) -> Any: + """ + Listen update by current and child routers + + :param update_type: + :param update: + :param event: + :param from_user: + :param chat: + :param kwargs: + :return: + """ + user_token = None + if from_user: + user_token = User.set_current(from_user) + chat_token = None + if chat: + chat_token = Chat.set_current(chat) + + kwargs.update(event_update=update, event_router=self) + observer = self.observers[update_type] + try: + async for result in observer.trigger(event, update=update, **kwargs): return result - raise SkipHandler + for router in self.sub_routers: + try: + return await router.listen_update( + update_type=update_type, + update=update, + event=event, + from_user=from_user, + chat=chat, + **kwargs, + ) + except SkipHandler: + continue + + raise SkipHandler + finally: + if user_token: + User.reset_current(user_token) + if chat_token: + Chat.reset_current(chat_token) async def emit_startup(self, *args: Any, **kwargs: Any) -> None: """ diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index 3a9f6304..ca66c1ad 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -337,8 +337,10 @@ class TestRouter: async def test_nested_router_listen_update(self): router1 = Router() router2 = Router() + router3 = Router() router1.include_router(router2) - observer = router2.message_handler + router1.include_router(router3) + observer = router3.message_handler @observer() async def my_handler(event: Message, **kwargs: Any): @@ -359,7 +361,7 @@ class TestRouter: result = await router1._listen_update(update, test="PASS") assert isinstance(result, dict) assert result["event_update"] == update - assert result["event_router"] == router2 + assert result["event_router"] == router3 assert result["test"] == "PASS" @pytest.mark.asyncio From e4cd4c1763d141eda0df4b8d89f47a34c95fa66f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 11 Apr 2020 23:06:41 +0300 Subject: [PATCH 13/18] Add message reply&answer methods for dice --- aiogram/api/types/message.py | 47 ++++++++++++++++++++++++++++++++++++ docs/api/types/message.md | 1 + 2 files changed, 48 insertions(+) diff --git a/aiogram/api/types/message.py b/aiogram/api/types/message.py index f4b9e655..19d2b036 100644 --- a/aiogram/api/types/message.py +++ b/aiogram/api/types/message.py @@ -50,6 +50,7 @@ if TYPE_CHECKING: # pragma: no cover SendMessage, SendPhoto, SendPoll, + SendDice, SendSticker, SendVenue, SendVideo, @@ -1084,6 +1085,52 @@ class Message(TelegramObject): reply_markup=reply_markup, ) + def reply_dice( + self, + disable_notification: Optional[bool] = None, + reply_markup: Optional[ + Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] + ] = None, + ) -> SendDice: + """ + Reply with dice + + :param disable_notification: + :param reply_markup: + :return: + """ + from ..methods import SendDice + + return SendDice( + chat_id=self.chat.id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id, + reply_markup=reply_markup, + ) + + def answer_dice( + self, + disable_notification: Optional[bool] = None, + reply_markup: Optional[ + Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] + ] = None, + ) -> SendDice: + """ + Answer with dice + + :param disable_notification: + :param reply_markup: + :return: + """ + from ..methods import SendDice + + return SendDice( + chat_id=self.chat.id, + disable_notification=disable_notification, + reply_to_message_id=None, + reply_markup=reply_markup, + ) + def reply_sticker( self, sticker: Union[InputFile, str], diff --git a/docs/api/types/message.md b/docs/api/types/message.md index c265d656..6e7b088d 100644 --- a/docs/api/types/message.md +++ b/docs/api/types/message.md @@ -92,6 +92,7 @@ This methods has the same specification with the API but without `chat_id` and ` | `reply_video_note` | `answer_video_note` | [Bot.send_video_note](../methods/send_video_note.md) | Reply or Answer with video_note | | `reply_venue` | `answer_venue` | [Bot.send_venue](../methods/send_venue.md) | Reply or Answer with venue | | `reply_poll` | `answer_poll` | [Bot.send_poll](../methods/send_poll.md) | Reply or Answer with poll | +| `reply_dice` | `answer_dice` | [Bot.send_dice](../methods/send_dice.md) | Reply or Answer with dice | | `reply_invoice` | `answer_invoice` | [Bot.send_invoice](../methods/send_invoice.md) | Reply or Answer with invoice | From 5b6ec599b1d5f725b7cdeff59299371d165cafeb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 20:27:32 +0300 Subject: [PATCH 14/18] Add middlewares (API + Docs + Tests) --- aiogram/__init__.py | 2 + aiogram/api/types/message.py | 2 + aiogram/dispatcher/event/observer.py | 62 +++- aiogram/dispatcher/middlewares/__init__.py | 0 aiogram/dispatcher/middlewares/abstract.py | 61 ++++ aiogram/dispatcher/middlewares/base.py | 300 ++++++++++++++++++ aiogram/dispatcher/middlewares/manager.py | 71 +++++ aiogram/dispatcher/middlewares/types.py | 34 ++ aiogram/dispatcher/router.py | 35 +- aiogram/loggers.py | 1 + docs/assets/images/basics_middleware.png | Bin 0 -> 32505 bytes docs/assets/images/middleware_pipeline.png | Bin 0 -> 16408 bytes .../images/middleware_pipeline_nested.png | Bin 0 -> 34635 bytes docs/dispatcher/middlewares/basics.md | 111 +++++++ docs/dispatcher/middlewares/index.md | 65 ++++ docs/index.md | 8 +- docs/todo.md | 5 +- mkdocs.yml | 5 +- tests/test_api/test_types/test_message.py | 13 + .../test_middlewares/__init__.py | 0 .../test_middlewares/test_base.py | 241 ++++++++++++++ .../test_middlewares/test_manager.py | 82 +++++ tests/test_dispatcher/test_router.py | 9 + tests/test_utils/test_markdown.py | 55 ++-- 24 files changed, 1120 insertions(+), 42 deletions(-) create mode 100644 aiogram/dispatcher/middlewares/__init__.py create mode 100644 aiogram/dispatcher/middlewares/abstract.py create mode 100644 aiogram/dispatcher/middlewares/base.py create mode 100644 aiogram/dispatcher/middlewares/manager.py create mode 100644 aiogram/dispatcher/middlewares/types.py create mode 100644 docs/assets/images/basics_middleware.png create mode 100644 docs/assets/images/middleware_pipeline.png create mode 100644 docs/assets/images/middleware_pipeline_nested.png create mode 100644 docs/dispatcher/middlewares/basics.md create mode 100644 docs/dispatcher/middlewares/index.md create mode 100644 tests/test_dispatcher/test_middlewares/__init__.py create mode 100644 tests/test_dispatcher/test_middlewares/test_base.py create mode 100644 tests/test_dispatcher/test_middlewares/test_manager.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8975399c..816a1f57 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -3,6 +3,7 @@ from .api.client import session from .api.client.bot import Bot from .dispatcher import filters, handler from .dispatcher.dispatcher import Dispatcher +from .dispatcher.middlewares.base import BaseMiddleware from .dispatcher.router import Router try: @@ -22,6 +23,7 @@ __all__ = ( "session", "Dispatcher", "Router", + "BaseMiddleware", "filters", "handler", ) diff --git a/aiogram/api/types/message.py b/aiogram/api/types/message.py index 19d2b036..32894498 100644 --- a/aiogram/api/types/message.py +++ b/aiogram/api/types/message.py @@ -240,6 +240,8 @@ class Message(TelegramObject): return ContentType.PASSPORT_DATA if self.poll: return ContentType.POLL + if self.dice: + return ContentType.DICE return ContentType.UNKNOWN diff --git a/aiogram/dispatcher/event/observer.py b/aiogram/dispatcher/event/observer.py index 93f4aac6..756d57f2 100644 --- a/aiogram/dispatcher/event/observer.py +++ b/aiogram/dispatcher/event/observer.py @@ -1,21 +1,12 @@ from __future__ import annotations from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Dict, - Generator, - List, - Optional, - Type, -) +from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, Generator, List, Type from pydantic import ValidationError from ..filters.base import BaseFilter +from ..middlewares.types import MiddlewareStep, UpdateType from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType if TYPE_CHECKING: # pragma: no cover @@ -95,10 +86,8 @@ class TelegramEventObserver(EventObserver): """ registry: List[Type[BaseFilter]] = [] - router: Optional[Router] = self.router - while router: + for router in self.router.chain: observer = router.observers[self.event_name] - router = router.parent_router for filter_ in observer.filters: if filter_ in registry: @@ -133,6 +122,37 @@ class TelegramEventObserver(EventObserver): return filters + async def trigger_middleware( + self, step: MiddlewareStep, event: UpdateType, data: Dict[str, Any], result: Any = None, + ) -> None: + """ + Trigger middlewares chain + + :param step: + :param event: + :param data: + :param result: + :return: + """ + reverse = step == MiddlewareStep.POST_PROCESS + recursive = self.event_name == "update" or step == MiddlewareStep.PROCESS + + if self.event_name == "update": + routers = self.router.chain + else: + routers = self.router.chain_head + for router in routers: + await router.middleware.trigger( + step=step, + event_name=self.event_name, + event=event, + data=data, + result=result, + reverse=reverse, + ) + if not recursive: + break + def register( self, callback: HandlerType, *filters: FilterType, **bound_filters: Any ) -> HandlerType: @@ -153,12 +173,24 @@ class TelegramEventObserver(EventObserver): Propagate event to handlers and stops propagation on first match. Handler will be called when all its filters is pass. """ + event = args[0] + await self.trigger_middleware(step=MiddlewareStep.PRE_PROCESS, event=event, data=kwargs) for handler in self.handlers: result, data = await handler.check(*args, **kwargs) if result: kwargs.update(data) + await self.trigger_middleware( + step=MiddlewareStep.PROCESS, event=event, data=kwargs + ) try: - yield await handler.call(*args, **kwargs) + response = await handler.call(*args, **kwargs) + await self.trigger_middleware( + step=MiddlewareStep.POST_PROCESS, + event=event, + data=kwargs, + result=response, + ) + yield response except SkipHandler: continue break diff --git a/aiogram/dispatcher/middlewares/__init__.py b/aiogram/dispatcher/middlewares/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/middlewares/abstract.py b/aiogram/dispatcher/middlewares/abstract.py new file mode 100644 index 00000000..eac16534 --- /dev/null +++ b/aiogram/dispatcher/middlewares/abstract.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional + +from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType + +if TYPE_CHECKING: # pragma: no cover + from aiogram.dispatcher.middlewares.manager import MiddlewareManager + + +class AbstractMiddleware(ABC): + """ + Abstract class for middleware. + """ + + def __init__(self) -> None: + self._manager: Optional[MiddlewareManager] = None + + @property + def manager(self) -> MiddlewareManager: + """ + Instance of MiddlewareManager + """ + if self._manager is None: + raise RuntimeError("Middleware is not configured!") + return self._manager + + def setup(self, manager: MiddlewareManager, _stack_level: int = 1) -> AbstractMiddleware: + """ + Mark middleware as configured + + :param manager: + :param _stack_level: + :return: + """ + if self.configured: + return manager.setup(self, _stack_level=_stack_level + 1) + + self._manager = manager + return self + + @property + def configured(self) -> bool: + """ + Check middleware is configured + + :return: + """ + return bool(self._manager) + + @abstractmethod + async def trigger( + self, + step: MiddlewareStep, + event_name: str, + event: UpdateType, + data: Dict[str, Any], + result: Any = None, + ) -> Any: # pragma: no cover + pass diff --git a/aiogram/dispatcher/middlewares/base.py b/aiogram/dispatcher/middlewares/base.py new file mode 100644 index 00000000..2ec921b7 --- /dev/null +++ b/aiogram/dispatcher/middlewares/base.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict + +from aiogram.dispatcher.middlewares.abstract import AbstractMiddleware +from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType + +if TYPE_CHECKING: # pragma: no cover + from aiogram.api.types import ( + CallbackQuery, + ChosenInlineResult, + InlineQuery, + Message, + Poll, + PollAnswer, + PreCheckoutQuery, + ShippingQuery, + Update, + ) + + +class BaseMiddleware(AbstractMiddleware): + """ + Base class for middleware. + + All methods on the middle always must be coroutines and name starts with "on_" like "on_process_message". + """ + + async def trigger( + self, + step: MiddlewareStep, + event_name: str, + event: UpdateType, + data: Dict[str, Any], + result: Any = None, + ) -> Any: + """ + Trigger action. + + :param step: + :param event_name: + :param event: + :param data: + :param result: + :return: + """ + handler_name = f"on_{step.value}_{event_name}" + handler = getattr(self, handler_name, None) + if not handler: + return None + args = (event, result, data) if step == MiddlewareStep.POST_PROCESS else (event, data) + return await handler(*args) + + if TYPE_CHECKING: # pragma: no cover + # ============================================================================================= + # Event that triggers before process + # ============================================================================================= + async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any: + """ + Event that triggers before process update + """ + + async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any: + """ + Event that triggers before process message + """ + + async def on_pre_process_edited_message( + self, edited_message: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process edited_message + """ + + async def on_pre_process_channel_post( + self, channel_post: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process channel_post + """ + + async def on_pre_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process edited_channel_post + """ + + async def on_pre_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process inline_query + """ + + async def on_pre_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process chosen_inline_result + """ + + async def on_pre_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process callback_query + """ + + async def on_pre_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process shipping_query + """ + + async def on_pre_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process pre_checkout_query + """ + + async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: + """ + Event that triggers before process poll + """ + + async def on_pre_process_poll_answer( + self, poll_answer: PollAnswer, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers before process poll_answer + """ + + # ============================================================================================= + # Event that triggers on process after filters. + # ============================================================================================= + async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any: + """ + Event that triggers on process update + """ + + async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any: + """ + Event that triggers on process message + """ + + async def on_process_edited_message( + self, edited_message: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process edited_message + """ + + async def on_process_channel_post( + self, channel_post: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process channel_post + """ + + async def on_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process edited_channel_post + """ + + async def on_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process inline_query + """ + + async def on_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process chosen_inline_result + """ + + async def on_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process callback_query + """ + + async def on_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process shipping_query + """ + + async def on_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process pre_checkout_query + """ + + async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: + """ + Event that triggers on process poll + """ + + async def on_process_poll_answer( + self, poll_answer: PollAnswer, data: Dict[str, Any] + ) -> Any: + """ + Event that triggers on process poll_answer + """ + + # ============================================================================================= + # Event that triggers after process . + # ============================================================================================= + async def on_post_process_update( + self, update: Update, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing update + """ + + async def on_post_process_message( + self, message: Message, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing message + """ + + async def on_post_process_edited_message( + self, edited_message: Message, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing edited_message + """ + + async def on_post_process_channel_post( + self, channel_post: Message, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing channel_post + """ + + async def on_post_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing edited_channel_post + """ + + async def on_post_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing inline_query + """ + + async def on_post_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing chosen_inline_result + """ + + async def on_post_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing callback_query + """ + + async def on_post_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing shipping_query + """ + + async def on_post_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing pre_checkout_query + """ + + async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any: + """ + Event that triggers after processing poll + """ + + async def on_post_process_poll_answer( + self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing poll_answer + """ diff --git a/aiogram/dispatcher/middlewares/manager.py b/aiogram/dispatcher/middlewares/manager.py new file mode 100644 index 00000000..39a6230d --- /dev/null +++ b/aiogram/dispatcher/middlewares/manager.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List +from warnings import warn + +from .abstract import AbstractMiddleware +from .types import MiddlewareStep, UpdateType + +if TYPE_CHECKING: # pragma: no cover + from aiogram.dispatcher.router import Router + + +class MiddlewareManager: + """ + Middleware manager. + """ + + def __init__(self, router: Router) -> None: + self.router = router + self.middlewares: List[AbstractMiddleware] = [] + + def setup(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware: + """ + Setup middleware + + :param middleware: + :param _stack_level: + :return: + """ + if not isinstance(middleware, AbstractMiddleware): + raise TypeError( + f"`middleware` should be instance of BaseMiddleware, not {type(middleware)}" + ) + if middleware.configured: + if middleware.manager is self: + warn( + f"Middleware {middleware} is already configured for this Router " + "That's mean re-installing of this middleware has no effect.", + category=RuntimeWarning, + stacklevel=_stack_level + 1, + ) + return middleware + raise ValueError( + f"Middleware is already configured for another manager {middleware.manager} " + f"in router {middleware.manager.router}!" + ) + + self.middlewares.append(middleware) + middleware.setup(self) + return middleware + + async def trigger( + self, + step: MiddlewareStep, + event_name: str, + event: UpdateType, + data: Dict[str, Any], + result: Any = None, + reverse: bool = False, + ) -> Any: + """ + Call action to middlewares with args lilt. + """ + middlewares = reversed(self.middlewares) if reverse else self.middlewares + for middleware in middlewares: + await middleware.trigger( + step=step, event_name=event_name, event=event, data=data, result=result + ) + + def __contains__(self, item: AbstractMiddleware) -> bool: + return item in self.middlewares diff --git a/aiogram/dispatcher/middlewares/types.py b/aiogram/dispatcher/middlewares/types.py new file mode 100644 index 00000000..3d1da420 --- /dev/null +++ b/aiogram/dispatcher/middlewares/types.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from enum import Enum +from typing import Union + +from aiogram.api.types import ( + CallbackQuery, + ChosenInlineResult, + InlineQuery, + Message, + Poll, + PollAnswer, + PreCheckoutQuery, + ShippingQuery, + Update, +) + +UpdateType = Union[ + CallbackQuery, + ChosenInlineResult, + InlineQuery, + Message, + Poll, + PollAnswer, + PreCheckoutQuery, + ShippingQuery, + Update, +] + + +class MiddlewareStep(Enum): + PRE_PROCESS = "pre_process" + PROCESS = "process" + POST_PROCESS = "post_process" diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 699c82c3..888117be 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -1,13 +1,15 @@ from __future__ import annotations import warnings -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, Generator, List, Optional, Union from ..api.types import Chat, TelegramObject, Update, User from ..utils.imports import import_module from ..utils.warnings import CodeHasNoEffect from .event.observer import EventObserver, SkipHandler, TelegramEventObserver from .filters import BUILTIN_FILTERS +from .middlewares.abstract import AbstractMiddleware +from .middlewares.manager import MiddlewareManager class Router: @@ -46,6 +48,7 @@ class Router: ) self.poll_handler = TelegramEventObserver(router=self, event_name="poll") self.poll_answer_handler = TelegramEventObserver(router=self, event_name="poll_answer") + self.middleware = MiddlewareManager(router=self) self.startup = EventObserver() self.shutdown = EventObserver() @@ -74,6 +77,36 @@ class Router: for builtin_filter in BUILTIN_FILTERS.get(name, ()): observer.bind_filter(builtin_filter) + @property + def chain_head(self) -> Generator[Router, None, None]: + router: Optional[Router] = self + while router: + yield router + router = router.parent_router + + @property + def chain_tail(self) -> Generator[Router, None, None]: + yield self + for router in self.sub_routers: + yield from router.chain_tail + + @property + def chain(self) -> Generator[Router, None, None]: + yield from self.chain_head + tail = self.chain_tail + next(tail) # Skip self + yield from tail + + def use(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware: + """ + Use middleware + + :param middleware: + :param _stack_level: + :return: + """ + return self.middleware.setup(middleware, _stack_level=_stack_level + 1) + @property def parent_router(self) -> Optional[Router]: return self._parent_router diff --git a/aiogram/loggers.py b/aiogram/loggers.py index 0352c0df..5b5a8eba 100644 --- a/aiogram/loggers.py +++ b/aiogram/loggers.py @@ -1,3 +1,4 @@ import logging dispatcher = logging.getLogger("aiogram.dispatcher") +middlewares = logging.getLogger("aiogram.middlewares") diff --git a/docs/assets/images/basics_middleware.png b/docs/assets/images/basics_middleware.png new file mode 100644 index 0000000000000000000000000000000000000000..a797fd384a1d630c1d2a631dc88a8856dc374313 GIT binary patch literal 32505 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 ZqWl}EBMTb=eA51tw77y;mB_b%{{x-X#IOJW literal 0 HcmV?d00001 diff --git a/docs/assets/images/middleware_pipeline.png b/docs/assets/images/middleware_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb20d6f5caed16baa685b6dc07213f7e927a34b GIT binary patch literal 16408 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 T6xdxq2mU>fRFo(Z)ARZ-fB8-# literal 0 HcmV?d00001 diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md new file mode 100644 index 00000000..973ffe98 --- /dev/null +++ b/docs/dispatcher/middlewares/basics.md @@ -0,0 +1,111 @@ +# Basics + +All middlewares should be made with `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) as base class. + +For example: + +```python3 +class MyMiddleware(BaseMiddleware): ... +``` + +And then use next pattern in naming callback functions in middleware: `on_{step}_{event}` + +Where is: + +- `#!python3 step`: + - `#!python3 pre_process` + - `#!python3 process` + - `#!python3 post_process` +- `#!python3 event`: + - `#!python3 update` + - `#!python3 message` + - `#!python3 edited_message` + - `#!python3 channel_post` + - `#!python3 edited_channel_post` + - `#!python3 inline_query` + - `#!python3 chosen_inline_result` + - `#!python3 callback_query` + - `#!python3 shipping_query` + - `#!python3 pre_checkout_query` + - `#!python3 poll` + - `#!python3 poll_answer` + +## Connecting middleware with router + +Middlewares can be connected with router by next ways: + +1. `#!python3 router.use(MyMiddleware())` (**recommended**) +1. `#!python3 router.middleware.setup(MyMiddleware())` +1. `#!python3 MyMiddleware().setup(router.middleware)` (**not recommended**) + +!!! warning + One instance of middleware **can't** be registered twice in single or many middleware managers + +## The specification of step callbacks + +### Pre-process step + +| Argument | Type | Description | +| --- | --- | --- | +| event name | Any of event type (Update, Message and etc.) | Event | +| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | + +Returns `#!python3 Any` + +### Process step + +| Argument | Type | Description | +| --- | --- | --- | +| event name | Any of event type (Update, Message and etc.) | Event | +| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | + +Returns `#!python3 Any` + +### Post-Process step + +| Argument | Type | Description | +| --- | --- | --- | +| event name | Any of event type (Update, Message and etc.) | Event | +| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) | +| `#!python3 result` | `#!python3 Dict[str, Any]` | Response from handlers | + +Returns `#!python3 Any` + +## Full list of available callbacks + +- `#!python3 on_pre_process_update` - will be triggered on **pre process** `#!python3 update` event +- `#!python3 on_process_update` - will be triggered on **process** `#!python3 update` event +- `#!python3 on_post_process_update` - will be triggered on **post process** `#!python3 update` event +- `#!python3 on_pre_process_message` - will be triggered on **pre process** `#!python3 message` event +- `#!python3 on_process_message` - will be triggered on **process** `#!python3 message` event +- `#!python3 on_post_process_message` - will be triggered on **post process** `#!python3 message` event +- `#!python3 on_pre_process_edited_message` - will be triggered on **pre process** `#!python3 edited_message` event +- `#!python3 on_process_edited_message` - will be triggered on **process** `#!python3 edited_message` event +- `#!python3 on_post_process_edited_message` - will be triggered on **post process** `#!python3 edited_message` event +- `#!python3 on_pre_process_channel_post` - will be triggered on **pre process** `#!python3 channel_post` event +- `#!python3 on_process_channel_post` - will be triggered on **process** `#!python3 channel_post` event +- `#!python3 on_post_process_channel_post` - will be triggered on **post process** `#!python3 channel_post` event +- `#!python3 on_pre_process_edited_channel_post` - will be triggered on **pre process** `#!python3 edited_channel_post` event +- `#!python3 on_process_edited_channel_post` - will be triggered on **process** `#!python3 edited_channel_post` event +- `#!python3 on_post_process_edited_channel_post` - will be triggered on **post process** `#!python3 edited_channel_post` event +- `#!python3 on_pre_process_inline_query` - will be triggered on **pre process** `#!python3 inline_query` event +- `#!python3 on_process_inline_query` - will be triggered on **process** `#!python3 inline_query` event +- `#!python3 on_post_process_inline_query` - will be triggered on **post process** `#!python3 inline_query` event +- `#!python3 on_pre_process_chosen_inline_result` - will be triggered on **pre process** `#!python3 chosen_inline_result` event +- `#!python3 on_process_chosen_inline_result` - will be triggered on **process** `#!python3 chosen_inline_result` event +- `#!python3 on_post_process_chosen_inline_result` - will be triggered on **post process** `#!python3 chosen_inline_result` event +- `#!python3 on_pre_process_callback_query` - will be triggered on **pre process** `#!python3 callback_query` event +- `#!python3 on_process_callback_query` - will be triggered on **process** `#!python3 callback_query` event +- `#!python3 on_post_process_callback_query` - will be triggered on **post process** `#!python3 callback_query` event +- `#!python3 on_pre_process_shipping_query` - will be triggered on **pre process** `#!python3 shipping_query` event +- `#!python3 on_process_shipping_query` - will be triggered on **process** `#!python3 shipping_query` event +- `#!python3 on_post_process_shipping_query` - will be triggered on **post process** `#!python3 shipping_query` event +- `#!python3 on_pre_process_pre_checkout_query` - will be triggered on **pre process** `#!python3 pre_checkout_query` event +- `#!python3 on_process_pre_checkout_query` - will be triggered on **process** `#!python3 pre_checkout_query` event +- `#!python3 on_post_process_pre_checkout_query` - will be triggered on **post process** `#!python3 pre_checkout_query` event +- `#!python3 on_pre_process_poll` - will be triggered on **pre process** `#!python3 poll` event +- `#!python3 on_process_poll` - will be triggered on **process** `#!python3 poll` event +- `#!python3 on_post_process_poll` - will be triggered on **post process** `#!python3 poll` event +- `#!python3 on_pre_process_poll_answer` - will be triggered on **pre process** `#!python3 poll_answer` event +- `#!python3 on_process_poll_answer` - will be triggered on **process** `#!python3 poll_answer` event +- `#!python3 on_post_process_poll_answer` - will be triggered on **post process** `#!python3 poll_answer` event diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md new file mode 100644 index 00000000..114646d6 --- /dev/null +++ b/docs/dispatcher/middlewares/index.md @@ -0,0 +1,65 @@ +# Overview + +**aiogram**'s provides powerful mechanism for customizing event handlers via middlewares. + +Middlewares in bot framework seems like Middlewares mechanism in powerful web-frameworks +(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), +[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), +[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) +with small difference - here is implemented many layers of processing +(named as [pipeline](#event-pipeline)). + +!!! info + Middleware is function that triggered on every event received from + Telegram Bot API in many points on processing pipeline. + +## Base theory + +As many books and other literature in internet says: +> Middleware is reusable software that leverages patterns and frameworks to bridge +>the gap between the functional requirements of applications and the underlying operating systems, +> network protocol stacks, and databases. + +Middleware can modify, extend or reject processing event before-, +on- or after- processing of that event. + +[![middlewares](../../assets/images/basics_middleware.png)](../../assets/images/basics_middleware.png) + +_(Click on image to zoom it)_ + +## Event pipeline + +As described below middleware an interact with event in many stages of pipeline. + +Simple workflow: + +1. Dispatcher receive an [Update](../../api/types/update.md) +1. Call **pre-process** update middleware in all routers tree +1. Filter Update over handlers +1. Call **process** update middleware in all routers tree +1. Router detects event type (Message, Callback query, etc.) +1. Router triggers **pre-process** middleware of specific type +1. Pass event over [filters](../filters/index.md) to detect specific handler +1. Call **process** middleware for specific type (only when handler for this event exists) +1. *Do magick*. Call handler (Read more [Event observers](../router.md#event-observers)) +1. Call **post-process** middleware +1. Call **post-process** update middleware in all routers tree +1. Emit response into webhook (when it needed) + +### Pipeline in pictures: + +#### Simple pipeline + +[![middlewares](../../assets/images/middleware_pipeline.png)](../../assets/images/middleware_pipeline.png) + +_(Click on image to zoom it)_ + +#### Nested routers pipeline + +[![middlewares](../../assets/images/middleware_pipeline_nested.png)](../../assets/images/middleware_pipeline_nested.png) + +_(Click on image to zoom it)_ + +## Read more + +- [Middleware Basics](basics.md) diff --git a/docs/index.md b/docs/index.md index 6961a181..57f6fa9b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,19 +15,19 @@ Documentation for version 3.0 [WIP] [^1] ## Features -- Asynchronous +- Asynchronous ([asyncio docs](https://docs.python.org/3/library/asyncio.html), [PEP-492](https://www.python.org/dev/peps/pep-0492/)) - [Supports Telegram Bot API v{!_api_version.md!}](api/index.md) - [Updates router](dispatcher/index.md) (Blueprints) - Finite State Machine -- Middlewares +- [Middlewares](dispatcher/middlewares/index.md) - [Replies into Webhook](https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates) !!! note Before start using **aiogram** is highly recommend to know how to work with [asyncio](https://docs.python.org/3/library/asyncio.html). - + Also if you has questions you can go to our community chats in Telegram: - + - [English language](https://t.me/aiogram) - [Russian language](https://t.me/aiogram_ru) diff --git a/docs/todo.md b/docs/todo.md index 02c99d9a..c06407f3 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -23,8 +23,8 @@ - [x] ContentTypes - [x] Text - [ ] ... - - [ ] Middlewares - - [ ] Engine + - [x] Middlewares + - [x] Engine - [ ] Builtin middlewares - [ ] ... - [ ] Webhook @@ -41,6 +41,7 @@ - [x] Dispatcher - [x] Router - [x] Observers + - [x] Middleware - [ ] Filters - [ ] Utils - [x] Helper diff --git a/mkdocs.yml b/mkdocs.yml index d64deb09..47631d6a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,7 +17,7 @@ theme: logo: 'assets/images/logo.png' extra: - version: 3.0.0a2 + version: 3.0.0a3 plugins: - search @@ -249,6 +249,9 @@ nav: - dispatcher/class_based_handlers/poll.md - dispatcher/class_based_handlers/pre_checkout_query.md - dispatcher/class_based_handlers/shipping_query.md + - Middlewares: + - dispatcher/middlewares/index.md + - dispatcher/middlewares/basics.md - todo.md - Build reports: - reports.md diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index 4b645e11..1254bd31 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -7,6 +7,7 @@ from aiogram.api.methods import ( SendAnimation, SendAudio, SendContact, + SendDice, SendDocument, SendGame, SendInvoice, @@ -26,6 +27,7 @@ from aiogram.api.types import ( Audio, Chat, Contact, + Dice, Document, EncryptedCredentials, Game, @@ -391,6 +393,16 @@ class TestMessage: ), ContentType.POLL, ], + [ + Message( + message_id=42, + date=datetime.datetime.now(), + chat=Chat(id=42, type="private"), + dice=Dice(value=6), + from_user=User(id=42, is_bot=False, first_name="Test"), + ), + ContentType.DICE, + ], [ Message( message_id=42, @@ -431,6 +443,7 @@ class TestMessage: ["", dict(text="test"), SendMessage], ["photo", dict(photo="photo"), SendPhoto], ["poll", dict(question="Q?", options=[]), SendPoll], + ["dice", dict(), SendDice], ["sticker", dict(sticker="sticker"), SendSticker], ["sticker", dict(sticker="sticker"), SendSticker], [ diff --git a/tests/test_dispatcher/test_middlewares/__init__.py b/tests/test_dispatcher/test_middlewares/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_dispatcher/test_middlewares/test_base.py b/tests/test_dispatcher/test_middlewares/test_base.py new file mode 100644 index 00000000..203028ec --- /dev/null +++ b/tests/test_dispatcher/test_middlewares/test_base.py @@ -0,0 +1,241 @@ +import datetime +from typing import Any, Dict, Type + +import pytest + +from aiogram.api.types import ( + CallbackQuery, + Chat, + ChosenInlineResult, + InlineQuery, + Message, + Poll, + PollAnswer, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore + + +class MyMiddleware(BaseMiddleware): + async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any: + return "update" + + async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any: + return "message" + + async def on_pre_process_edited_message( + self, edited_message: Message, data: Dict[str, Any] + ) -> Any: + return "edited_message" + + async def on_pre_process_channel_post( + self, channel_post: Message, data: Dict[str, Any] + ) -> Any: + return "channel_post" + + async def on_pre_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any] + ) -> Any: + return "edited_channel_post" + + async def on_pre_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any] + ) -> Any: + return "inline_query" + + async def on_pre_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] + ) -> Any: + return "chosen_inline_result" + + async def on_pre_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any] + ) -> Any: + return "callback_query" + + async def on_pre_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any] + ) -> Any: + return "shipping_query" + + async def on_pre_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] + ) -> Any: + return "pre_checkout_query" + + async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: + return "poll" + + async def on_pre_process_poll_answer( + self, poll_answer: PollAnswer, data: Dict[str, Any] + ) -> Any: + return "poll_answer" + + async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any: + return "update" + + async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any: + return "message" + + async def on_process_edited_message( + self, edited_message: Message, data: Dict[str, Any] + ) -> Any: + return "edited_message" + + async def on_process_channel_post(self, channel_post: Message, data: Dict[str, Any]) -> Any: + return "channel_post" + + async def on_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any] + ) -> Any: + return "edited_channel_post" + + async def on_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any] + ) -> Any: + return "inline_query" + + async def on_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any] + ) -> Any: + return "chosen_inline_result" + + async def on_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any] + ) -> Any: + return "callback_query" + + async def on_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any] + ) -> Any: + return "shipping_query" + + async def on_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any] + ) -> Any: + return "pre_checkout_query" + + async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any: + return "poll" + + async def on_process_poll_answer(self, poll_answer: PollAnswer, data: Dict[str, Any]) -> Any: + return "poll_answer" + + async def on_post_process_update( + self, update: Update, data: Dict[str, Any], result: Any + ) -> Any: + return "update" + + async def on_post_process_message( + self, message: Message, data: Dict[str, Any], result: Any + ) -> Any: + return "message" + + async def on_post_process_edited_message( + self, edited_message: Message, data: Dict[str, Any], result: Any + ) -> Any: + return "edited_message" + + async def on_post_process_channel_post( + self, channel_post: Message, data: Dict[str, Any], result: Any + ) -> Any: + return "channel_post" + + async def on_post_process_edited_channel_post( + self, edited_channel_post: Message, data: Dict[str, Any], result: Any + ) -> Any: + return "edited_channel_post" + + async def on_post_process_inline_query( + self, inline_query: InlineQuery, data: Dict[str, Any], result: Any + ) -> Any: + return "inline_query" + + async def on_post_process_chosen_inline_result( + self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any + ) -> Any: + return "chosen_inline_result" + + async def on_post_process_callback_query( + self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any + ) -> Any: + return "callback_query" + + async def on_post_process_shipping_query( + self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any + ) -> Any: + return "shipping_query" + + async def on_post_process_pre_checkout_query( + self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any + ) -> Any: + return "pre_checkout_query" + + async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any: + return "poll" + + async def on_post_process_poll_answer( + self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any + ) -> Any: + return "poll_answer" + + +UPDATE = Update(update_id=42) +MESSAGE = Message(message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private")) +POLL_ANSWER = PollAnswer( + poll_id="poll", user=User(id=42, is_bot=False, first_name="Test"), option_ids=[0] +) + + +class TestBaseMiddleware: + @pytest.mark.asyncio + @pytest.mark.parametrize( + "middleware_cls,should_be_awaited", [[MyMiddleware, True], [BaseMiddleware, False]] + ) + @pytest.mark.parametrize( + "step", [MiddlewareStep.PRE_PROCESS, MiddlewareStep.PROCESS, MiddlewareStep.POST_PROCESS] + ) + @pytest.mark.parametrize( + "event_name,event", + [["update", UPDATE], ["message", MESSAGE], ["poll_answer", POLL_ANSWER],], + ) + async def test_trigger( + self, + step: MiddlewareStep, + event_name: str, + event: UpdateType, + middleware_cls: Type[BaseMiddleware], + should_be_awaited: bool, + ): + middleware = middleware_cls() + + with patch( + f"tests.test_dispatcher.test_middlewares.test_base." + f"MyMiddleware.on_{step.value}_{event_name}", + new_callable=CoroutineMock, + ) as mocked_call: + response = await middleware.trigger( + step=step, event_name=event_name, event=event, data={} + ) + if should_be_awaited: + mocked_call.assert_awaited() + assert response is not None + else: + mocked_call.assert_not_awaited() + assert response is None + + def test_not_configured(self): + middleware = BaseMiddleware() + assert not middleware.configured + + with pytest.raises(RuntimeError): + manager = middleware.manager diff --git a/tests/test_dispatcher/test_middlewares/test_manager.py b/tests/test_dispatcher/test_middlewares/test_manager.py new file mode 100644 index 00000000..0e23f1b2 --- /dev/null +++ b/tests/test_dispatcher/test_middlewares/test_manager.py @@ -0,0 +1,82 @@ +import pytest + +from aiogram import Router +from aiogram.api.types import Update +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.dispatcher.middlewares.manager import MiddlewareManager +from aiogram.dispatcher.middlewares.types import MiddlewareStep + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore + + +@pytest.fixture("function") +def router(): + return Router() + + +@pytest.fixture("function") +def manager(router: Router): + return MiddlewareManager(router) + + +class TestManager: + def test_setup(self, manager: MiddlewareManager): + middleware = BaseMiddleware() + returned = manager.setup(middleware) + assert returned is middleware + assert middleware.configured + assert middleware.manager is manager + assert middleware in manager + + @pytest.mark.parametrize("obj", [object, object(), None, BaseMiddleware]) + def test_setup_invalid_type(self, manager: MiddlewareManager, obj): + with pytest.raises(TypeError): + assert manager.setup(obj) + + def test_configure_twice_different_managers(self, manager: MiddlewareManager, router: Router): + middleware = BaseMiddleware() + manager.setup(middleware) + + assert middleware.configured + + new_manager = MiddlewareManager(router) + with pytest.raises(ValueError): + new_manager.setup(middleware) + with pytest.raises(ValueError): + middleware.setup(new_manager) + + def test_configure_twice(self, manager: MiddlewareManager): + middleware = BaseMiddleware() + manager.setup(middleware) + + assert middleware.configured + + with pytest.warns(RuntimeWarning, match="is already configured for this Router"): + manager.setup(middleware) + + with pytest.warns(RuntimeWarning, match="is already configured for this Router"): + middleware.setup(manager) + + @pytest.mark.asyncio + @pytest.mark.parametrize("count", range(5)) + async def test_trigger(self, manager: MiddlewareManager, count: int): + for _ in range(count): + manager.setup(BaseMiddleware()) + + with patch( + "aiogram.dispatcher.middlewares.base.BaseMiddleware.trigger", + new_callable=CoroutineMock, + ) as mocked_call: + await manager.trigger( + step=MiddlewareStep.PROCESS, + event_name="update", + event=Update(update_id=42), + data={}, + result=None, + reverse=True, + ) + + assert mocked_call.await_count == count diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index ca66c1ad..eacb8d0c 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -18,6 +18,7 @@ from aiogram.api.types import ( User, ) from aiogram.dispatcher.event.observer import SkipHandler +from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.dispatcher.router import Router from aiogram.utils.warnings import CodeHasNoEffect @@ -407,3 +408,11 @@ class TestRouter: await router1.emit_shutdown() assert results == [2, 1, 2] + + def test_use(self): + router = Router() + + middleware = router.use(BaseMiddleware()) + assert isinstance(middleware, BaseMiddleware) + assert middleware.configured + assert middleware.manager == router.middleware diff --git a/tests/test_utils/test_markdown.py b/tests/test_utils/test_markdown.py index b9da8f46..792c1bb4 100644 --- a/tests/test_utils/test_markdown.py +++ b/tests/test_utils/test_markdown.py @@ -2,37 +2,54 @@ from typing import Any, Callable, Optional, Tuple import pytest -from aiogram.utils import markdown +from aiogram.utils.markdown import ( + bold, + code, + hbold, + hcode, + hide_link, + hitalic, + hlink, + hpre, + hstrikethrough, + hunderline, + italic, + link, + pre, + strikethrough, + text, + underline, +) class TestMarkdown: @pytest.mark.parametrize( "func,args,sep,result", [ - [markdown.text, ("test", "test"), " ", "test test"], - [markdown.text, ("test", "test"), "\n", "test\ntest"], - [markdown.text, ("test", "test"), None, "test test"], - [markdown.bold, ("test", "test"), " ", "*test test*"], - [markdown.hbold, ("test", "test"), " ", "test test"], - [markdown.italic, ("test", "test"), " ", "_test test_\r"], - [markdown.hitalic, ("test", "test"), " ", "test test"], - [markdown.code, ("test", "test"), " ", "`test test`"], - [markdown.hcode, ("test", "test"), " ", "test test"], - [markdown.pre, ("test", "test"), " ", "```test test```"], - [markdown.hpre, ("test", "test"), " ", "
test test
"], - [markdown.underline, ("test", "test"), " ", "__test test__"], - [markdown.hunderline, ("test", "test"), " ", "test test"], - [markdown.strikethrough, ("test", "test"), " ", "~test test~"], - [markdown.hstrikethrough, ("test", "test"), " ", "test test"], - [markdown.link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"], + [text, ("test", "test"), " ", "test test"], + [text, ("test", "test"), "\n", "test\ntest"], + [text, ("test", "test"), None, "test test"], + [bold, ("test", "test"), " ", "*test test*"], + [hbold, ("test", "test"), " ", "test test"], + [italic, ("test", "test"), " ", "_test test_\r"], + [hitalic, ("test", "test"), " ", "test test"], + [code, ("test", "test"), " ", "`test test`"], + [hcode, ("test", "test"), " ", "test test"], + [pre, ("test", "test"), " ", "```test test```"], + [hpre, ("test", "test"), " ", "
test test
"], + [underline, ("test", "test"), " ", "__test test__"], + [hunderline, ("test", "test"), " ", "test test"], + [strikethrough, ("test", "test"), " ", "~test test~"], + [hstrikethrough, ("test", "test"), " ", "test test"], + [link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"], [ - markdown.hlink, + hlink, ("test", "https://aiogram.dev"), None, '
test', ], [ - markdown.hide_link, + hide_link, ("https://aiogram.dev",), None, '', From 569a9c807c5efd4ac9d1443ef5a2932cbac62f3d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 20:41:10 +0300 Subject: [PATCH 15/18] adwanced --- docs/dispatcher/middlewares/index.md | 4 ++-- docs/index.md | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md index 114646d6..12baf473 100644 --- a/docs/dispatcher/middlewares/index.md +++ b/docs/dispatcher/middlewares/index.md @@ -1,8 +1,8 @@ # Overview -**aiogram**'s provides powerful mechanism for customizing event handlers via middlewares. +**aiogram** provides powerful mechanism for customizing event handlers via middlewares. -Middlewares in bot framework seems like Middlewares mechanism in powerful web-frameworks +Middlewares in bot framework seems like Middlewares mechanism in web-frameworks (like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares), [fastapi](https://fastapi.tiangolo.com/tutorial/middleware/), [Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.) diff --git a/docs/index.md b/docs/index.md index 57f6fa9b..06c9a3b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,13 +2,14 @@ Documentation for version 3.0 [WIP] [^1] -[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) -[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-{!_api_version.md!}-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) -[![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) +[![MIT License](https://img.shields.io/pypi/l/aiogram.svg)](https://opensource.org/licenses/MIT) +[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg)](https://pypi.python.org/pypi/aiogram) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-{!_api_version.md!}-blue.svg?logo=telegram)](https://core.telegram.org/bots/api) +[![Tests](https://github.com/aiogram/aiogram/workflows/Tests/badge.svg?branch=dev-3.x)](https://github.com/aiogram/aiogram/actions) +[![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg)](https://pypi.python.org/pypi/aiogram) +[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg)](https://pypi.python.org/pypi/aiogram) +[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg)](https://pypi.python.org/pypi/aiogram) +[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg)](https://t.me/aiogram_live) **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. From 9e673998f0266a3a911b6f1e45056e2ed30ab8fb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 22:13:25 +0300 Subject: [PATCH 16/18] Errors handler --- aiogram/dispatcher/event/observer.py | 24 ++++++- aiogram/dispatcher/filters/__init__.py | 1 + aiogram/dispatcher/middlewares/base.py | 17 +++++ aiogram/dispatcher/middlewares/types.py | 1 + aiogram/dispatcher/router.py | 12 ++++ docs/dispatcher/middlewares/basics.md | 4 ++ docs/dispatcher/middlewares/index.md | 12 ++++ .../test_middlewares/test_base.py | 18 +++++- tests/test_dispatcher/test_router.py | 64 ++++++++++++++++++- 9 files changed, 150 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/event/observer.py b/aiogram/dispatcher/event/observer.py index 756d57f2..cea2eb6a 100644 --- a/aiogram/dispatcher/event/observer.py +++ b/aiogram/dispatcher/event/observer.py @@ -1,7 +1,18 @@ from __future__ import annotations from itertools import chain -from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, Generator, List, Type +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Callable, + Dict, + Generator, + List, + NoReturn, + Optional, + Type, +) from pydantic import ValidationError @@ -17,6 +28,17 @@ class SkipHandler(Exception): pass +class CancelHandler(Exception): + pass + + +def skip(message: Optional[str] = None) -> NoReturn: + """ + Raise an SkipHandler + """ + raise SkipHandler(message or "Event skipped") + + class EventObserver: """ Base events observer diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 25db7020..b9612ad4 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -27,4 +27,5 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { "pre_checkout_query": (), "poll": (), "poll_answer": (), + "errors": (), } diff --git a/aiogram/dispatcher/middlewares/base.py b/aiogram/dispatcher/middlewares/base.py index 2ec921b7..8766f9dc 100644 --- a/aiogram/dispatcher/middlewares/base.py +++ b/aiogram/dispatcher/middlewares/base.py @@ -133,6 +133,11 @@ class BaseMiddleware(AbstractMiddleware): Event that triggers before process poll_answer """ + async def on_pre_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: + """ + Event that triggers before process error + """ + # ============================================================================================= # Event that triggers on process after filters. # ============================================================================================= @@ -214,6 +219,11 @@ class BaseMiddleware(AbstractMiddleware): Event that triggers on process poll_answer """ + async def on_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: + """ + Event that triggers on process error + """ + # ============================================================================================= # Event that triggers after process . # ============================================================================================= @@ -298,3 +308,10 @@ class BaseMiddleware(AbstractMiddleware): """ Event that triggers after processing poll_answer """ + + async def on_post_process_error( + self, exception: Exception, data: Dict[str, Any], result: Any + ) -> Any: + """ + Event that triggers after processing error + """ diff --git a/aiogram/dispatcher/middlewares/types.py b/aiogram/dispatcher/middlewares/types.py index 3d1da420..bc173025 100644 --- a/aiogram/dispatcher/middlewares/types.py +++ b/aiogram/dispatcher/middlewares/types.py @@ -25,6 +25,7 @@ UpdateType = Union[ PreCheckoutQuery, ShippingQuery, Update, + BaseException, ] diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 888117be..dab48c25 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -48,6 +48,8 @@ class Router: ) self.poll_handler = TelegramEventObserver(router=self, event_name="poll") self.poll_answer_handler = TelegramEventObserver(router=self, event_name="poll_answer") + self.errors_handler = TelegramEventObserver(router=self, event_name="error") + self.middleware = MiddlewareManager(router=self) self.startup = EventObserver() @@ -66,6 +68,7 @@ class Router: "pre_checkout_query": self.pre_checkout_query_handler, "poll": self.poll_handler, "poll_answer": self.poll_answer_handler, + "error": self.errors_handler, } # Root handler @@ -291,6 +294,15 @@ class Router: continue raise SkipHandler + + except SkipHandler: + raise + + except Exception as e: + async for result in self.errors_handler.trigger(e, **kwargs): + return result + raise + finally: if user_token: User.reset_current(user_token) diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md index 973ffe98..83b58f07 100644 --- a/docs/dispatcher/middlewares/basics.md +++ b/docs/dispatcher/middlewares/basics.md @@ -29,6 +29,7 @@ Where is: - `#!python3 pre_checkout_query` - `#!python3 poll` - `#!python3 poll_answer` + - `#!python3 error` ## Connecting middleware with router @@ -109,3 +110,6 @@ Returns `#!python3 Any` - `#!python3 on_pre_process_poll_answer` - will be triggered on **pre process** `#!python3 poll_answer` event - `#!python3 on_process_poll_answer` - will be triggered on **process** `#!python3 poll_answer` event - `#!python3 on_post_process_poll_answer` - will be triggered on **post process** `#!python3 poll_answer` event +- `#!python3 on_pre_process_error` - will be triggered on **pre process** `#!python3 error` event +- `#!python3 on_process_error` - will be triggered on **process** `#!python3 error` event +- `#!python3 on_post_process_error` - will be triggered on **post process** `#!python3 error` event diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md index 12baf473..6815a565 100644 --- a/docs/dispatcher/middlewares/index.md +++ b/docs/dispatcher/middlewares/index.md @@ -46,6 +46,18 @@ Simple workflow: 1. Call **post-process** update middleware in all routers tree 1. Emit response into webhook (when it needed) +!!! warning + When filters does not match any handler with this event the `#!python3 process` + step will not be called. + +!!! warning + When exception will be caused in handlers pipeline will be stopped immediately + and then start processing error via errors handler and it own middleware callbacks. + +!!! warning + Middlewares for updates will be called for all routers in tree but callbacks for events + will be called only for specific branch of routers. + ### Pipeline in pictures: #### Simple pipeline diff --git a/tests/test_dispatcher/test_middlewares/test_base.py b/tests/test_dispatcher/test_middlewares/test_base.py index 203028ec..7899324d 100644 --- a/tests/test_dispatcher/test_middlewares/test_base.py +++ b/tests/test_dispatcher/test_middlewares/test_base.py @@ -80,6 +80,9 @@ class MyMiddleware(BaseMiddleware): ) -> Any: return "poll_answer" + async def on_pre_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: + return "error" + async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any: return "update" @@ -130,6 +133,9 @@ class MyMiddleware(BaseMiddleware): async def on_process_poll_answer(self, poll_answer: PollAnswer, data: Dict[str, Any]) -> Any: return "poll_answer" + async def on_process_error(self, exception: Exception, data: Dict[str, Any]) -> Any: + return "error" + async def on_post_process_update( self, update: Update, data: Dict[str, Any], result: Any ) -> Any: @@ -188,6 +194,11 @@ class MyMiddleware(BaseMiddleware): ) -> Any: return "poll_answer" + async def on_post_process_error( + self, exception: Exception, data: Dict[str, Any], result: Any + ) -> Any: + return "error" + UPDATE = Update(update_id=42) MESSAGE = Message(message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private")) @@ -206,7 +217,12 @@ class TestBaseMiddleware: ) @pytest.mark.parametrize( "event_name,event", - [["update", UPDATE], ["message", MESSAGE], ["poll_answer", POLL_ANSWER],], + [ + ["update", UPDATE], + ["message", MESSAGE], + ["poll_answer", POLL_ANSWER], + ["error", Exception("KABOOM")], + ], ) async def test_trigger( self, diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index eacb8d0c..2d26a445 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -17,7 +17,7 @@ from aiogram.api.types import ( Update, User, ) -from aiogram.dispatcher.event.observer import SkipHandler +from aiogram.dispatcher.event.observer import SkipHandler, skip from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.dispatcher.router import Router from aiogram.utils.warnings import CodeHasNoEffect @@ -416,3 +416,65 @@ class TestRouter: assert isinstance(middleware, BaseMiddleware) assert middleware.configured assert middleware.manager == router.middleware + + def test_skip(self): + with pytest.raises(SkipHandler): + skip() + with pytest.raises(SkipHandler, match="KABOOM"): + skip("KABOOM") + + @pytest.mark.asyncio + async def test_exception_handler_catch_exceptions(self): + root_router = Router() + router = Router() + root_router.include_router(router) + + @router.message_handler() + async def message_handler(message: Message): + raise Exception("KABOOM") + + update = Update( + update_id=42, + message=Message( + message_id=42, + date=datetime.datetime.now(), + text="test", + chat=Chat(id=42, type="private"), + from_user=User(id=42, is_bot=False, first_name="Test"), + ), + ) + with pytest.raises(Exception, match="KABOOM"): + await root_router.listen_update( + update_type="message", + update=update, + event=update.message, + from_user=update.message.from_user, + chat=update.message.chat, + ) + + @root_router.errors_handler() + async def root_error_handler(exception: Exception): + return exception + + response = await root_router.listen_update( + update_type="message", + update=update, + event=update.message, + from_user=update.message.from_user, + chat=update.message.chat, + ) + assert isinstance(response, Exception) + assert str(response) == "KABOOM" + + @router.errors_handler() + async def error_handler(exception: Exception): + return "KABOOM" + + response = await root_router.listen_update( + update_type="message", + update=update, + event=update.message, + from_user=update.message.from_user, + chat=update.message.chat, + ) + assert response == "KABOOM" From 0fbd2819f90aba5b9d69b1e63f307d8f165098c9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 23:20:44 +0300 Subject: [PATCH 17/18] Add filters and class based handler for errors --- aiogram/dispatcher/filters/__init__.py | 5 +- aiogram/dispatcher/filters/exception.py | 36 +++++++++++++ aiogram/dispatcher/handler/__init__.py | 2 + aiogram/dispatcher/handler/error.py | 9 ++++ docs/dispatcher/class_based_handlers/error.md | 29 +++++++++++ docs/dispatcher/filters/exception.md | 27 ++++++++++ docs/dispatcher/router.md | 7 +++ mkdocs.yml | 2 + .../test_filters/test_exception.py | 51 +++++++++++++++++++ 9 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 aiogram/dispatcher/filters/exception.py create mode 100644 aiogram/dispatcher/handler/error.py create mode 100644 docs/dispatcher/class_based_handlers/error.md create mode 100644 docs/dispatcher/filters/exception.md create mode 100644 tests/test_dispatcher/test_filters/test_exception.py diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index b9612ad4..e59259be 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -3,6 +3,7 @@ from typing import Dict, Tuple, Type from .base import BaseFilter from .command import Command, CommandObject from .content_types import ContentTypesFilter +from .exception import ExceptionMessageFilter, ExceptionTypeFilter from .text import Text __all__ = ( @@ -12,6 +13,8 @@ __all__ = ( "Command", "CommandObject", "ContentTypesFilter", + "ExceptionMessageFilter", + "ExceptionTypeFilter", ) BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { @@ -27,5 +30,5 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { "pre_checkout_query": (), "poll": (), "poll_answer": (), - "errors": (), + "error": (ExceptionMessageFilter, ExceptionTypeFilter), } diff --git a/aiogram/dispatcher/filters/exception.py b/aiogram/dispatcher/filters/exception.py new file mode 100644 index 00000000..8291291a --- /dev/null +++ b/aiogram/dispatcher/filters/exception.py @@ -0,0 +1,36 @@ +import re +from typing import Any, Dict, Pattern, Tuple, Type, Union, cast + +from pydantic import validator + +from aiogram.dispatcher.filters import BaseFilter + + +class ExceptionTypeFilter(BaseFilter): + exception: Union[Type[Exception], Tuple[Type[Exception]]] + + class Config: + arbitrary_types_allowed = True + + async def __call__(self, exception: Exception) -> Union[bool, Dict[str, Any]]: + return isinstance(exception, self.exception) + + +class ExceptionMessageFilter(BaseFilter): + match: Union[str, Pattern[str]] + + class Config: + arbitrary_types_allowed = True + + @validator("match") + def _validate_match(cls, value: Union[str, Pattern[str]]) -> Union[str, Pattern[str]]: + if isinstance(value, str): + return re.compile(value) + return value + + async def __call__(self, exception: Exception) -> Union[bool, Dict[str, Any]]: + pattern = cast(Pattern[str], self.match) + result = pattern.match(str(exception)) + if not result: + return False + return {"match_exception": result} diff --git a/aiogram/dispatcher/handler/__init__.py b/aiogram/dispatcher/handler/__init__.py index 49ae18d2..b2c5c9ef 100644 --- a/aiogram/dispatcher/handler/__init__.py +++ b/aiogram/dispatcher/handler/__init__.py @@ -1,6 +1,7 @@ from .base import BaseHandler, BaseHandlerMixin from .callback_query import CallbackQueryHandler from .chosen_inline_result import ChosenInlineResultHandler +from .error import ErrorHandler from .inline_query import InlineQueryHandler from .message import MessageHandler, MessageHandlerCommandMixin from .poll import PollHandler @@ -12,6 +13,7 @@ __all__ = ( "BaseHandlerMixin", "CallbackQueryHandler", "ChosenInlineResultHandler", + "ErrorHandler", "InlineQueryHandler", "MessageHandler", "MessageHandlerCommandMixin", diff --git a/aiogram/dispatcher/handler/error.py b/aiogram/dispatcher/handler/error.py new file mode 100644 index 00000000..dc7953bd --- /dev/null +++ b/aiogram/dispatcher/handler/error.py @@ -0,0 +1,9 @@ +from abc import ABC + +from aiogram.dispatcher.handler.base import BaseHandler + + +class ErrorHandler(BaseHandler[Exception], ABC): + """ + Base class for errors handlers + """ diff --git a/docs/dispatcher/class_based_handlers/error.md b/docs/dispatcher/class_based_handlers/error.md new file mode 100644 index 00000000..94efdb93 --- /dev/null +++ b/docs/dispatcher/class_based_handlers/error.md @@ -0,0 +1,29 @@ +# ErrorHandler + +There is base class for error handlers. + +## Simple usage: +```pyhton3 +from aiogram.handlers import ErrorHandler + +... + +@router.errors_handler() +class MyHandler(ErrorHandler): + async def handle(self) -> Any: + log.exception( + "Cause unexpected exception %s: %s", + self.event.__class__.__name__, + self.event + ) +``` + +## Extension + +This base handler is subclass of [BaseHandler](basics.md#basehandler) + +## Related pages + +- [BaseHandler](basics.md#basehandler) +- [Router.errors_handler](../router.md#errors) +- [Filters](../filters/exception.md) diff --git a/docs/dispatcher/filters/exception.md b/docs/dispatcher/filters/exception.md new file mode 100644 index 00000000..c24e47b3 --- /dev/null +++ b/docs/dispatcher/filters/exception.md @@ -0,0 +1,27 @@ +# Exceptions +This filters can be helpful for handling errors from the text messages. + +## ExceptionTypeFilter + +Allow to match exception by type + +### Specification +| Argument | Type | Description | +| --- | --- | --- | +| `exception` | `#!python3 Union[Type[Exception], Tuple[Type[Exception]]]` | Exception type(s) | + + +## ExceptionMessageFilter + +Allow to match exception by message + +### Specification +| Argument | Type | Description | +| --- | --- | --- | +| `match` | `#!python3 Union[str, Pattern[str]]` | Regexp pattern | + +## Allowed handlers + +Allowed update types for this filters: + +- `error` diff --git a/docs/dispatcher/router.md b/docs/dispatcher/router.md index 30625281..84d18ed9 100644 --- a/docs/dispatcher/router.md +++ b/docs/dispatcher/router.md @@ -116,6 +116,13 @@ async def poll_answer_handler(poll_answer: types.PollAnswer) -> Any: pass ``` Is useful for handling [polls answers](../api/types/poll_answer.md) +### Errors +```python3 +@router.errors_handler() +async def error_handler(exception: Exception) -> Any: pass +``` +Is useful for handling errors from other handlers + ## Nested routers diff --git a/mkdocs.yml b/mkdocs.yml index 47631d6a..747b5f4e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -240,6 +240,7 @@ nav: - dispatcher/filters/text.md - dispatcher/filters/command.md - dispatcher/filters/content_types.md + - dispatcher/filters/exception.md - Class based handlers: - dispatcher/class_based_handlers/basics.md - dispatcher/class_based_handlers/message.md @@ -249,6 +250,7 @@ nav: - dispatcher/class_based_handlers/poll.md - dispatcher/class_based_handlers/pre_checkout_query.md - dispatcher/class_based_handlers/shipping_query.md + - dispatcher/class_based_handlers/error.md - Middlewares: - dispatcher/middlewares/index.md - dispatcher/middlewares/basics.md diff --git a/tests/test_dispatcher/test_filters/test_exception.py b/tests/test_dispatcher/test_filters/test_exception.py new file mode 100644 index 00000000..4dd6d5d9 --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_exception.py @@ -0,0 +1,51 @@ +import re + +import pytest + +from aiogram.dispatcher.filters import ExceptionMessageFilter, ExceptionTypeFilter + + +class TestExceptionMessageFilter: + @pytest.mark.parametrize("value", ["value", re.compile("value")]) + def test_converter(self, value): + obj = ExceptionMessageFilter(match=value) + assert isinstance(obj.match, re.Pattern) + + @pytest.mark.asyncio + async def test_match(self): + obj = ExceptionMessageFilter(match="KABOOM") + + result = await obj(Exception()) + assert not result + + result = await obj(Exception("KABOOM")) + assert isinstance(result, dict) + assert "match_exception" in result + + +class MyException(Exception): + pass + + +class MyAnotherException(MyException): + pass + + +class TestExceptionTypeFilter: + @pytest.mark.asyncio + @pytest.mark.parametrize( + "exception,value", + [ + [Exception(), False], + [ValueError(), False], + [TypeError(), False], + [MyException(), True], + [MyAnotherException(), True], + ], + ) + async def test_check(self, exception: Exception, value: bool): + obj = ExceptionTypeFilter(exception=MyException) + + result = await obj(exception) + + assert result == value From 01c6303d67bd12327be1867f98ad01e17af11f16 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 23:29:10 +0300 Subject: [PATCH 18/18] Add extensions for class-based error handler --- aiogram/dispatcher/handler/error.py | 8 +++++ docs/dispatcher/class_based_handlers/error.md | 9 ++++-- .../test_handler/test_error.py | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/test_dispatcher/test_handler/test_error.py diff --git a/aiogram/dispatcher/handler/error.py b/aiogram/dispatcher/handler/error.py index dc7953bd..bc4ecdce 100644 --- a/aiogram/dispatcher/handler/error.py +++ b/aiogram/dispatcher/handler/error.py @@ -7,3 +7,11 @@ class ErrorHandler(BaseHandler[Exception], ABC): """ Base class for errors handlers """ + + @property + def exception_name(self) -> str: + return self.event.__class__.__name__ + + @property + def exception_message(self) -> str: + return str(self.event) diff --git a/docs/dispatcher/class_based_handlers/error.md b/docs/dispatcher/class_based_handlers/error.md index 94efdb93..842689a5 100644 --- a/docs/dispatcher/class_based_handlers/error.md +++ b/docs/dispatcher/class_based_handlers/error.md @@ -13,14 +13,17 @@ class MyHandler(ErrorHandler): async def handle(self) -> Any: log.exception( "Cause unexpected exception %s: %s", - self.event.__class__.__name__, - self.event + self.exception_name, + self.exception_message ) ``` ## Extension -This base handler is subclass of [BaseHandler](basics.md#basehandler) +This base handler is subclass of [BaseHandler](basics.md#basehandler) with some extensions: + +- `#!python3 self.exception_name` is alias for `#!python3 self.event.__class__.__name__` +- `#!python3 self.exception_message` is alias for `#!python3 str(self.event)` ## Related pages diff --git a/tests/test_dispatcher/test_handler/test_error.py b/tests/test_dispatcher/test_handler/test_error.py new file mode 100644 index 00000000..093d9b0a --- /dev/null +++ b/tests/test_dispatcher/test_handler/test_error.py @@ -0,0 +1,29 @@ +from typing import Any + +import pytest + +from aiogram.api.types import ( + CallbackQuery, + InlineQuery, + Poll, + PollOption, + ShippingAddress, + ShippingQuery, + User, +) +from aiogram.dispatcher.handler import ErrorHandler, PollHandler + + +class TestErrorHandler: + @pytest.mark.asyncio + async def test_extensions(self): + event = KeyError("kaboom") + + class MyHandler(ErrorHandler): + async def handle(self) -> Any: + assert self.event == event + assert self.exception_name == event.__class__.__name__ + assert self.exception_message == str(event) + return True + + assert await MyHandler(event)