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._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

View file

@ -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.

View file

@ -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`."""

View file

@ -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 <https://git.io/fAvYd>`_
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 <https://git.io/fAvYd>`_
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.

View file

@ -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,

View file

@ -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

View file

@ -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:

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
"""
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