From 49122162b69f8d5f5f0f6a534be903c59b4544c3 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 9 Oct 2021 15:14:45 +0200 Subject: [PATCH 1/4] Accept file pathes for private key and password --- telegram/ext/builders.py | 46 ++++++++++++++++++++++++------- telegram/ext/picklepersistence.py | 8 +++--- telegram/files/file.py | 3 +- telegram/request.py | 4 +-- telegram/utils/files.py | 4 +-- telegram/utils/types.py | 5 +++- tests/data/private.key | 30 ++++++++++++++++++++ tests/data/private_key.password | 1 + tests/test_builders.py | 20 ++++++++++++++ 9 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 tests/data/private.key create mode 100644 tests/data/private_key.password diff --git a/telegram/ext/builders.py b/telegram/ext/builders.py index e910854c236..bc008e9f1fa 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 ( @@ -41,7 +42,7 @@ from telegram.ext.utils.types import CCT, UD, CD, BD, BT, JQ, PT from telegram.utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE 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 if TYPE_CHECKING: @@ -349,14 +350,23 @@ def _set_request(self: BuilderType, request: Request) -> BuilderType: 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 @@ def request(self: BuilderType, request: Request) -> BuilderType: """ 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 @@ def private_key(self: BuilderType, private_key: bytes, password: bytes = None) - /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 files content + will be read automatically. + password (:obj:`bytes`): Optional. The corresponding password or the + file path of a file that contains the password. In the latter case, the files + content will be read automatically. Returns: :class:`DispatcherBuilder`: The same builder with the updated argument. @@ -958,7 +976,11 @@ def request(self: BuilderType, request: Request) -> BuilderType: """ 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 @@ def private_key(self: BuilderType, private_key: bytes, password: bytes = None) - /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 files content + will be read automatically. + password (:obj:`bytes`): Optional. The corresponding password or the + file path of a file that contains the password. In the latter case, the files + 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 25211453e68..01488ebb85f 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -28,12 +28,12 @@ overload, cast, DefaultDict, - Union, ) from telegram.ext import BasePersistence, PersistenceInput from .utils.types import UD, CD, BD, ConversationDict, CDCData from .contexttypes import ContextTypes +from ..utils.types import FilePathInput class PicklePersistence(BasePersistence[UD, CD, BD]): @@ -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 @@ def __init__( @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 @@ def __init__( 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/files/file.py b/telegram/files/file.py index 0f0d859f91a..902939f6796 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -26,6 +26,7 @@ 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 @@ def __init__( 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/request.py b/telegram/request.py index b0496751994..a4d65a5f5f4 100644 --- a/telegram/request.py +++ b/telegram/request.py @@ -73,7 +73,7 @@ TimedOut, Unauthorized, ) -from telegram.utils.types import JSONDict +from telegram.utils.types import JSONDict, FilePathInput # pylint: disable=unused-argument @@ -385,7 +385,7 @@ def retrieve(self, url: str, timeout: float = None) -> bytes: 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/telegram/utils/files.py b/telegram/utils/files.py index c6972c087b5..1e34065e840 100644 --- a/telegram/utils/files.py +++ b/telegram/utils/files.py @@ -31,13 +31,13 @@ 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 d943b78a050..01f9306fb13 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -43,7 +43,10 @@ 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/tests/data/private.key b/tests/data/private.key new file mode 100644 index 00000000000..db67f944c54 --- /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 00000000000..11a50f99ea0 --- /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 aa6a828e14b..04cd4768f95 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 @@ -250,3 +251,22 @@ def __init__(self, arg, **kwargs): 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 From 1482685be96ee935629cf963bfb06844d9655052 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 9 Oct 2021 17:06:34 +0200 Subject: [PATCH 2/4] Fix tests --- tests/test_builders.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_builders.py b/tests/test_builders.py index 04cd4768f95..b604fadcb00 100644 --- a/tests/test_builders.py +++ b/tests/test_builders.py @@ -63,7 +63,9 @@ def test_mutually_exclusive_for_bot(self, builder, method, description): 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 +86,9 @@ def test_mutually_exclusive_for_dispatcher(self, builder, method, description): 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 +106,9 @@ def test_mutually_exclusive_for_dispatcher(self, builder, method, description): 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' From b776b5d2a4deeb1538b18082e5f70ccc8b3a5389 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 10 Oct 2021 13:40:04 +0200 Subject: [PATCH 3/4] review --- telegram/ext/builders.py | 16 ++++++++-------- telegram/ext/picklepersistence.py | 6 +++--- telegram/ext/updater.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/telegram/ext/builders.py b/telegram/ext/builders.py index bc008e9f1fa..5257933068d 100644 --- a/telegram/ext/builders.py +++ b/telegram/ext/builders.py @@ -631,11 +631,11 @@ def private_key( Args: 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 files content + file path of a file that contains the key. In the latter case, the file's content will be read automatically. - password (:obj:`bytes`): Optional. The corresponding password or the - file path of a file that contains the password. In the latter case, the files - 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. @@ -989,11 +989,11 @@ def private_key( Args: 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 files content + file path of a file that contains the key. In the latter case, the file's content will be read automatically. - password (:obj:`bytes`): Optional. The corresponding password or the - file path of a file that contains the password. In the latter case, the files - 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 01488ebb85f..d568d9520ac 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -30,10 +30,10 @@ DefaultDict, ) +from telegram.utils.types import FilePathInput from telegram.ext import BasePersistence, PersistenceInput -from .utils.types import UD, CD, BD, ConversationDict, CDCData -from .contexttypes import ContextTypes -from ..utils.types import FilePathInput +from telegram.ext.utils.types import UD, CD, BD, ConversationDict, CDCData +from telegram.ext.contexttypes import ContextTypes class PicklePersistence(BasePersistence[UD, CD, BD]): diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 5b61059b3ee..02ba0add3a1 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -39,11 +39,11 @@ ) 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 .utils.stack import was_called_by -from .utils.types import BT -from ..utils.warnings import warn +from telegram.ext.utils.stack import was_called_by +from telegram.ext.utils.types import BT if TYPE_CHECKING: from .builders import InitUpdaterBuilder From 78396ee885ea6144578bf1c5147aa4334e8895b1 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 10 Oct 2021 20:56:54 +0200 Subject: [PATCH 4/4] Review --- telegram/ext/_builders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/ext/_builders.py b/telegram/ext/_builders.py index c39b70a5e76..fae6cd48a63 100644 --- a/telegram/ext/_builders.py +++ b/telegram/ext/_builders.py @@ -633,7 +633,7 @@ def private_key( 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 (: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. @@ -991,7 +991,7 @@ def private_key( 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 (: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.