Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion telegram/_files/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions telegram/_utils/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 4 additions & 1 deletion telegram/_utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`."""

Expand Down
46 changes: 36 additions & 10 deletions telegram/ext/_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -38,7 +39,7 @@

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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -608,16 +618,24 @@ 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`.

.. seealso:: `passportbot.py <https://github.com/python-telegram-bot/python-telegram-bot\
/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.
Expand Down Expand Up @@ -958,16 +976,24 @@ 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`.

.. seealso:: `passportbot.py <https://github.com/python-telegram-bot/python-telegram-bot\
/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.
Expand Down
8 changes: 4 additions & 4 deletions telegram/ext/_picklepersistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion telegram/ext/_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
4 changes: 2 additions & 2 deletions telegram/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
TimedOut,
Unauthorized,
)
from telegram._utils.types import JSONDict
from telegram._utils.types import JSONDict, FilePathInput


# pylint: disable=unused-argument
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions tests/data/private.key
Original file line number Diff line number Diff line change
@@ -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-----
1 change: 1 addition & 0 deletions tests/data/private_key.password
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-telegram-bot
32 changes: 29 additions & 3 deletions tests/test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -63,7 +64,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)

Expand All @@ -84,7 +87,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}'
):
Expand All @@ -102,7 +107,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'
Expand Down Expand Up @@ -251,3 +258,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