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
install:
poetry install
poetry install -E fast -E redis -E proxy -E i18n -E docs
$(py) pre-commit install
.PHONY: clean

View file

@ -231,6 +231,14 @@ class Bot(ContextInstanceMixin["Bot"]):
async for chunk in stream:
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(
self,
file_path: str,
@ -254,15 +262,26 @@ class Bot(ContextInstanceMixin["Bot"]):
if destination is None:
destination = io.BytesIO()
url = self.session.api.file_url(self.__token, file_path)
stream = self.session.stream_content(url=url, timeout=timeout, chunk_size=chunk_size)
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
close_stream = False
if self.session.api.is_local:
stream = self.__aiofiles_reader(
self.session.api.wrap_local_file(file_path), chunk_size=chunk_size
)
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(
self,

View file

@ -1,4 +1,10 @@
from dataclasses import dataclass
from typing import Any, Protocol
class WrapLocalFileCallbackCallbackProtocol(Protocol):
def __call__(self, value: str) -> str:
pass
@dataclass(frozen=True)
@ -8,8 +14,13 @@ class TelegramAPIServer:
"""
base: str
"""Base URL"""
file: str
"""Files URL"""
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:
"""
@ -32,19 +43,18 @@ class TelegramAPIServer:
return self.file.format(token=token, path=path)
@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
: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`
"""
base = base.rstrip("/")
return cls(
base=f"{base}/bot{{token}}/{{method}}",
file=f"{base}/file/bot{{token}}/{{path}}",
is_local=is_local,
**kwargs,
)

View file

@ -1,14 +1,19 @@
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 ...types import TelegramObject
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[
BaseMiddleware, Callable[[NextMiddlewareType, TelegramObject, Dict[str, Any]], Awaitable[Any]]
BaseMiddleware,
Callable[
[NextMiddlewareType[MiddlewareEventType], MiddlewareEventType, Dict[str, Any]],
Awaitable[Any],
],
]
UNHANDLED = sentinel.UNHANDLED

View file

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

View file

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

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 aiogram.dispatcher.filters import BaseFilter
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):
@ -35,6 +38,9 @@ class Text(BaseFilter):
text_ignore_case: bool = False
"""Ignore case when checks"""
class Config:
arbitrary_types_allowed = True
@root_validator
def _validate_constraints(cls, values: Dict[str, Any]) -> Dict[str, Any]:
# 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.strategy import FSMStrategy, apply_strategy
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__(
self,
storage: BaseStorage,
@ -21,8 +21,8 @@ class FSMContextMiddleware(BaseMiddleware[Update]):
async def __call__(
self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
event: Update,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
bot: Bot = cast(Bot, data["bot"])

View file

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

View file

@ -1,10 +1,12 @@
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")
class BaseMiddleware(ABC, Generic[T]):
class BaseMiddleware(ABC):
"""
Generic middleware class
"""
@ -12,8 +14,8 @@ class BaseMiddleware(ABC, Generic[T]):
@abstractmethod
async def __call__(
self,
handler: Callable[[T, Dict[str, Any]], Awaitable[Any]],
event: T,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any: # pragma: no cover
"""

View file

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

View file

@ -2,16 +2,18 @@ from contextlib import contextmanager
from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Tuple
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__(
self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
event: Update,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
if not isinstance(event, Update):
raise RuntimeError("UserContextMiddleware got an unexpected event type!")
chat, user = self.resolve_event_context(event=event)
with self.context(chat=chat, user=user):
if user is not None:

View file

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

View file

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