Add Parameter read_file_handle to InputFile (#4388)

This commit is contained in:
Bibo-Joshi 2024-08-02 22:28:38 +02:00 committed by GitHub
parent e637d1733c
commit 3a49372591
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 206 additions and 72 deletions

View file

@ -1305,8 +1305,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): Photo to send.
photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`telegram.PhotoSize`): Photo to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
@ -1465,9 +1465,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Audio`): Audio file to send.
|fileinput|
audio (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Audio`): Audio file to
send. |fileinput|
Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2
@ -1617,8 +1617,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Document`): File to send.
document (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Document`): File to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Document` object to send.
@ -1755,8 +1755,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Sticker`): Sticker to send.
sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Sticker`): Sticker to send.
|fileinput| Video stickers can only be sent by a ``file_id``. Video and animated
stickers can't be sent via an HTTP URL.
@ -1895,8 +1895,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): Video file to send.
video (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`telegram.Video`): Video file to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Video` object to send.
@ -2059,8 +2059,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.VideoNote`): Video note to send.
video_note (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.VideoNote`): Video note
to send.
Pass a file_id as String to send a video note that exists on the Telegram
servers (recommended) or upload a new video using multipart/form-data.
|uploadinput|
@ -2209,9 +2210,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation`): Animation to send.
|fileinput|
animation (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Animation`): Animation to
send. |fileinput|
Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2
@ -2371,8 +2372,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Voice`): Voice file to send.
voice (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`telegram.Voice`): Voice file to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Voice` object to send.
@ -6370,8 +6371,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args:
user_id (:obj:`int`): User identifier of sticker file owner.
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`):
A file with the sticker in the ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"``
sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path`): A file with the sticker in the
``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"``
format. See `here <https://core.telegram.org/stickers>`_ for technical requirements
. |uploadinput|
@ -6695,8 +6697,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 21.1
thumbnail (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
optional): A **.WEBP** or **.PNG** image with the thumbnail, must
thumbnail (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path`, optional): A **.WEBP** or **.PNG** image
with the thumbnail, must
be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE`
kilobytes in size and have width and height of exactly
:tg-const:`telegram.constants.StickerSetLimit.STATIC_THUMB_DIMENSIONS` px, or a

View file

@ -22,7 +22,7 @@ import mimetypes
from typing import IO, Optional, Union
from uuid import uuid4
from telegram._utils.files import load_file
from telegram._utils.files import guess_file_name, load_file
from telegram._utils.strings import TextEncoding
from telegram._utils.types import FieldTuple
@ -53,9 +53,36 @@ class InputFile:
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`.
read_file_handle (:obj:`bool`, optional): If :obj:`True` and :paramref:`obj` is a file
handle, the data will be read from the file handle on initialization of this object.
If :obj:`False`, the file handle will be passed on to the
`networking backend <telegram.request.BaseRequest.do_request>`_ which will have to
handle the reading. Defaults to :obj:`True`.
Tip:
If you upload extremely large files, you may want to set this to :obj:`False` to
avoid reading the complete file into memory. Additionally, this may be supported
better by the networking backend (in particular it is handled better by
the default :class:`~telegram.request.HTTPXRequest`).
Important:
If you set this to :obj:`False`, you have to ensure that the file handle is still
open when the request is made. In particular, the following snippet can *not* work
as expected.
.. code-block:: python
with open('file.txt', 'rb') as file:
input_file = InputFile(file, read_file_handle=False)
# here the file handle is already closed and the upload will fail
await bot.send_document(chat_id, input_file)
.. versionadded:: NEXT.VERSION
Attributes:
input_file_content (:obj:`bytes`): The binary content of the file to send.
input_file_content (:obj:`bytes` | :class:`IO`): The binary content of the file to send.
attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in
the request to Telegram should point to the multipart data via a an URI of the form
``attach://<attach_name>`` URI.
@ -71,14 +98,18 @@ class InputFile:
obj: Union[IO[bytes], bytes, str],
filename: Optional[str] = None,
attach: bool = False,
read_file_handle: bool = True,
):
if isinstance(obj, bytes):
self.input_file_content: bytes = obj
self.input_file_content: Union[bytes, IO[bytes]] = obj
elif isinstance(obj, str):
self.input_file_content = obj.encode(TextEncoding.UTF_8)
else:
elif read_file_handle:
reported_filename, self.input_file_content = load_file(obj)
filename = filename or reported_filename
else:
self.input_file_content = obj
filename = filename or guess_file_name(obj)
self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None
@ -95,8 +126,11 @@ class InputFile:
def field_tuple(self) -> FieldTuple:
"""Field tuple representing the contents of the file for upload to the Telegram servers.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
Returns:
Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]:
Tuple[:obj:`str`, :obj:`bytes` | :class:`IO`, :obj:`str`]:
"""
return self.filename, self.input_file_content, self.mimetype

View file

@ -50,8 +50,8 @@ class InputMedia(TelegramObject):
Args:
media_type (:obj:`str`): Type of media that the instance represents.
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation` | :class:`telegram.Audio` | \
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Animation` | :class:`telegram.Audio` | \
:class:`telegram.Document` | :class:`telegram.PhotoSize` | \
:class:`telegram.Video`): File to send.
|fileinputnopath|
@ -128,8 +128,9 @@ class InputPaidMedia(TelegramObject):
Args:
type (:obj:`str`): Type of media that the instance represents.
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.PhotoSize` | :class:`telegram.Video`): File
to send. |fileinputnopath|
Lastly you can pass an existing telegram media object of the corresponding type
to send.
@ -167,8 +168,8 @@ class InputPaidMediaPhoto(InputPaidMedia):
.. versionadded:: 21.4
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
Attributes:
@ -207,8 +208,8 @@ class InputPaidMediaVideo(InputPaidMedia):
changed by Telegram.
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Video` object to send.
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): |thumbdocstringnopath|
@ -278,8 +279,8 @@ class InputMediaAnimation(InputMedia):
|removed_thumb_note|
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Animation`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2
@ -401,8 +402,8 @@ class InputMediaPhoto(InputMedia):
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
.. versionchanged:: 13.2
@ -501,8 +502,8 @@ class InputMediaVideo(InputMedia):
|removed_thumb_note|
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Video` object to send.
.. versionchanged:: 13.2
@ -639,8 +640,8 @@ class InputMediaAudio(InputMedia):
|removed_thumb_note|
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Audio`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Audio`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2
@ -743,8 +744,8 @@ class InputMediaDocument(InputMedia):
|removed_thumb_note|
Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Document`): File to send. |fileinputnopath|
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`telegram.Document`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Document` object to send.
.. versionchanged:: 13.2

View file

@ -41,7 +41,8 @@ class InputSticker(TelegramObject):
order of the arguments has changed.
Args:
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The
sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path`): The
added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via
HTTP URL.
emoji_list (Sequence[:obj:`str`]): Sequence of

View file

@ -61,14 +61,21 @@ def load_file(
except AttributeError:
return None, cast(Union[bytes, "InputFile", str, Path], obj)
if hasattr(obj, "name") and not isinstance(obj.name, int):
filename = Path(obj.name).name
else:
filename = None
filename = guess_file_name(obj)
return filename, contents
def guess_file_name(obj: FileInput) -> Optional[str]:
"""If the input is a file handle, read name and return it. Otherwise, return
the input unchanged.
"""
if hasattr(obj, "name") and not isinstance(obj.name, int):
return Path(obj.name).name
return None
def is_local_file(obj: Optional[FilePathInput]) -> bool:
"""
Checks if a given string is a file on local system.
@ -110,8 +117,8 @@ def parse_file_input( # pylint: disable=too-many-return-statements
attribute.
Args:
file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | Telegram media object): The
input to parse.
file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | :class:`~telegram.InputFile`\
| Telegram media object): The input to parse.
tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
:class:`telegram.Animation`.
filename (:obj:`str`, optional): The filename. Only relevant in case an

View file

@ -82,7 +82,7 @@ ReplyMarkup = Union[
.. versionadded:: 20.0
"""
FieldTuple = Tuple[str, bytes, str]
FieldTuple = Tuple[str, Union[bytes, IO[bytes]], str]
"""Alias for return type of `InputFile.field_tuple`."""
UploadFileDict = Dict[str, FieldTuple]
"""Dictionary containing file data to be uploaded to the API."""

View file

@ -129,7 +129,11 @@ class RequestData:
@property
def multipart_data(self) -> UploadFileDict:
"""Gives the files contained in this object as mapping of part name to encoded content."""
"""Gives the files contained in this object as mapping of part name to encoded content.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
"""
multipart_data: UploadFileDict = {}
for param in self._parameters:
m_data = param.multipart_data

View file

@ -77,7 +77,11 @@ class RequestParameter:
@property
def multipart_data(self) -> Optional[UploadFileDict]:
"""A dict with the file data to upload, if any."""
"""A dict with the file data to upload, if any.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
"""
if not self.input_files:
return None
return {

View file

@ -19,7 +19,7 @@
import contextlib
import subprocess
import sys
from io import BytesIO
from io import BufferedReader, BytesIO
import pytest
@ -66,21 +66,45 @@ class TestInputFileWithoutRequest:
assert input_file.attach_name is None
assert input_file.attach_uri is None
def test_mimetypes(self):
@pytest.mark.parametrize("read_file_handle", [True, False])
def test_mimetypes_file_handle(self, read_file_handle):
# Only test a few to make sure logic works okay
assert InputFile(data_file("telegram.jpg").open("rb")).mimetype == "image/jpeg"
assert (
InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
).mimetype
== "image/jpeg"
)
# For some reason python can guess the type on macOS
assert InputFile(data_file("telegram.webp").open("rb")).mimetype in [
assert InputFile(
data_file("telegram.webp").open("rb"), read_file_handle=read_file_handle
).mimetype in [
"application/octet-stream",
"image/webp",
]
assert InputFile(data_file("telegram.mp3").open("rb")).mimetype == "audio/mpeg"
assert (
InputFile(
data_file("telegram.mp3").open("rb"), read_file_handle=read_file_handle
).mimetype
== "audio/mpeg"
)
# For some reason windows drops the trailing i
assert InputFile(data_file("telegram.midi").open("rb")).mimetype in [
assert InputFile(
data_file("telegram.midi").open("rb"), read_file_handle=read_file_handle
).mimetype in [
"audio/mid",
"audio/midi",
]
# Test string file
assert (
InputFile(
data_file("text_file.txt").open("rb"), read_file_handle=read_file_handle
).mimetype
== "text/plain"
)
def test_mimetypes_other(self):
# Test guess from file
assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg"
assert InputFile(BytesIO(b"blah"), filename="tg.mp3").mimetype == "audio/mpeg"
@ -92,20 +116,49 @@ class TestInputFileWithoutRequest:
)
assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream"
# Test string file
assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain"
def test_filenames(self):
assert InputFile(data_file("telegram.jpg").open("rb")).filename == "telegram.jpg"
assert InputFile(data_file("telegram.jpg").open("rb"), filename="blah").filename == "blah"
@pytest.mark.parametrize("read_file_handle", [True, False])
def test_filenames(self, read_file_handle):
assert (
InputFile(data_file("telegram.jpg").open("rb"), filename="blah.jpg").filename
InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
).filename
== "telegram.jpg"
)
assert (
InputFile(
data_file("telegram.jpg").open("rb"),
filename="blah",
read_file_handle=read_file_handle,
).filename
== "blah"
)
assert (
InputFile(
data_file("telegram.jpg").open("rb"),
filename="blah.jpg",
read_file_handle=read_file_handle,
).filename
== "blah.jpg"
)
assert InputFile(data_file("telegram").open("rb")).filename == "telegram"
assert InputFile(data_file("telegram").open("rb"), filename="blah").filename == "blah"
assert (
InputFile(data_file("telegram").open("rb"), filename="blah.jpg").filename == "blah.jpg"
InputFile(data_file("telegram").open("rb"), read_file_handle=read_file_handle).filename
== "telegram"
)
assert (
InputFile(
data_file("telegram").open("rb"),
filename="blah",
read_file_handle=read_file_handle,
).filename
== "blah"
)
assert (
InputFile(
data_file("telegram").open("rb"),
filename="blah.jpg",
read_file_handle=read_file_handle,
).filename
== "blah.jpg"
)
class MockedFileobject:
@ -140,6 +193,19 @@ class TestInputFileWithoutRequest:
== "blah.jpg"
)
@pytest.mark.parametrize("read_file_handle", [True, False])
def test_read_file_handle(self, read_file_handle):
input_file = InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
)
content = input_file.field_tuple[1]
if read_file_handle:
assert isinstance(content, bytes)
assert content == data_file("telegram.jpg").read_bytes()
else:
assert isinstance(content, BufferedReader)
assert content.read() == data_file("telegram.jpg").read_bytes()
class TestInputFileWithRequest:
async def test_send_bytes(self, bot, chat_id):

View file

@ -30,6 +30,7 @@ import httpx
import pytest
from httpx import AsyncHTTPTransport
from telegram import InputFile
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.strings import TextEncoding
from telegram.error import (
@ -48,6 +49,7 @@ from telegram.request._httpxrequest import HTTPXRequest
from telegram.request._requestparameter import RequestParameter
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
from tests.auxil.files import data_file
from tests.auxil.networking import NonchalantHttpxRequest
from tests.auxil.slots import mro_slots
@ -821,3 +823,15 @@ class TestHTTPXRequestWithRequest:
task_2.exception()
except (asyncio.CancelledError, asyncio.InvalidStateError):
pass
async def test_input_file_postponed_read(self, bot, chat_id):
"""Here we test that `read_file_handle=False` is correctly handled by HTTPXRequest.
Since manually building the RequestData object has no real benefit, we simply use the Bot
for that.
"""
message = await bot.send_document(
document=InputFile(data_file("telegram.jpg").open("rb"), read_file_handle=False),
chat_id=chat_id,
)
assert message.document
assert message.document.file_name == "telegram.jpg"

View file

@ -117,7 +117,7 @@ PTB_EXTRA_PARAMS = {
"PassportElementError": {"source", "type", "message"},
"InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"},
"InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"},
"InputFile": {"attach", "filename", "obj"},
"InputFile": {"attach", "filename", "obj", "read_file_handle"},
"MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls
"ChatBoostSource": {"source"}, # attributes common to all subclasses
"MessageOrigin": {"type", "date"}, # attributes common to all subclasses