Skip to content

Commit

Permalink
Add Bot.do_api_request (#4084)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bibo-Joshi committed Feb 7, 2024
1 parent 7e9537e commit 29866e2
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 17 deletions.
3 changes: 2 additions & 1 deletion docs/auxil/kwargs_insertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
),
(
" api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments"
" to be passed to the Telegram API."
" to be passed to the Telegram API. See :meth:`~telegram.Bot.do_api_request` for"
" limitations."
),
"",
]
Expand Down
102 changes: 99 additions & 3 deletions telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,12 @@
from telegram._utils.files import is_local_file, parse_file_input
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.strings import to_camel_case
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
from telegram._utils.warnings import warn
from telegram._webhookinfo import WebhookInfo
from telegram.constants import InlineQueryLimit
from telegram.error import InvalidToken
from telegram.error import EndPointNotFound, InvalidToken
from telegram.request import BaseRequest, RequestData
from telegram.request._httpxrequest import HTTPXRequest
from telegram.request._requestparameter import RequestParameter
Expand Down Expand Up @@ -147,8 +148,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Note:
* Most bot methods have the argument ``api_kwargs`` which allows passing arbitrary keywords
to the Telegram API. This can be used to access new features of the API before they are
incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for
passing files.
incorporated into PTB. The limitations to this argument are the same as the ones
described in :meth:`do_api_request`.
* Bots should not be serialized since if you for e.g. change the bots token, then your
serialized instance will not reflect that change. Trying to pickle a bot instance will
raise :exc:`pickle.PicklingError`. Trying to deepcopy a bot instance will raise
Expand Down Expand Up @@ -762,6 +763,101 @@ async def shutdown(self) -> None:
await asyncio.gather(self._request[0].shutdown(), self._request[1].shutdown())
self._initialized = False

@_log
async def do_api_request(
self,
endpoint: str,
api_kwargs: Optional[JSONDict] = None,
return_type: Optional[Type[TelegramObject]] = 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,
) -> Any:
"""Do a request to the Telegram API.
This method is here to make it easier to use new API methods that are not yet supported
by this library.
Hint:
Since PTB does not know which arguments are passed to this method, some caution is
necessary in terms of PTBs utility functionalities. In particular
* passing objects of any class defined in the :mod:`telegram` module is supported
* when uploading files, a :class:`telegram.InputFile` must be passed as the value for
the corresponding argument. Passing a file path or file-like object will not work.
File paths will work only in combination with :paramref:`~Bot.local_mode`.
* when uploading files, PTB can still correctly determine that
a special write timeout value should be used instead of the default
:paramref:`telegram.request.HTTPXRequest.write_timeout`.
* insertion of default values specified via :class:`telegram.ext.Defaults` will not
work (only relevant for :class:`telegram.ext.ExtBot`).
* The only exception is :class:`telegram.ext.Defaults.tzinfo`, which will be correctly
applied to :class:`datetime.datetime` objects.
.. versionadded:: NEXT.VERSION
Args:
endpoint (:obj:`str`): The API endpoint to use, e.g. ``getMe`` or ``get_me``.
api_kwargs (:obj:`dict`, optional): The keyword arguments to pass to the API call.
If not specified, no arguments are passed.
return_type (:class:`telegram.TelegramObject`, optional): If specified, the result of
the API call will be deserialized into an instance of this class or tuple of
instances of this class. If not specified, the raw result of the API call will be
returned.
Returns:
The result of the API call. If :paramref:`return_type` is not specified, this is a
:obj:`dict` or :obj:`bool`, otherwise an instance of :paramref:`return_type` or a
tuple of :paramref:`return_type`.
Raises:
:class:`telegram.error.TelegramError`
"""
if hasattr(self, endpoint):
self._warn(
(
f"Please use 'Bot.{endpoint}' instead of "
f"'Bot.do_api_request(\"{endpoint}\", ...)'"
),
PTBDeprecationWarning,
stacklevel=3,
)

camel_case_endpoint = to_camel_case(endpoint)
try:
result = await self._post(
camel_case_endpoint,
api_kwargs=api_kwargs,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
)
except InvalidToken as exc:
# 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, that's why we have special handling for
# that here rather than in BaseRequest._request_wrapper
if self._initialized:
raise EndPointNotFound(
f"Endpoint '{camel_case_endpoint}' not found in Bot API"
) from exc

raise InvalidToken(
"Either the bot token was rejected by Telegram or the endpoint "
f"'{camel_case_endpoint}' does not exist."
) from exc

if return_type is None or isinstance(result, bool):
return result

if isinstance(result, list):
return return_type.de_list(result, self)
return return_type.de_json(result, self)

@_log
async def get_me(
self,
Expand Down
38 changes: 38 additions & 0 deletions telegram/_utils/strings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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 helper functions related to string manipulation.
Warning:
Contents of this module are intended to be used internally by the library and *not* by the
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""


def to_camel_case(snake_str: str) -> str:
"""Converts a snake_case string to camelCase.
Args:
snake_str (:obj:`str`): The string to convert.
Returns:
:obj:`str`: The converted string.
"""
components = snake_str.split("_")
return components[0] + "".join(x.title() for x in components[1:])
11 changes: 11 additions & 0 deletions telegram/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"BadRequest",
"ChatMigrated",
"Conflict",
"EndPointNotFound",
"Forbidden",
"InvalidToken",
"NetworkError",
Expand Down Expand Up @@ -133,6 +134,16 @@ def __init__(self, message: Optional[str] = None) -> None:
super().__init__("Invalid token" if message is None else message)


class EndPointNotFound(TelegramError):
"""Raised when the requested endpoint is not found. Only relevant for
:meth:`telegram.Bot.do_api_request`.
.. versionadded:: NEXT.VERSION
"""

__slots__ = ()


class NetworkError(TelegramError):
"""Base class for exceptions due to networking errors.
Expand Down
23 changes: 23 additions & 0 deletions telegram/ext/_extbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
SentWebAppMessage,
Sticker,
StickerSet,
TelegramObject,
Update,
User,
UserProfilePhotos,
Expand Down Expand Up @@ -644,6 +645,28 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ

return res

async def do_api_request(
self,
endpoint: str,
api_kwargs: Optional[JSONDict] = None,
return_type: Optional[Type[TelegramObject]] = 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,
rate_limit_args: Optional[RLARGS] = None,
) -> Any:
return await super().do_api_request(
endpoint=endpoint,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
return_type=return_type,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
)

async def stop_poll(
self,
chat_id: Union[int, str],
Expand Down
2 changes: 1 addition & 1 deletion telegram/request/_baserequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ async def _request_wrapper(
# TG returns 404 Not found for
# 1) malformed tokens
# 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod
# We can basically rule out 2) since we don't let users make requests manually
# 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
Expand Down

0 comments on commit 29866e2

Please sign in to comment.