diff --git a/telegram/_files/file.py b/telegram/_files/file.py index 592f9cebc..c9647ec9a 100644 --- a/telegram/_files/file.py +++ b/telegram/_files/file.py @@ -26,6 +26,7 @@ from typing import IO, TYPE_CHECKING, Any, Optional, Union from telegram import TelegramObject from telegram._passport.credentials import decrypt from telegram._utils.files import is_local_file +from telegram._utils.types import FilePathInput if TYPE_CHECKING: from telegram import Bot, FileCredentials @@ -96,7 +97,7 @@ class File(TelegramObject): self._id_attrs = (self.file_unique_id,) 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]: """ Download this file. By default, the file is saved in the current working directory with its diff --git a/telegram/_utils/files.py b/telegram/_utils/files.py index 53dcebd26..a85432408 100644 --- a/telegram/_utils/files.py +++ b/telegram/_utils/files.py @@ -31,13 +31,13 @@ Warning: from pathlib import Path 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: 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. diff --git a/telegram/_utils/types.py b/telegram/_utils/types.py index 15f77eec2..71500ba2d 100644 --- a/telegram/_utils/types.py +++ b/telegram/_utils/types.py @@ -43,7 +43,10 @@ if TYPE_CHECKING: FileLike = Union[IO, '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, a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`.""" diff --git a/telegram/ext/_builders.py b/telegram/ext/_builders.py index 082605cf6..fae6cd48a 100644 --- a/telegram/ext/_builders.py +++ b/telegram/ext/_builders.py @@ -21,6 +21,7 @@ # flake8: noqa: E501 # pylint: disable=line-too-long """This module contains the Builder classes for the telegram.ext module.""" +from pathlib import Path from queue import Queue from threading import Event from typing import ( @@ -38,7 +39,7 @@ from typing import ( from telegram import Bot 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.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE 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 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: if self._bot is not DEFAULT_NONE: raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'bot instance')) if self._dispatcher_check: 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 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) - 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 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 `_ Args: - private_key (:obj:`bytes`): The private key. - password (:obj:`bytes`): Optional. The corresponding password. + private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the + 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: :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) - 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 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 `_ Args: - private_key (:obj:`bytes`): The private key. - password (:obj:`bytes`): Optional. The corresponding password. + private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the + 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: :class:`UpdaterBuilder`: The same builder with the updated argument. diff --git a/telegram/ext/_picklepersistence.py b/telegram/ext/_picklepersistence.py index 30b682228..8c94f64e7 100644 --- a/telegram/ext/_picklepersistence.py +++ b/telegram/ext/_picklepersistence.py @@ -28,9 +28,9 @@ from typing import ( overload, cast, DefaultDict, - Union, ) +from telegram._utils.types import FilePathInput from telegram.ext import BasePersistence, PersistenceInput from telegram.ext._contexttypes import ContextTypes from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData @@ -107,7 +107,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): @overload def __init__( self: 'PicklePersistence[Dict, Dict, Dict]', - filepath: Union[Path, str], + filepath: FilePathInput, store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, @@ -117,7 +117,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): @overload def __init__( self: 'PicklePersistence[UD, CD, BD]', - filepath: Union[Path, str], + filepath: FilePathInput, store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, @@ -127,7 +127,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): def __init__( self, - filepath: Union[Path, str], + filepath: FilePathInput, store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index 53c24e808..42da66695 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -39,11 +39,11 @@ from typing import ( ) from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError +from telegram._utils.warnings import warn from telegram.ext import Dispatcher from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer from telegram.ext._utils.stack import was_called_by from telegram.ext._utils.types import BT -from telegram._utils.warnings import warn if TYPE_CHECKING: from .builders import InitUpdaterBuilder diff --git a/telegram/request.py b/telegram/request.py index 4b3b52ecd..c3d4a426b 100644 --- a/telegram/request.py +++ b/telegram/request.py @@ -73,7 +73,7 @@ from telegram.error import ( TimedOut, Unauthorized, ) -from telegram._utils.types import JSONDict +from telegram._utils.types import JSONDict, FilePathInput # pylint: disable=unused-argument @@ -385,7 +385,7 @@ class Request: 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. Args: diff --git a/tests/data/private.key b/tests/data/private.key new file mode 100644 index 000000000..db67f944c --- /dev/null +++ b/tests/data/private.key @@ -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----- \ No newline at end of file diff --git a/tests/data/private_key.password b/tests/data/private_key.password new file mode 100644 index 000000000..11a50f99e --- /dev/null +++ b/tests/data/private_key.password @@ -0,0 +1 @@ +python-telegram-bot \ No newline at end of file diff --git a/tests/test_builders.py b/tests/test_builders.py index fb7101c5b..9fff1ae1d 100644 --- a/tests/test_builders.py +++ b/tests/test_builders.py @@ -20,6 +20,7 @@ """ We mainly test on UpdaterBuilder because it has all methods that DispatcherBuilder already has """ +from pathlib import Path from random import randint from threading import Event @@ -63,7 +64,9 @@ class TestBuilder: pytest.skip(f'{builder.__class__} has no method called {method}') # 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}'): builder.bot(None) @@ -84,7 +87,9 @@ class TestBuilder: pytest.skip(f'{builder.__class__} has no method called {method}') # 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( RuntimeError, match=f'`dispatcher` may only be set, if no {description}' ): @@ -102,7 +107,9 @@ class TestBuilder: builder = builder.__class__() builder.dispatcher(None) 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: with pytest.raises( RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance' @@ -251,3 +258,22 @@ class TestBuilder: else: assert isinstance(obj, CustomDispatcher) 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