Skip to content

Commit 0cb8d50

Browse files
committed
Accept File Paths for Updater/DispatcherBuilder.private_key (python-telegram-bot#2724)
1 parent 5275c45 commit 0cb8d50

10 files changed

Lines changed: 111 additions & 24 deletions

File tree

telegram/_files/file.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from telegram import TelegramObject
2727
from telegram._passport.credentials import decrypt
2828
from telegram._utils.files import is_local_file
29+
from telegram._utils.types import FilePathInput
2930

3031
if TYPE_CHECKING:
3132
from telegram import Bot, FileCredentials
@@ -96,7 +97,7 @@ def __init__(
9697
self._id_attrs = (self.file_unique_id,)
9798

9899
def download(
99-
self, custom_path: Union[Path, str] = None, out: IO = None, timeout: int = None
100+
self, custom_path: FilePathInput = None, out: IO = None, timeout: int = None
100101
) -> Union[Path, IO]:
101102
"""
102103
Download this file. By default, the file is saved in the current working directory with its

telegram/_utils/files.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
from pathlib import Path
3232
from typing import Optional, Union, Type, Any, cast, IO, TYPE_CHECKING
3333

34-
from telegram._utils.types import FileInput
34+
from telegram._utils.types import FileInput, FilePathInput
3535

3636
if TYPE_CHECKING:
3737
from telegram import TelegramObject, InputFile
3838

3939

40-
def is_local_file(obj: Optional[Union[str, Path]]) -> bool:
40+
def is_local_file(obj: Optional[FilePathInput]) -> bool:
4141
"""
4242
Checks if a given string is a file on local system.
4343

telegram/_utils/types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@
4343
FileLike = Union[IO, 'InputFile']
4444
"""Either an open file handler or a :class:`telegram.InputFile`."""
4545

46-
FileInput = Union[str, bytes, FileLike, Path]
46+
FilePathInput = Union[str, Path]
47+
"""A filepath either as string or as :obj:`pathlib.Path` object."""
48+
49+
FileInput = Union[FilePathInput, bytes, FileLike]
4750
"""Valid input for passing files to Telegram. Either a file id as string, a file like object,
4851
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""
4952

telegram/ext/_builders.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# flake8: noqa: E501
2222
# pylint: disable=line-too-long
2323
"""This module contains the Builder classes for the telegram.ext module."""
24+
from pathlib import Path
2425
from queue import Queue
2526
from threading import Event
2627
from typing import (
@@ -38,7 +39,7 @@
3839

3940
from telegram import Bot
4041
from telegram.request import Request
41-
from telegram._utils.types import ODVInput, DVInput
42+
from telegram._utils.types import ODVInput, DVInput, FilePathInput
4243
from telegram._utils.warnings import warn
4344
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE
4445
from telegram.ext import Dispatcher, JobQueue, Updater, ExtBot, ContextTypes, CallbackContext
@@ -349,14 +350,23 @@ def _set_request(self: BuilderType, request: Request) -> BuilderType:
349350
return self
350351

351352
def _set_private_key(
352-
self: BuilderType, private_key: bytes, password: bytes = None
353+
self: BuilderType,
354+
private_key: Union[bytes, FilePathInput],
355+
password: Union[bytes, FilePathInput] = None,
353356
) -> BuilderType:
354357
if self._bot is not DEFAULT_NONE:
355358
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'bot instance'))
356359
if self._dispatcher_check:
357360
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'Dispatcher instance'))
358-
self._private_key = private_key
359-
self._private_key_password = password
361+
362+
self._private_key = (
363+
private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes()
364+
)
365+
if password is None or isinstance(password, bytes):
366+
self._private_key_password = password
367+
else:
368+
self._private_key_password = Path(password).read_bytes()
369+
360370
return self
361371

362372
def _set_defaults(self: BuilderType, defaults: 'Defaults') -> BuilderType:
@@ -608,16 +618,24 @@ def request(self: BuilderType, request: Request) -> BuilderType:
608618
"""
609619
return self._set_request(request)
610620

611-
def private_key(self: BuilderType, private_key: bytes, password: bytes = None) -> BuilderType:
621+
def private_key(
622+
self: BuilderType,
623+
private_key: Union[bytes, FilePathInput],
624+
password: Union[bytes, FilePathInput] = None,
625+
) -> BuilderType:
612626
"""Sets the private key and corresponding password for decryption of telegram passport data
613627
to be used for :attr:`telegram.ext.Dispatcher.bot`.
614628
615629
.. seealso:: `passportbot.py <https://github.com/python-telegram-bot/python-telegram-bot\
616630
/tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_
617631
618632
Args:
619-
private_key (:obj:`bytes`): The private key.
620-
password (:obj:`bytes`): Optional. The corresponding password.
633+
private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the
634+
file path of a file that contains the key. In the latter case, the file's content
635+
will be read automatically.
636+
password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding
637+
password or the file path of a file that contains the password. In the latter case,
638+
the file's content will be read automatically.
621639
622640
Returns:
623641
:class:`DispatcherBuilder`: The same builder with the updated argument.
@@ -958,16 +976,24 @@ def request(self: BuilderType, request: Request) -> BuilderType:
958976
"""
959977
return self._set_request(request)
960978

961-
def private_key(self: BuilderType, private_key: bytes, password: bytes = None) -> BuilderType:
979+
def private_key(
980+
self: BuilderType,
981+
private_key: Union[bytes, FilePathInput],
982+
password: Union[bytes, FilePathInput] = None,
983+
) -> BuilderType:
962984
"""Sets the private key and corresponding password for decryption of telegram passport data
963985
to be used for :attr:`telegram.ext.Updater.bot`.
964986
965987
.. seealso:: `passportbot.py <https://github.com/python-telegram-bot/python-telegram-bot\
966988
/tree/master/examples#passportbotpy>`_, `Telegram Passports <https://git.io/fAvYd>`_
967989
968990
Args:
969-
private_key (:obj:`bytes`): The private key.
970-
password (:obj:`bytes`): Optional. The corresponding password.
991+
private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the
992+
file path of a file that contains the key. In the latter case, the file's content
993+
will be read automatically.
994+
password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding
995+
password or the file path of a file that contains the password. In the latter case,
996+
the file's content will be read automatically.
971997
972998
Returns:
973999
:class:`UpdaterBuilder`: The same builder with the updated argument.

telegram/ext/_picklepersistence.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
overload,
2929
cast,
3030
DefaultDict,
31-
Union,
3231
)
3332

33+
from telegram._utils.types import FilePathInput
3434
from telegram.ext import BasePersistence, PersistenceInput
3535
from telegram.ext._contexttypes import ContextTypes
3636
from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData
@@ -107,7 +107,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
107107
@overload
108108
def __init__(
109109
self: 'PicklePersistence[Dict, Dict, Dict]',
110-
filepath: Union[Path, str],
110+
filepath: FilePathInput,
111111
store_data: PersistenceInput = None,
112112
single_file: bool = True,
113113
on_flush: bool = False,
@@ -117,7 +117,7 @@ def __init__(
117117
@overload
118118
def __init__(
119119
self: 'PicklePersistence[UD, CD, BD]',
120-
filepath: Union[Path, str],
120+
filepath: FilePathInput,
121121
store_data: PersistenceInput = None,
122122
single_file: bool = True,
123123
on_flush: bool = False,
@@ -127,7 +127,7 @@ def __init__(
127127

128128
def __init__(
129129
self,
130-
filepath: Union[Path, str],
130+
filepath: FilePathInput,
131131
store_data: PersistenceInput = None,
132132
single_file: bool = True,
133133
on_flush: bool = False,

telegram/ext/_updater.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
)
4040

4141
from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError
42+
from telegram._utils.warnings import warn
4243
from telegram.ext import Dispatcher
4344
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
4445
from telegram.ext._utils.stack import was_called_by
4546
from telegram.ext._utils.types import BT
46-
from telegram._utils.warnings import warn
4747

4848
if TYPE_CHECKING:
4949
from .builders import InitUpdaterBuilder

telegram/request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
TimedOut,
7474
Unauthorized,
7575
)
76-
from telegram._utils.types import JSONDict
76+
from telegram._utils.types import JSONDict, FilePathInput
7777

7878

7979
# pylint: disable=unused-argument
@@ -385,7 +385,7 @@ def retrieve(self, url: str, timeout: float = None) -> bytes:
385385

386386
return self._request_wrapper('GET', url, **urlopen_kwargs)
387387

388-
def download(self, url: str, filepath: Union[Path, str], timeout: float = None) -> None:
388+
def download(self, url: str, filepath: FilePathInput, timeout: float = None) -> None:
389389
"""Download a file by its URL.
390390
391391
Args:

tests/data/private.key

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
Proc-Type: 4,ENCRYPTED
3+
DEK-Info: AES-128-CBC,C4A419CEBF7D18FB5E1D98D6DDAEAD5F
4+
5+
LHkVkhpWH0KU4UrdUH4DMNGqAZkRzSwO8CqEkowQrrkdRyFwJQCgsgIywkDQsqyh
6+
bvIkRpRb2gwQ1D9utrRQ1IFsJpreulErSPxx47b1xwXhMiX0vOzWprhZ8mYYrAZH
7+
T9o7YXgUuF7Dk8Am51rZH50mWHUEljjkIlH2RQg1QFQr4recrZxlA3Ypn/SvOf0P
8+
gaYrBvcX0am1JSqar0BA9sQO6u1STBjUm/e4csAubutxg/k/N69zlMcr098lqGWO
9+
ppQmFa0grg3S2lUSuh42MYGtzluemrtWiktjrHKtm33zQX4vIgnMjuDZO4maqLD/
10+
qHvbixY2TX28gHsoIednr2C9p/rBl8uItDlVyqWengykcDYczii0Pa8PKRmseOJh
11+
sHGum3u5WTRRv41jK7i7PBeKsKHxMxLqTroXpCfx59XzGB5kKiPhG9Zm6NY7BZ3j
12+
JA02+RKwlmm4v64XLbTVtV+2M4pk1cOaRx8CTB1Coe0uN+o+kJwMffqKioeaB9lE
13+
zs9At5rdSpamG1G+Eop6hqGjYip8cLDaa9yuStIo0eOt/Q6YtU9qHOyMlOywptof
14+
hJUMPoFjO06nsME69QvzRu9CPMGIcj4GAVYn1He6LoRVj59skPAUcn1DpytL9Ghi
15+
9r7rLCRCExX32MuIxBq+fWBd//iOTkvnSlISc2MjXSYWu0QhKUvVZgy23pA3RH6X
16+
px/dPdw1jF4WTlJL7IEaF3eOLgKqfYebHa+i2E64ncECvsl8WFb/T+ru1qa4n3RB
17+
HPIaBRzPSqF1nc5BIQD12GPf/A7lq1pJpcQQN7gTkpUwJ8ydPB45sadHrc3Fz1C5
18+
XPvL3eLfCEau2Wrz4IVgMTJ61lQnzSZG9Z+R0JYpd1+SvNpbm9YdocDYam8wIFS3
19+
9RsJOKCansvOXfuXp26gggzsAP3mXq/DV1e86ramRbMyczSd3v+EsKmsttW0oWC6
20+
Hhuozy11w6Q+jgsiSBrOFJ0JwgHAaCGb4oFluYzTOgdrmPgQomrz16TJLjjmn56B
21+
9msoVGH5Kk/ifVr9waFuQFhcUfoWUUPZB3GrSGpr3Rz5XCh/BuXQDW8mDu29odzD
22+
6hDoNITsPv+y9F/BvqWOK+JeL+wP/F+AnciGMzIDnP4a4P4yj8Gf2rr1Eriok6wz
23+
aQr6NwnKsT4UAqjlmQ+gdPE4Joxk/ixlD41TZ97rq0LUSx2bcanM8GXZUjL74EuB
24+
TVABCeIX2ADBwHZ6v2HEkZvK7Miy23FP75JmLdNXw4GTcYmqD1bPIfsxgUkSwG63
25+
t0ChOqi9VdT62eAs5wShwhcrjc4xztjn6kypFu55a0neNr2qKYrwFo3QgZAbKWc1
26+
5jfS4kAq0gxyoQTCZnGhbbL095q3Sy7GV3EaW4yk78EuRwPFOqVUQ0D5tvrKsPT4
27+
B5AlxlarcDcMQayWKLj2pWmQm3YVlx5NfoRkSbd14h6ZryzDhG8ZfooLQ5dFh1ba
28+
f8+YbBtvFshzUDYdnr0fS0RYc/WtYmfJdb4+Fkc268BkJzg43rMSrdzaleS6jypU
29+
vzPs8WO0xU1xCIgB92vqZ+/4OlFwjbHHoQlnFHdNPbrfc8INbtLZgLCrELw4UEga
30+
-----END RSA PRIVATE KEY-----

tests/data/private_key.password

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-telegram-bot

tests/test_builders.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"""
2121
We mainly test on UpdaterBuilder because it has all methods that DispatcherBuilder already has
2222
"""
23+
from pathlib import Path
2324
from random import randint
2425
from threading import Event
2526

@@ -63,7 +64,9 @@ def test_mutually_exclusive_for_bot(self, builder, method, description):
6364
pytest.skip(f'{builder.__class__} has no method called {method}')
6465

6566
# First that e.g. `bot` can't be set if `request` was already set
66-
getattr(builder, method)(1)
67+
# We pass the private key since `private_key` is the only method that doesn't just save
68+
# the passed value
69+
getattr(builder, method)(Path('tests/data/private.key'))
6770
with pytest.raises(RuntimeError, match=f'`bot` may only be set, if no {description}'):
6871
builder.bot(None)
6972

@@ -84,7 +87,9 @@ def test_mutually_exclusive_for_dispatcher(self, builder, method, description):
8487
pytest.skip(f'{builder.__class__} has no method called {method}')
8588

8689
# First that e.g. `dispatcher` can't be set if `bot` was already set
87-
getattr(builder, method)(None)
90+
# We pass the private key since `private_key` is the only method that doesn't just save
91+
# the passed value
92+
getattr(builder, method)(Path('tests/data/private.key'))
8893
with pytest.raises(
8994
RuntimeError, match=f'`dispatcher` may only be set, if no {description}'
9095
):
@@ -102,7 +107,9 @@ def test_mutually_exclusive_for_dispatcher(self, builder, method, description):
102107
builder = builder.__class__()
103108
builder.dispatcher(None)
104109
if method != 'dispatcher_class':
105-
getattr(builder, method)(None)
110+
# We pass the private key since `private_key` is the only method that doesn't just save
111+
# the passed value
112+
getattr(builder, method)(Path('tests/data/private.key'))
106113
else:
107114
with pytest.raises(
108115
RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance'
@@ -251,3 +258,22 @@ def __init__(self, arg, **kwargs):
251258
else:
252259
assert isinstance(obj, CustomDispatcher)
253260
assert obj.arg == 2
261+
262+
@pytest.mark.parametrize('input_type', ('bytes', 'str', 'Path'))
263+
def test_all_private_key_input_types(self, builder, bot, input_type):
264+
private_key = Path('tests/data/private.key')
265+
password = Path('tests/data/private_key.password')
266+
267+
if input_type == 'bytes':
268+
private_key = private_key.read_bytes()
269+
password = password.read_bytes()
270+
if input_type == 'str':
271+
private_key = str(private_key)
272+
password = str(password)
273+
274+
builder.token(bot.token).private_key(
275+
private_key=private_key,
276+
password=password,
277+
)
278+
bot = builder.build().bot
279+
assert bot.private_key

0 commit comments

Comments
 (0)