README.md edited
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# !/usr/bin/env python
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains classes that handle the networking backend of ``python-telegram-bot``."""
|
||||
|
||||
from ._baserequest import BaseRequest
|
||||
from ._httpxrequest import HTTPXRequest
|
||||
from ._requestdata import RequestData
|
||||
|
||||
__all__ = ("BaseRequest", "HTTPXRequest", "RequestData")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an abstract class to make POST and GET requests."""
|
||||
import abc
|
||||
import json
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from http import HTTPStatus
|
||||
from types import TracebackType
|
||||
from typing import Final, Optional, TypeVar, Union, final
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE as _DEFAULT_NONE
|
||||
from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram._version import __version__ as ptb_ver
|
||||
from telegram.error import (
|
||||
BadRequest,
|
||||
ChatMigrated,
|
||||
Conflict,
|
||||
Forbidden,
|
||||
InvalidToken,
|
||||
NetworkError,
|
||||
RetryAfter,
|
||||
TelegramError,
|
||||
)
|
||||
from telegram.request._requestdata import RequestData
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
RT = TypeVar("RT", bound="BaseRequest")
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="BaseRequest")
|
||||
|
||||
|
||||
class BaseRequest(
|
||||
AbstractAsyncContextManager["BaseRequest"],
|
||||
abc.ABC,
|
||||
):
|
||||
"""Abstract interface class that allows python-telegram-bot to make requests to the Bot API.
|
||||
Can be implemented via different asyncio HTTP libraries. An implementation of this class
|
||||
must implement all abstract methods and properties.
|
||||
|
||||
Instances of this class can be used as asyncio context managers, where
|
||||
|
||||
.. code:: python
|
||||
|
||||
async with request_object:
|
||||
# code
|
||||
|
||||
is roughly equivalent to
|
||||
|
||||
.. code:: python
|
||||
|
||||
try:
|
||||
await request_object.initialize()
|
||||
# code
|
||||
finally:
|
||||
await request_object.shutdown()
|
||||
|
||||
.. seealso:: :meth:`__aenter__` and :meth:`__aexit__`.
|
||||
|
||||
Tip:
|
||||
JSON encoding and decoding is done with the standard library's :mod:`json` by default.
|
||||
To use a custom library for this, you can override :meth:`parse_json_payload` and implement
|
||||
custom logic to encode the keys of :attr:`telegram.request.RequestData.parameters`.
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Builder Pattern <Builder-Pattern>`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
USER_AGENT: Final[str] = f"python-telegram-bot v{ptb_ver} (https://python-telegram-bot.org)"
|
||||
""":obj:`str`: A description that can be used as user agent for requests made to the Bot API.
|
||||
"""
|
||||
DEFAULT_NONE: Final[DefaultValue[None]] = _DEFAULT_NONE
|
||||
""":class:`object`: A special object that indicates that an argument of a function was not
|
||||
explicitly passed. Used for the timeout parameters of :meth:`post` and :meth:`do_request`.
|
||||
|
||||
Example:
|
||||
When calling ``request.post(url)``, ``request`` should use the default timeouts set on
|
||||
initialization. When calling ``request.post(url, connect_timeout=5, read_timeout=None)``,
|
||||
``request`` should use ``5`` for the connect timeout and :obj:`None` for the read timeout.
|
||||
|
||||
Use ``if parameter is (not) BaseRequest.DEFAULT_NONE:`` to check if the parameter was set.
|
||||
"""
|
||||
|
||||
async def __aenter__(self: RT) -> RT:
|
||||
"""|async_context_manager| :meth:`initializes <initialize>` the Request.
|
||||
|
||||
Returns:
|
||||
The initialized Request instance.
|
||||
|
||||
Raises:
|
||||
:exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown`
|
||||
is called in this case.
|
||||
"""
|
||||
try:
|
||||
await self.initialize()
|
||||
except Exception:
|
||||
await self.shutdown()
|
||||
raise
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""|async_context_manager| :meth:`shuts down <shutdown>` the Request."""
|
||||
# Make sure not to return `True` so that exceptions are not suppressed
|
||||
# https://docs.python.org/3/reference/datamodel.html?#object.__aexit__
|
||||
await self.shutdown()
|
||||
|
||||
@property
|
||||
def read_timeout(self) -> Optional[float]:
|
||||
"""This property must return the default read timeout in seconds used by this class.
|
||||
More precisely, the returned value should be the one used when
|
||||
:paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`.
|
||||
|
||||
.. versionadded:: 20.7
|
||||
|
||||
Warning:
|
||||
For now this property does not need to be implemented by subclasses and will raise
|
||||
:exc:`NotImplementedError` if accessed without being overridden. However, in future
|
||||
versions, this property will be abstract and must be implemented by subclasses.
|
||||
|
||||
Returns:
|
||||
:obj:`float` | :obj:`None`: The read timeout in seconds.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Stop & clear resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@final
|
||||
async def post(
|
||||
self,
|
||||
url: str,
|
||||
request_data: Optional[RequestData] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> Union[JSONDict, list[JSONDict], bool]:
|
||||
"""Makes a request to the Bot API handles the return code and parses the answer.
|
||||
|
||||
Warning:
|
||||
This method will be called by the methods of :class:`telegram.Bot` and should *not* be
|
||||
called manually.
|
||||
|
||||
Args:
|
||||
url (:obj:`str`): The URL to request.
|
||||
request_data (:class:`telegram.request.RequestData`, optional): An object containing
|
||||
information about parameters and files to upload for the request.
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a write operation to complete (in terms of
|
||||
a network socket; i.e. POSTing a request or uploading a file) instead of the time
|
||||
specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
|
||||
maximum amount of time (in seconds) to wait for a connection attempt to a server
|
||||
to succeed instead of the time specified during creating of this object. Defaults
|
||||
to :attr:`DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a connection to become available instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
|
||||
Returns:
|
||||
The JSON response of the Bot API.
|
||||
|
||||
"""
|
||||
result = await self._request_wrapper(
|
||||
url=url,
|
||||
method="POST",
|
||||
request_data=request_data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
)
|
||||
json_data = self.parse_json_payload(result)
|
||||
# For successful requests, the results are in the 'result' entry
|
||||
# see https://core.telegram.org/bots/api#making-requests
|
||||
return json_data["result"]
|
||||
|
||||
@final
|
||||
async def retrieve(
|
||||
self,
|
||||
url: str,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> bytes:
|
||||
"""Retrieve the contents of a file by its URL.
|
||||
|
||||
Warning:
|
||||
This method will be called by the methods of :class:`telegram.Bot` and should *not* be
|
||||
called manually.
|
||||
|
||||
Args:
|
||||
url (:obj:`str`): The web location we want to retrieve.
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a write operation to complete (in terms of
|
||||
a network socket; i.e. POSTing a request or uploading a file) instead of the time
|
||||
specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
|
||||
maximum amount of time (in seconds) to wait for a connection attempt to a server
|
||||
to succeed instead of the time specified during creating of this object. Defaults
|
||||
to :attr:`DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a connection to become available instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
|
||||
Returns:
|
||||
:obj:`bytes`: The files contents.
|
||||
|
||||
"""
|
||||
return await self._request_wrapper(
|
||||
url=url,
|
||||
method="GET",
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
)
|
||||
|
||||
async def _request_wrapper(
|
||||
self,
|
||||
url: str,
|
||||
method: str,
|
||||
request_data: Optional[RequestData] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> bytes:
|
||||
"""Wraps the real implementation request method.
|
||||
|
||||
Performs the following tasks:
|
||||
* Handle the various HTTP response codes.
|
||||
* Parse the Telegram server response.
|
||||
|
||||
Args:
|
||||
url (:obj:`str`): The URL to request.
|
||||
method (:obj:`str`): HTTP method (i.e. 'POST', 'GET', etc.).
|
||||
request_data (:class:`telegram.request.RequestData`, optional): An object containing
|
||||
information about parameters and files to upload for the request.
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a write operation to complete (in terms of
|
||||
a network socket; i.e. POSTing a request or uploading a file) instead of the time
|
||||
specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
|
||||
maximum amount of time (in seconds) to wait for a connection attempt to a server
|
||||
to succeed instead of the time specified during creating of this object. Defaults
|
||||
to :attr:`DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a connection to become available instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
|
||||
Returns:
|
||||
bytes: The payload part of the HTTP server response.
|
||||
|
||||
Raises:
|
||||
TelegramError
|
||||
|
||||
"""
|
||||
# Import needs to be here since HTTPXRequest is a subclass of BaseRequest
|
||||
from telegram.request import HTTPXRequest # pylint: disable=import-outside-toplevel
|
||||
|
||||
# 20 is the documented default value for all the media related bot methods and custom
|
||||
# implementations of BaseRequest may explicitly rely on that. Hence, we follow the
|
||||
# standard deprecation policy and deprecate starting with version 20.7.
|
||||
# For our own implementation HTTPXRequest, we can handle that ourselves, so we skip the
|
||||
# warning in that case.
|
||||
has_files = request_data and request_data.multipart_data
|
||||
if (
|
||||
has_files
|
||||
and not isinstance(self, HTTPXRequest)
|
||||
and isinstance(write_timeout, DefaultValue)
|
||||
):
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request"
|
||||
" will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
|
||||
"for *all* methods of the `Bot` class, including methods sending media.",
|
||||
),
|
||||
stacklevel=3,
|
||||
)
|
||||
write_timeout = 20
|
||||
|
||||
try:
|
||||
code, payload = await self.do_request(
|
||||
url=url,
|
||||
method=method,
|
||||
request_data=request_data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
)
|
||||
except TelegramError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise NetworkError(f"Unknown error in HTTP implementation: {exc!r}") from exc
|
||||
|
||||
if HTTPStatus.OK <= code <= 299:
|
||||
# 200-299 range are HTTP success statuses
|
||||
return payload
|
||||
|
||||
response_data = self.parse_json_payload(payload)
|
||||
|
||||
description = response_data.get("description")
|
||||
message = description if description else "Unknown HTTPError"
|
||||
|
||||
# In some special cases, we can raise more informative exceptions:
|
||||
# see https://core.telegram.org/bots/api#responseparameters and
|
||||
# https://core.telegram.org/bots/api#making-requests
|
||||
# TGs response also has the fields 'ok' and 'error_code'.
|
||||
# However, we rather rely on the HTTP status code for now.
|
||||
parameters = response_data.get("parameters")
|
||||
if parameters:
|
||||
migrate_to_chat_id = parameters.get("migrate_to_chat_id")
|
||||
if migrate_to_chat_id:
|
||||
raise ChatMigrated(migrate_to_chat_id)
|
||||
retry_after = parameters.get("retry_after")
|
||||
if retry_after:
|
||||
raise RetryAfter(retry_after)
|
||||
|
||||
message += f"\nThe server response contained unknown parameters: {parameters}"
|
||||
|
||||
if code == HTTPStatus.FORBIDDEN: # 403
|
||||
raise Forbidden(message)
|
||||
if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED): # 404 and 401
|
||||
# TG returns 404 Not found for
|
||||
# 1) malformed tokens
|
||||
# 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod
|
||||
# 2) is relevant only for Bot.do_api_request, where we have special handing for it.
|
||||
# TG returns 401 Unauthorized for correctly formatted tokens that are not valid
|
||||
raise InvalidToken(message)
|
||||
if code == HTTPStatus.BAD_REQUEST: # 400
|
||||
raise BadRequest(message)
|
||||
if code == HTTPStatus.CONFLICT: # 409
|
||||
raise Conflict(message)
|
||||
if code == HTTPStatus.BAD_GATEWAY: # 502
|
||||
raise NetworkError(description or "Bad Gateway")
|
||||
raise NetworkError(f"{message} ({code})")
|
||||
|
||||
@staticmethod
|
||||
def parse_json_payload(payload: bytes) -> JSONDict:
|
||||
"""Parse the JSON returned from Telegram.
|
||||
|
||||
Tip:
|
||||
By default, this method uses the standard library's :func:`json.loads` and
|
||||
``errors="replace"`` in :meth:`bytes.decode`.
|
||||
You can override it to customize either of these behaviors.
|
||||
|
||||
Args:
|
||||
payload (:obj:`bytes`): The UTF-8 encoded JSON payload as returned by Telegram.
|
||||
|
||||
Returns:
|
||||
dict: A JSON parsed as Python dict with results.
|
||||
|
||||
Raises:
|
||||
TelegramError: If loading the JSON data failed
|
||||
"""
|
||||
decoded_s = payload.decode(TextEncoding.UTF_8, "replace")
|
||||
try:
|
||||
return json.loads(decoded_s)
|
||||
except ValueError as exc:
|
||||
_LOGGER.exception('Can not load invalid JSON data: "%s"', decoded_s)
|
||||
raise TelegramError("Invalid server response") from exc
|
||||
|
||||
@abc.abstractmethod
|
||||
async def do_request(
|
||||
self,
|
||||
url: str,
|
||||
method: str,
|
||||
request_data: Optional[RequestData] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> tuple[int, bytes]:
|
||||
"""Makes a request to the Bot API. Must be implemented by a subclass.
|
||||
|
||||
Warning:
|
||||
This method will be called by :meth:`post` and :meth:`retrieve`. It should *not* be
|
||||
called manually.
|
||||
|
||||
Args:
|
||||
url (:obj:`str`): The URL to request.
|
||||
method (:obj:`str`): HTTP method (i.e. ``'POST'``, ``'GET'``, etc.).
|
||||
request_data (:class:`telegram.request.RequestData`, optional): An object containing
|
||||
information about parameters and files to upload for the request.
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a write operation to complete (in terms of
|
||||
a network socket; i.e. POSTing a request or uploading a file) instead of the time
|
||||
specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
|
||||
maximum amount of time (in seconds) to wait for a connection attempt to a server
|
||||
to succeed instead of the time specified during creating of this object. Defaults
|
||||
to :attr:`DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a connection to become available instead
|
||||
of the time specified during creating of this object. Defaults to
|
||||
:attr:`DEFAULT_NONE`.
|
||||
|
||||
Returns:
|
||||
tuple[:obj:`int`, :obj:`bytes`]: The HTTP return code & the payload part of the server
|
||||
response.
|
||||
"""
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains methods to make POST and GET requests using the httpx library."""
|
||||
from collections.abc import Collection
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import httpx
|
||||
|
||||
from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import HTTPVersion, ODVInput, SocketOpt
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.error import NetworkError, TimedOut
|
||||
from telegram.request._baserequest import BaseRequest
|
||||
from telegram.request._requestdata import RequestData
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
# Note to future devs:
|
||||
# Proxies are currently only tested manually. The httpx development docs have a nice guide on that:
|
||||
# https://www.python-httpx.org/contributing/#development-proxy-setup (also saved on archive.org)
|
||||
# That also works with socks5. Just pass `--mode socks5` to mitmproxy
|
||||
|
||||
_LOGGER = get_logger(__name__, "HTTPXRequest")
|
||||
|
||||
|
||||
class HTTPXRequest(BaseRequest):
|
||||
"""Implementation of :class:`~telegram.request.BaseRequest` using the library
|
||||
`httpx <https://www.python-httpx.org>`_.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
connection_pool_size (:obj:`int`, optional): Number of connections to keep in the
|
||||
connection pool. Defaults to ``1``.
|
||||
|
||||
Note:
|
||||
Independent of the value, one additional connection will be reserved for
|
||||
:meth:`telegram.Bot.get_updates`.
|
||||
proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward
|
||||
compatibility. Defaults to :obj:`None`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server.
|
||||
This value is used unless a different value is passed to :meth:`do_request`.
|
||||
Defaults to ``5``.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a write operation to complete (in terms of
|
||||
a network socket; i.e. POSTing a request or uploading a file).
|
||||
This value is used unless a different value is passed to :meth:`do_request`.
|
||||
Defaults to ``5``.
|
||||
|
||||
Hint:
|
||||
This timeout is used for all requests except for those that upload media/files.
|
||||
For the latter, :paramref:`media_write_timeout` is used.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
|
||||
maximum amount of time (in seconds) to wait for a connection attempt to a server
|
||||
to succeed. This value is used unless a different value is passed to
|
||||
:meth:`do_request`. Defaults to ``5``.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a connection to become available.
|
||||
This value is used unless a different value is passed to :meth:`do_request`.
|
||||
Defaults to ``1``.
|
||||
|
||||
Warning:
|
||||
With a finite pool timeout, you must expect :exc:`telegram.error.TimedOut`
|
||||
exceptions to be thrown when more requests are made simultaneously than there are
|
||||
connections in the connection pool!
|
||||
http_version (:obj:`str`, optional): If ``"2"`` or ``"2.0"``, HTTP/2 will be used instead
|
||||
of HTTP/1.1. Defaults to ``"1.1"``.
|
||||
|
||||
.. versionadded:: 20.1
|
||||
.. versionchanged:: 20.2
|
||||
Reset the default version to 1.1.
|
||||
|
||||
.. versionchanged:: 20.5
|
||||
Accept ``"2"`` as a valid value.
|
||||
socket_options (Collection[:obj:`tuple`], optional): Socket options to be passed to the
|
||||
underlying `library \
|
||||
<https://www.encode.io/httpcore/async/#httpcore.AsyncConnectionPool.__init__>`_.
|
||||
|
||||
Note:
|
||||
The values accepted by this parameter depend on the operating system.
|
||||
This is a low-level parameter and should only be used if you are familiar with
|
||||
these concepts.
|
||||
|
||||
.. versionadded:: 20.7
|
||||
proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``, optional): The URL to a proxy server,
|
||||
a ``httpx.Proxy`` object or a ``httpx.URL`` object. For example
|
||||
``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`.
|
||||
|
||||
Note:
|
||||
* The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or
|
||||
``ALL_PROXY``. See `the docs of httpx`_ for more info.
|
||||
* HTTPS proxies can be configured by passing a ``httpx.Proxy`` object with
|
||||
a corresponding ``ssl_context``.
|
||||
* For Socks5 support, additional dependencies are required. Make sure to install
|
||||
PTB via :command:`pip install "python-telegram-bot[socks]"` in this case.
|
||||
* Socks5 proxies can not be set via environment variables.
|
||||
|
||||
.. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies
|
||||
|
||||
.. versionadded:: 20.7
|
||||
media_write_timeout (:obj:`float` | :obj:`None`, optional): Like :paramref:`write_timeout`,
|
||||
but used only for requests that upload media/files. This value is used unless a
|
||||
different value is passed to :paramref:`do_request.write_timeout` of
|
||||
:meth:`do_request`. Defaults to ``20`` seconds.
|
||||
|
||||
.. versionadded:: 21.0
|
||||
httpx_kwargs (dict[:obj:`str`, Any], optional): Additional keyword arguments to be passed
|
||||
to the `httpx.AsyncClient <https://www.python-httpx.org/api/#asyncclient>`_
|
||||
constructor.
|
||||
|
||||
Warning:
|
||||
This parameter is intended for advanced users that want to fine-tune the behavior
|
||||
of the underlying ``httpx`` client. The values passed here will override all the
|
||||
defaults set by ``python-telegram-bot`` and all other parameters passed to
|
||||
:class:`HTTPXRequest`. The only exception is the :paramref:`media_write_timeout`
|
||||
parameter, which is not passed to the client constructor.
|
||||
No runtime warnings will be issued about parameters that are overridden in this
|
||||
way.
|
||||
|
||||
.. versionadded:: 21.6
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_client", "_client_kwargs", "_http_version", "_media_write_timeout")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection_pool_size: int = 1,
|
||||
proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
|
||||
read_timeout: Optional[float] = 5.0,
|
||||
write_timeout: Optional[float] = 5.0,
|
||||
connect_timeout: Optional[float] = 5.0,
|
||||
pool_timeout: Optional[float] = 1.0,
|
||||
http_version: HTTPVersion = "1.1",
|
||||
socket_options: Optional[Collection[SocketOpt]] = None,
|
||||
proxy: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
|
||||
media_write_timeout: Optional[float] = 20.0,
|
||||
httpx_kwargs: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
if proxy_url is not None and proxy is not None:
|
||||
raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.")
|
||||
|
||||
if proxy_url is not None:
|
||||
proxy = proxy_url
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._http_version = http_version
|
||||
self._media_write_timeout = media_write_timeout
|
||||
timeout = httpx.Timeout(
|
||||
connect=connect_timeout,
|
||||
read=read_timeout,
|
||||
write=write_timeout,
|
||||
pool=pool_timeout,
|
||||
)
|
||||
limits = httpx.Limits(
|
||||
max_connections=connection_pool_size,
|
||||
max_keepalive_connections=connection_pool_size,
|
||||
)
|
||||
|
||||
if http_version not in ("1.1", "2", "2.0"):
|
||||
raise ValueError("`http_version` must be either '1.1', '2.0' or '2'.")
|
||||
|
||||
http1 = http_version == "1.1"
|
||||
http_kwargs = {"http1": http1, "http2": not http1}
|
||||
transport = (
|
||||
httpx.AsyncHTTPTransport(
|
||||
socket_options=socket_options,
|
||||
)
|
||||
if socket_options
|
||||
else None
|
||||
)
|
||||
self._client_kwargs = {
|
||||
"timeout": timeout,
|
||||
"proxy": proxy,
|
||||
"limits": limits,
|
||||
"transport": transport,
|
||||
**http_kwargs,
|
||||
**(httpx_kwargs or {}),
|
||||
}
|
||||
|
||||
try:
|
||||
self._client = self._build_client()
|
||||
except ImportError as exc:
|
||||
if "httpx[http2]" not in str(exc) and "httpx[socks]" not in str(exc):
|
||||
raise
|
||||
|
||||
if "httpx[socks]" in str(exc):
|
||||
raise RuntimeError(
|
||||
"To use Socks5 proxies, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[socks]"`.'
|
||||
) from exc
|
||||
raise RuntimeError(
|
||||
"To use HTTP/2, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[http2]"`.'
|
||||
) from exc
|
||||
|
||||
@property
|
||||
def http_version(self) -> str:
|
||||
"""
|
||||
:obj:`str`: Used HTTP version, see :paramref:`http_version`.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
"""
|
||||
return self._http_version
|
||||
|
||||
@property
|
||||
def read_timeout(self) -> Optional[float]:
|
||||
"""See :attr:`BaseRequest.read_timeout`.
|
||||
|
||||
Returns:
|
||||
:obj:`float` | :obj:`None`: The default read timeout in seconds as passed to
|
||||
:paramref:`HTTPXRequest.read_timeout`.
|
||||
"""
|
||||
return self._client.timeout.read
|
||||
|
||||
def _build_client(self) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(**self._client_kwargs)
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""See :meth:`BaseRequest.initialize`."""
|
||||
if self._client.is_closed:
|
||||
self._client = self._build_client()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""See :meth:`BaseRequest.shutdown`."""
|
||||
if self._client.is_closed:
|
||||
_LOGGER.debug("This HTTPXRequest is already shut down. Returning.")
|
||||
return
|
||||
|
||||
await self._client.aclose()
|
||||
|
||||
async def do_request(
|
||||
self,
|
||||
url: str,
|
||||
method: str,
|
||||
request_data: Optional[RequestData] = None,
|
||||
read_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE,
|
||||
) -> tuple[int, bytes]:
|
||||
"""See :meth:`BaseRequest.do_request`."""
|
||||
if self._client.is_closed:
|
||||
raise RuntimeError("This HTTPXRequest is not initialized!")
|
||||
|
||||
files = request_data.multipart_data if request_data else None
|
||||
data = request_data.json_parameters if request_data else None
|
||||
|
||||
# If user did not specify timeouts (for e.g. in a bot method), use the default ones when we
|
||||
# created this instance.
|
||||
if isinstance(read_timeout, DefaultValue):
|
||||
read_timeout = self._client.timeout.read
|
||||
if isinstance(connect_timeout, DefaultValue):
|
||||
connect_timeout = self._client.timeout.connect
|
||||
if isinstance(pool_timeout, DefaultValue):
|
||||
pool_timeout = self._client.timeout.pool
|
||||
|
||||
if isinstance(write_timeout, DefaultValue):
|
||||
write_timeout = self._client.timeout.write if not files else self._media_write_timeout
|
||||
|
||||
timeout = httpx.Timeout(
|
||||
connect=connect_timeout,
|
||||
read=read_timeout,
|
||||
write=write_timeout,
|
||||
pool=pool_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
res = await self._client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers={"User-Agent": self.USER_AGENT},
|
||||
timeout=timeout,
|
||||
files=files,
|
||||
data=data,
|
||||
)
|
||||
except httpx.TimeoutException as err:
|
||||
if isinstance(err, httpx.PoolTimeout):
|
||||
raise TimedOut(
|
||||
message=(
|
||||
"Pool timeout: All connections in the connection pool are occupied. "
|
||||
"Request was *not* sent to Telegram. Consider adjusting the connection "
|
||||
"pool size or the pool timeout."
|
||||
)
|
||||
) from err
|
||||
raise TimedOut from err
|
||||
except httpx.HTTPError as err:
|
||||
# HTTPError must come last as its the base httpx exception class
|
||||
# TODO p4: do something smart here; for now just raise NetworkError
|
||||
|
||||
# We include the class name for easier debugging. Especially useful if the error
|
||||
# message of `err` is empty.
|
||||
raise NetworkError(f"httpx.{err.__class__.__name__}: {err}") from err
|
||||
|
||||
return res.status_code, res.content
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains a class that holds the parameters of a request to the Bot API."""
|
||||
import json
|
||||
from typing import Any, Optional, Union, final
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import UploadFileDict
|
||||
from telegram.request._requestparameter import RequestParameter
|
||||
|
||||
|
||||
@final
|
||||
class RequestData:
|
||||
"""Instances of this class collect the data needed for one request to the Bot API, including
|
||||
all parameters and files to be sent along with the request.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Warning:
|
||||
How exactly instances of this are created should be considered an implementation detail
|
||||
and not part of PTBs public API. Users should exclusively rely on the documented
|
||||
attributes, properties and methods.
|
||||
|
||||
Attributes:
|
||||
contains_files (:obj:`bool`): Whether this object contains files to be uploaded via
|
||||
``multipart/form-data``.
|
||||
"""
|
||||
|
||||
__slots__ = ("_parameters", "contains_files")
|
||||
|
||||
def __init__(self, parameters: Optional[list[RequestParameter]] = None):
|
||||
self._parameters: list[RequestParameter] = parameters or []
|
||||
self.contains_files: bool = any(param.input_files for param in self._parameters)
|
||||
|
||||
@property
|
||||
def parameters(self) -> dict[str, Union[str, int, list[Any], dict[Any, Any]]]:
|
||||
"""Gives the parameters as mapping of parameter name to the parameter value, which can be
|
||||
a single object of type :obj:`int`, :obj:`float`, :obj:`str` or :obj:`bool` or any
|
||||
(possibly nested) composition of lists, tuples and dictionaries, where each entry, key
|
||||
and value is of one of the mentioned types.
|
||||
|
||||
Returns:
|
||||
dict[:obj:`str`, Union[:obj:`str`, :obj:`int`, list[any], dict[any, any]]]
|
||||
"""
|
||||
return {
|
||||
param.name: param.value # type: ignore[misc]
|
||||
for param in self._parameters
|
||||
if param.value is not None
|
||||
}
|
||||
|
||||
@property
|
||||
def json_parameters(self) -> dict[str, str]:
|
||||
"""Gives the parameters as mapping of parameter name to the respective JSON encoded
|
||||
value.
|
||||
|
||||
Tip:
|
||||
By default, this property uses the standard library's :func:`json.dumps`.
|
||||
To use a custom library for JSON encoding, you can directly encode the keys of
|
||||
:attr:`parameters` - note that string valued keys should not be JSON encoded.
|
||||
|
||||
Returns:
|
||||
dict[:obj:`str`, :obj:`str`]
|
||||
"""
|
||||
return {
|
||||
param.name: param.json_value
|
||||
for param in self._parameters
|
||||
if param.json_value is not None
|
||||
}
|
||||
|
||||
def url_encoded_parameters(self, encode_kwargs: Optional[dict[str, Any]] = None) -> str:
|
||||
"""Encodes the parameters with :func:`urllib.parse.urlencode`.
|
||||
|
||||
Args:
|
||||
encode_kwargs (dict[:obj:`str`, any], optional): Additional keyword arguments to pass
|
||||
along to :func:`urllib.parse.urlencode`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`
|
||||
"""
|
||||
if encode_kwargs:
|
||||
return urlencode(self.json_parameters, **encode_kwargs)
|
||||
return urlencode(self.json_parameters)
|
||||
|
||||
def parametrized_url(self, url: str, encode_kwargs: Optional[dict[str, Any]] = None) -> str:
|
||||
"""Shortcut for attaching the return value of :meth:`url_encoded_parameters` to the
|
||||
:paramref:`url`.
|
||||
|
||||
Args:
|
||||
url (:obj:`str`): The URL the parameters will be attached to.
|
||||
encode_kwargs (dict[:obj:`str`, any], optional): Additional keyword arguments to pass
|
||||
along to :func:`urllib.parse.urlencode`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`
|
||||
"""
|
||||
url_parameters = self.url_encoded_parameters(encode_kwargs=encode_kwargs)
|
||||
return f"{url}?{url_parameters}"
|
||||
|
||||
@property
|
||||
def json_payload(self) -> bytes:
|
||||
"""The :attr:`parameters` as UTF-8 encoded JSON payload.
|
||||
|
||||
Tip:
|
||||
By default, this property uses the standard library's :func:`json.dumps`.
|
||||
To use a custom library for JSON encoding, you can directly encode the keys of
|
||||
:attr:`parameters` - note that string valued keys should not be JSON encoded.
|
||||
|
||||
Returns:
|
||||
:obj:`bytes`
|
||||
"""
|
||||
return json.dumps(self.json_parameters).encode(TextEncoding.UTF_8)
|
||||
|
||||
@property
|
||||
def multipart_data(self) -> UploadFileDict:
|
||||
"""Gives the files contained in this object as mapping of part name to encoded content.
|
||||
|
||||
.. versionchanged:: 21.5
|
||||
Content may now be a file handle.
|
||||
"""
|
||||
multipart_data: UploadFileDict = {}
|
||||
for param in self._parameters:
|
||||
m_data = param.multipart_data
|
||||
if m_data:
|
||||
multipart_data.update(m_data)
|
||||
return multipart_data
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains a class that describes a single parameter of a request to the Bot API."""
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, final
|
||||
|
||||
from telegram._files.inputfile import InputFile
|
||||
from telegram._files.inputmedia import InputMedia, InputPaidMedia
|
||||
from telegram._files.inputsticker import InputSticker
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.datetime import to_timestamp
|
||||
from telegram._utils.enum import StringEnum
|
||||
from telegram._utils.types import UploadFileDict
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(repr=True, eq=False, order=False, frozen=True)
|
||||
class RequestParameter:
|
||||
"""Instances of this class represent a single parameter to be sent along with a request to
|
||||
the Bot API.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Warning:
|
||||
This class intended is to be used internally by the library and *not* by the user. Changes
|
||||
to this class are not considered breaking changes and may not be documented in the
|
||||
changelog.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The name of the parameter.
|
||||
value (:obj:`object` | :obj:`None`): The value of the parameter. Must be JSON-dumpable.
|
||||
input_files (list[:class:`telegram.InputFile`], optional): A list of files that should be
|
||||
uploaded along with this parameter.
|
||||
|
||||
Attributes:
|
||||
name (:obj:`str`): The name of the parameter.
|
||||
value (:obj:`object` | :obj:`None`): The value of the parameter.
|
||||
input_files (list[:class:`telegram.InputFile` | :obj:`None`): A list of files that should
|
||||
be uploaded along with this parameter.
|
||||
"""
|
||||
|
||||
__slots__ = ("input_files", "name", "value")
|
||||
|
||||
name: str
|
||||
value: object
|
||||
input_files: Optional[list[InputFile]]
|
||||
|
||||
@property
|
||||
def json_value(self) -> Optional[str]:
|
||||
"""The JSON dumped :attr:`value` or :obj:`None` if :attr:`value` is :obj:`None`.
|
||||
The latter can currently only happen if :attr:`input_files` has exactly one element that
|
||||
must not be uploaded via an attach:// URI.
|
||||
"""
|
||||
if isinstance(self.value, str):
|
||||
return self.value
|
||||
if self.value is None:
|
||||
return None
|
||||
return json.dumps(self.value)
|
||||
|
||||
@property
|
||||
def multipart_data(self) -> Optional[UploadFileDict]:
|
||||
"""A dict with the file data to upload, if any.
|
||||
|
||||
.. versionchanged:: 21.5
|
||||
Content may now be a file handle.
|
||||
"""
|
||||
if not self.input_files:
|
||||
return None
|
||||
return {
|
||||
(input_file.attach_name or self.name): input_file.field_tuple
|
||||
for input_file in self.input_files
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _value_and_input_files_from_input( # pylint: disable=too-many-return-statements
|
||||
value: object,
|
||||
) -> tuple[object, list[InputFile]]:
|
||||
"""Converts `value` into something that we can json-dump. Returns two values:
|
||||
1. the JSON-dumpable value. May be `None` in case the value is an InputFile which must
|
||||
not be uploaded via an attach:// URI
|
||||
2. A list of InputFiles that should be uploaded for this value
|
||||
|
||||
Note that we handle files differently depending on whether attaching them via an URI of the
|
||||
form attach://<name> is documented to be allowed or not.
|
||||
There was some confusion whether this worked for all files, so that we stick to the
|
||||
documented ways for now.
|
||||
See https://github.com/tdlib/telegram-bot-api/issues/167 and
|
||||
https://github.com/tdlib/telegram-bot-api/issues/259
|
||||
|
||||
This method only does some special casing for our own helper class StringEnum, but not
|
||||
for general enums. This is because:
|
||||
* tg.constants currently only uses IntEnum as second enum type and json dumping that
|
||||
is no problem
|
||||
* if a user passes a custom enum, it's unlikely that we can actually properly handle it
|
||||
even with some special casing.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return to_timestamp(value), []
|
||||
if isinstance(value, StringEnum):
|
||||
return value.value, []
|
||||
if isinstance(value, InputFile):
|
||||
if value.attach_uri:
|
||||
return value.attach_uri, [value]
|
||||
return None, [value]
|
||||
|
||||
if isinstance(value, (InputMedia, InputPaidMedia)) and isinstance(value.media, InputFile):
|
||||
# We call to_dict and change the returned dict instead of overriding
|
||||
# value.media in case the same value is reused for another request
|
||||
data = value.to_dict()
|
||||
if value.media.attach_uri:
|
||||
data["media"] = value.media.attach_uri
|
||||
else:
|
||||
data.pop("media", None)
|
||||
|
||||
thumbnail = data.get("thumbnail", None)
|
||||
if isinstance(thumbnail, InputFile):
|
||||
if thumbnail.attach_uri:
|
||||
data["thumbnail"] = thumbnail.attach_uri
|
||||
else:
|
||||
data.pop("thumbnail", None)
|
||||
return data, [value.media, thumbnail]
|
||||
|
||||
return data, [value.media]
|
||||
if isinstance(value, InputSticker) and isinstance(value.sticker, InputFile):
|
||||
# We call to_dict and change the returned dict instead of overriding
|
||||
# value.sticker in case the same value is reused for another request
|
||||
data = value.to_dict()
|
||||
data["sticker"] = value.sticker.attach_uri
|
||||
return data, [value.sticker]
|
||||
|
||||
if isinstance(value, TelegramObject):
|
||||
# Needs to be last, because InputMedia is a subclass of TelegramObject
|
||||
return value.to_dict(), []
|
||||
return value, []
|
||||
|
||||
@classmethod
|
||||
def from_input(cls, key: str, value: object) -> "RequestParameter":
|
||||
"""Builds an instance of this class for a given key-value pair that represents the raw
|
||||
input as passed along from a method of :class:`telegram.Bot`.
|
||||
"""
|
||||
if not isinstance(value, (str, bytes)) and isinstance(value, Sequence):
|
||||
param_values = []
|
||||
input_files = []
|
||||
for obj in value:
|
||||
param_value, input_file = cls._value_and_input_files_from_input(obj)
|
||||
if param_value is not None:
|
||||
param_values.append(param_value)
|
||||
input_files.extend(input_file)
|
||||
return RequestParameter(
|
||||
name=key, value=param_values, input_files=input_files if input_files else None
|
||||
)
|
||||
|
||||
param_value, input_files = cls._value_and_input_files_from_input(value)
|
||||
return RequestParameter(
|
||||
name=key, value=param_value, input_files=input_files if input_files else None
|
||||
)
|
||||
Reference in New Issue
Block a user