diff --git a/aiogram/client/session/aiohttp.py b/aiogram/client/session/aiohttp.py index 279f61f9..0ff33781 100644 --- a/aiogram/client/session/aiohttp.py +++ b/aiogram/client/session/aiohttp.py @@ -17,13 +17,15 @@ from typing import ( ) import certifi +import msgspec from aiohttp import BasicAuth, ClientError, ClientSession, FormData, TCPConnector from aiogram.methods import TelegramMethod from ...exceptions import TelegramNetworkError from ...methods.base import TelegramType -from ...types import InputFile +from ...types import UNSET_PARSE_MODE, InputFile +from ...types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT from .base import BaseSession if TYPE_CHECKING: @@ -121,7 +123,10 @@ class AiohttpSession(BaseSession): await self.close() if self._session is None or self._session.closed: - self._session = ClientSession(connector=self._connector_type(**self._connector_init)) + self._session = ClientSession( + connector=self._connector_type(**self._connector_init), + json_serialize=self.json_dumps, + ) self._should_reset_connector = False return self._session @@ -133,10 +138,12 @@ class AiohttpSession(BaseSession): def build_form_data(self, bot: Bot, method: TelegramMethod[TelegramType]) -> FormData: form = FormData(quote_fields=False) files: Dict[str, InputFile] = {} - for key, value in method.dict().items(): - value = self.prepare_value(value, bot=bot, files=files) + for key in method.__struct_fields__: + value = self.prepare_value(getattr(method, key), bot=bot, files=files) if not value: continue + if isinstance(value, bytes): + value = value.decode("utf-8") form.add_field(key, value) for key, value in files.items(): form.add_field(key, value, filename=value.filename or key) @@ -148,13 +155,30 @@ class AiohttpSession(BaseSession): session = await self.create_session() url = self.api.api_url(token=bot.token, method=method.__api_method__) - form = self.build_form_data(bot=bot, method=method) + def enc_hook(obj): + if obj is UNSET_PARSE_MODE: + return bot.parse_mode + if obj is UNSET_DISABLE_WEB_PAGE_PREVIEW: + return bot.disable_web_page_preview + if obj is UNSET_PROTECT_CONTENT: + return bot.protect_content + raise ValueError(f"Unknown object: {obj}") + + headers = {} + try: + form = msgspec.json.encode(method, enc_hook=enc_hook) + headers = {"content-type": "application/json"} + except ValueError: + form = self.build_form_data(bot=bot, method=method) try: async with session.post( - url, data=form, timeout=self.timeout if timeout is None else timeout + url, + data=form, + headers=headers, + timeout=self.timeout if timeout is None else timeout, ) as resp: - raw_result = await resp.text() + raw_result = await resp.read() except asyncio.TimeoutError: raise TelegramNetworkError(method=method, message="Request timeout error") except ClientError as e: diff --git a/aiogram/client/session/base.py b/aiogram/client/session/base.py index a66eb7cc..1842fccb 100644 --- a/aiogram/client/session/base.py +++ b/aiogram/client/session/base.py @@ -19,7 +19,7 @@ from typing import ( cast, ) -from pydantic import ValidationError +import msgspec from aiogram.exceptions import ( ClientDecodeError, @@ -56,8 +56,8 @@ class BaseSession(abc.ABC): def __init__( self, api: TelegramAPIServer = PRODUCTION, - json_loads: _JsonLoads = json.loads, - json_dumps: _JsonDumps = json.dumps, + json_loads: _JsonLoads = msgspec.json.decode, + json_dumps: _JsonDumps = msgspec.json.encode, timeout: float = DEFAULT_TIMEOUT, ) -> None: """ @@ -81,17 +81,12 @@ class BaseSession(abc.ABC): Check response status """ try: - json_data = self.json_loads(content) - except Exception as e: + response = method.build_response(content) + except (msgspec.ValidationError, msgspec.DecodeError) as e: # Handled error type can't be classified as specific error # in due to decoder can be customized and raise any exception - raise ClientDecodeError("Failed to decode object", e, content) - - try: - response = method.build_response(json_data) - except ValidationError as e: - raise ClientDecodeError("Failed to deserialize object", e, json_data) + raise ClientDecodeError("Failed to decode object", e, str(content)) if HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED and response.ok: return response @@ -175,7 +170,7 @@ class BaseSession(abc.ABC): """ Prepare value before send """ - if value is None: + if value in (None, msgspec.UNSET): return None if isinstance(value, str): return value diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 9c61a447..d31392fc 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -8,6 +8,8 @@ from asyncio import CancelledError, Event, Future, Lock from contextlib import suppress from typing import Any, AsyncGenerator, Dict, List, Optional, Union +import msgspec.json + from .. import loggers from ..client.bot import Bot from ..exceptions import TelegramAPIError @@ -167,7 +169,9 @@ class Dispatcher(Router): ) Bot.reset_current(token) - async def feed_raw_update(self, bot: Bot, update: Dict[str, Any], **kwargs: Any) -> Any: + async def feed_raw_update( + self, bot: Bot, update: Union[str, Dict[str, Any]], **kwargs: Any + ) -> Any: """ Main entry point for incoming updates with automatic Dict->Update serializer @@ -175,7 +179,10 @@ class Dispatcher(Router): :param update: :param kwargs: """ - parsed_update = Update(**update) + if isinstance(update, dict): + parsed_update = msgspec.from_builtins(update, type=Update) + else: + parsed_update = msgspec.json.decode(update, type=Update) return await self.feed_update(bot=bot, update=parsed_update, **kwargs) @classmethod @@ -202,6 +209,7 @@ class Dispatcher(Router): while True: try: updates = await bot(get_updates, **kwargs) + # print('updates:', updates) except Exception as e: failed = True # In cases when Telegram Bot API was inaccessible don't need to stop polling @@ -364,10 +372,13 @@ class Dispatcher(Router): raise async def feed_webhook_update( - self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: float = 55, **kwargs: Any + self, bot: Bot, update: Union[bytes, str, Update], _timeout: float = 55, **kwargs: Any ) -> Optional[TelegramMethod[TelegramType]]: if not isinstance(update, Update): # Allow to use raw updates - update = Update(**update) + if isinstance(update, dict): + update = msgspec.from_builtins(update, type=Update) + else: + update = msgspec.json.decode(update, type=Update) ctx = contextvars.copy_context() loop = asyncio.get_running_loop() diff --git a/aiogram/filters/callback_data.py b/aiogram/filters/callback_data.py index 05d7783d..42823ae6 100644 --- a/aiogram/filters/callback_data.py +++ b/aiogram/filters/callback_data.py @@ -172,11 +172,17 @@ class CallbackQueryFilter(Filter): async def __call__(self, query: CallbackQuery) -> Union[Literal[False], Dict[str, Any]]: if not isinstance(query, CallbackQuery) or not query.data: return False - try: - callback_data = self.callback_data.unpack(query.data) - except (TypeError, ValueError): + prefix, *parts = query.data.split(self.callback_data.__separator__) + if prefix != self.callback_data.__prefix__: return False - if self.rule is None or self.rule.resolve(callback_data): - return {"callback_data": callback_data} + if query._callback_data is None: + try: + query._callback_data = self.callback_data.unpack(query.data) + except (TypeError, ValueError): + return False + + if query._callback_data is not None: + if self.rule is None or self.rule.resolve(query._callback_data): + return {"callback_data": query._callback_data} return False diff --git a/aiogram/filters/state.py b/aiogram/filters/state.py index 5ad65ae5..b21a7418 100644 --- a/aiogram/filters/state.py +++ b/aiogram/filters/state.py @@ -27,7 +27,7 @@ class StateFilter(Filter): ) async def __call__( - self, obj: Union[TelegramObject], raw_state: Optional[str] = None + self, obj: Union[TelegramObject], raw_state: Optional[str] = None, **kwargs ) -> Union[bool, Dict[str, Any]]: allowed_states = cast(Sequence[StateType], self.states) for allowed_state in allowed_states: diff --git a/aiogram/methods/answer_inline_query.py b/aiogram/methods/answer_inline_query.py index 7b6b6d88..a1994651 100644 --- a/aiogram/methods/answer_inline_query.py +++ b/aiogram/methods/answer_inline_query.py @@ -1,8 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional - -from pydantic import Field +from typing import List, Optional from ..types import InlineQueryResult, InlineQueryResultsButton from .base import TelegramMethod @@ -32,12 +30,12 @@ class AnswerInlineQuery(TelegramMethod[bool]): """Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you don't support pagination. Offset length can't exceed 64 bytes.""" button: Optional[InlineQueryResultsButton] = None """A JSON-serialized object describing a button to be shown above inline query results""" - switch_pm_parameter: Optional[str] = Field(None, deprecated=True) + switch_pm_parameter: Optional[str] = None """`Deep-linking `_ parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only :code:`A-Z`, :code:`a-z`, :code:`0-9`, :code:`_` and :code:`-` are allowed. .. deprecated:: API:6.7 https://core.telegram.org/bots/api-changelog#april-21-2023""" - switch_pm_text: Optional[str] = Field(None, deprecated=True) + switch_pm_text: Optional[str] = None """If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter *switch_pm_parameter* .. deprecated:: API:6.7 diff --git a/aiogram/methods/base.py b/aiogram/methods/base.py index 33045bf0..1f57cd0d 100644 --- a/aiogram/methods/base.py +++ b/aiogram/methods/base.py @@ -1,13 +1,22 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Optional, TypeVar +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 +import msgspec + +from aiogram.types import * # noqa from ..types import InputFile, ResponseParameters -from ..types.base import UNSET_TYPE if TYPE_CHECKING: from ..client.bot import Bot @@ -15,44 +24,30 @@ if TYPE_CHECKING: TelegramType = TypeVar("TelegramType", bound=Any) -class Request(BaseModel): +class Request(msgspec.Struct, weakref=True): method: str data: Dict[str, Optional[Any]] files: Optional[Dict[str, InputFile]] - class Config(BaseConfig): - arbitrary_types_allowed = True - -class Response(GenericModel, Generic[TelegramType]): +class Response(msgspec.Struct, Generic[TelegramType], weakref=True, kw_only=True): ok: bool - result: Optional[TelegramType] = None + result: Optional[Union[Any, None]] = None description: Optional[str] = None error_code: Optional[int] = None parameters: Optional[ResponseParameters] = None -class TelegramMethod(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 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. - """ - return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)} +class TelegramMethod( + msgspec.Struct, + Generic[TelegramType], + omit_defaults=True, + weakref=True, + kw_only=True, + forbid_unknown_fields=False, +): + method: str = msgspec.UNSET @property @abstractmethod @@ -64,15 +59,13 @@ class TelegramMethod(ABC, BaseModel, Generic[TelegramType]): def __api_method__(self) -> str: 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]: + def build_response(self, data: Union[str, bytes, Dict[str, Any]]) -> Response[TelegramType]: # noinspection PyTypeChecker - return Response[self.__returning__](**data) # type: ignore + if isinstance(data, (bytes, str)): + data = msgspec.json.decode(data) + if "result" in data: + data["result"] = msgspec.from_builtins(data["result"], type=self.__returning__) + return msgspec.from_builtins(data, type=Response) async def emit(self, bot: Bot) -> TelegramType: return await bot(self) diff --git a/aiogram/methods/copy_message.py b/aiogram/methods/copy_message.py index 5e34bb7d..81de7230 100644 --- a/aiogram/methods/copy_message.py +++ b/aiogram/methods/copy_message.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -35,13 +37,13 @@ class CopyMessage(TelegramMethod[MessageId]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" caption: Optional[str] = None """New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the new caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of *parse_mode*""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/edit_message_caption.py b/aiogram/methods/edit_message_caption.py index 66cdeac9..c0a2e0a8 100644 --- a/aiogram/methods/edit_message_caption.py +++ b/aiogram/methods/edit_message_caption.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import UNSET_PARSE_MODE, InlineKeyboardMarkup, Message, MessageEntity from .base import TelegramMethod @@ -24,7 +26,7 @@ class EditMessageCaption(TelegramMethod[Union[Message, bool]]): """Required if *chat_id* and *message_id* are not specified. Identifier of the inline message""" caption: Optional[str] = None """New caption of the message, 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the message caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" diff --git a/aiogram/methods/edit_message_text.py b/aiogram/methods/edit_message_text.py index 79887c97..26f23241 100644 --- a/aiogram/methods/edit_message_text.py +++ b/aiogram/methods/edit_message_text.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import UNSET_PARSE_MODE, InlineKeyboardMarkup, Message, MessageEntity from ..types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW from .base import TelegramMethod @@ -25,11 +27,13 @@ class EditMessageText(TelegramMethod[Union[Message, bool]]): """Required if *inline_message_id* is not specified. Identifier of the message to edit""" inline_message_id: Optional[str] = None """Required if *chat_id* and *message_id* are not specified. Identifier of the inline message""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the message text. See `formatting options `_ for more details.""" entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in message text, which can be specified instead of *parse_mode*""" - disable_web_page_preview: Optional[bool] = UNSET_DISABLE_WEB_PAGE_PREVIEW + disable_web_page_preview: Optional[bool] = msgspec.field( + default_factory=lambda: UNSET_DISABLE_WEB_PAGE_PREVIEW + ) """Disables link previews for links in this message""" reply_markup: Optional[InlineKeyboardMarkup] = None """A JSON-serialized object for an `inline keyboard `_.""" diff --git a/aiogram/methods/forward_message.py b/aiogram/methods/forward_message.py index 45c084a0..06181833 100644 --- a/aiogram/methods/forward_message.py +++ b/aiogram/methods/forward_message.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union +import msgspec + from ..types import Message from ..types.base import UNSET_PROTECT_CONTENT from .base import TelegramMethod @@ -27,5 +29,5 @@ class ForwardMessage(TelegramMethod[Message]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the forwarded message from forwarding and saving""" diff --git a/aiogram/methods/send_animation.py b/aiogram/methods/send_animation.py index 14029d25..505a67d6 100644 --- a/aiogram/methods/send_animation.py +++ b/aiogram/methods/send_animation.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -42,7 +44,7 @@ class SendAnimation(TelegramMethod[Message]): """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 . :ref:`More information on Sending Files » `""" caption: Optional[str] = None """Animation caption (may also be used when resending animation by *file_id*), 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the animation caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -50,7 +52,7 @@ class SendAnimation(TelegramMethod[Message]): """Pass :code:`True` if the animation needs to be covered with a spoiler animation""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_audio.py b/aiogram/methods/send_audio.py index e0f4b900..e0917e0a 100644 --- a/aiogram/methods/send_audio.py +++ b/aiogram/methods/send_audio.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, Union +from typing import List, Optional, Union + +import msgspec from ..types import ( UNSET_PARSE_MODE, @@ -35,7 +37,7 @@ class SendAudio(TelegramMethod[Message]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" caption: Optional[str] = None """Audio caption, 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the audio caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -49,7 +51,7 @@ class SendAudio(TelegramMethod[Message]): """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 . :ref:`More information on Sending Files » `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_contact.py b/aiogram/methods/send_contact.py index 35473338..ef6204ff 100644 --- a/aiogram/methods/send_contact.py +++ b/aiogram/methods/send_contact.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Union + +import msgspec from ..types import ( ForceReply, @@ -37,7 +39,7 @@ class SendContact(TelegramMethod[Message]): """Additional data about the contact in the form of a `vCard `_, 0-2048 bytes""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_dice.py b/aiogram/methods/send_dice.py index 9fa2ce70..7f8d4c34 100644 --- a/aiogram/methods/send_dice.py +++ b/aiogram/methods/send_dice.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Union + +import msgspec from ..types import ( ForceReply, @@ -31,7 +33,7 @@ class SendDice(TelegramMethod[Message]): """Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '⚽', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '⚽', and values 1-64 for '🎰'. Defaults to '🎲'""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_document.py b/aiogram/methods/send_document.py index 5c83beb3..8049c42a 100644 --- a/aiogram/methods/send_document.py +++ b/aiogram/methods/send_document.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -36,7 +38,7 @@ class SendDocument(TelegramMethod[Message]): """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 . :ref:`More information on Sending Files » `""" caption: Optional[str] = None """Document caption (may also be used when resending documents by *file_id*), 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the document caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -44,7 +46,7 @@ class SendDocument(TelegramMethod[Message]): """Disables automatic server-side content type detection for files uploaded using multipart/form-data""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_game.py b/aiogram/methods/send_game.py index ab55a459..f33c392f 100644 --- a/aiogram/methods/send_game.py +++ b/aiogram/methods/send_game.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional +import msgspec + from ..types import InlineKeyboardMarkup, Message from ..types.base import UNSET_PROTECT_CONTENT from .base import TelegramMethod @@ -25,7 +27,7 @@ class SendGame(TelegramMethod[Message]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_invoice.py b/aiogram/methods/send_invoice.py index d08002a9..921d7b14 100644 --- a/aiogram/methods/send_invoice.py +++ b/aiogram/methods/send_invoice.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import InlineKeyboardMarkup, LabeledPrice, Message from ..types.base import UNSET_PROTECT_CONTENT from .base import TelegramMethod @@ -65,7 +67,7 @@ class SendInvoice(TelegramMethod[Message]): """Pass :code:`True` if the final price depends on the shipping method""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_location.py b/aiogram/methods/send_location.py index 845b4741..870fd8bb 100644 --- a/aiogram/methods/send_location.py +++ b/aiogram/methods/send_location.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union +import msgspec + from ..types import ( ForceReply, InlineKeyboardMarkup, @@ -41,7 +43,7 @@ class SendLocation(TelegramMethod[Message]): """For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_media_group.py b/aiogram/methods/send_media_group.py index fd8dbb25..ac13a3ab 100644 --- a/aiogram/methods/send_media_group.py +++ b/aiogram/methods/send_media_group.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( InputMediaAudio, InputMediaDocument, @@ -31,7 +33,7 @@ class SendMediaGroup(TelegramMethod[List[Message]]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" disable_notification: Optional[bool] = None """Sends messages `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent messages from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the messages are a reply, ID of the original message""" diff --git a/aiogram/methods/send_message.py b/aiogram/methods/send_message.py index 9b7dae7b..79ff2f05 100644 --- a/aiogram/methods/send_message.py +++ b/aiogram/methods/send_message.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -31,15 +33,17 @@ class SendMessage(TelegramMethod[Message]): """Text of the message to be sent, 1-4096 characters after entities parsing""" message_thread_id: Optional[int] = None """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the message text. See `formatting options `_ for more details.""" entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in message text, which can be specified instead of *parse_mode*""" - disable_web_page_preview: Optional[bool] = UNSET_DISABLE_WEB_PAGE_PREVIEW + disable_web_page_preview: Optional[bool] = msgspec.field( + default_factory=lambda: UNSET_DISABLE_WEB_PAGE_PREVIEW + ) """Disables link previews for links in this message""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_photo.py b/aiogram/methods/send_photo.py index 236d8710..26b3ffe9 100644 --- a/aiogram/methods/send_photo.py +++ b/aiogram/methods/send_photo.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -34,7 +36,7 @@ class SendPhoto(TelegramMethod[Message]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" caption: Optional[str] = None """Photo caption (may also be used when resending photos by *file_id*), 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the photo caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -42,7 +44,7 @@ class SendPhoto(TelegramMethod[Message]): """Pass :code:`True` if the photo needs to be covered with a spoiler animation""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_poll.py b/aiogram/methods/send_poll.py index fca3dc58..97e056ec 100644 --- a/aiogram/methods/send_poll.py +++ b/aiogram/methods/send_poll.py @@ -3,6 +3,8 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -44,7 +46,7 @@ class SendPoll(TelegramMethod[Message]): """0-based identifier of the correct answer option, required for polls in quiz mode""" explanation: Optional[str] = None """Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing""" - explanation_parse_mode: Optional[str] = UNSET_PARSE_MODE + explanation_parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the explanation. See `formatting options `_ for more details.""" explanation_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the poll explanation, which can be specified instead of *parse_mode*""" @@ -56,7 +58,7 @@ class SendPoll(TelegramMethod[Message]): """Pass :code:`True` if the poll needs to be immediately closed. This can be useful for poll preview.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_sticker.py b/aiogram/methods/send_sticker.py index 8429b105..68f69699 100644 --- a/aiogram/methods/send_sticker.py +++ b/aiogram/methods/send_sticker.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union +import msgspec + from ..types import ( ForceReply, InlineKeyboardMarkup, @@ -34,7 +36,7 @@ class SendSticker(TelegramMethod[Message]): """Emoji associated with the sticker; only for just uploaded stickers""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_venue.py b/aiogram/methods/send_venue.py index d0859770..53f66391 100644 --- a/aiogram/methods/send_venue.py +++ b/aiogram/methods/send_venue.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union +import msgspec + from ..types import ( ForceReply, InlineKeyboardMarkup, @@ -45,7 +47,7 @@ class SendVenue(TelegramMethod[Message]): """Google Places type of the venue. (See `supported types `_.)""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_video.py b/aiogram/methods/send_video.py index a894e335..e53ed5d0 100644 --- a/aiogram/methods/send_video.py +++ b/aiogram/methods/send_video.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -42,7 +44,7 @@ class SendVideo(TelegramMethod[Message]): """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 . :ref:`More information on Sending Files » `""" caption: Optional[str] = None """Video caption (may also be used when resending videos by *file_id*), 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the video caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -52,7 +54,7 @@ class SendVideo(TelegramMethod[Message]): """Pass :code:`True` if the uploaded video is suitable for streaming""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_video_note.py b/aiogram/methods/send_video_note.py index 761c91a2..5aa10b53 100644 --- a/aiogram/methods/send_video_note.py +++ b/aiogram/methods/send_video_note.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union +import msgspec + from ..types import ( ForceReply, InlineKeyboardMarkup, @@ -38,7 +40,7 @@ class SendVideoNote(TelegramMethod[Message]): """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 . :ref:`More information on Sending Files » `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/methods/send_voice.py b/aiogram/methods/send_voice.py index ae692ad6..7a6f8cb5 100644 --- a/aiogram/methods/send_voice.py +++ b/aiogram/methods/send_voice.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union +import msgspec + from ..types import ( UNSET_PARSE_MODE, ForceReply, @@ -34,7 +36,7 @@ class SendVoice(TelegramMethod[Message]): """Unique identifier for the target message thread (topic) of the forum; for forum supergroups only""" caption: Optional[str] = None """Voice message caption, 0-1024 characters after entities parsing""" - parse_mode: Optional[str] = UNSET_PARSE_MODE + parse_mode: Optional[str] = msgspec.field(default_factory=lambda: UNSET_PARSE_MODE) """Mode for parsing entities in the voice message caption. See `formatting options `_ for more details.""" caption_entities: Optional[List[MessageEntity]] = None """A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*""" @@ -42,7 +44,7 @@ class SendVoice(TelegramMethod[Message]): """Duration of the voice message in seconds""" disable_notification: Optional[bool] = None """Sends the message `silently `_. Users will receive a notification with no sound.""" - protect_content: Optional[bool] = UNSET_PROTECT_CONTENT + protect_content: Optional[bool] = msgspec.field(default_factory=lambda: UNSET_PROTECT_CONTENT) """Protects the contents of the sent message from forwarding and saving""" reply_to_message_id: Optional[int] = None """If the message is a reply, ID of the original message""" diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index 15354156..8e8146ca 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class Animation(TelegramObject): diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 0a147c36..4c831f42 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class Audio(TelegramObject): diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 707e328c..12e094de 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,26 +1,19 @@ -import datetime from typing import Any from unittest.mock import sentinel -from pydantic import BaseModel, Extra +import msgspec from aiogram.utils.mixins import ContextInstanceMixin -class TelegramObject(ContextInstanceMixin["TelegramObject"], BaseModel): - class Config: - use_enum_values = True - orm_mode = True - extra = Extra.allow - validate_assignment = True - allow_mutation = False - allow_population_by_field_name = True - json_encoders = {datetime.datetime: lambda dt: int(dt.timestamp())} +class TelegramObject( + ContextInstanceMixin["TelegramObject"], msgspec.Struct, omit_defaults=True, weakref=True +): + ... class MutableTelegramObject(TelegramObject): - class Config: - allow_mutation = True + ... # special sentinel object which used in situation when None might be a useful value diff --git a/aiogram/types/bot_command_scope_all_chat_administrators.py b/aiogram/types/bot_command_scope_all_chat_administrators.py index e9f6a969..cb1b92d2 100644 --- a/aiogram/types/bot_command_scope_all_chat_administrators.py +++ b/aiogram/types/bot_command_scope_all_chat_administrators.py @@ -1,17 +1,17 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeAllChatAdministrators(BotCommandScope): +class BotCommandScopeAllChatAdministrators(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering all group and supergroup chat administrators. Source: https://core.telegram.org/bots/api#botcommandscopeallchatadministrators """ - type: str = Field(BotCommandScopeType.ALL_CHAT_ADMINISTRATORS, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.ALL_CHAT_ADMINISTRATORS) """Scope type, must be *all_chat_administrators*""" diff --git a/aiogram/types/bot_command_scope_all_group_chats.py b/aiogram/types/bot_command_scope_all_group_chats.py index f9675ad6..12592895 100644 --- a/aiogram/types/bot_command_scope_all_group_chats.py +++ b/aiogram/types/bot_command_scope_all_group_chats.py @@ -1,17 +1,17 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeAllGroupChats(BotCommandScope): +class BotCommandScopeAllGroupChats(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering all group and supergroup chats. Source: https://core.telegram.org/bots/api#botcommandscopeallgroupchats """ - type: str = Field(BotCommandScopeType.ALL_GROUP_CHATS, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.ALL_GROUP_CHATS) """Scope type, must be *all_group_chats*""" diff --git a/aiogram/types/bot_command_scope_all_private_chats.py b/aiogram/types/bot_command_scope_all_private_chats.py index f13e2866..d13b502b 100644 --- a/aiogram/types/bot_command_scope_all_private_chats.py +++ b/aiogram/types/bot_command_scope_all_private_chats.py @@ -1,17 +1,17 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeAllPrivateChats(BotCommandScope): +class BotCommandScopeAllPrivateChats(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering all private chats. Source: https://core.telegram.org/bots/api#botcommandscopeallprivatechats """ - type: str = Field(BotCommandScopeType.ALL_PRIVATE_CHATS, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.ALL_PRIVATE_CHATS) """Scope type, must be *all_private_chats*""" diff --git a/aiogram/types/bot_command_scope_chat.py b/aiogram/types/bot_command_scope_chat.py index d96bc6f4..be3775cf 100644 --- a/aiogram/types/bot_command_scope_chat.py +++ b/aiogram/types/bot_command_scope_chat.py @@ -2,20 +2,20 @@ from __future__ import annotations from typing import Union -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeChat(BotCommandScope): +class BotCommandScopeChat(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering a specific chat. Source: https://core.telegram.org/bots/api#botcommandscopechat """ - type: str = Field(BotCommandScopeType.CHAT, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.CHAT) """Scope type, must be *chat*""" chat_id: Union[int, str] """Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)""" diff --git a/aiogram/types/bot_command_scope_chat_administrators.py b/aiogram/types/bot_command_scope_chat_administrators.py index 824dc5a1..8443e963 100644 --- a/aiogram/types/bot_command_scope_chat_administrators.py +++ b/aiogram/types/bot_command_scope_chat_administrators.py @@ -2,20 +2,20 @@ from __future__ import annotations from typing import Union -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeChatAdministrators(BotCommandScope): +class BotCommandScopeChatAdministrators(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering all administrators of a specific group or supergroup chat. Source: https://core.telegram.org/bots/api#botcommandscopechatadministrators """ - type: str = Field(BotCommandScopeType.CHAT_ADMINISTRATORS, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.CHAT_ADMINISTRATORS) """Scope type, must be *chat_administrators*""" chat_id: Union[int, str] """Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)""" diff --git a/aiogram/types/bot_command_scope_chat_member.py b/aiogram/types/bot_command_scope_chat_member.py index e9fb0dda..79e6cb1f 100644 --- a/aiogram/types/bot_command_scope_chat_member.py +++ b/aiogram/types/bot_command_scope_chat_member.py @@ -2,20 +2,20 @@ from __future__ import annotations from typing import Union -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeChatMember(BotCommandScope): +class BotCommandScopeChatMember(BotCommandScope, kw_only=True): """ Represents the `scope `_ of bot commands, covering a specific member of a group or supergroup chat. Source: https://core.telegram.org/bots/api#botcommandscopechatmember """ - type: str = Field(BotCommandScopeType.CHAT_MEMBER, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.CHAT_MEMBER) """Scope type, must be *chat_member*""" chat_id: Union[int, str] """Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)""" diff --git a/aiogram/types/bot_command_scope_default.py b/aiogram/types/bot_command_scope_default.py index 79825631..3df0da66 100644 --- a/aiogram/types/bot_command_scope_default.py +++ b/aiogram/types/bot_command_scope_default.py @@ -1,17 +1,17 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import BotCommandScopeType from .bot_command_scope import BotCommandScope -class BotCommandScopeDefault(BotCommandScope): +class BotCommandScopeDefault(BotCommandScope, kw_only=True): """ Represents the default `scope `_ of bot commands. Default commands are used if no commands with a `narrower scope `_ are specified for the user. Source: https://core.telegram.org/bots/api#botcommandscopedefault """ - type: str = Field(BotCommandScopeType.DEFAULT, const=True) + type: str = msgspec.field(default_factory=lambda: BotCommandScopeType.DEFAULT) """Scope type, must be *default*""" diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 5812bb13..1ea4c9c9 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -1,18 +1,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, ClassVar, Optional -from pydantic import Field +import msgspec from .base import TelegramObject if TYPE_CHECKING: from ..methods import AnswerCallbackQuery - from .message import Message - from .user import User +from .message import Message +from .user import User -class CallbackQuery(TelegramObject): +class CallbackQuery(TelegramObject, kw_only=True): """ This object represents an incoming callback query from a callback button in an `inline keyboard `_. If the button that originated the query was attached to a message sent by the bot, the field *message* will be present. If the button was attached to a message sent via the bot (in `inline mode `_), the field *inline_message_id* will be present. Exactly one of the fields *data* or *game_short_name* will be present. @@ -23,7 +23,7 @@ class CallbackQuery(TelegramObject): id: str """Unique identifier for this query""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """Sender""" chat_instance: str """Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. Useful for high scores in :class:`aiogram.methods.games.Games`.""" @@ -36,6 +36,8 @@ class CallbackQuery(TelegramObject): game_short_name: Optional[str] = None """*Optional*. Short name of a `Game `_ to be returned, serves as the unique identifier for the game""" + _callback_data = None + def answer( self, text: Optional[str] = None, diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 4bb6d688..21f2fe49 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -35,12 +35,14 @@ if TYPE_CHECKING: UnpinAllChatMessages, UnpinChatMessage, ) - from .chat_location import ChatLocation - from .chat_permissions import ChatPermissions - from .chat_photo import ChatPhoto - from .input_file import InputFile from .message import Message +from . import message +from .chat_location import ChatLocation +from .chat_permissions import ChatPermissions +from .chat_photo import ChatPhoto +from .input_file import InputFile + class Chat(TelegramObject): """ @@ -83,7 +85,7 @@ class Chat(TelegramObject): """*Optional*. Description, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" invite_link: Optional[str] = None """*Optional*. Primary invite link, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" - pinned_message: Optional[Message] = None + pinned_message: Optional[message.Message] = None """*Optional*. The most recent pinned message (by sending date). Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" permissions: Optional[ChatPermissions] = None """*Optional*. Default chat member permissions, for groups and supergroups. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" diff --git a/aiogram/types/chat_invite_link.py b/aiogram/types/chat_invite_link.py index 7476f9dc..d482bda2 100644 --- a/aiogram/types/chat_invite_link.py +++ b/aiogram/types/chat_invite_link.py @@ -4,9 +4,7 @@ import datetime from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class ChatInviteLink(TelegramObject): diff --git a/aiogram/types/chat_join_request.py b/aiogram/types/chat_join_request.py index ca4f8bb7..c7cf68f6 100644 --- a/aiogram/types/chat_join_request.py +++ b/aiogram/types/chat_join_request.py @@ -3,20 +3,19 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Any, Optional -from pydantic import Field +import msgspec from .base import TelegramObject if TYPE_CHECKING: from ..methods import ApproveChatJoinRequest, DeclineChatJoinRequest -if TYPE_CHECKING: - from .chat import Chat - from .chat_invite_link import ChatInviteLink - from .user import User +from .chat import Chat +from .chat_invite_link import ChatInviteLink +from .user import User -class ChatJoinRequest(TelegramObject): +class ChatJoinRequest(TelegramObject, kw_only=True): """ Represents a join request sent to a chat. @@ -25,7 +24,7 @@ class ChatJoinRequest(TelegramObject): chat: Chat """Chat to which the request was sent""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """User that sent the join request""" user_chat_id: int """Identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier. The bot can use this identifier for 24 hours to send messages until the join request is processed, assuming no other administrator contacted the user.""" diff --git a/aiogram/types/chat_location.py b/aiogram/types/chat_location.py index 89b28d7c..b428c8bb 100644 --- a/aiogram/types/chat_location.py +++ b/aiogram/types/chat_location.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from .base import TelegramObject - -if TYPE_CHECKING: - from .location import Location +from .location import Location class ChatLocation(TelegramObject): diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 6bf5b79e..c8ec71e5 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -4,9 +4,7 @@ import datetime from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class ChatMember(TelegramObject): diff --git a/aiogram/types/chat_member_administrator.py b/aiogram/types/chat_member_administrator.py index 94fc76c3..9d890875 100644 --- a/aiogram/types/chat_member_administrator.py +++ b/aiogram/types/chat_member_administrator.py @@ -2,23 +2,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberAdministrator(ChatMember): +class ChatMemberAdministrator(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that has some additional privileges. Source: https://core.telegram.org/bots/api#chatmemberadministrator """ - status: str = Field(ChatMemberStatus.ADMINISTRATOR, const=True) + status: str = ChatMemberStatus.ADMINISTRATOR """The member's status in the chat, always 'administrator'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_banned.py b/aiogram/types/chat_member_banned.py index 85c07f51..21b4a596 100644 --- a/aiogram/types/chat_member_banned.py +++ b/aiogram/types/chat_member_banned.py @@ -3,23 +3,19 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberBanned(ChatMember): +class ChatMemberBanned(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that was banned in the chat and can't return to the chat or view chat messages. Source: https://core.telegram.org/bots/api#chatmemberbanned """ - status: str = Field(ChatMemberStatus.KICKED, const=True) + status: str = ChatMemberStatus.KICKED """The member's status in the chat, always 'kicked'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_left.py b/aiogram/types/chat_member_left.py index 6d7968c1..b0c2333d 100644 --- a/aiogram/types/chat_member_left.py +++ b/aiogram/types/chat_member_left.py @@ -2,23 +2,19 @@ from __future__ import annotations from typing import TYPE_CHECKING -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberLeft(ChatMember): +class ChatMemberLeft(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that isn't currently a member of the chat, but may join it themselves. Source: https://core.telegram.org/bots/api#chatmemberleft """ - status: str = Field(ChatMemberStatus.LEFT, const=True) + status: str = ChatMemberStatus.LEFT """The member's status in the chat, always 'left'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_member.py b/aiogram/types/chat_member_member.py index 303a7d9d..3171b1fd 100644 --- a/aiogram/types/chat_member_member.py +++ b/aiogram/types/chat_member_member.py @@ -2,23 +2,19 @@ from __future__ import annotations from typing import TYPE_CHECKING -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberMember(ChatMember): +class ChatMemberMember(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that has no additional privileges or restrictions. Source: https://core.telegram.org/bots/api#chatmembermember """ - status: str = Field(ChatMemberStatus.MEMBER, const=True) + status: str = ChatMemberStatus.MEMBER """The member's status in the chat, always 'member'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_owner.py b/aiogram/types/chat_member_owner.py index e7c64fc1..362cfe3b 100644 --- a/aiogram/types/chat_member_owner.py +++ b/aiogram/types/chat_member_owner.py @@ -2,23 +2,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberOwner(ChatMember): +class ChatMemberOwner(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that owns the chat and has all administrator privileges. Source: https://core.telegram.org/bots/api#chatmemberowner """ - status: str = Field(ChatMemberStatus.CREATOR, const=True) + status: str = ChatMemberStatus.CREATOR """The member's status in the chat, always 'creator'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_restricted.py b/aiogram/types/chat_member_restricted.py index b6f4f556..1aa0ff87 100644 --- a/aiogram/types/chat_member_restricted.py +++ b/aiogram/types/chat_member_restricted.py @@ -3,23 +3,19 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING -from pydantic import Field - from ..enums import ChatMemberStatus from .chat_member import ChatMember - -if TYPE_CHECKING: - from .user import User +from .user import User -class ChatMemberRestricted(ChatMember): +class ChatMemberRestricted(ChatMember, kw_only=True, tag=True): """ Represents a `chat member `_ that is under certain restrictions in the chat. Supergroups only. Source: https://core.telegram.org/bots/api#chatmemberrestricted """ - status: str = Field(ChatMemberStatus.RESTRICTED, const=True) + status: str = ChatMemberStatus.RESTRICTED """The member's status in the chat, always 'restricted'""" user: User """Information about the user""" diff --git a/aiogram/types/chat_member_updated.py b/aiogram/types/chat_member_updated.py index 5931da6f..8b2366f6 100644 --- a/aiogram/types/chat_member_updated.py +++ b/aiogram/types/chat_member_updated.py @@ -3,23 +3,21 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Optional, Union -from pydantic import Field +import msgspec from .base import TelegramObject - -if TYPE_CHECKING: - from .chat import Chat - from .chat_invite_link import ChatInviteLink - from .chat_member_administrator import ChatMemberAdministrator - from .chat_member_banned import ChatMemberBanned - from .chat_member_left import ChatMemberLeft - from .chat_member_member import ChatMemberMember - from .chat_member_owner import ChatMemberOwner - from .chat_member_restricted import ChatMemberRestricted - from .user import User +from .chat import Chat +from .chat_invite_link import ChatInviteLink +from .chat_member_administrator import ChatMemberAdministrator +from .chat_member_banned import ChatMemberBanned +from .chat_member_left import ChatMemberLeft +from .chat_member_member import ChatMemberMember +from .chat_member_owner import ChatMemberOwner +from .chat_member_restricted import ChatMemberRestricted +from .user import User -class ChatMemberUpdated(TelegramObject): +class ChatMemberUpdated(TelegramObject, kw_only=True): """ This object represents changes in the status of a chat member. @@ -28,7 +26,7 @@ class ChatMemberUpdated(TelegramObject): chat: Chat """Chat the user belongs to""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """Performer of the action, which resulted in the change""" date: datetime.datetime """Date the change was done in Unix time""" diff --git a/aiogram/types/chosen_inline_result.py b/aiogram/types/chosen_inline_result.py index 0ec211f7..288b0785 100644 --- a/aiogram/types/chosen_inline_result.py +++ b/aiogram/types/chosen_inline_result.py @@ -2,16 +2,14 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field +import msgspec from .base import TelegramObject - -if TYPE_CHECKING: - from .location import Location - from .user import User +from .location import Location +from .user import User -class ChosenInlineResult(TelegramObject): +class ChosenInlineResult(TelegramObject, kw_only=True): """ Represents a `result `_ of an inline query that was chosen by the user and sent to their chat partner. **Note:** It is necessary to enable `inline feedback `_ via `@BotFather `_ in order to receive these objects in updates. @@ -21,7 +19,7 @@ class ChosenInlineResult(TelegramObject): result_id: str """The unique identifier for the result that was chosen""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """The user that chose the result""" query: str """The query that was used to obtain the result""" diff --git a/aiogram/types/document.py b/aiogram/types/document.py index 837bf4ec..2ae5b01e 100644 --- a/aiogram/types/document.py +++ b/aiogram/types/document.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class Document(TelegramObject): diff --git a/aiogram/types/encrypted_passport_element.py b/aiogram/types/encrypted_passport_element.py index 27506298..fe767b2e 100644 --- a/aiogram/types/encrypted_passport_element.py +++ b/aiogram/types/encrypted_passport_element.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .passport_file import PassportFile +from .passport_file import PassportFile class EncryptedPassportElement(TelegramObject): diff --git a/aiogram/types/force_reply.py b/aiogram/types/force_reply.py index c27af31e..71728a2d 100644 --- a/aiogram/types/force_reply.py +++ b/aiogram/types/force_reply.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import Optional -from pydantic import Field - from .base import MutableTelegramObject @@ -21,7 +19,7 @@ class ForceReply(MutableTelegramObject): Source: https://core.telegram.org/bots/api#forcereply """ - force_reply: bool = Field(True, const=True) + force_reply: bool = True """Shows reply interface to the user, as if they manually selected the bot's message and tapped 'Reply'""" input_field_placeholder: Optional[str] = None """*Optional*. The placeholder to be shown in the input field when the reply is active; 1-64 characters""" diff --git a/aiogram/types/game.py b/aiogram/types/game.py index f9b03bd1..03c2b8cc 100644 --- a/aiogram/types/game.py +++ b/aiogram/types/game.py @@ -2,12 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional +from .animation import Animation from .base import TelegramObject - -if TYPE_CHECKING: - from .animation import Animation - from .message_entity import MessageEntity - from .photo_size import PhotoSize +from .message_entity import MessageEntity +from .photo_size import PhotoSize class Game(TelegramObject): diff --git a/aiogram/types/game_high_score.py b/aiogram/types/game_high_score.py index 30ec941a..bbc3fded 100644 --- a/aiogram/types/game_high_score.py +++ b/aiogram/types/game_high_score.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class GameHighScore(TelegramObject): diff --git a/aiogram/types/inline_keyboard_button.py b/aiogram/types/inline_keyboard_button.py index 977fae8d..b46a55cc 100644 --- a/aiogram/types/inline_keyboard_button.py +++ b/aiogram/types/inline_keyboard_button.py @@ -3,12 +3,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import MutableTelegramObject - -if TYPE_CHECKING: - from .callback_game import CallbackGame - from .login_url import LoginUrl - from .switch_inline_query_chosen_chat import SwitchInlineQueryChosenChat - from .web_app_info import WebAppInfo +from .callback_game import CallbackGame +from .login_url import LoginUrl +from .switch_inline_query_chosen_chat import SwitchInlineQueryChosenChat +from .web_app_info import WebAppInfo class InlineKeyboardButton(MutableTelegramObject): diff --git a/aiogram/types/inline_keyboard_markup.py b/aiogram/types/inline_keyboard_markup.py index 473e292c..4318970a 100644 --- a/aiogram/types/inline_keyboard_markup.py +++ b/aiogram/types/inline_keyboard_markup.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List from .base import MutableTelegramObject - -if TYPE_CHECKING: - from .inline_keyboard_button import InlineKeyboardButton +from .inline_keyboard_button import InlineKeyboardButton class InlineKeyboardMarkup(MutableTelegramObject): diff --git a/aiogram/types/inline_query.py b/aiogram/types/inline_query.py index 55eab72f..6f632b1e 100644 --- a/aiogram/types/inline_query.py +++ b/aiogram/types/inline_query.py @@ -2,19 +2,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, List, Optional -from pydantic import Field +import msgspec from .base import TelegramObject if TYPE_CHECKING: from ..methods import AnswerInlineQuery - from .inline_query_result import InlineQueryResult - from .inline_query_results_button import InlineQueryResultsButton - from .location import Location - from .user import User +from .inline_query_result import InlineQueryResult +from .inline_query_results_button import InlineQueryResultsButton +from .location import Location +from .user import User -class InlineQuery(TelegramObject): +class InlineQuery(TelegramObject, kw_only=True): """ This object represents an incoming inline query. When the user sends an empty query, your bot could return some default or trending results. @@ -23,7 +23,7 @@ class InlineQuery(TelegramObject): id: str """Unique identifier for this query""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """Sender""" query: str """Text of the query (up to 256 characters)""" diff --git a/aiogram/types/inline_query_result_article.py b/aiogram/types/inline_query_result_article.py index dd65b595..8d38b37f 100644 --- a/aiogram/types/inline_query_result_article.py +++ b/aiogram/types/inline_query_result_article.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field +import msgspec from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -12,14 +12,14 @@ if TYPE_CHECKING: from .input_message_content import InputMessageContent -class InlineQueryResultArticle(InlineQueryResult): +class InlineQueryResultArticle(InlineQueryResult, kw_only=True): """ Represents a link to an article or web page. Source: https://core.telegram.org/bots/api#inlinequeryresultarticle """ - type: str = Field(InlineQueryResultType.ARTICLE, const=True) + type: str = msgspec.field(default_factory=lambda: InlineQueryResultType.ARTICLE) """Type of the result, must be *article*""" id: str """Unique identifier for this result, 1-64 Bytes""" diff --git a/aiogram/types/inline_query_result_audio.py b/aiogram/types/inline_query_result_audio.py index 3b830fc8..ea89ab33 100644 --- a/aiogram/types/inline_query_result_audio.py +++ b/aiogram/types/inline_query_result_audio.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field +import msgspec from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE @@ -14,7 +14,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultAudio(InlineQueryResult): +class InlineQueryResultAudio(InlineQueryResult, kw_only=True): """ Represents a link to an MP3 audio file. By default, this audio file will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the audio. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -22,7 +22,7 @@ class InlineQueryResultAudio(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultaudio """ - type: str = Field(InlineQueryResultType.AUDIO, const=True) + type: str = msgspec.field(default_factory=lambda: InlineQueryResultType.AUDIO) """Type of the result, must be *audio*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_audio.py b/aiogram/types/inline_query_result_cached_audio.py index 8358f723..33b00365 100644 --- a/aiogram/types/inline_query_result_cached_audio.py +++ b/aiogram/types/inline_query_result_cached_audio.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field +import msgspec from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE @@ -14,7 +14,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedAudio(InlineQueryResult): +class InlineQueryResultCachedAudio(InlineQueryResult, kw_only=True): """ Represents a link to an MP3 audio file stored on the Telegram servers. By default, this audio file will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the audio. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -22,7 +22,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultcachedaudio """ - type: str = Field(InlineQueryResultType.AUDIO, const=True) + type: str = msgspec.field(default_factory=lambda: InlineQueryResultType.AUDIO) """Type of the result, must be *audio*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_document.py b/aiogram/types/inline_query_result_cached_document.py index 5513971f..32d52622 100644 --- a/aiogram/types/inline_query_result_cached_document.py +++ b/aiogram/types/inline_query_result_cached_document.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,7 +12,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedDocument(InlineQueryResult): +class InlineQueryResultCachedDocument(InlineQueryResult, kw_only=True): """ Represents a link to a file stored on the Telegram servers. By default, this file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the file. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -22,7 +20,7 @@ class InlineQueryResultCachedDocument(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultcacheddocument """ - type: str = Field(InlineQueryResultType.DOCUMENT, const=True) + type: str = InlineQueryResultType.DOCUMENT """Type of the result, must be *document*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_gif.py b/aiogram/types/inline_query_result_cached_gif.py index 7aa5c535..bf1ec0ec 100644 --- a/aiogram/types/inline_query_result_cached_gif.py +++ b/aiogram/types/inline_query_result_cached_gif.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedGif(InlineQueryResult): +class InlineQueryResultCachedGif(InlineQueryResult, kw_only=True): """ Represents a link to an animated GIF file stored on the Telegram servers. By default, this animated GIF file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with specified content instead of the animation. Source: https://core.telegram.org/bots/api#inlinequeryresultcachedgif """ - type: str = Field(InlineQueryResultType.GIF, const=True) + type: str = InlineQueryResultType.GIF """Type of the result, must be *gif*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_mpeg4_gif.py b/aiogram/types/inline_query_result_cached_mpeg4_gif.py index 539de025..82257e32 100644 --- a/aiogram/types/inline_query_result_cached_mpeg4_gif.py +++ b/aiogram/types/inline_query_result_cached_mpeg4_gif.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): +class InlineQueryResultCachedMpeg4Gif(InlineQueryResult, kw_only=True): """ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. By default, this animated MPEG-4 file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the animation. Source: https://core.telegram.org/bots/api#inlinequeryresultcachedmpeg4gif """ - type: str = Field(InlineQueryResultType.MPEG4_GIF, const=True) + type: str = InlineQueryResultType.MPEG4_GIF """Type of the result, must be *mpeg4_gif*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_photo.py b/aiogram/types/inline_query_result_cached_photo.py index fc892826..64b0e420 100644 --- a/aiogram/types/inline_query_result_cached_photo.py +++ b/aiogram/types/inline_query_result_cached_photo.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedPhoto(InlineQueryResult): +class InlineQueryResultCachedPhoto(InlineQueryResult, kw_only=True): """ Represents a link to a photo stored on the Telegram servers. By default, this photo will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the photo. Source: https://core.telegram.org/bots/api#inlinequeryresultcachedphoto """ - type: str = Field(InlineQueryResultType.PHOTO, const=True) + type: str = InlineQueryResultType.PHOTO """Type of the result, must be *photo*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_sticker.py b/aiogram/types/inline_query_result_cached_sticker.py index 3d75c29c..2d5f6e10 100644 --- a/aiogram/types/inline_query_result_cached_sticker.py +++ b/aiogram/types/inline_query_result_cached_sticker.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -12,7 +10,7 @@ if TYPE_CHECKING: from .input_message_content import InputMessageContent -class InlineQueryResultCachedSticker(InlineQueryResult): +class InlineQueryResultCachedSticker(InlineQueryResult, kw_only=True): """ Represents a link to a sticker stored on the Telegram servers. By default, this sticker will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the sticker. **Note:** This will only work in Telegram versions released after 9 April, 2016 for static stickers and after 06 July, 2019 for `animated stickers `_. Older clients will ignore them. @@ -20,7 +18,7 @@ class InlineQueryResultCachedSticker(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultcachedsticker """ - type: str = Field(InlineQueryResultType.STICKER, const=True) + type: str = InlineQueryResultType.STICKER """Type of the result, must be *sticker*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_video.py b/aiogram/types/inline_query_result_cached_video.py index acda3962..ba7f3042 100644 --- a/aiogram/types/inline_query_result_cached_video.py +++ b/aiogram/types/inline_query_result_cached_video.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedVideo(InlineQueryResult): +class InlineQueryResultCachedVideo(InlineQueryResult, kw_only=True): """ Represents a link to a video file stored on the Telegram servers. By default, this video file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the video. Source: https://core.telegram.org/bots/api#inlinequeryresultcachedvideo """ - type: str = Field(InlineQueryResultType.VIDEO, const=True) + type: str = InlineQueryResultType.VIDEO """Type of the result, must be *video*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_cached_voice.py b/aiogram/types/inline_query_result_cached_voice.py index 3f03b2fe..b5bca0ae 100644 --- a/aiogram/types/inline_query_result_cached_voice.py +++ b/aiogram/types/inline_query_result_cached_voice.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,7 +12,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultCachedVoice(InlineQueryResult): +class InlineQueryResultCachedVoice(InlineQueryResult, kw_only=True): """ Represents a link to a voice message stored on the Telegram servers. By default, this voice message will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the voice message. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -22,7 +20,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultcachedvoice """ - type: str = Field(InlineQueryResultType.VOICE, const=True) + type: str = InlineQueryResultType.VOICE """Type of the result, must be *voice*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_contact.py b/aiogram/types/inline_query_result_contact.py index 0f88bb9f..f56c4d17 100644 --- a/aiogram/types/inline_query_result_contact.py +++ b/aiogram/types/inline_query_result_contact.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -12,7 +10,7 @@ if TYPE_CHECKING: from .input_message_content import InputMessageContent -class InlineQueryResultContact(InlineQueryResult): +class InlineQueryResultContact(InlineQueryResult, kw_only=True): """ Represents a contact with a phone number. By default, this contact will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the contact. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -20,7 +18,7 @@ class InlineQueryResultContact(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultcontact """ - type: str = Field(InlineQueryResultType.CONTACT, const=True) + type: str = InlineQueryResultType.CONTACT """Type of the result, must be *contact*""" id: str """Unique identifier for this result, 1-64 Bytes""" diff --git a/aiogram/types/inline_query_result_document.py b/aiogram/types/inline_query_result_document.py index 7698e4c6..aafc165c 100644 --- a/aiogram/types/inline_query_result_document.py +++ b/aiogram/types/inline_query_result_document.py @@ -2,19 +2,17 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field +import msgspec from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE +from .inline_keyboard_markup import InlineKeyboardMarkup from .inline_query_result import InlineQueryResult - -if TYPE_CHECKING: - from .inline_keyboard_markup import InlineKeyboardMarkup - from .input_message_content import InputMessageContent - from .message_entity import MessageEntity +from .input_message_content import InputMessageContent +from .message_entity import MessageEntity -class InlineQueryResultDocument(InlineQueryResult): +class InlineQueryResultDocument(InlineQueryResult, kw_only=True): """ Represents a link to a file. By default, this file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the file. Currently, only **.PDF** and **.ZIP** files can be sent using this method. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -22,7 +20,7 @@ class InlineQueryResultDocument(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultdocument """ - type: str = Field(InlineQueryResultType.DOCUMENT, const=True) + type: str = msgspec.field(default_factory=lambda: InlineQueryResultType.DOCUMENT) """Type of the result, must be *document*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_game.py b/aiogram/types/inline_query_result_game.py index e4d92a61..13efae3a 100644 --- a/aiogram/types/inline_query_result_game.py +++ b/aiogram/types/inline_query_result_game.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -11,7 +9,7 @@ if TYPE_CHECKING: from .inline_keyboard_markup import InlineKeyboardMarkup -class InlineQueryResultGame(InlineQueryResult): +class InlineQueryResultGame(InlineQueryResult, kw_only=True): """ Represents a `Game `_. **Note:** This will only work in Telegram versions released after October 1, 2016. Older clients will not display any inline results if a game result is among them. @@ -19,7 +17,7 @@ class InlineQueryResultGame(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultgame """ - type: str = Field(InlineQueryResultType.GAME, const=True) + type: str = InlineQueryResultType.GAME """Type of the result, must be *game*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_gif.py b/aiogram/types/inline_query_result_gif.py index b38cfa1a..bad089b5 100644 --- a/aiogram/types/inline_query_result_gif.py +++ b/aiogram/types/inline_query_result_gif.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultGif(InlineQueryResult): +class InlineQueryResultGif(InlineQueryResult, kw_only=True): """ Represents a link to an animated GIF file. By default, this animated GIF file will be sent by the user with optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the animation. Source: https://core.telegram.org/bots/api#inlinequeryresultgif """ - type: str = Field(InlineQueryResultType.GIF, const=True) + type: str = InlineQueryResultType.GIF """Type of the result, must be *gif*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_location.py b/aiogram/types/inline_query_result_location.py index aa1f6d88..02f5c5c4 100644 --- a/aiogram/types/inline_query_result_location.py +++ b/aiogram/types/inline_query_result_location.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field +import msgspec from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -12,7 +12,7 @@ if TYPE_CHECKING: from .input_message_content import InputMessageContent -class InlineQueryResultLocation(InlineQueryResult): +class InlineQueryResultLocation(InlineQueryResult, kw_only=True): """ Represents a location on a map. By default, the location will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the location. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -20,7 +20,7 @@ class InlineQueryResultLocation(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultlocation """ - type: str = Field(InlineQueryResultType.LOCATION, const=True) + type: str = msgspec.field(default_factory=lambda: InlineQueryResultType.LOCATION) """Type of the result, must be *location*""" id: str """Unique identifier for this result, 1-64 Bytes""" diff --git a/aiogram/types/inline_query_result_mpeg4_gif.py b/aiogram/types/inline_query_result_mpeg4_gif.py index 06c73620..cd1c24e0 100644 --- a/aiogram/types/inline_query_result_mpeg4_gif.py +++ b/aiogram/types/inline_query_result_mpeg4_gif.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultMpeg4Gif(InlineQueryResult): +class InlineQueryResultMpeg4Gif(InlineQueryResult, kw_only=True): """ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). By default, this animated MPEG-4 file will be sent by the user with optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the animation. Source: https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif """ - type: str = Field(InlineQueryResultType.MPEG4_GIF, const=True) + type: str = InlineQueryResultType.MPEG4_GIF """Type of the result, must be *mpeg4_gif*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_photo.py b/aiogram/types/inline_query_result_photo.py index 6ed85276..4e10fec6 100644 --- a/aiogram/types/inline_query_result_photo.py +++ b/aiogram/types/inline_query_result_photo.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,14 +12,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultPhoto(InlineQueryResult): +class InlineQueryResultPhoto(InlineQueryResult, kw_only=True): """ Represents a link to a photo. By default, this photo will be sent by the user with optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the photo. Source: https://core.telegram.org/bots/api#inlinequeryresultphoto """ - type: str = Field(InlineQueryResultType.PHOTO, const=True) + type: str = InlineQueryResultType.PHOTO """Type of the result, must be *photo*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_venue.py b/aiogram/types/inline_query_result_venue.py index 0c92a008..4b44c2a5 100644 --- a/aiogram/types/inline_query_result_venue.py +++ b/aiogram/types/inline_query_result_venue.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .inline_query_result import InlineQueryResult @@ -12,7 +10,7 @@ if TYPE_CHECKING: from .input_message_content import InputMessageContent -class InlineQueryResultVenue(InlineQueryResult): +class InlineQueryResultVenue(InlineQueryResult, kw_only=True): """ Represents a venue. By default, the venue will be sent by the user. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the venue. **Note:** This will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. @@ -20,7 +18,7 @@ class InlineQueryResultVenue(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultvenue """ - type: str = Field(InlineQueryResultType.VENUE, const=True) + type: str = InlineQueryResultType.VENUE """Type of the result, must be *venue*""" id: str """Unique identifier for this result, 1-64 Bytes""" diff --git a/aiogram/types/inline_query_result_video.py b/aiogram/types/inline_query_result_video.py index ba14d066..d73ee48a 100644 --- a/aiogram/types/inline_query_result_video.py +++ b/aiogram/types/inline_query_result_video.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,7 +12,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultVideo(InlineQueryResult): +class InlineQueryResultVideo(InlineQueryResult, kw_only=True): """ Represents a link to a page containing an embedded video player or a video file. By default, this video file will be sent by the user with an optional caption. Alternatively, you can use *input_message_content* to send a message with the specified content instead of the video. @@ -23,7 +21,7 @@ class InlineQueryResultVideo(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultvideo """ - type: str = Field(InlineQueryResultType.VIDEO, const=True) + type: str = InlineQueryResultType.VIDEO """Type of the result, must be *video*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_result_voice.py b/aiogram/types/inline_query_result_voice.py index 7de1f6cf..ef5ce3c9 100644 --- a/aiogram/types/inline_query_result_voice.py +++ b/aiogram/types/inline_query_result_voice.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field - from ..enums import InlineQueryResultType from .base import UNSET_PARSE_MODE from .inline_query_result import InlineQueryResult @@ -14,7 +12,7 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InlineQueryResultVoice(InlineQueryResult): +class InlineQueryResultVoice(InlineQueryResult, kw_only=True): """ 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. @@ -22,7 +20,7 @@ class InlineQueryResultVoice(InlineQueryResult): Source: https://core.telegram.org/bots/api#inlinequeryresultvoice """ - type: str = Field(InlineQueryResultType.VOICE, const=True) + type: str = InlineQueryResultType.VOICE """Type of the result, must be *voice*""" id: str """Unique identifier for this result, 1-64 bytes""" diff --git a/aiogram/types/inline_query_results_button.py b/aiogram/types/inline_query_results_button.py index 4a8f59df..7758838b 100644 --- a/aiogram/types/inline_query_results_button.py +++ b/aiogram/types/inline_query_results_button.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .web_app_info import WebAppInfo +from .web_app_info import WebAppInfo class InlineQueryResultsButton(TelegramObject): diff --git a/aiogram/types/input_media_animation.py b/aiogram/types/input_media_animation.py index eabc4b5e..700d7c44 100644 --- a/aiogram/types/input_media_animation.py +++ b/aiogram/types/input_media_animation.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union -from pydantic import Field - from ..enums import InputMediaType from .base import UNSET_PARSE_MODE from .input_media import InputMedia @@ -13,14 +11,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InputMediaAnimation(InputMedia): +class InputMediaAnimation(InputMedia, kw_only=True): """ Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. Source: https://core.telegram.org/bots/api#inputmediaanimation """ - type: str = Field(InputMediaType.ANIMATION, const=True) + type: str = InputMediaType.ANIMATION """Type of the result, must be *animation*""" media: 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. :ref:`More information on Sending Files » `""" diff --git a/aiogram/types/input_media_audio.py b/aiogram/types/input_media_audio.py index 8de62a6f..47d8bb29 100644 --- a/aiogram/types/input_media_audio.py +++ b/aiogram/types/input_media_audio.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union -from pydantic import Field - from ..enums import InputMediaType from .base import UNSET_PARSE_MODE from .input_media import InputMedia @@ -13,14 +11,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InputMediaAudio(InputMedia): +class InputMediaAudio(InputMedia, kw_only=True): """ Represents an audio file to be treated as music to be sent. Source: https://core.telegram.org/bots/api#inputmediaaudio """ - type: str = Field(InputMediaType.AUDIO, const=True) + type: str = InputMediaType.AUDIO """Type of the result, must be *audio*""" media: 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. :ref:`More information on Sending Files » `""" diff --git a/aiogram/types/input_media_document.py b/aiogram/types/input_media_document.py index 9ef4d52f..e31b5052 100644 --- a/aiogram/types/input_media_document.py +++ b/aiogram/types/input_media_document.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union -from pydantic import Field - from ..enums import InputMediaType from .base import UNSET_PARSE_MODE from .input_media import InputMedia @@ -13,14 +11,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InputMediaDocument(InputMedia): +class InputMediaDocument(InputMedia, kw_only=True): """ Represents a general file to be sent. Source: https://core.telegram.org/bots/api#inputmediadocument """ - type: str = Field(InputMediaType.DOCUMENT, const=True) + type: str = InputMediaType.DOCUMENT """Type of the result, must be *document*""" media: 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. :ref:`More information on Sending Files » `""" diff --git a/aiogram/types/input_media_photo.py b/aiogram/types/input_media_photo.py index 0f9500e8..211a31c6 100644 --- a/aiogram/types/input_media_photo.py +++ b/aiogram/types/input_media_photo.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union -from pydantic import Field - from ..enums import InputMediaType from .base import UNSET_PARSE_MODE from .input_media import InputMedia @@ -13,14 +11,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InputMediaPhoto(InputMedia): +class InputMediaPhoto(InputMedia, kw_only=True): """ Represents a photo to be sent. Source: https://core.telegram.org/bots/api#inputmediaphoto """ - type: str = Field(InputMediaType.PHOTO, const=True) + type: str = InputMediaType.PHOTO """Type of the result, must be *photo*""" media: 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. :ref:`More information on Sending Files » `""" diff --git a/aiogram/types/input_media_video.py b/aiogram/types/input_media_video.py index 89d4df91..7892fd81 100644 --- a/aiogram/types/input_media_video.py +++ b/aiogram/types/input_media_video.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union -from pydantic import Field - from ..enums import InputMediaType from .base import UNSET_PARSE_MODE from .input_media import InputMedia @@ -13,14 +11,14 @@ if TYPE_CHECKING: from .message_entity import MessageEntity -class InputMediaVideo(InputMedia): +class InputMediaVideo(InputMedia, kw_only=True): """ Represents a video to be sent. Source: https://core.telegram.org/bots/api#inputmediavideo """ - type: str = Field(InputMediaType.VIDEO, const=True) + type: str = InputMediaType.VIDEO """Type of the result, must be *video*""" media: 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. :ref:`More information on Sending Files » `""" diff --git a/aiogram/types/keyboard_button.py b/aiogram/types/keyboard_button.py index 08d4ce37..7885caf1 100644 --- a/aiogram/types/keyboard_button.py +++ b/aiogram/types/keyboard_button.py @@ -3,12 +3,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import MutableTelegramObject - -if TYPE_CHECKING: - from .keyboard_button_poll_type import KeyboardButtonPollType - from .keyboard_button_request_chat import KeyboardButtonRequestChat - from .keyboard_button_request_user import KeyboardButtonRequestUser - from .web_app_info import WebAppInfo +from .keyboard_button_poll_type import KeyboardButtonPollType +from .keyboard_button_request_chat import KeyboardButtonRequestChat +from .keyboard_button_request_user import KeyboardButtonRequestUser +from .web_app_info import WebAppInfo class KeyboardButton(MutableTelegramObject): diff --git a/aiogram/types/menu_button.py b/aiogram/types/menu_button.py index b37c2390..f473f158 100644 --- a/aiogram/types/menu_button.py +++ b/aiogram/types/menu_button.py @@ -3,12 +3,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import MutableTelegramObject - -if TYPE_CHECKING: - from .web_app_info import WebAppInfo +from .web_app_info import WebAppInfo -class MenuButton(MutableTelegramObject): +class MenuButton(MutableTelegramObject, tag_field="op", tag=str.lower): """ This object describes the bot's menu button in a private chat. It should be one of diff --git a/aiogram/types/menu_button_commands.py b/aiogram/types/menu_button_commands.py index 62a9061c..a8a73b3e 100644 --- a/aiogram/types/menu_button_commands.py +++ b/aiogram/types/menu_button_commands.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import MenuButtonType from .menu_button import MenuButton @@ -13,5 +13,5 @@ class MenuButtonCommands(MenuButton): Source: https://core.telegram.org/bots/api#menubuttoncommands """ - type: str = Field(MenuButtonType.COMMANDS, const=True) + type: str = msgspec.field(default_factory=lambda: MenuButtonType.COMMANDS) """Type of the button, must be *commands*""" diff --git a/aiogram/types/menu_button_default.py b/aiogram/types/menu_button_default.py index dc754ec0..7a4eeda3 100644 --- a/aiogram/types/menu_button_default.py +++ b/aiogram/types/menu_button_default.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import Field +import msgspec from ..enums import MenuButtonType from .menu_button import MenuButton @@ -13,5 +13,5 @@ class MenuButtonDefault(MenuButton): Source: https://core.telegram.org/bots/api#menubuttondefault """ - type: str = Field(MenuButtonType.DEFAULT, const=True) + type: str = msgspec.field(default_factory=lambda: MenuButtonType.DEFAULT) """Type of the button, must be *default*""" diff --git a/aiogram/types/menu_button_web_app.py b/aiogram/types/menu_button_web_app.py index f77ed2ea..40659779 100644 --- a/aiogram/types/menu_button_web_app.py +++ b/aiogram/types/menu_button_web_app.py @@ -2,23 +2,21 @@ from __future__ import annotations from typing import TYPE_CHECKING -from pydantic import Field +import msgspec from ..enums import MenuButtonType from .menu_button import MenuButton - -if TYPE_CHECKING: - from .web_app_info import WebAppInfo +from .web_app_info import WebAppInfo -class MenuButtonWebApp(MenuButton): +class MenuButtonWebApp(MenuButton, kw_only=True): """ Represents a menu button, which launches a `Web App `_. Source: https://core.telegram.org/bots/api#menubuttonwebapp """ - type: str = Field(MenuButtonType.WEB_APP, const=True) + type: str = msgspec.field(default_factory=lambda: MenuButtonType.WEB_APP) """Type of the button, must be *web_app*""" text: str """Text on the button""" diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a057ef97..5e55443a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime from typing import TYPE_CHECKING, Any, List, Optional, Union -from pydantic import Field +import msgspec from aiogram.utils.text_decorations import ( TextDecoration, @@ -50,56 +50,57 @@ if TYPE_CHECKING: StopMessageLiveLocation, UnpinChatMessage, ) - from .animation import Animation - from .audio import Audio - from .chat import Chat - from .chat_shared import ChatShared - from .contact import Contact - from .dice import Dice - from .document import Document - from .force_reply import ForceReply - from .forum_topic_closed import ForumTopicClosed - from .forum_topic_created import ForumTopicCreated - from .forum_topic_edited import ForumTopicEdited - from .forum_topic_reopened import ForumTopicReopened - from .game import Game - from .general_forum_topic_hidden import GeneralForumTopicHidden - from .general_forum_topic_unhidden import GeneralForumTopicUnhidden - from .inline_keyboard_markup import InlineKeyboardMarkup - from .input_file import InputFile - from .input_media import InputMedia - from .input_media_audio import InputMediaAudio - from .input_media_document import InputMediaDocument - from .input_media_photo import InputMediaPhoto - from .input_media_video import InputMediaVideo - from .invoice import Invoice - from .labeled_price import LabeledPrice - from .location import Location - from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged - from .message_entity import MessageEntity - from .passport_data import PassportData - from .photo_size import PhotoSize - from .poll import Poll - from .proximity_alert_triggered import ProximityAlertTriggered - from .reply_keyboard_markup import ReplyKeyboardMarkup - from .reply_keyboard_remove import ReplyKeyboardRemove - from .sticker import Sticker - from .successful_payment import SuccessfulPayment - from .user import User - from .user_shared import UserShared - from .venue import Venue - from .video import Video - from .video_chat_ended import VideoChatEnded - from .video_chat_participants_invited import VideoChatParticipantsInvited - from .video_chat_scheduled import VideoChatScheduled - from .video_chat_started import VideoChatStarted - from .video_note import VideoNote - from .voice import Voice - from .web_app_data import WebAppData - from .write_access_allowed import WriteAccessAllowed + +from .animation import Animation +from .audio import Audio +from .chat import Chat +from .chat_shared import ChatShared +from .contact import Contact +from .dice import Dice +from .document import Document +from .force_reply import ForceReply +from .forum_topic_closed import ForumTopicClosed +from .forum_topic_created import ForumTopicCreated +from .forum_topic_edited import ForumTopicEdited +from .forum_topic_reopened import ForumTopicReopened +from .game import Game +from .general_forum_topic_hidden import GeneralForumTopicHidden +from .general_forum_topic_unhidden import GeneralForumTopicUnhidden +from .inline_keyboard_markup import InlineKeyboardMarkup +from .input_file import InputFile +from .input_media import InputMedia +from .input_media_audio import InputMediaAudio +from .input_media_document import InputMediaDocument +from .input_media_photo import InputMediaPhoto +from .input_media_video import InputMediaVideo +from .invoice import Invoice +from .labeled_price import LabeledPrice +from .location import Location +from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged +from .message_entity import MessageEntity +from .passport_data import PassportData +from .photo_size import PhotoSize +from .poll import Poll +from .proximity_alert_triggered import ProximityAlertTriggered +from .reply_keyboard_markup import ReplyKeyboardMarkup +from .reply_keyboard_remove import ReplyKeyboardRemove +from .sticker import Sticker +from .successful_payment import SuccessfulPayment +from .user import User +from .user_shared import UserShared +from .venue import Venue +from .video import Video +from .video_chat_ended import VideoChatEnded +from .video_chat_participants_invited import VideoChatParticipantsInvited +from .video_chat_scheduled import VideoChatScheduled +from .video_chat_started import VideoChatStarted +from .video_note import VideoNote +from .voice import Voice +from .web_app_data import WebAppData +from .write_access_allowed import WriteAccessAllowed -class Message(TelegramObject): +class Message(TelegramObject, kw_only=True): """ This object represents a message. @@ -108,13 +109,13 @@ class Message(TelegramObject): message_id: int """Unique message identifier inside this chat""" - date: datetime.datetime + date: datetime.datetime | int """Date the message was sent in Unix time""" chat: Chat """Conversation the message belongs to""" message_thread_id: Optional[int] = None """*Optional*. Unique identifier of a message thread to which the message belongs; for supergroups only""" - from_user: Optional[User] = Field(None, alias="from") + from_user: Optional[User] = msgspec.field(name="from", default=None) """*Optional*. Sender of the message; empty for messages sent to channels. For backward compatibility, the field contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat.""" sender_chat: Optional[Chat] = None """*Optional*. Sender of the message, sent on behalf of a chat. For example, the channel itself for channel posts, the supergroup itself for messages from anonymous group administrators, the linked channel for messages automatically forwarded to the discussion group. For backward compatibility, the field *from* contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat.""" diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 39773a66..b517c7b1 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -4,9 +4,7 @@ from typing import TYPE_CHECKING, Optional from ..utils.text_decorations import add_surrogates, remove_surrogates from .base import MutableTelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class MessageEntity(MutableTelegramObject): diff --git a/aiogram/types/order_info.py b/aiogram/types/order_info.py index bf354b5e..ad89f6bc 100644 --- a/aiogram/types/order_info.py +++ b/aiogram/types/order_info.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .shipping_address import ShippingAddress +from .shipping_address import ShippingAddress class OrderInfo(TelegramObject): diff --git a/aiogram/types/passport_data.py b/aiogram/types/passport_data.py index 18523c05..6a709a10 100644 --- a/aiogram/types/passport_data.py +++ b/aiogram/types/passport_data.py @@ -3,10 +3,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List from .base import TelegramObject - -if TYPE_CHECKING: - from .encrypted_credentials import EncryptedCredentials - from .encrypted_passport_element import EncryptedPassportElement +from .encrypted_credentials import EncryptedCredentials +from .encrypted_passport_element import EncryptedPassportElement class PassportData(TelegramObject): diff --git a/aiogram/types/passport_element_error_data_field.py b/aiogram/types/passport_element_error_data_field.py index f5b0b67b..2409ad74 100644 --- a/aiogram/types/passport_element_error_data_field.py +++ b/aiogram/types/passport_element_error_data_field.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorDataField(PassportElementError): +class PassportElementErrorDataField(PassportElementError, kw_only=True): """ Represents an issue in one of the data fields that was provided by the user. The error is considered resolved when the field's value changes. Source: https://core.telegram.org/bots/api#passportelementerrordatafield """ - source: str = Field("data", const=True) + source: str = "data" """Error source, must be *data*""" type: str """The section of the user's Telegram Passport which has the error, one of 'personal_details', 'passport', 'driver_license', 'identity_card', 'internal_passport', 'address'""" diff --git a/aiogram/types/passport_element_error_file.py b/aiogram/types/passport_element_error_file.py index 217deaaa..b7583906 100644 --- a/aiogram/types/passport_element_error_file.py +++ b/aiogram/types/passport_element_error_file.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorFile(PassportElementError): +class PassportElementErrorFile(PassportElementError, kw_only=True): """ Represents an issue with a document scan. The error is considered resolved when the file with the document scan changes. Source: https://core.telegram.org/bots/api#passportelementerrorfile """ - source: str = Field("file", const=True) + source: str = "file" """Error source, must be *file*""" type: str """The section of the user's Telegram Passport which has the issue, one of 'utility_bill', 'bank_statement', 'rental_agreement', 'passport_registration', 'temporary_registration'""" diff --git a/aiogram/types/passport_element_error_files.py b/aiogram/types/passport_element_error_files.py index 6f42d693..fdb91307 100644 --- a/aiogram/types/passport_element_error_files.py +++ b/aiogram/types/passport_element_error_files.py @@ -2,19 +2,17 @@ from __future__ import annotations from typing import List -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorFiles(PassportElementError): +class PassportElementErrorFiles(PassportElementError, kw_only=True): """ Represents an issue with a list of scans. The error is considered resolved when the list of files containing the scans changes. Source: https://core.telegram.org/bots/api#passportelementerrorfiles """ - source: str = Field("files", const=True) + source: str = "files" """Error source, must be *files*""" type: str """The section of the user's Telegram Passport which has the issue, one of 'utility_bill', 'bank_statement', 'rental_agreement', 'passport_registration', 'temporary_registration'""" diff --git a/aiogram/types/passport_element_error_front_side.py b/aiogram/types/passport_element_error_front_side.py index 98e60acd..ca147220 100644 --- a/aiogram/types/passport_element_error_front_side.py +++ b/aiogram/types/passport_element_error_front_side.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorFrontSide(PassportElementError): +class PassportElementErrorFrontSide(PassportElementError, kw_only=True): """ Represents an issue with the front side of a document. The error is considered resolved when the file with the front side of the document changes. Source: https://core.telegram.org/bots/api#passportelementerrorfrontside """ - source: str = Field("front_side", const=True) + source: str = "front_side" """Error source, must be *front_side*""" type: str """The section of the user's Telegram Passport which has the issue, one of 'passport', 'driver_license', 'identity_card', 'internal_passport'""" diff --git a/aiogram/types/passport_element_error_reverse_side.py b/aiogram/types/passport_element_error_reverse_side.py index 0c6073ba..bcc016d7 100644 --- a/aiogram/types/passport_element_error_reverse_side.py +++ b/aiogram/types/passport_element_error_reverse_side.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorReverseSide(PassportElementError): +class PassportElementErrorReverseSide(PassportElementError, kw_only=True): """ Represents an issue with the reverse side of a document. The error is considered resolved when the file with reverse side of the document changes. Source: https://core.telegram.org/bots/api#passportelementerrorreverseside """ - source: str = Field("reverse_side", const=True) + source: str = "reverse_side" """Error source, must be *reverse_side*""" type: str """The section of the user's Telegram Passport which has the issue, one of 'driver_license', 'identity_card'""" diff --git a/aiogram/types/passport_element_error_selfie.py b/aiogram/types/passport_element_error_selfie.py index 4a3b2fe1..d32d5c70 100644 --- a/aiogram/types/passport_element_error_selfie.py +++ b/aiogram/types/passport_element_error_selfie.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorSelfie(PassportElementError): +class PassportElementErrorSelfie(PassportElementError, kw_only=True): """ Represents an issue with the selfie with a document. The error is considered resolved when the file with the selfie changes. Source: https://core.telegram.org/bots/api#passportelementerrorselfie """ - source: str = Field("selfie", const=True) + source: str = "selfie" """Error source, must be *selfie*""" type: str """The section of the user's Telegram Passport which has the issue, one of 'passport', 'driver_license', 'identity_card', 'internal_passport'""" diff --git a/aiogram/types/passport_element_error_translation_file.py b/aiogram/types/passport_element_error_translation_file.py index d4106453..693de2d4 100644 --- a/aiogram/types/passport_element_error_translation_file.py +++ b/aiogram/types/passport_element_error_translation_file.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorTranslationFile(PassportElementError): +class PassportElementErrorTranslationFile(PassportElementError, kw_only=True): """ Represents an issue with one of the files that constitute the translation of a document. The error is considered resolved when the file changes. Source: https://core.telegram.org/bots/api#passportelementerrortranslationfile """ - source: str = Field("translation_file", const=True) + source: str = "translation_file" """Error source, must be *translation_file*""" type: str """Type of element of the user's Telegram Passport which has the issue, one of 'passport', 'driver_license', 'identity_card', 'internal_passport', 'utility_bill', 'bank_statement', 'rental_agreement', 'passport_registration', 'temporary_registration'""" diff --git a/aiogram/types/passport_element_error_translation_files.py b/aiogram/types/passport_element_error_translation_files.py index df0db64f..e50593ec 100644 --- a/aiogram/types/passport_element_error_translation_files.py +++ b/aiogram/types/passport_element_error_translation_files.py @@ -2,19 +2,17 @@ from __future__ import annotations from typing import List -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorTranslationFiles(PassportElementError): +class PassportElementErrorTranslationFiles(PassportElementError, kw_only=True): """ Represents an issue with the translated version of a document. The error is considered resolved when a file with the document translation change. Source: https://core.telegram.org/bots/api#passportelementerrortranslationfiles """ - source: str = Field("translation_files", const=True) + source: str = "translation_files" """Error source, must be *translation_files*""" type: str """Type of element of the user's Telegram Passport which has the issue, one of 'passport', 'driver_license', 'identity_card', 'internal_passport', 'utility_bill', 'bank_statement', 'rental_agreement', 'passport_registration', 'temporary_registration'""" diff --git a/aiogram/types/passport_element_error_unspecified.py b/aiogram/types/passport_element_error_unspecified.py index 39d0c417..75f4f41c 100644 --- a/aiogram/types/passport_element_error_unspecified.py +++ b/aiogram/types/passport_element_error_unspecified.py @@ -1,18 +1,16 @@ from __future__ import annotations -from pydantic import Field - from .passport_element_error import PassportElementError -class PassportElementErrorUnspecified(PassportElementError): +class PassportElementErrorUnspecified(PassportElementError, kw_only=True): """ Represents an issue in an unspecified place. The error is considered resolved when new data is added. Source: https://core.telegram.org/bots/api#passportelementerrorunspecified """ - source: str = Field("unspecified", const=True) + source: str = "unspecified" """Error source, must be *unspecified*""" type: str """Type of element of the user's Telegram Passport which has the issue""" diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index fb979a58..71d3c78f 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -4,10 +4,8 @@ import datetime from typing import TYPE_CHECKING, List, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .message_entity import MessageEntity - from .poll_option import PollOption +from .message_entity import MessageEntity +from .poll_option import PollOption class Poll(TelegramObject): diff --git a/aiogram/types/poll_answer.py b/aiogram/types/poll_answer.py index c2cd7456..433bb5f7 100644 --- a/aiogram/types/poll_answer.py +++ b/aiogram/types/poll_answer.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class PollAnswer(TelegramObject): diff --git a/aiogram/types/pre_checkout_query.py b/aiogram/types/pre_checkout_query.py index f2e49170..8d4b59c6 100644 --- a/aiogram/types/pre_checkout_query.py +++ b/aiogram/types/pre_checkout_query.py @@ -2,17 +2,17 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from pydantic import Field +import msgspec from .base import TelegramObject if TYPE_CHECKING: from ..methods import AnswerPreCheckoutQuery - from .order_info import OrderInfo - from .user import User +from .order_info import OrderInfo +from .user import User -class PreCheckoutQuery(TelegramObject): +class PreCheckoutQuery(TelegramObject, kw_only=True): """ This object contains information about an incoming pre-checkout query. @@ -21,7 +21,7 @@ class PreCheckoutQuery(TelegramObject): id: str """Unique query identifier""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """User who sent the query""" currency: str """Three-letter ISO 4217 `currency `_ code""" diff --git a/aiogram/types/proximity_alert_triggered.py b/aiogram/types/proximity_alert_triggered.py index 8275cd26..02869478 100644 --- a/aiogram/types/proximity_alert_triggered.py +++ b/aiogram/types/proximity_alert_triggered.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class ProximityAlertTriggered(TelegramObject): diff --git a/aiogram/types/reply_keyboard_remove.py b/aiogram/types/reply_keyboard_remove.py index 1e2c2da9..ba16ad5c 100644 --- a/aiogram/types/reply_keyboard_remove.py +++ b/aiogram/types/reply_keyboard_remove.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import Optional -from pydantic import Field - from .base import MutableTelegramObject @@ -14,7 +12,7 @@ class ReplyKeyboardRemove(MutableTelegramObject): Source: https://core.telegram.org/bots/api#replykeyboardremove """ - remove_keyboard: bool = Field(True, const=True) + remove_keyboard: bool = True """Requests clients to remove the custom keyboard (user will not be able to summon this keyboard; if you want to hide the keyboard from sight but keep it accessible, use *one_time_keyboard* in :class:`aiogram.types.reply_keyboard_markup.ReplyKeyboardMarkup`)""" selective: Optional[bool] = None """*Optional*. Use this parameter if you want to remove the keyboard for specific users only. Targets: 1) users that are @mentioned in the *text* of the :class:`aiogram.types.message.Message` object; 2) if the bot's message is a reply (has *reply_to_message_id*), sender of the original message.""" diff --git a/aiogram/types/shipping_query.py b/aiogram/types/shipping_query.py index df00e38d..547a837a 100644 --- a/aiogram/types/shipping_query.py +++ b/aiogram/types/shipping_query.py @@ -2,18 +2,18 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional -from pydantic import Field +import msgspec from .base import TelegramObject if TYPE_CHECKING: from ..methods import AnswerShippingQuery - from ..types import ShippingOption - from .shipping_address import ShippingAddress - from .user import User +from ..types import ShippingOption +from .shipping_address import ShippingAddress +from .user import User -class ShippingQuery(TelegramObject): +class ShippingQuery(TelegramObject, kw_only=True): """ This object contains information about an incoming shipping query. @@ -22,7 +22,7 @@ class ShippingQuery(TelegramObject): id: str """Unique query identifier""" - from_user: User = Field(..., alias="from") + from_user: User = msgspec.field(name="from") """User who sent the query""" invoice_payload: str """Bot specified invoice payload""" diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 1bac276f..90ffabb5 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -6,9 +6,9 @@ from .base import TelegramObject if TYPE_CHECKING: from ..methods import DeleteStickerFromSet, SetStickerPositionInSet - from .file import File - from .mask_position import MaskPosition - from .photo_size import PhotoSize +from .file import File +from .mask_position import MaskPosition +from .photo_size import PhotoSize class Sticker(TelegramObject): diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py index d212acd0..38fad455 100644 --- a/aiogram/types/sticker_set.py +++ b/aiogram/types/sticker_set.py @@ -3,10 +3,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize - from .sticker import Sticker +from .photo_size import PhotoSize +from .sticker import Sticker class StickerSet(TelegramObject): diff --git a/aiogram/types/successful_payment.py b/aiogram/types/successful_payment.py index d8b0e90c..7714a7ea 100644 --- a/aiogram/types/successful_payment.py +++ b/aiogram/types/successful_payment.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .order_info import OrderInfo +from .order_info import OrderInfo class SuccessfulPayment(TelegramObject): diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 9f9eb413..a03d2310 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -4,21 +4,19 @@ from typing import TYPE_CHECKING, Optional, cast from ..utils.mypy_hacks import lru_cache from .base import TelegramObject - -if TYPE_CHECKING: - from .callback_query import CallbackQuery - from .chat_join_request import ChatJoinRequest - from .chat_member_updated import ChatMemberUpdated - from .chosen_inline_result import ChosenInlineResult - from .inline_query import InlineQuery - from .message import Message - from .poll import Poll - from .poll_answer import PollAnswer - from .pre_checkout_query import PreCheckoutQuery - from .shipping_query import ShippingQuery +from .callback_query import CallbackQuery +from .chat_join_request import ChatJoinRequest +from .chat_member_updated import ChatMemberUpdated +from .chosen_inline_result import ChosenInlineResult +from .inline_query import InlineQuery +from .message import Message +from .poll import Poll +from .poll_answer import PollAnswer +from .pre_checkout_query import PreCheckoutQuery +from .shipping_query import ShippingQuery -class Update(TelegramObject): +class Update(TelegramObject, weakref=True): """ This `object `_ represents an incoming update. diff --git a/aiogram/types/user_profile_photos.py b/aiogram/types/user_profile_photos.py index ad1197bf..76d8e7b4 100644 --- a/aiogram/types/user_profile_photos.py +++ b/aiogram/types/user_profile_photos.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class UserProfilePhotos(TelegramObject): diff --git a/aiogram/types/venue.py b/aiogram/types/venue.py index 49caceff..af71390a 100644 --- a/aiogram/types/venue.py +++ b/aiogram/types/venue.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .location import Location +from .location import Location class Venue(TelegramObject): diff --git a/aiogram/types/video.py b/aiogram/types/video.py index ca43493a..ba78931e 100644 --- a/aiogram/types/video.py +++ b/aiogram/types/video.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class Video(TelegramObject): diff --git a/aiogram/types/video_chat_participants_invited.py b/aiogram/types/video_chat_participants_invited.py index 3361f8ee..97e4f765 100644 --- a/aiogram/types/video_chat_participants_invited.py +++ b/aiogram/types/video_chat_participants_invited.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, List from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User +from .user import User class VideoChatParticipantsInvited(TelegramObject): diff --git a/aiogram/types/video_note.py b/aiogram/types/video_note.py index 03654824..a7d4017e 100644 --- a/aiogram/types/video_note.py +++ b/aiogram/types/video_note.py @@ -3,9 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from .base import TelegramObject - -if TYPE_CHECKING: - from .photo_size import PhotoSize +from .photo_size import PhotoSize class VideoNote(TelegramObject): diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py index d21316f9..0a6d1841 100644 --- a/aiogram/utils/web_app.py +++ b/aiogram/utils/web_app.py @@ -1,15 +1,16 @@ import hashlib import hmac -import json from datetime import datetime from operator import itemgetter -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Union from urllib.parse import parse_qsl +import msgspec + from aiogram.types import TelegramObject -class WebAppUser(TelegramObject): +class WebAppUser(TelegramObject, kw_only=True): """ This object contains the data of the Web App user. @@ -36,7 +37,7 @@ class WebAppUser(TelegramObject): Only returned for Web Apps launched from the attachment menu.""" -class WebAppInitData(TelegramObject): +class WebAppInitData(TelegramObject, kw_only=True): """ This object contains data that is transferred to the Web App when it is opened. It is empty if the Web App was launched from a keyboard button. @@ -97,7 +98,7 @@ def check_webapp_signature(token: str, init_data: str) -> bool: def parse_webapp_init_data( init_data: str, *, - loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = None, ) -> WebAppInitData: """ Parse WebApp init data and return it as WebAppInitData object @@ -114,16 +115,18 @@ def parse_webapp_init_data( if (value.startswith("[") and value.endswith("]")) or ( value.startswith("{") and value.endswith("}") ): - value = loads(value) + value = msgspec.json.decode(value) + if key == "auth_date": + value = datetime.utcfromtimestamp(int(value)) result[key] = value - return WebAppInitData(**result) + return msgspec.from_builtins(result, type=WebAppInitData) def safe_parse_webapp_init_data( token: str, init_data: str, *, - loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = None, ) -> WebAppInitData: """ Validate raw WebApp init data and return it as WebAppInitData object diff --git a/aiogram/webhook/aiohttp_server.py b/aiogram/webhook/aiohttp_server.py index 4406f1ff..1ae09f0c 100644 --- a/aiogram/webhook/aiohttp_server.py +++ b/aiogram/webhook/aiohttp_server.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from asyncio import Transport from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, cast +import msgspec from aiohttp import MultipartWriter, web from aiohttp.abc import Application from aiohttp.typedefs import Handler @@ -15,6 +16,9 @@ from aiogram.methods.base import TelegramType from aiogram.types import InputFile from aiogram.webhook.security import IPFilter +from ..types import UNSET_PARSE_MODE +from ..types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT + def setup_application(app: Application, dispatcher: Dispatcher, /, **kwargs: Any) -> None: """ @@ -131,18 +135,14 @@ class BaseRequestHandler(ABC): """ pass - async def _background_feed_update(self, bot: Bot, update: Dict[str, Any]) -> None: + async def _background_feed_update(self, bot: Bot, update: str) -> None: result = await self.dispatcher.feed_raw_update(bot=bot, update=update, **self.data) if isinstance(result, TelegramMethod): await self.dispatcher.silent_call_request(bot=bot, result=result) async def _handle_request_background(self, bot: Bot, request: web.Request) -> web.Response: - asyncio.create_task( - self._background_feed_update( - bot=bot, update=await request.json(loads=bot.session.json_loads) - ) - ) - return web.json_response({}, dumps=bot.session.json_dumps) + asyncio.create_task(self._background_feed_update(bot=bot, update=await request.text())) + return web.Response(body=b"{}", headers={"Content-Type": "application/json"}) def _build_response_writer( self, bot: Bot, result: Optional[TelegramMethod[TelegramType]] @@ -158,8 +158,9 @@ class BaseRequestHandler(ABC): payload.set_content_disposition("form-data", name="method") files: Dict[str, InputFile] = {} - for key, value in result.dict().items(): - value = bot.session.prepare_value(value, bot=bot, files=files) + + for key in result.__struct_fields__: + value = bot.session.prepare_value(getattr(result, key), bot=bot, files=files) if not value: continue payload = writer.append(value) @@ -178,10 +179,29 @@ class BaseRequestHandler(ABC): async def _handle_request(self, bot: Bot, request: web.Request) -> web.Response: result: Optional[TelegramMethod[Any]] = await self.dispatcher.feed_webhook_update( bot, - await request.json(loads=bot.session.json_loads), + await request.text(), **self.data, ) - return web.Response(body=self._build_response_writer(bot=bot, result=result)) + + def enc_hook(obj): + if obj is UNSET_PARSE_MODE: + return bot.parse_mode + if obj is UNSET_DISABLE_WEB_PAGE_PREVIEW: + return bot.disable_web_page_preview + if obj is UNSET_PROTECT_CONTENT: + return bot.protect_content + raise ValueError(f"Unknown object: {obj}") + + headers = {} + try: + if result is not None: + result.method = result.__api_method__ + result: bytes = msgspec.json.encode(result, enc_hook=enc_hook) + headers = {"content-type": "application/json"} + except ValueError: + result: MultipartWriter = self._build_response_writer(bot=bot, result=result) + + return web.Response(body=result, headers=headers) async def handle(self, request: web.Request) -> web.Response: bot = await self.resolve_bot(request) diff --git a/aiogram/webhook/asqi.py b/aiogram/webhook/asqi.py new file mode 100644 index 00000000..8cd9dcc3 --- /dev/null +++ b/aiogram/webhook/asqi.py @@ -0,0 +1,102 @@ +from typing import Any + +import msgspec.json + +from aiogram import Bot, Dispatcher + +from ..types import UNSET_PARSE_MODE +from ..types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT + + +class ASGIApplication: + def __init__( + self, + bot: Bot, + dispatcher: Dispatcher, + handle_in_background: bool = False, + lifespan_data: dict = None, + **data: Any, + ) -> None: + """ + :param dispatcher: instance of :class:`aiogram.dispatcher.dispatcher.Dispatcher` + :param handle_in_background: immediately respond to the Telegram instead of + waiting end of handler process + """ + self.bot = bot + self.dispatcher = dispatcher + self.handle_in_background = handle_in_background + self.data = data + lifespan_data = lifespan_data or {} + self.lifespan_data = { + "bot": self.bot, + "dispatcher": self.dispatcher, + **lifespan_data, + **self.dispatcher.workflow_data, + } + + async def _read_body(self, receive): + """ + Read and return the entire body from an incoming ASGI message. + """ + body = b"" + more_body = True + + while more_body: + message = await receive() + body += message.get("body", b"") + more_body = message.get("more_body", False) + + return body + + async def _lifespan(self, scope, receive, send): + while True: + message = await receive() + if message["type"] == "lifespan.startup": + await self.dispatcher.emit_startup(**self.lifespan_data) + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": + await self.dispatcher.emit_shutdown(**self.lifespan_data) + await send({"type": "lifespan.shutdown.complete"}) + return + + async def __call__(self, scope, receive, send): + if scope["type"] == "lifespan": + await self._lifespan(scope, receive, send) + return + + result = await self.dispatcher.feed_webhook_update( + self.bot, + await self._read_body(receive), + **self.data, + ) + + if result is not None: + result.method = result.__api_method__ + + def enc_hook(obj): + if obj is UNSET_PARSE_MODE: + return self.bot.parse_mode + if obj is UNSET_DISABLE_WEB_PAGE_PREVIEW: + return self.bot.disable_web_page_preview + if obj is UNSET_PROTECT_CONTENT: + return self.bot.protect_content + raise ValueError(f"Unknown object: {obj}") + + response = msgspec.json.encode(result, enc_hook=enc_hook) + + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(response)).encode()), + ], + } + ) + await send( + { + "type": "http.response.body", + "body": response, + } + ) diff --git a/tests/mocked_bot.py b/tests/mocked_bot.py index 29c477a0..c326b1d8 100644 --- a/tests/mocked_bot.py +++ b/tests/mocked_bot.py @@ -1,6 +1,8 @@ from collections import deque from typing import TYPE_CHECKING, AsyncGenerator, Deque, Optional, Type +import msgspec + from aiogram import Bot from aiogram.client.session.base import BaseSession from aiogram.methods import TelegramMethod @@ -35,7 +37,7 @@ class MockedSession(BaseSession): self.requests.append(method) response: Response[TelegramType] = self.responses.pop() self.check_response( - method=method, status_code=response.error_code, content=response.json() + method=method, status_code=response.error_code, content=msgspec.to_builtins(response) ) return response.result # type: ignore diff --git a/tests/test_api/test_client/test_session/test_aiohttp_session.py b/tests/test_api/test_client/test_session/test_aiohttp_session.py index fd7cabfd..6aee8329 100644 --- a/tests/test_api/test_client/test_session/test_aiohttp_session.py +++ b/tests/test_api/test_client/test_session/test_aiohttp_session.py @@ -104,7 +104,7 @@ class TestAiohttpSession: mocked_close.assert_called_once() def test_build_form_data_with_data_only(self, bot: MockedBot): - class TestMethod(TelegramMethod[bool]): + class TestMethod(TelegramMethod[bool], kw_only=True): __api_method__ = "test" __returning__ = bool 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 48d43cef..875f2cde 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 @@ -3,6 +3,7 @@ import json from typing import Any, AsyncContextManager, AsyncGenerator, Optional from unittest.mock import AsyncMock, patch +import msgspec import pytest from pytz import utc @@ -61,8 +62,8 @@ class TestBaseSession: def test_default_props(self): session = CustomSession() assert session.api == PRODUCTION - assert session.json_loads == json.loads - assert session.json_dumps == json.dumps + assert session.json_loads == msgspec.json.decode + assert session.json_dumps == msgspec.json.encode def custom_loads(*_): return json.loads @@ -91,14 +92,14 @@ class TestBaseSession: [None, None], ["text", "text"], [ChatType.PRIVATE, "private"], - [TopicIconColor.RED, "16478047"], - [42, "42"], - [True, "true"], - [["test"], '["test"]'], - [["test", ["test"]], '["test", ["test"]]'], - [[{"test": "pass", "spam": None}], '[{"test": "pass"}]'], - [{"test": "pass", "number": 42, "spam": None}, '{"test": "pass", "number": 42}'], - [{"foo": {"test": "pass", "spam": None}}, '{"foo": {"test": "pass"}}'], + [TopicIconColor.RED, b"16478047"], + [42, b"42"], + [True, b"true"], + [["test"], b'["test"]'], + [["test", ["test"]], b'["test",["test"]]'], + [[{"test": "pass", "spam": None}], b'[{"test":"pass"}]'], + [{"test": "pass", "number": 42, "spam": None}, b'{"test":"pass","number":42}'], + [{"foo": {"test": "pass", "spam": None}}, b'{"foo":{"test":"pass"}}'], [ datetime.datetime( year=2017, month=5, day=17, hour=4, minute=11, second=42, tzinfo=utc @@ -126,9 +127,9 @@ class TestBaseSession: ) assert bot.session.prepare_value(UNSET_PARSE_MODE, bot=bot, files={}) == "HTML" assert ( - bot.session.prepare_value(UNSET_DISABLE_WEB_PAGE_PREVIEW, bot=bot, files={}) == "true" + bot.session.prepare_value(UNSET_DISABLE_WEB_PAGE_PREVIEW, bot=bot, files={}) == b"true" ) - assert bot.session.prepare_value(UNSET_PROTECT_CONTENT, bot=bot, files={}) == "true" + assert bot.session.prepare_value(UNSET_PROTECT_CONTENT, bot=bot, files={}) == b"true" def test_prepare_value_defaults_unset(self): bot = MockedBot() @@ -188,7 +189,7 @@ class TestBaseSession: session = CustomSession() method = DeleteMessage(chat_id=42, message_id=42) - with pytest.raises(ClientDecodeError, match="JSONDecodeError"): + with pytest.raises(ClientDecodeError): session.check_response( method=method, status_code=200, @@ -199,7 +200,7 @@ class TestBaseSession: session = CustomSession() method = DeleteMessage(chat_id=42, message_id=42) - with pytest.raises(ClientDecodeError, match="ValidationError"): + with pytest.raises(ClientDecodeError): session.check_response( method=method, status_code=200, diff --git a/tests/test_api/test_methods/test_approve_chat_join_request.py b/tests/test_api/test_methods/test_approve_chat_join_request.py index 6b097702..a64b3d8b 100755 --- a/tests/test_api/test_methods/test_approve_chat_join_request.py +++ b/tests/test_api/test_methods/test_approve_chat_join_request.py @@ -4,7 +4,7 @@ from tests.mocked_bot import MockedBot class TestApproveChatJoinRequest: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=None) + prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=True) response: bool = await bot.approve_chat_join_request( chat_id=-42, diff --git a/tests/test_api/test_methods/test_base.py b/tests/test_api/test_methods/test_base.py index f2351d40..fc91e20a 100644 --- a/tests/test_api/test_methods/test_base.py +++ b/tests/test_api/test_methods/test_base.py @@ -13,11 +13,18 @@ class TestTelegramMethodRemoveUnset: [ [{}, set()], [{"foo": "bar"}, {"foo"}], - [{"foo": "bar", "baz": sentinel.DEFAULT}, {"foo"}], + [ + { + "foo": "bar", + }, + {"foo"}, + ], ], ) def test_remove_unset(self, values, names): - validated = TelegramMethod.remove_unset(values) + import msgspec + + validated = msgspec.to_builtins(values) assert set(validated.keys()) == names diff --git a/tests/test_api/test_methods/test_get_my_commands.py b/tests/test_api/test_methods/test_get_my_commands.py index 41a7a7d0..2daf2f5a 100644 --- a/tests/test_api/test_methods/test_get_my_commands.py +++ b/tests/test_api/test_methods/test_get_my_commands.py @@ -7,7 +7,7 @@ from tests.mocked_bot import MockedBot class TestGetMyCommands: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetMyCommands, ok=True, result=None) + prepare_result = bot.add_result_for(GetMyCommands, ok=True, result=[]) response: List[BotCommand] = await bot.get_my_commands() request = bot.get_request() diff --git a/tests/test_api/test_methods/test_reopen_forum_topic.py b/tests/test_api/test_methods/test_reopen_forum_topic.py index ff4c9b66..4fdd617f 100644 --- a/tests/test_api/test_methods/test_reopen_forum_topic.py +++ b/tests/test_api/test_methods/test_reopen_forum_topic.py @@ -4,7 +4,7 @@ from tests.mocked_bot import MockedBot class TestReopenForumTopic: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(ReopenForumTopic, ok=True, result=None) + prepare_result = bot.add_result_for(ReopenForumTopic, ok=True, result=True) response: bool = await bot.reopen_forum_topic( chat_id=42, diff --git a/tests/test_api/test_methods/test_send_dice.py b/tests/test_api/test_methods/test_send_dice.py index 35196ee6..3652ad95 100644 --- a/tests/test_api/test_methods/test_send_dice.py +++ b/tests/test_api/test_methods/test_send_dice.py @@ -1,11 +1,20 @@ from aiogram.methods import Request, SendDice -from aiogram.types import Message +from aiogram.types import Chat, Message from tests.mocked_bot import MockedBot class TestSendDice: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SendDice, ok=True, result=None) + prepare_result = bot.add_result_for( + SendDice, + ok=True, + result=Message( + message_id=42, + date=123, + text="text", + chat=Chat(id=42, type="private"), + ), + ) response: Message = await bot.send_dice(chat_id=42) request = bot.get_request() diff --git a/tests/test_api/test_methods/test_set_my_commands.py b/tests/test_api/test_methods/test_set_my_commands.py index 16066b73..b89dc710 100644 --- a/tests/test_api/test_methods/test_set_my_commands.py +++ b/tests/test_api/test_methods/test_set_my_commands.py @@ -5,7 +5,7 @@ from tests.mocked_bot import MockedBot class TestSetMyCommands: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetMyCommands, ok=True, result=None) + prepare_result = bot.add_result_for(SetMyCommands, ok=True, result=True) response: bool = await bot.set_my_commands( commands=[], diff --git a/tests/test_api/test_methods/test_set_sticker_set_thumbnail.py b/tests/test_api/test_methods/test_set_sticker_set_thumbnail.py index 6c33f862..97ec54f8 100644 --- a/tests/test_api/test_methods/test_set_sticker_set_thumbnail.py +++ b/tests/test_api/test_methods/test_set_sticker_set_thumbnail.py @@ -4,7 +4,7 @@ from tests.mocked_bot import MockedBot class TestSetStickerSetThumbnail: async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetStickerSetThumbnail, ok=True, result=None) + prepare_result = bot.add_result_for(SetStickerSetThumbnail, ok=True, result=True) response: bool = await bot.set_sticker_set_thumbnail(name="test", user_id=42) request = bot.get_request() diff --git a/tests/test_api/test_types/test_chat.py b/tests/test_api/test_types/test_chat.py index 03cb8963..6f32c18b 100644 --- a/tests/test_api/test_types/test_chat.py +++ b/tests/test_api/test_types/test_chat.py @@ -178,7 +178,7 @@ class TestChat: def test_delete_photo(self): chat = Chat(id=-42, type="supergroup") - method = chat.delete_photo(description="test") + method = chat.delete_photo() assert method.chat_id == chat.id def test_set_photo(self): diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index 1b15327f..e42d7418 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -422,7 +422,7 @@ TEST_FORUM_TOPIC_EDITED = Message( from_user=User(id=42, is_bot=False, first_name="Test"), forum_topic_edited=ForumTopicEdited( name="test_edited", - icon_color=0xFFD67E, + # icon_color=0xFFD67E, ), ) TEST_FORUM_TOPIC_CLOSED = Message( diff --git a/tests/test_api/test_types/test_reply_keyboard_remove.py b/tests/test_api/test_types/test_reply_keyboard_remove.py index 984932a5..88b4f73b 100644 --- a/tests/test_api/test_types/test_reply_keyboard_remove.py +++ b/tests/test_api/test_types/test_reply_keyboard_remove.py @@ -10,7 +10,7 @@ class TestReplyKeyboardRemove: def test_remove_keyboard_default_is_true(self): assert ( - ReplyKeyboardRemove.__fields__["remove_keyboard"].default is True + ReplyKeyboardRemove().remove_keyboard is True ), "Remove keyboard has incorrect default value!" @pytest.mark.parametrize( diff --git a/tests/test_api/test_types/test_user.py b/tests/test_api/test_types/test_user.py index 9f28175b..c018d608 100644 --- a/tests/test_api/test_types/test_user.py +++ b/tests/test_api/test_types/test_user.py @@ -54,5 +54,5 @@ class TestUser: def test_get_profile_photos(self): user = User(id=42, is_bot=False, first_name="Test", last_name="User") - method = user.get_profile_photos(description="test") + method = user.get_profile_photos(offset=0, limit=10) assert method.user_id == user.id diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index 41ecef1b..cf1dbf27 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -178,7 +178,19 @@ class TestDispatcher: async def test_silent_call_request(self, bot: MockedBot, caplog): dispatcher = Dispatcher() - bot.add_result_for(SendMessage, ok=False, error_code=400, description="Kaboom") + bot.add_result_for( + SendMessage, + ok=False, + error_code=400, + description="Kaboom", + result=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"), + ), + ) await dispatcher.silent_call_request(bot, SendMessage(chat_id=42, text="test")) log_records = [rec.message for rec in caplog.records] assert len(log_records) == 1 @@ -239,6 +251,7 @@ class TestDispatcher: channel_post=Message( message_id=42, date=datetime.datetime.now(), + from_user=User(id=42, is_bot=False, first_name="test"), text="test", chat=Chat(id=-42, type="private"), ), @@ -253,6 +266,7 @@ class TestDispatcher: edited_channel_post=Message( message_id=42, date=datetime.datetime.now(), + from_user=User(id=42, is_bot=False, first_name="test"), text="test", chat=Chat(id=-42, type="private"), ), diff --git a/tests/test_dispatcher/test_event/test_handler.py b/tests/test_dispatcher/test_event/test_handler.py index f7000d8e..da512977 100644 --- a/tests/test_dispatcher/test_event/test_handler.py +++ b/tests/test_dispatcher/test_event/test_handler.py @@ -145,7 +145,6 @@ class TestFilterObject: def test_post_init(self): case = F.test filter_obj = FilterObject(callback=case) - print(filter_obj.callback) assert filter_obj.callback == case.resolve diff --git a/tests/test_filters/test_chat_member_updated.py b/tests/test_filters/test_chat_member_updated.py index f3fdce66..b1ab97ec 100644 --- a/tests/test_filters/test_chat_member_updated.py +++ b/tests/test_filters/test_chat_member_updated.py @@ -1,5 +1,6 @@ from datetime import datetime +import msgspec import pytest from aiogram.filters.chat_member_updated import ( @@ -340,11 +341,17 @@ class TestChatMemberUpdatedStatusFilter: "can_send_other_messages": True, "can_add_web_page_previews": True, } + old = msgspec.to_builtins(old) + old["update"] = update + old = msgspec.from_builtins(old, ChatMember) + new = msgspec.to_builtins(new) + new["update"] = update + new = msgspec.from_builtins(new, ChatMember) event = ChatMemberUpdated( chat=Chat(id=42, type="test"), from_user=user, - old_chat_member=old.copy(update=update), - new_chat_member=new.copy(update=update), + old_chat_member=old, + new_chat_member=new, date=datetime.now(), ) diff --git a/tests/test_utils/test_link.py b/tests/test_utils/test_link.py index 77419441..f0276703 100644 --- a/tests/test_utils/test_link.py +++ b/tests/test_utils/test_link.py @@ -6,10 +6,10 @@ import pytest from aiogram.utils.link import ( BRANCH, + create_channel_bot_link, create_telegram_link, create_tg_link, docs_url, - create_channel_bot_link, ) diff --git a/tests/test_webhook/test_aiohtt_server.py b/tests/test_webhook/test_aiohtt_server.py index 5be8f43e..374fb1f3 100644 --- a/tests/test_webhook/test_aiohtt_server.py +++ b/tests/test_webhook/test_aiohtt_server.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Any, Dict from unittest.mock import AsyncMock, patch +import msgspec.json import pytest from aiohttp import MultipartReader, web from aiohttp.test_utils import TestClient @@ -123,12 +124,9 @@ class TestSimpleRequestHandler: resp = await self.make_reqest(client=client) assert resp.status == 200 - assert resp.content_type == "multipart/form-data" - result = {} - reader = MultipartReader.from_response(resp) - while part := await reader.next(): - value = await part.read() - result[part.name] = value.decode() + assert resp.content_type == "application/json" + result = msgspec.json.decode(await resp.read()) + print("RESULT:", result) assert result["method"] == "sendMessage" assert result["text"] == "PASS" @@ -150,12 +148,8 @@ class TestSimpleRequestHandler: resp = await self.make_reqest(client=client, text="spam") assert resp.status == 200 - assert resp.content_type == "multipart/form-data" - result = {} - reader = MultipartReader.from_response(resp) - while part := await reader.next(): - value = await part.read() - result[part.name] = value.decode() + assert resp.content_type == "application/json" + result = msgspec.json.decode(await resp.read()) assert not result async def test_reply_into_webhook_background(self, bot: MockedBot, aiohttp_client):