Added base code and make code improvements

This commit is contained in:
Alex Root Junior 2021-09-20 22:28:52 +03:00
parent 5bd1162f57
commit bd2a348aa0
16 changed files with 522 additions and 424 deletions

View file

@ -47,7 +47,7 @@ help:
.PHONY: install .PHONY: install
install: install:
poetry install poetry install -E fast -E redis -E proxy -E i18n -E docs
$(py) pre-commit install $(py) pre-commit install
.PHONY: clean .PHONY: clean

View file

@ -231,6 +231,14 @@ class Bot(ContextInstanceMixin["Bot"]):
async for chunk in stream: async for chunk in stream:
await f.write(chunk) await f.write(chunk)
@classmethod
async def __aiofiles_reader(
cls, file: str, chunk_size: int = 65536
) -> AsyncGenerator[bytes, None]:
async with aiofiles.open(file, "rb") as f:
while chunk := await f.read(chunk_size):
yield chunk
async def download_file( async def download_file(
self, self,
file_path: str, file_path: str,
@ -254,15 +262,26 @@ class Bot(ContextInstanceMixin["Bot"]):
if destination is None: if destination is None:
destination = io.BytesIO() destination = io.BytesIO()
url = self.session.api.file_url(self.__token, file_path) close_stream = False
stream = self.session.stream_content(url=url, timeout=timeout, chunk_size=chunk_size) if self.session.api.is_local:
stream = self.__aiofiles_reader(
if isinstance(destination, (str, pathlib.Path)): self.session.api.wrap_local_file(file_path), chunk_size=chunk_size
return await self.__download_file(destination=destination, stream=stream)
else:
return await self.__download_file_binary_io(
destination=destination, seek=seek, stream=stream
) )
close_stream = True
else:
url = self.session.api.file_url(self.__token, file_path)
stream = self.session.stream_content(url=url, timeout=timeout, chunk_size=chunk_size)
try:
if isinstance(destination, (str, pathlib.Path)):
return await self.__download_file(destination=destination, stream=stream)
else:
return await self.__download_file_binary_io(
destination=destination, seek=seek, stream=stream
)
finally:
if close_stream:
await stream.aclose()
async def download( async def download(
self, self,

View file

@ -1,4 +1,10 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Protocol
class WrapLocalFileCallbackCallbackProtocol(Protocol):
def __call__(self, value: str) -> str:
pass
@dataclass(frozen=True) @dataclass(frozen=True)
@ -8,8 +14,13 @@ class TelegramAPIServer:
""" """
base: str base: str
"""Base URL"""
file: str file: str
"""Files URL"""
is_local: bool = False is_local: bool = False
"""Mark this server is in `local mode <https://core.telegram.org/bots/api#using-a-local-bot-api-server>`_."""
wrap_local_file: WrapLocalFileCallbackCallbackProtocol = lambda v: v
"""Callback to wrap files path in local mode"""
def api_url(self, token: str, method: str) -> str: def api_url(self, token: str, method: str) -> str:
""" """
@ -32,19 +43,18 @@ class TelegramAPIServer:
return self.file.format(token=token, path=path) return self.file.format(token=token, path=path)
@classmethod @classmethod
def from_base(cls, base: str, is_local: bool = False) -> "TelegramAPIServer": def from_base(cls, base: str, **kwargs: Any) -> "TelegramAPIServer":
""" """
Use this method to auto-generate TelegramAPIServer instance from base URL Use this method to auto-generate TelegramAPIServer instance from base URL
:param base: Base URL :param base: Base URL
:param is_local: Mark this server is in `local mode <https://core.telegram.org/bots/api#using-a-local-bot-api-server>`_.
:return: instance of :class:`TelegramAPIServer` :return: instance of :class:`TelegramAPIServer`
""" """
base = base.rstrip("/") base = base.rstrip("/")
return cls( return cls(
base=f"{base}/bot{{token}}/{{method}}", base=f"{base}/bot{{token}}/{{method}}",
file=f"{base}/file/bot{{token}}/{{path}}", file=f"{base}/file/bot{{token}}/{{path}}",
is_local=is_local, **kwargs,
) )

View file

@ -1,14 +1,19 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Awaitable, Callable, Dict, NoReturn, Optional, Union from typing import Any, Awaitable, Callable, Dict, NoReturn, Optional, TypeVar, Union
from unittest.mock import sentinel from unittest.mock import sentinel
from ...types import TelegramObject from ...types import TelegramObject
from ..middlewares.base import BaseMiddleware from ..middlewares.base import BaseMiddleware
NextMiddlewareType = Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]] MiddlewareEventType = TypeVar("MiddlewareEventType", bound=TelegramObject)
NextMiddlewareType = Callable[[MiddlewareEventType, Dict[str, Any]], Awaitable[Any]]
MiddlewareType = Union[ MiddlewareType = Union[
BaseMiddleware, Callable[[NextMiddlewareType, TelegramObject, Dict[str, Any]], Awaitable[Any]] BaseMiddleware,
Callable[
[NextMiddlewareType[MiddlewareEventType], MiddlewareEventType, Dict[str, Any]],
Awaitable[Any],
],
] ]
UNHANDLED = sentinel.UNHANDLED UNHANDLED = sentinel.UNHANDLED

View file

@ -8,7 +8,14 @@ from pydantic import ValidationError
from ...types import TelegramObject from ...types import TelegramObject
from ..filters.base import BaseFilter from ..filters.base import BaseFilter
from .bases import REJECTED, UNHANDLED, MiddlewareType, NextMiddlewareType, SkipHandler from .bases import (
REJECTED,
UNHANDLED,
MiddlewareEventType,
MiddlewareType,
NextMiddlewareType,
SkipHandler,
)
from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@ -29,8 +36,8 @@ class TelegramEventObserver:
self.handlers: List[HandlerObject] = [] self.handlers: List[HandlerObject] = []
self.filters: List[Type[BaseFilter]] = [] self.filters: List[Type[BaseFilter]] = []
self.outer_middlewares: List[MiddlewareType] = [] self.outer_middlewares: List[MiddlewareType[TelegramObject]] = []
self.middlewares: List[MiddlewareType] = [] self.middlewares: List[MiddlewareType[TelegramObject]] = []
# Re-used filters check method from already implemented handler object # Re-used filters check method from already implemented handler object
# with dummy callback which never will be used # with dummy callback which never will be used
@ -78,7 +85,7 @@ class TelegramEventObserver:
yield filter_ yield filter_
registry.append(filter_) registry.append(filter_)
def _resolve_middlewares(self, *, outer: bool = False) -> List[MiddlewareType]: def _resolve_middlewares(self, *, outer: bool = False) -> List[MiddlewareType[TelegramObject]]:
""" """
Get all middlewares in a tree Get all middlewares in a tree
:param *: :param *:
@ -137,8 +144,8 @@ class TelegramEventObserver:
@classmethod @classmethod
def _wrap_middleware( def _wrap_middleware(
cls, middlewares: List[MiddlewareType], handler: HandlerType cls, middlewares: List[MiddlewareType[MiddlewareEventType]], handler: HandlerType
) -> NextMiddlewareType: ) -> NextMiddlewareType[MiddlewareEventType]:
@functools.wraps(handler) @functools.wraps(handler)
def mapper(event: TelegramObject, kwargs: Dict[str, Any]) -> Any: def mapper(event: TelegramObject, kwargs: Dict[str, Any]) -> Any:
return handler(event, **kwargs) return handler(event, **kwargs)
@ -194,8 +201,11 @@ class TelegramEventObserver:
def middleware( def middleware(
self, self,
middleware: Optional[MiddlewareType] = None, middleware: Optional[MiddlewareType[TelegramObject]] = None,
) -> Union[Callable[[MiddlewareType], MiddlewareType], MiddlewareType]: ) -> Union[
Callable[[MiddlewareType[TelegramObject]], MiddlewareType[TelegramObject]],
MiddlewareType[TelegramObject],
]:
""" """
Decorator for registering inner middlewares Decorator for registering inner middlewares
@ -215,7 +225,7 @@ class TelegramEventObserver:
<event>.middleware(my_middleware) # via method <event>.middleware(my_middleware) # via method
""" """
def wrapper(m: MiddlewareType) -> MiddlewareType: def wrapper(m: MiddlewareType[TelegramObject]) -> MiddlewareType[TelegramObject]:
self.middlewares.append(m) self.middlewares.append(m)
return m return m
@ -225,8 +235,11 @@ class TelegramEventObserver:
def outer_middleware( def outer_middleware(
self, self,
middleware: Optional[MiddlewareType] = None, middleware: Optional[MiddlewareType[TelegramObject]] = None,
) -> Union[Callable[[MiddlewareType], MiddlewareType], MiddlewareType]: ) -> Union[
Callable[[MiddlewareType[TelegramObject]], MiddlewareType[TelegramObject]],
MiddlewareType[TelegramObject],
]:
""" """
Decorator for registering outer middlewares Decorator for registering outer middlewares
@ -246,7 +259,7 @@ class TelegramEventObserver:
<event>.outer_middleware(my_middleware) # via method <event>.outer_middleware(my_middleware) # via method
""" """
def wrapper(m: MiddlewareType) -> MiddlewareType: def wrapper(m: MiddlewareType[TelegramObject]) -> MiddlewareType[TelegramObject]:
self.outer_middlewares.append(m) self.outer_middlewares.append(m)
return m return m

View file

@ -4,6 +4,7 @@ from .base import BaseFilter
from .command import Command, CommandObject from .command import Command, CommandObject
from .content_types import ContentTypesFilter from .content_types import ContentTypesFilter
from .exception import ExceptionMessageFilter, ExceptionTypeFilter from .exception import ExceptionMessageFilter, ExceptionTypeFilter
from .state import StateFilter
from .text import Text from .text import Text
__all__ = ( __all__ = (
@ -15,21 +16,46 @@ __all__ = (
"ContentTypesFilter", "ContentTypesFilter",
"ExceptionMessageFilter", "ExceptionMessageFilter",
"ExceptionTypeFilter", "ExceptionTypeFilter",
"StateFilter",
) )
BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = { BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
"message": (Text, Command, ContentTypesFilter), "message": (
"edited_message": (Text, Command, ContentTypesFilter), Text,
"channel_post": (Text, ContentTypesFilter), Command,
"edited_channel_post": (Text, ContentTypesFilter), ContentTypesFilter,
"inline_query": (Text,), StateFilter,
"chosen_inline_result": (), ),
"callback_query": (Text,), "edited_message": (
"shipping_query": (), Text,
"pre_checkout_query": (), Command,
"poll": (), ContentTypesFilter,
"poll_answer": (), StateFilter,
"my_chat_member": (), ),
"chat_member": (), "channel_post": (
Text,
ContentTypesFilter,
StateFilter,
),
"edited_channel_post": (
Text,
ContentTypesFilter,
StateFilter,
),
"inline_query": (
Text,
StateFilter,
),
"chosen_inline_result": (StateFilter,),
"callback_query": (
Text,
StateFilter,
),
"shipping_query": (StateFilter,),
"pre_checkout_query": (StateFilter,),
"poll": (StateFilter,),
"poll_answer": (StateFilter,),
"my_chat_member": (StateFilter,),
"chat_member": (StateFilter,),
"error": (ExceptionMessageFilter, ExceptionTypeFilter), "error": (ExceptionMessageFilter, ExceptionTypeFilter),
} }

View file

@ -1,11 +1,14 @@
from typing import Any, Dict, Optional, Sequence, Union from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union
from pydantic import root_validator from pydantic import root_validator
from aiogram.dispatcher.filters import BaseFilter from aiogram.dispatcher.filters import BaseFilter
from aiogram.types import CallbackQuery, InlineQuery, Message, Poll from aiogram.types import CallbackQuery, InlineQuery, Message, Poll
TextType = str if TYPE_CHECKING:
from aiogram.utils.i18n.lazy_proxy import LazyProxy
TextType = Union[str, "LazyProxy"]
class Text(BaseFilter): class Text(BaseFilter):
@ -35,6 +38,9 @@ class Text(BaseFilter):
text_ignore_case: bool = False text_ignore_case: bool = False
"""Ignore case when checks""" """Ignore case when checks"""
class Config:
arbitrary_types_allowed = True
@root_validator @root_validator
def _validate_constraints(cls, values: Dict[str, Any]) -> Dict[str, Any]: def _validate_constraints(cls, values: Dict[str, Any]) -> Dict[str, Any]:
# Validate that only one text filter type is presented # Validate that only one text filter type is presented

View file

@ -5,10 +5,10 @@ from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.storage.base import BaseStorage from aiogram.dispatcher.fsm.storage.base import BaseStorage
from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy
from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.types import Update from aiogram.types import TelegramObject
class FSMContextMiddleware(BaseMiddleware[Update]): class FSMContextMiddleware(BaseMiddleware):
def __init__( def __init__(
self, self,
storage: BaseStorage, storage: BaseStorage,
@ -21,8 +21,8 @@ class FSMContextMiddleware(BaseMiddleware[Update]):
async def __call__( async def __call__(
self, self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: Update, event: TelegramObject,
data: Dict[str, Any], data: Dict[str, Any],
) -> Any: ) -> Any:
bot: Bot = cast(Bot, data["bot"]) bot: Bot = cast(Bot, data["bot"])

View file

@ -129,8 +129,8 @@ class StatesGroup(metaclass=StatesGroupMeta):
return cls return cls
return cls.__parent__.get_root() return cls.__parent__.get_root()
def __call__(cls, event: TelegramObject, raw_state: Optional[str] = None) -> bool: def __call__(self, event: TelegramObject, raw_state: Optional[str] = None) -> bool:
return raw_state in type(cls).__all_states_names__ return raw_state in type(self).__all_states_names__
def __str__(self) -> str: def __str__(self) -> str:
return f"StatesGroup {type(self).__full_group_name__}" return f"StatesGroup {type(self).__full_group_name__}"

View file

@ -1,10 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Awaitable, Callable, Dict, Generic, TypeVar from typing import Any, Awaitable, Callable, Dict, TypeVar
from aiogram.types import TelegramObject
T = TypeVar("T") T = TypeVar("T")
class BaseMiddleware(ABC, Generic[T]): class BaseMiddleware(ABC):
""" """
Generic middleware class Generic middleware class
""" """
@ -12,8 +14,8 @@ class BaseMiddleware(ABC, Generic[T]):
@abstractmethod @abstractmethod
async def __call__( async def __call__(
self, self,
handler: Callable[[T, Dict[str, Any]], Awaitable[Any]], handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: T, event: TelegramObject,
data: Dict[str, Any], data: Dict[str, Any],
) -> Any: # pragma: no cover ) -> Any: # pragma: no cover
""" """

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
from ...types import Update from ...types import TelegramObject
from ..event.bases import UNHANDLED, CancelHandler, SkipHandler from ..event.bases import UNHANDLED, CancelHandler, SkipHandler
from .base import BaseMiddleware from .base import BaseMiddleware
@ -10,14 +10,14 @@ if TYPE_CHECKING: # pragma: no cover
from ..router import Router from ..router import Router
class ErrorsMiddleware(BaseMiddleware[Update]): class ErrorsMiddleware(BaseMiddleware):
def __init__(self, router: Router): def __init__(self, router: Router):
self.router = router self.router = router
async def __call__( async def __call__(
self, self,
handler: Callable[[Any, Dict[str, Any]], Awaitable[Any]], handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: Any, event: TelegramObject,
data: Dict[str, Any], data: Dict[str, Any],
) -> Any: ) -> Any:
try: try:

View file

@ -2,16 +2,18 @@ from contextlib import contextmanager
from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Tuple from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Tuple
from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.types import Chat, Update, User from aiogram.types import Chat, TelegramObject, Update, User
class UserContextMiddleware(BaseMiddleware[Update]): class UserContextMiddleware(BaseMiddleware):
async def __call__( async def __call__(
self, self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: Update, event: TelegramObject,
data: Dict[str, Any], data: Dict[str, Any],
) -> Any: ) -> Any:
if not isinstance(event, Update):
raise RuntimeError("UserContextMiddleware got an unexpected event type!")
chat, user = self.resolve_event_context(event=event) chat, user = self.resolve_event_context(event=event)
with self.context(chat=chat, user=user): with self.context(chat=chat, user=user):
if user is not None: if user is not None:

View file

@ -216,8 +216,7 @@ class OrderedHelperMeta(type):
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys) setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
# ref: https://gitter.im/python/typing?at=5da98cc5fa637359fc9cbfe1 return cls
return cast(OrderedHelperMeta, cls)
class OrderedHelper(Helper, metaclass=OrderedHelperMeta): class OrderedHelper(Helper, metaclass=OrderedHelperMeta):

View file

@ -32,3 +32,6 @@ ignore_missing_imports = True
[mypy-aioredis] [mypy-aioredis]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-babel.*]
ignore_missing_imports = True

684
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -32,32 +32,39 @@ classifiers = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
magic-filter = "^1.0.0" magic-filter = "^1.0.2"
aiohttp = "^3.7.4" aiohttp = "^3.7.4"
pydantic = "^1.8.1" pydantic = "^1.8.2"
Babel = "^2.9.1" aiofiles = "^0.7.0"
aiofiles = "^0.6.0"
async_lru = "^1.0.2" async_lru = "^1.0.2"
# Fast
uvloop = { version = "^0.16.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true }
# i18n
Babel = { version = "^2.9.1", optional = true }
# Proxy
aiohttp-socks = { version = "^0.5.5", optional = true } aiohttp-socks = { version = "^0.5.5", optional = true }
aioredis = { version = "^2.0.0", allow-prereleases = true, optional = true } # Redis
sphinx = { version = "^3.1.0", optional = true } aioredis = { version = "^2.0.0", optional = true }
# Docs
Sphinx = { version = "^4.2.0", optional = true }
sphinx-intl = { version = "^2.0.1", optional = true } sphinx-intl = { version = "^2.0.1", optional = true }
sphinx-autobuild = { version = "^2020.9.1", optional = true } sphinx-autobuild = { version = "^2021.3.14", optional = true }
sphinx-copybutton = { version = "^0.3.1", optional = true } sphinx-copybutton = { version = "^0.4.0", optional = true }
furo = { version = "^2021.6.18-beta.36", optional = true } furo = { version = "^2021.9.8", optional = true }
sphinx-prompt = { version = "^1.3.0", optional = true } sphinx-prompt = { version = "^1.5.0", optional = true }
Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true } Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true }
towncrier = { version = "^21.3.0", optional = true }
pygments = { version = "^2.4", optional = true }
pymdown-extensions = { version = "^8.0", optional = true }
markdown-include = { version = "^0.6", optional = true }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
aiohttp-socks = "^0.5"
aioredis = { version = "^2.0.0a1", allow-prereleases = true }
ipython = "^7.22.0" ipython = "^7.22.0"
uvloop = { version = "^0.15.2", markers = "sys_platform == 'darwin' or sys_platform == 'linux'" }
black = "^21.4b2" black = "^21.4b2"
isort = "^5.8.0" isort = "^5.8.0"
flake8 = "^3.9.1" flake8 = "^3.9.1"
flake8-html = "^0.4.1" flake8-html = "^0.4.1"
mypy = "^0.812" mypy = "^0.910"
pytest = "^6.2.3" pytest = "^6.2.3"
pytest-html = "^3.1.1" pytest-html = "^3.1.1"
pytest-asyncio = "^0.15.1" pytest-asyncio = "^0.15.1"
@ -68,27 +75,17 @@ pytest-cov = "^2.11.1"
aresponses = "^2.1.4" aresponses = "^2.1.4"
asynctest = "^0.13.0" asynctest = "^0.13.0"
toml = "^0.10.2" toml = "^0.10.2"
pygments = "^2.4"
pymdown-extensions = "^8.0"
markdown-include = "^0.6"
pre-commit = "^2.15.0" pre-commit = "^2.15.0"
packaging = "^20.3" packaging = "^20.3"
typing-extensions = "^3.7.4" typing-extensions = "^3.7.4"
sphinx = "^3.1.0"
sphinx-intl = "^2.0.1"
sphinx-autobuild = "^2020.9.1"
sphinx-copybutton = "^0.3.1"
furo = "^2021.6.18-beta.36"
sphinx-prompt = "^1.3.0"
Sphinx-Substitution-Extensions = "^2020.9.30"
towncrier = "^21.3.0"
diagrams = "^0.20.0"
[tool.poetry.extras] [tool.poetry.extras]
fast = ["uvloop"] fast = ["uvloop"]
redis = ["aioredis"] redis = ["aioredis"]
proxy = ["aiohttp-socks"] proxy = ["aiohttp-socks"]
i18n = ["Babel"]
docs = [ docs = [
"sphinx", "sphinx",
"sphinx-intl", "sphinx-intl",
@ -98,6 +95,10 @@ docs = [
"black", "black",
"sphinx-prompt", "sphinx-prompt",
"Sphinx-Substitution-Extensions", "Sphinx-Substitution-Extensions",
"towncrier",
"pygments",
"pymdown-extensions",
"markdown-include",
] ]
[tool.black] [tool.black]