aiogram/aiogram/methods/base.py

159 lines
5.2 KiB
Python

from __future__ import annotations
import abc
import secrets
from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Optional, TypeVar, Union
from pydantic import BaseConfig, BaseModel, Extra, root_validator
from pydantic.generics import GenericModel
from ..types import UNSET, InputFile, ResponseParameters
if TYPE_CHECKING:
from ..client.bot import Bot
TelegramType = TypeVar("TelegramType", bound=Any)
class Request(BaseModel):
method: str
data: Dict[str, Optional[Any]]
files: Optional[Dict[str, InputFile]]
class Config(BaseConfig):
arbitrary_types_allowed = True
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},
}
class Response(GenericModel, Generic[TelegramType]):
ok: bool
result: Optional[TelegramType] = None
description: Optional[str] = None
error_code: Optional[int] = None
parameters: Optional[ResponseParameters] = None
class TelegramMethod(abc.ABC, BaseModel, Generic[TelegramType]):
class Config(BaseConfig):
# use_enum_values = True
extra = Extra.allow
allow_population_by_field_name = True
arbitrary_types_allowed = True
orm_mode = True
smart_union = True # https://github.com/aiogram/aiogram/issues/901
@root_validator(pre=True)
def remove_unset(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""
Remove UNSET from `parse_mode` before fields validation.
We use UNSET as a sentinel value for `parse_mode` and replace it to real value later.
It isn't a problem when it's just default value for a model field, but UNSET might be passing to
a model initialization from `Bot.method_name`, so we must take care of it and
remove it before fields validation.
"""
for parse_mode_attribute in {"parse_mode", "explanation_parse_mode"}:
if parse_mode_attribute in values and values[parse_mode_attribute] is UNSET:
values.pop(parse_mode_attribute)
return values
@property
@abc.abstractmethod
def __returning__(self) -> type: # pragma: no cover
pass
@abc.abstractmethod
def build_request(self, bot: Bot) -> Request: # pragma: no cover
pass
def dict(self, **kwargs: Any) -> Any:
# override dict of pydantic.BaseModel to overcome exporting request_timeout field
exclude = kwargs.pop("exclude", set())
return super().dict(exclude=exclude, **kwargs)
def build_response(self, data: Dict[str, Any]) -> Response[TelegramType]:
# noinspection PyTypeChecker
return Response[self.__returning__](**data) # type: ignore
async def emit(self, bot: Bot) -> TelegramType:
return await bot(self)
as_ = emit
def __await__(self) -> Generator[Any, None, TelegramType]:
from aiogram.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]) -> None:
if not value:
return
if name == "thumb":
tag = secrets.token_urlsafe(10)
files[tag] = value
data["thumb"] = f"attach://{tag}"
elif isinstance(value, InputFile):
files[name] = value
else:
data[name] = value
def prepare_input_media(data: Dict[str, Any], files: Dict[str, InputFile]) -> None:
for input_media in data.get("media", []): # type: Dict[str, Union[str, InputFile]]
if (
"media" in input_media
and input_media["media"]
and isinstance(input_media["media"], InputFile)
):
tag = secrets.token_urlsafe(10)
files[tag] = input_media.pop("media") # type: ignore
input_media["media"] = f"attach://{tag}"
def prepare_media_file(data: Dict[str, Any], files: Dict[str, InputFile]) -> None:
if (
data["media"]
and "media" in data["media"]
and isinstance(data["media"]["media"], InputFile)
):
tag = secrets.token_urlsafe(10)
files[tag] = data["media"].pop("media")
data["media"]["media"] = f"attach://{tag}"
def prepare_parse_mode(
bot: Bot,
root: Any,
parse_mode_property: str = "parse_mode",
entities_property: str = "entities",
) -> None:
"""
Find and set parse_mode with highest priority.
Developer can manually set parse_mode for each message (or message-like) object,
but if parse_mode was unset we should use value from Bot object.
We can't use None for "unset state", because None itself is the parse_mode option.
"""
if isinstance(root, list):
for item in root:
prepare_parse_mode(
bot=bot,
root=item,
parse_mode_property=parse_mode_property,
entities_property=entities_property,
)
elif root.get(parse_mode_property, UNSET) is UNSET:
if bot.parse_mode and root.get(entities_property, None) is None:
root[parse_mode_property] = bot.parse_mode
else:
root[parse_mode_property] = None