Accept File Paths for Updater/DispatcherBuilder.private_key (#2724)

This commit is contained in:
Bibo-Joshi 2021-10-11 20:24:52 +02:00 committed by Hinrich Mahler
parent 5275c45199
commit 0cb8d50aea
10 changed files with 111 additions and 24 deletions

View file

@ -26,6 +26,7 @@ from typing import IO, TYPE_CHECKING, Any, Optional, Union
from telegram import TelegramObject from telegram import TelegramObject
from telegram._passport.credentials import decrypt from telegram._passport.credentials import decrypt
from telegram._utils.files import is_local_file from telegram._utils.files import is_local_file
from telegram._utils.types import FilePathInput
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import Bot, FileCredentials from telegram import Bot, FileCredentials
@ -96,7 +97,7 @@ class File(TelegramObject):
self._id_attrs = (self.file_unique_id,) self._id_attrs = (self.file_unique_id,)
def download( def download(
self, custom_path: Union[Path, str] = None, out: IO = None, timeout: int = None self, custom_path: FilePathInput = None, out: IO = None, timeout: int = None
) -> Union[Path, IO]: ) -> Union[Path, IO]:
""" """
Download this file. By default, the file is saved in the current working directory with its Download this file. By default, the file is saved in the current working directory with its

View file

@ -31,13 +31,13 @@ Warning:
from pathlib import Path from pathlib import Path
from typing import Optional, Union, Type, Any, cast, IO, TYPE_CHECKING from typing import Optional, Union, Type, Any, cast, IO, TYPE_CHECKING
from telegram._utils.types import FileInput from telegram._utils.types import FileInput, FilePathInput
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import TelegramObject, InputFile from telegram import TelegramObject, InputFile
def is_local_file(obj: Optional[Union[str, Path]]) -> bool: def is_local_file(obj: Optional[FilePathInput]) -> bool:
""" """
Checks if a given string is a file on local system. Checks if a given string is a file on local system.

View file

@ -43,7 +43,10 @@ if TYPE_CHECKING:
FileLike = Union[IO, 'InputFile'] FileLike = Union[IO, 'InputFile']
"""Either an open file handler or a :class:`telegram.InputFile`.""" """Either an open file handler or a :class:`telegram.InputFile`."""
FileInput = Union[str, bytes, FileLike, Path] FilePathInput = Union[str, Path]
"""A filepath either as string or as :obj:`pathlib.Path` object."""
FileInput = Union[FilePathInput, bytes, FileLike]
"""Valid input for passing files to Telegram. Either a file id as string, a file like object, """Valid input for passing files to Telegram. Either a file id as string, a file like object,
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`.""" a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""

View file

@ -21,6 +21,7 @@
# flake8: noqa: E501 # flake8: noqa: E501
# pylint: disable=line-too-long # pylint: disable=line-too-long
"""This module contains the Builder classes for the telegram.ext module.""" """This module contains the Builder classes for the telegram.ext module."""
from pathlib import Path
from queue import Queue from queue import Queue
from threading import Event from threading import Event
from typing import ( from typing import (
@ -38,7 +39,7 @@ from typing import (
from telegram import Bot from telegram import Bot
from telegram.request import Request from telegram.request import Request
from telegram._utils.types import ODVInput, DVInput from telegram._utils.types import ODVInput, DVInput, FilePathInput
from telegram._utils.warnings import warn from telegram._utils.warnings import warn
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE
from telegram.ext import Dispatcher, JobQueue, Updater, ExtBot, ContextTypes, CallbackContext from telegram.ext import Dispatcher, JobQueue, Updater, ExtBot, ContextTypes, CallbackContext
@ -349,14 +350,23 @@ class _BaseBuilder(Generic[ODT, BT, CCT, UD, CD, BD, JQ, PT]):
return self return self
def _set_private_key( def _set_private_key(
self: BuilderType, private_key: bytes, password: bytes = None self: BuilderType,
private_key: Union[bytes, FilePathInput],
password: Union[bytes, FilePathInput] = None,
) -> BuilderType: ) -> BuilderType:
if self._bot is not DEFAULT_NONE: if self._bot is not DEFAULT_NONE:
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'bot instance')) raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'bot instance'))
if self._dispatcher_check: if self._dispatcher_check:
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'Dispatcher instance')) raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'Dispatcher instance'))
self._private_key = private_key
self._private_key_password = password self._private_key = (
private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes()
)
if password is None or isinstance(password, bytes):
self._private_key_password = password
else:
self._private_key_password = Path(password).read_bytes()
return self return self
def _set_defaults(self: BuilderType, defaults: 'Defaults') -> BuilderType: def _set_defaults(self: BuilderType, defaults: 'Defaults') -> BuilderType:
@ -608,7 +618,11 @@ class DispatcherBuilder(_BaseBuilder[ODT, BT, CCT, UD, CD, BD, JQ, PT]):
""" """
return self._set_request(request) return self._set_request(request)
def private_key(self: BuilderType, private_key: bytes, password: bytes = None) -> BuilderType: def private_key(
self: BuilderType,
private_key: Union[bytes, FilePathInput],
password: Union[bytes, FilePathInput] = None,
) -> BuilderType:
"""Sets the private key and corresponding password for decryption of telegram passport data """Sets the private key and corresponding password for decryption of telegram passport data
to be used for :attr:`telegram.ext.Dispatcher.bot`. to be used for :attr:`telegram.ext.Dispatcher.bot`.
@ -616,8 +630,12 @@ class DispatcherBuilder(_BaseBuilder[ODT, BT, CCT, UD, CD, BD, JQ, PT]):
/tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_ /tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_
Args: Args:
private_key (:obj:`bytes`): The private key. private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the
password (:obj:`bytes`): Optional. The corresponding password. file path of a file that contains the key. In the latter case, the file's content
will be read automatically.
password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding
password or the file path of a file that contains the password. In the latter case,
the file's content will be read automatically.
Returns: Returns:
:class:`DispatcherBuilder`: The same builder with the updated argument. :class:`DispatcherBuilder`: The same builder with the updated argument.
@ -958,7 +976,11 @@ class UpdaterBuilder(_BaseBuilder[ODT, BT, CCT, UD, CD, BD, JQ, PT]):
""" """
return self._set_request(request) return self._set_request(request)
def private_key(self: BuilderType, private_key: bytes, password: bytes = None) -> BuilderType: def private_key(
self: BuilderType,
private_key: Union[bytes, FilePathInput],
password: Union[bytes, FilePathInput] = None,
) -> BuilderType:
"""Sets the private key and corresponding password for decryption of telegram passport data """Sets the private key and corresponding password for decryption of telegram passport data
to be used for :attr:`telegram.ext.Updater.bot`. to be used for :attr:`telegram.ext.Updater.bot`.
@ -966,8 +988,12 @@ class UpdaterBuilder(_BaseBuilder[ODT, BT, CCT, UD, CD, BD, JQ, PT]):
/tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_ /tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_
Args: Args:
private_key (:obj:`bytes`): The private key. private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the
password (:obj:`bytes`): Optional. The corresponding password. file path of a file that contains the key. In the latter case, the file's content
will be read automatically.
password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding
password or the file path of a file that contains the password. In the latter case,
the file's content will be read automatically.
Returns: Returns:
:class:`UpdaterBuilder`: The same builder with the updated argument. :class:`UpdaterBuilder`: The same builder with the updated argument.

View file

@ -28,9 +28,9 @@ from typing import (
overload, overload,
cast, cast,
DefaultDict, DefaultDict,
Union,
) )
from telegram._utils.types import FilePathInput
from telegram.ext import BasePersistence, PersistenceInput from telegram.ext import BasePersistence, PersistenceInput
from telegram.ext._contexttypes import ContextTypes from telegram.ext._contexttypes import ContextTypes
from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData
@ -107,7 +107,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
@overload @overload
def __init__( def __init__(
self: 'PicklePersistence[Dict, Dict, Dict]', self: 'PicklePersistence[Dict, Dict, Dict]',
filepath: Union[Path, str], filepath: FilePathInput,
store_data: PersistenceInput = None, store_data: PersistenceInput = None,
single_file: bool = True, single_file: bool = True,
on_flush: bool = False, on_flush: bool = False,
@ -117,7 +117,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
@overload @overload
def __init__( def __init__(
self: 'PicklePersistence[UD, CD, BD]', self: 'PicklePersistence[UD, CD, BD]',
filepath: Union[Path, str], filepath: FilePathInput,
store_data: PersistenceInput = None, store_data: PersistenceInput = None,
single_file: bool = True, single_file: bool = True,
on_flush: bool = False, on_flush: bool = False,
@ -127,7 +127,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
def __init__( def __init__(
self, self,
filepath: Union[Path, str], filepath: FilePathInput,
store_data: PersistenceInput = None, store_data: PersistenceInput = None,
single_file: bool = True, single_file: bool = True,
on_flush: bool = False, on_flush: bool = False,

View file

@ -39,11 +39,11 @@ from typing import (
) )
from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError
from telegram._utils.warnings import warn
from telegram.ext import Dispatcher from telegram.ext import Dispatcher
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
from telegram.ext._utils.stack import was_called_by from telegram.ext._utils.stack import was_called_by
from telegram.ext._utils.types import BT from telegram.ext._utils.types import BT
from telegram._utils.warnings import warn
if TYPE_CHECKING: if TYPE_CHECKING:
from .builders import InitUpdaterBuilder from .builders import InitUpdaterBuilder

View file

@ -73,7 +73,7 @@ from telegram.error import (
TimedOut, TimedOut,
Unauthorized, Unauthorized,
) )
from telegram._utils.types import JSONDict from telegram._utils.types import JSONDict, FilePathInput
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -385,7 +385,7 @@ class Request:
return self._request_wrapper('GET', url, **urlopen_kwargs) return self._request_wrapper('GET', url, **urlopen_kwargs)
def download(self, url: str, filepath: Union[Path, str], timeout: float = None) -> None: def download(self, url: str, filepath: FilePathInput, timeout: float = None) -> None:
"""Download a file by its URL. """Download a file by its URL.
Args: Args:

30
tests/data/private.key Normal file
View file

@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,C4A419CEBF7D18FB5E1D98D6DDAEAD5F
LHkVkhpWH0KU4UrdUH4DMNGqAZkRzSwO8CqEkowQrrkdRyFwJQCgsgIywkDQsqyh
bvIkRpRb2gwQ1D9utrRQ1IFsJpreulErSPxx47b1xwXhMiX0vOzWprhZ8mYYrAZH
T9o7YXgUuF7Dk8Am51rZH50mWHUEljjkIlH2RQg1QFQr4recrZxlA3Ypn/SvOf0P
gaYrBvcX0am1JSqar0BA9sQO6u1STBjUm/e4csAubutxg/k/N69zlMcr098lqGWO
ppQmFa0grg3S2lUSuh42MYGtzluemrtWiktjrHKtm33zQX4vIgnMjuDZO4maqLD/
qHvbixY2TX28gHsoIednr2C9p/rBl8uItDlVyqWengykcDYczii0Pa8PKRmseOJh
sHGum3u5WTRRv41jK7i7PBeKsKHxMxLqTroXpCfx59XzGB5kKiPhG9Zm6NY7BZ3j
JA02+RKwlmm4v64XLbTVtV+2M4pk1cOaRx8CTB1Coe0uN+o+kJwMffqKioeaB9lE
zs9At5rdSpamG1G+Eop6hqGjYip8cLDaa9yuStIo0eOt/Q6YtU9qHOyMlOywptof
hJUMPoFjO06nsME69QvzRu9CPMGIcj4GAVYn1He6LoRVj59skPAUcn1DpytL9Ghi
9r7rLCRCExX32MuIxBq+fWBd//iOTkvnSlISc2MjXSYWu0QhKUvVZgy23pA3RH6X
px/dPdw1jF4WTlJL7IEaF3eOLgKqfYebHa+i2E64ncECvsl8WFb/T+ru1qa4n3RB
HPIaBRzPSqF1nc5BIQD12GPf/A7lq1pJpcQQN7gTkpUwJ8ydPB45sadHrc3Fz1C5
XPvL3eLfCEau2Wrz4IVgMTJ61lQnzSZG9Z+R0JYpd1+SvNpbm9YdocDYam8wIFS3
9RsJOKCansvOXfuXp26gggzsAP3mXq/DV1e86ramRbMyczSd3v+EsKmsttW0oWC6
Hhuozy11w6Q+jgsiSBrOFJ0JwgHAaCGb4oFluYzTOgdrmPgQomrz16TJLjjmn56B
9msoVGH5Kk/ifVr9waFuQFhcUfoWUUPZB3GrSGpr3Rz5XCh/BuXQDW8mDu29odzD
6hDoNITsPv+y9F/BvqWOK+JeL+wP/F+AnciGMzIDnP4a4P4yj8Gf2rr1Eriok6wz
aQr6NwnKsT4UAqjlmQ+gdPE4Joxk/ixlD41TZ97rq0LUSx2bcanM8GXZUjL74EuB
TVABCeIX2ADBwHZ6v2HEkZvK7Miy23FP75JmLdNXw4GTcYmqD1bPIfsxgUkSwG63
t0ChOqi9VdT62eAs5wShwhcrjc4xztjn6kypFu55a0neNr2qKYrwFo3QgZAbKWc1
5jfS4kAq0gxyoQTCZnGhbbL095q3Sy7GV3EaW4yk78EuRwPFOqVUQ0D5tvrKsPT4
B5AlxlarcDcMQayWKLj2pWmQm3YVlx5NfoRkSbd14h6ZryzDhG8ZfooLQ5dFh1ba
f8+YbBtvFshzUDYdnr0fS0RYc/WtYmfJdb4+Fkc268BkJzg43rMSrdzaleS6jypU
vzPs8WO0xU1xCIgB92vqZ+/4OlFwjbHHoQlnFHdNPbrfc8INbtLZgLCrELw4UEga
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1 @@
python-telegram-bot

View file

@ -20,6 +20,7 @@
""" """
We mainly test on UpdaterBuilder because it has all methods that DispatcherBuilder already has We mainly test on UpdaterBuilder because it has all methods that DispatcherBuilder already has
""" """
from pathlib import Path
from random import randint from random import randint
from threading import Event from threading import Event
@ -63,7 +64,9 @@ class TestBuilder:
pytest.skip(f'{builder.__class__} has no method called {method}') pytest.skip(f'{builder.__class__} has no method called {method}')
# First that e.g. `bot` can't be set if `request` was already set # First that e.g. `bot` can't be set if `request` was already set
getattr(builder, method)(1) # We pass the private key since `private_key` is the only method that doesn't just save
# the passed value
getattr(builder, method)(Path('tests/data/private.key'))
with pytest.raises(RuntimeError, match=f'`bot` may only be set, if no {description}'): with pytest.raises(RuntimeError, match=f'`bot` may only be set, if no {description}'):
builder.bot(None) builder.bot(None)
@ -84,7 +87,9 @@ class TestBuilder:
pytest.skip(f'{builder.__class__} has no method called {method}') pytest.skip(f'{builder.__class__} has no method called {method}')
# First that e.g. `dispatcher` can't be set if `bot` was already set # First that e.g. `dispatcher` can't be set if `bot` was already set
getattr(builder, method)(None) # We pass the private key since `private_key` is the only method that doesn't just save
# the passed value
getattr(builder, method)(Path('tests/data/private.key'))
with pytest.raises( with pytest.raises(
RuntimeError, match=f'`dispatcher` may only be set, if no {description}' RuntimeError, match=f'`dispatcher` may only be set, if no {description}'
): ):
@ -102,7 +107,9 @@ class TestBuilder:
builder = builder.__class__() builder = builder.__class__()
builder.dispatcher(None) builder.dispatcher(None)
if method != 'dispatcher_class': if method != 'dispatcher_class':
getattr(builder, method)(None) # We pass the private key since `private_key` is the only method that doesn't just save
# the passed value
getattr(builder, method)(Path('tests/data/private.key'))
else: else:
with pytest.raises( with pytest.raises(
RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance' RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance'
@ -251,3 +258,22 @@ class TestBuilder:
else: else:
assert isinstance(obj, CustomDispatcher) assert isinstance(obj, CustomDispatcher)
assert obj.arg == 2 assert obj.arg == 2
@pytest.mark.parametrize('input_type', ('bytes', 'str', 'Path'))
def test_all_private_key_input_types(self, builder, bot, input_type):
private_key = Path('tests/data/private.key')
password = Path('tests/data/private_key.password')
if input_type == 'bytes':
private_key = private_key.read_bytes()
password = password.read_bytes()
if input_type == 'str':
private_key = str(private_key)
password = str(password)
builder.token(bot.token).private_key(
private_key=private_key,
password=password,
)
bot = builder.build().bot
assert bot.private_key