From 636654cb712dd22985e1db46bd4c0503e5bdb712 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Wed, 2 Nov 2022 08:28:41 +0100 Subject: [PATCH] Split `File.download` Into `File.download_to_drive` And `File.download_to_memory` (#3223) --- examples/conversationbot.py | 4 +- examples/passportbot.py | 12 +-- telegram/_bot.py | 7 +- telegram/_files/file.py | 163 ++++++++++++++++++++++----------- tests/data/image_decrypted.jpg | Bin 0 -> 24017 bytes tests/data/image_encrypted.jpg | Bin 0 -> 24240 bytes tests/test_animation.py | 2 +- tests/test_audio.py | 2 +- tests/test_chatphoto.py | 4 +- tests/test_document.py | 2 +- tests/test_file.py | 149 ++++++++++++++++++++++++++---- tests/test_inputfile.py | 4 +- tests/test_photo.py | 2 +- tests/test_sticker.py | 2 +- tests/test_video.py | 2 +- tests/test_videonote.py | 2 +- tests/test_voice.py | 2 +- 17 files changed, 261 insertions(+), 98 deletions(-) create mode 100644 tests/data/image_decrypted.jpg create mode 100644 tests/data/image_encrypted.jpg diff --git a/examples/conversationbot.py b/examples/conversationbot.py index a323ab8c1..cb5e5d282 100644 --- a/examples/conversationbot.py +++ b/examples/conversationbot.py @@ -23,7 +23,7 @@ try: except ImportError: __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment] -if __version_info__ < (20, 0, 0, "alpha", 1): +if __version_info__ < (20, 0, 0, "beta", 0): raise RuntimeError( f"This example is not compatible with your current PTB version {TG_VER}. To view the " f"{TG_VER} version of this example, " @@ -81,7 +81,7 @@ async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Stores the photo and asks for a location.""" user = update.message.from_user photo_file = await update.message.photo[-1].get_file() - await photo_file.download("user_photo.jpg") + await photo_file.download_to_memory("user_photo.jpg") logger.info("Photo of %s: %s", user.first_name, "user_photo.jpg") await update.message.reply_text( "Gorgeous! Now, send me your location please, or send /skip if you don't want to." diff --git a/examples/passportbot.py b/examples/passportbot.py index ab466f669..8dd8dfa64 100644 --- a/examples/passportbot.py +++ b/examples/passportbot.py @@ -21,7 +21,7 @@ try: except ImportError: __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment] -if __version_info__ < (20, 0, 0, "alpha", 1): +if __version_info__ < (20, 0, 0, "beta", 0): raise RuntimeError( f"This example is not compatible with your current PTB version {TG_VER}. To view the " f"{TG_VER} version of this example, " @@ -77,25 +77,25 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: for file in data.files: actual_file = await file.get_file() print(actual_file) - await actual_file.download() + await actual_file.download_to_memory() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.front_side ): front_file = await data.front_side.get_file() print(data.type, front_file) - await front_file.download() + await front_file.download_to_memory() if data.type in ("driver_license" and "identity_card") and data.reverse_side: reverse_file = await data.reverse_side.get_file() print(data.type, reverse_file) - await reverse_file.download() + await reverse_file.download_to_memory() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.selfie ): selfie_file = await data.selfie.get_file() print(data.type, selfie_file) - await selfie_file.download() + await selfie_file.download_to_memory() if data.translation and data.type in ( "passport", "driver_license", @@ -111,7 +111,7 @@ async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: for file in data.translation: actual_file = await file.get_file() print(actual_file) - await actual_file.download() + await actual_file.download_to_memory() def main() -> None: diff --git a/telegram/_bot.py b/telegram/_bot.py index 6cbcf254a..9d57c18e7 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -3100,10 +3100,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager): Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD` in size. The file can then - be downloaded - with :meth:`telegram.File.download`. It is guaranteed that the link will be - valid for at least 1 hour. When the link expires, a new one can be requested by - calling get_file again. + be e.g. downloaded with :meth:`telegram.File.download_to_memory`. It is guaranteed that + the link will be valid for at least 1 hour. When the link expires, a new one can be + requested by calling get_file again. Note: This function may not preserve the original file name and MIME type. diff --git a/telegram/_files/file.py b/telegram/_files/file.py index 1ee3adabc..6ed77969c 100644 --- a/telegram/_files/file.py +++ b/telegram/_files/file.py @@ -21,7 +21,7 @@ import shutil import urllib.parse as urllib_parse from base64 import b64decode from pathlib import Path -from typing import IO, TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, BinaryIO, Optional from telegram._passport.credentials import decrypt from telegram._telegramobject import TelegramObject @@ -35,18 +35,22 @@ if TYPE_CHECKING: class File(TelegramObject): """ - This object represents a file ready to be downloaded. The file can be downloaded with - :attr:`download`. It is guaranteed that the link will be valid for at least 1 hour. When the - link expires, a new one can be requested by calling :meth:`telegram.Bot.get_file`. + This object represents a file ready to be downloaded. The file can be e.g. downloaded with + :attr:`download_to_memory`. It is guaranteed that the link will be valid for at least 1 hour. + When the link expires, a new one can be requested by calling :meth:`telegram.Bot.get_file`. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. + .. versionchanged:: 20.0: + ``download`` was split into :meth:`download_to_memory` and :meth:`download_to_object`. + Note: * Maximum file size to download is :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`. * If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`, - then it will automatically be decrypted as it downloads when you call :meth:`download()`. + then it will automatically be decrypted as it downloads when you call e.g. + :meth:`download_to_memory`. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download @@ -55,7 +59,8 @@ class File(TelegramObject): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`, optional): Optional. File size in bytes, if known. - file_path (:obj:`str`, optional): File path. Use :attr:`download` to get the file. + file_path (:obj:`str`, optional): File path. Use e.g. :meth:`download_to_memory` to get the + file. Attributes: file_id (:obj:`str`): Identifier for this file. @@ -63,8 +68,8 @@ class File(TelegramObject): is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`str`): Optional. File size in bytes. - file_path (:obj:`str`): Optional. File path. Use :meth:`download` to get the file. - + file_path (:obj:`str`): Optional. File path. Use e.g. :meth:`download_to_memory` to get + the file. """ __slots__ = ( @@ -97,38 +102,55 @@ class File(TelegramObject): self._id_attrs = (self.file_unique_id,) - async def download( + def _get_encoded_url(self) -> str: + """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" + sres = urllib_parse.urlsplit(str(self.file_path)) + return urllib_parse.urlunsplit( + urllib_parse.SplitResult( + sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment + ) + ) + + def _prepare_decrypt(self, buf: bytes) -> bytes: + return decrypt(b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf) + + async def download_to_memory( self, custom_path: FilePathInput = None, - out: IO = None, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, - ) -> Union[Path, IO]: + ) -> Path: """ Download this file. By default, the file is saved in the current working directory with its - original filename as reported by Telegram. If the file has no filename, it the file ID will - be used as filename. If a :paramref:`custom_path` is supplied, it will be saved to that - path instead. If :paramref:`out` is defined, the file contents will be saved to that object - using the :obj:`out.write` method. + original filename as reported by Telegram. If the file has no filename, the file ID will + be used as filename. If :paramref:`custom_path` is supplied as a :obj:`str` or + :obj:`pathlib.Path`, it will be saved to that path. Note: - * :paramref:`custom_path` and :paramref:`out` are mutually exclusive. - * If neither :paramref:`custom_path` nor :paramref:`out` is provided and - :attr:`file_path` is the path of a local file (which is the case when a Bot API - Server is running in local mode), this method will just return the path. + If :paramref:`custom_path` isn't provided and :attr:`file_path` is the path of a + local file (which is the case when a Bot API Server is running in local mode), this + method will just return the path. + + The only exception to this are encrypted files (e.g. a passport file). For these, a + file with the prefix `decrypted_` will be created in the same directory as the + original file in order to decrypt the file without changing the existing one + in-place. + .. versionchanged:: 20.0 * :paramref:`custom_path` parameter now also accepts :class:`pathlib.Path` as argument. * Returns :class:`pathlib.Path` object in cases where previously a :obj:`str` was returned. + * This method was previously called ``download``. It was split into + :meth:`download_to_memory` and :meth:`download_to_object`. + Args: - custom_path (:class:`pathlib.Path` | :obj:`str`, optional): Custom path. - out (:obj:`io.BufferedWriter`, optional): A file-like object. Must be opened for - writing in binary mode, if applicable. + custom_path (:class:`pathlib.Path` | :obj:`str` , optional): The path where the file + will be saved to. If not specified, will be saved in the current working directory. read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. @@ -143,32 +165,22 @@ class File(TelegramObject): :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. Returns: - :class:`pathlib.Path` | :obj:`io.BufferedWriter`: The same object as :paramref:`out` if - specified. Otherwise, returns the filename downloaded to or the file path of the - local file. - - Raises: - ValueError: If both :paramref:`custom_path` and :paramref:`out` are passed. + :class:`pathlib.Path`: Returns the Path object the file was downloaded to. """ - if custom_path is not None and out is not None: - raise ValueError("`custom_path` and `out` are mutually exclusive") - local_file = is_local_file(self.file_path) url = None if local_file else self._get_encoded_url() - path = Path(self.file_path) if local_file else None - if out: - if local_file: - buf = path.read_bytes() + # if _credentials exists we want to decrypt the file + if local_file and self._credentials: + file_to_decrypt = Path(self.file_path) + buf = self._prepare_decrypt(file_to_decrypt.read_bytes()) + if custom_path is not None: + path = Path(custom_path) else: - buf = await self.get_bot().request.retrieve(url) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) - out.write(buf) - return out + path = Path(str(file_to_decrypt.parent) + "/decrypted_" + file_to_decrypt.name) + path.write_bytes(buf) + return path if custom_path is not None and local_file: shutil.copyfile(self.file_path, str(custom_path)) @@ -191,20 +203,58 @@ class File(TelegramObject): pool_timeout=pool_timeout, ) if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + buf = self._prepare_decrypt(buf) filename.write_bytes(buf) return filename - def _get_encoded_url(self) -> str: - """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" - sres = urllib_parse.urlsplit(str(self.file_path)) - return urllib_parse.urlunsplit( - urllib_parse.SplitResult( - sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment + async def download_to_object( + self, + out: BinaryIO, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + ) -> None: + """ + Download this file into memory. :paramref:`out` needs to be supplied with a + :obj:`io.BufferedIOBase`, the file contents will be saved to that object using the + :obj:`out.write` method. + + .. versionadded:: 20.0 + + + Args: + out (:obj:`io.BufferedIOBase`): A file-like object. Must be opened for writing in + binary mode. + read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to + :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to + :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. + write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to + :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to + :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. + connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to + :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to + :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. + pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to + :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to + :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. + """ + local_file = is_local_file(self.file_path) + url = None if local_file else self._get_encoded_url() + path = Path(self.file_path) if local_file else None + if local_file: + buf = path.read_bytes() + else: + buf = await self.get_bot().request.retrieve( + url, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, ) - ) + if self._credentials: + buf = self._prepare_decrypt(buf) + out.write(buf) async def download_as_bytearray(self, buf: bytearray = None) -> bytearray: """Download this file and return it as a bytearray. @@ -219,10 +269,15 @@ class File(TelegramObject): """ if buf is None: buf = bytearray() + if is_local_file(self.file_path): - buf.extend(Path(self.file_path).read_bytes()) + bytes_data = Path(self.file_path).read_bytes() else: - buf.extend(await self.get_bot().request.retrieve(self._get_encoded_url())) + bytes_data = await self.get_bot().request.retrieve(self._get_encoded_url()) + if self._credentials: + buf.extend(self._prepare_decrypt(bytes_data)) + else: + buf.extend(bytes_data) return buf def set_credentials(self, credentials: "FileCredentials") -> None: diff --git a/tests/data/image_decrypted.jpg b/tests/data/image_decrypted.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4ee9244c64fbc05534ca29f5f52f471212e334ca GIT binary patch literal 24017 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%0AUAHK#pU zidJdv2IMxSHhn}Sq!33>#b6nEct#M4rOnY|N(xY&qsWF-qI=guungh2oUMA~Y-|BW z`tB>0rv^`OI`fK4WTj~*MtHj!I1@(x2Y%yY?U33YM`gk$d3-x8-$=Ah_yK zAh4LsHrjp$s_AAcge;P!VvU|*!%_f_TQ|+__!n3ZLFP?f<6DFz#!*QerAa<-@*^}j z-n6um6S95}5y%0AvMdvqniQbSl!Xn#R6Y{Z&Xp%Ms75TMVeNyru-FFfMN?}qRabB85bOA7ILHy7|tA8sijNmFN9hGI%qd93qaOWwYmJs3GB z)=&npZhFUYL0cs^#F9X1EubGH9Pfi|^G=jllG?*dORb?vS))LYfCrog^Ie1Y5!A5d zqXDsaFn_!ouCqaiTa1+z8WoAd)gY0xt4di?l|E9W$Z)IhO@n!$>OP`91teJB@8AP~ zQtuq)a3~#S7u!l!cFyBGWF|i1LugEN2AWDi-v_s>4Q9Kj`Y8K!EQK2%g`9x;qdu1t z5%j*wTmps)IB&1wunHBL)DOFkqLn12MFy3fw>&u%snY%mt%lt|K<{LnAdT`v?XcRN zT=@!>Axp}x{{V_j3(W^pb6D=M60~X3+nB;so=`Xc z00YdWU=`lCv8YRKBMM$il&MZBYv}@--%r#Xg7Q?Da~hi3Tu=B8WQ;a<$#WWt%9dx( zc2lal9r|Ezx&9#P4n$f=g*B;dC1_8(Jk|qdvc=XBUm%JXfZt#+q+U&!%vMxP{Xxkq zP{U`q?MJJo>Q2IOCn4%l=OrZ`ZxVgmxQjf=7hC*t3EKrD2rJx6)L4kxx##K*%5_AN zNxy(7438n$%S(vUb+kN$rR0>jo@50pws-je(cNkQ+OwiC$2scuSNsImNzJC47b z!g;%)#Wrm^ZoXROY3TAmFb9iWNo%6?7?Vh0RoWO63b)cSQY8vVp_D7MZ zNtCNykZ^8n{7|YCkPmPaM`+rFn8hU#IE)d>^D^3aT163;)(8I5CPs{&Wl7>jfaGpe z`4UxRtdT@)Zwb8b^4gN?Cfkp%OKhYnKM^oT_nFmUJ+ecMD{zp73kLjAIlgb3`R1%D z{{U)nwBx$0R!tckM!?yZ_4!V7Bs(ehkGzB2{QhH~&I_zcls{JINoVy&xizuvM-fl* zjv`%ubNZa;^)bgdnZlY$;yIu3b6c3_?78o{DMiH`H0HV}V8xYGBV$#vE zE|B?<#qf~D91yNLKh2|?)!)v9aGF;8vREArL4y{c{bgFI^pK&USg-V#9 zw2Cd~ty`=u5{or@%q+AED82<|7db`nYAl(GEZ!An7ZuA{?eOb9Qp;zFTryY0uW6@V zy}m7R>atta?VPHCrcV4Ay zq(OPwJ=;G6W0R{*;NmG|=Njsa&6o3S2eIox5lNVq!P?yI7eCuRA#LU*u!z>stj$5| zn8pf3YCAiqJx$qtqe{DJR}Hiqvpr90RmG;QU$rOtcF^mYw3WrYzo@i2Q0jW-?iTWk zb%ngYuYvVGvU$d9H&7_ITlE(5Wv({R`kG#)Q)6WJG3!Fzymh49 zxhpEsZsVg;?QC)uW}@xm@YJg^_588xMe}b)w6;pv^=P#1O0zFlbksej-loRNJ*LrR zX}UgIi&n^LP3zFItqWn2(o(P3QXHo04Wmnv!%Vb#@P;i8<6O3w-QgO1)|6?y8x1aV z4d7Z~rJ!jBn!TnPS|++Q^_xbRY-x6lQrH_tr*l!MTIn^TRm7KsMWJCxlGn2h%}WS` zLAuz8q)bJaZbSG+A~54AxUjHN=@a=PHzBD%|HJ?=5dZ-L0|NsB1q1^D0RR91009C6 z1ONpQ1`;7KF+d_g5EB$2G8QvXVM0-HBP9RY00;pC0RadA>v8e#+=?U)OOxWVaJTgZ zxq>$ZN$deX3=z#{ zv!64H&-!*lEDg@g)YkaLNfNb`wJ-kwmHYk5LR6J0_le4jo4Hm|Kqj!7+L{3(hzGDy zO(pjc>q{5wlPOLjYd6BNDjuh|N-0e>C6~tL#xaw(BO~9*HJZ&#xNFi zxN2A6`y|y5<4|1)|Zr8bxVx^+{m{W(@>SO zpY`IEgz~G#s%kdoVab&I*A(-FkTXD~bU)I(A^nu+uwKp#Y0t!QD`wHETYqaaQlzYI zPl5jcFl&!_QrL*noqF}DU6iFY6~bIOR(QZ1aahMRp}WP;7_1**y(!$A2BUU4OETij zVq3CEX)W6BY+RU2 zjrCLcQRP*1x-o8LOI^z1{P&dWg%}%@Fh=3!1BGUkQlrPK7^NMjHEp>+1@yYjvgFOd zl&u|4Y}Lx>#kqwocN>$_-&2nF{sX;st98x$bLn-vZOw~|33aiRj8*{YOP16|6!W)> zgOjB=@IeFAui(G!qT;)JmfcJ#O-*GlGCC_EDcp4}_AYUwI_;)E$EUPeY{1G7~QShP8~`INVgJT;{Q#PzQHD9)9#P>iA4u zKR!vy8qv>f%d+&GmeaX18dNp>wfa<@)DC(Cagn@Yv~e^&O(~zTWRW`~R7;drf@81L4-xd8*hy$HdGv~`vP?40-Ip|`B zMG{SD*-A`>#v}Vt!c=QeJ|tY8QhW2&G?JihA@btte+f_|P zZLxp`IwX*4;P{Ub=M^FH^Uf_`dMI@C^XX#X%9IXL4Nh>g73GK2OU|gL@#_$!{w&9k zwQWo-GcHL=oQI_%GwN}OY%n&rj2sTs2|X@5R;`_}kEcq;?^1Jw;(+PKSwx12fa!iK zE*tyhi*w4S4(-Y#w?mWJ3rA)5IUG}Q-*n-)nr>{3sxY*)RFsz^+v7$~5rK|6zVxL8 zg(&JNW=!Q_Cu(+%;}zeg4$_s4J0QrmGNY?ii%PIgQA6<0KpnJ{r1GnMwnf3*HxEcK zovVH|vNU9*ASWCE5-9W61KzIkgjh~4pK(sQ;?>kN-zeuvD>jtYv2gt=8Xx0|N#h+1 zQ)Mf8w2LLVs8L%3pMWFZ9}M%3u{b!bVK-<@WRsq-;9|6Esxh2s>rdEbhetT36}Cer zp*V-5GNfSFCa?#B;=&T88~nejo~H(L&sf%t{g-Q6e#106IL##UT6Xpr35@y{ms7a| zKK=X~*><&)N>oHRI}DM?&qFw*j2*{%lC-H7?MWss)Vl*v zLztANlBBDYgPyndK;&k%Y|vaxWSoU!i6vcZR*h+5D%`DM#i>{aBNdEs)5koAmeI>r z%}FXY)VT*x!FS{O3&k(Ny>75+;dW$46@?P`6`z!#i;le7LxtJfK$R#N*h&L&LV^D$r!-! z2q(5mhS7wMD^J`Uhb+a2JiJwmiNk}{?%FcS{wP?6XFD_Dr0nKW7W>vj= ze;vs_5*ARTsA=1Pl@TEQ#NckNRWs+x`Cy(g)*~t43~@knwzSMs8r2&-K_0C;Jj9fcq$w*( zNE{4Mpn0b~Dah2*er(kQrz$xr>kbu?$`tGErMpxy4kIm(f%qQPs;xnl{bNcPPU_Bl z4{8IAw3Vp}PTf5`ONO1vCmj4I$`NC@wMcZn`-@ish|gH^HjOK23t!ec54YQzxWq)r zt}?mtoQm?8SUBMK>_UfrzH3ih%o%A5Lc^#_sZS~6Be2!8HZ$@Qka?_7IO&jmv6SQ` zK=+3ka-=rXR`{rg6}D6FpSX(EHh-vZ>si85y{dB|SxHe+d=R6Qn$!A#G=#>K)Tr(! zb91i|V4+K0&fux?6!N+E>ST_=TTN^V{B)(8>6`k;xvX)|+@ysiNmiq<7|yC&O{jh< z$j=@I6i(XZCYzrrq~R-)w|^vslbZE^mrzoM?@-fLMM`lpo=QRjLP-a)=bU|wk|9w4qV_!fclwp z_zvpDEnx!!*KdB}@>@l+*&f`&P~LJe)5-W;t&Ch5QWtZ#KW=?Zxj<&IM}vwZ9zf*c ziNVEV12x>>Tdj{=UrTLT?QC1vN)k9UaC>!bcG&k86x+8OM5qoaF5@GDg(sQ>C{_sX zq^sq)v9)|3 zAg4Ui2~gWsT{Ps9mt{kS%r=tCJZFM);NprBp@VnQ%}FYLB)F0yJf`1KqLp_5j>H0J z?1*RT!r3tj@Rj+HaB3t6aN>a}?o-yNN2a-V*{{UuM zp-f7?Y~38Z?u$0rA}Y2mAfvfCKW2XH{CJ>(KNyzjlNYtPdOhRnlMdGs8d4g-tYWwV zfzKXW(O7*uT$!dV(svOnMo3Dt8SOoAVJcFU9zqTY1w1ceD*$%QW`%7Y;k@=tcNB3= zSZ)uYB`~y8D0Wbu$%)6Sb`Se*k#w0X@jY6r}K6ffeJIBx%cI)Y5il`A5FjBkK~Pe0p<;fdrDl@&DQY2mu2D z2|oa%B={NqPvZSi7qSS2Ir5zOL13aUWcnZ5V*daj8eP$HM}jOz8TxQQA|IWp@e2t) zBwFO25`IvM0U`9GfW1lJm3!e~GcOeG&)GgI0@|JucWXT<@XeQvTft|_lzOtZflZlvBxajd3TE<`h$A z8UmXr{n2gl$V>JVQ)V0Dn<)L!ZT|qAjFC;4ZwkR;DRPe`DKiQw8+Knre7BRA(k}?E zN^z2B9TrMur)tSDvfvl7O;3(Ta7B52LuSG{P& zh7mIuS}B;zd4`8%h7nuI*wB=>*rDB**>y7S6f1c<6g-Qqt8}jON`_*Zl)p3^ zqj}UTp_G|XZqR-&H<4^bN4(XTs6PtSggliXlY_BDVX3D7u=m z&Fw%?qKFYfXsv^En)k09Bh*3g!SR&a&)UjZ*K*BLmlrW;4Zg>jnp%{iO zI1s}~Qj+6vQxeTXyA7H~V4L`{qsoe5xG6@6W;wr_a}A5CkfbesmDNHk0Ub`%r7-Ih z1%M zxTD$mPKk}6nK>k4J+`MK5a#1v>`90qFG?{X{OnFj5l+bt-D1$}lY+33h=-*+iN358 pu@-VhCnDeCaS!XU3Hzszihbce@Sa2{OWiq(_S&2S8`%vf|JmrmA4dQH literal 0 HcmV?d00001 diff --git a/tests/data/image_encrypted.jpg b/tests/data/image_encrypted.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d23371f2a26ce729eb029fdd15aef28c78ca468d GIT binary patch literal 24240 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 literal 0 HcmV?d00001 diff --git a/tests/test_animation.py b/tests/test_animation.py index 0e0d46790..eda6a2fbc 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -129,7 +129,7 @@ class TestAnimation: assert new_file.file_id == animation.file_id assert new_file.file_path.startswith("https://") - new_filepath = await new_file.download("game.gif") + new_filepath = await new_file.download_to_memory("game.gif") assert new_filepath.is_file() diff --git a/tests/test_audio.py b/tests/test_audio.py index eea5a66ab..e8bd39fce 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -144,7 +144,7 @@ class TestAudio: assert new_file.file_unique_id == audio.file_unique_id assert str(new_file.file_path).startswith("https://") - await new_file.download("telegram.mp3") + await new_file.download_to_memory("telegram.mp3") assert path.is_file() diff --git a/tests/test_chatphoto.py b/tests/test_chatphoto.py index ff64dcb83..1b57381a8 100644 --- a/tests/test_chatphoto.py +++ b/tests/test_chatphoto.py @@ -86,7 +86,7 @@ class TestChatPhoto: assert new_file.file_unique_id == chat_photo.small_file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download(jpg_file) + await new_file.download_to_memory(jpg_file) assert jpg_file.is_file() @@ -95,7 +95,7 @@ class TestChatPhoto: assert new_file.file_unique_id == chat_photo.big_file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download(jpg_file) + await new_file.download_to_memory(jpg_file) assert jpg_file.is_file() diff --git a/tests/test_document.py b/tests/test_document.py index 4a9dbcc89..fca4bc9f1 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -119,7 +119,7 @@ class TestDocument: assert new_file.file_unique_id == document.file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download("telegram.png") + await new_file.download_to_memory("telegram.png") assert path.is_file() diff --git a/tests/test_file.py b/tests/test_file.py index ea11b4134..10ef115b6 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -23,7 +23,7 @@ from tempfile import TemporaryFile, mkstemp import pytest from flaky import flaky -from telegram import File, Voice +from telegram import File, FileCredentials, Voice from telegram.error import TelegramError from tests.conftest import data_file @@ -40,6 +40,39 @@ def file(bot): return file +@pytest.fixture(scope="class") +def encrypted_file(bot): + # check https://github.com/python-telegram-bot/python-telegram-bot/wiki/\ + # PTB-test-writing-knowledge-base#how-to-generate-encrypted-passport-files + # if you want to know the source of these values + fc = FileCredentials( + "Oq3G4sX+bKZthoyms1YlPqvWou9esb+z0Bi/KqQUG8s=", + "Pt7fKPgYWKA/7a8E64Ea1X8C+Wf7Ky1tF4ANBl63vl4=", + ) + ef = File(TestFile.file_id, TestFile.file_unique_id, TestFile.file_size, TestFile.file_path) + ef.set_bot(bot) + ef.set_credentials(fc) + return ef + + +@pytest.fixture(scope="class") +def encrypted_local_file(bot): + # check encrypted_file() for the source of the fc values + fc = FileCredentials( + "Oq3G4sX+bKZthoyms1YlPqvWou9esb+z0Bi/KqQUG8s=", + "Pt7fKPgYWKA/7a8E64Ea1X8C+Wf7Ky1tF4ANBl63vl4=", + ) + ef = File( + TestFile.file_id, + TestFile.file_unique_id, + TestFile.file_size, + file_path=str(data_file("image_encrypted.jpg")), + ) + ef.set_bot(bot) + ef.set_credentials(fc) + return ef + + @pytest.fixture(scope="class") def local_file(bot): file = File( @@ -95,16 +128,12 @@ class TestFile: with pytest.raises(TelegramError): await bot.get_file(file_id="") - async def test_download_mutually_exclusive(self, file): - with pytest.raises(ValueError, match="`custom_path` and `out` are mutually exclusive"): - await file.download("custom_path", "out") - async def test_download(self, monkeypatch, file): async def test(*args, **kwargs): return self.file_content monkeypatch.setattr(file.get_bot().request, "retrieve", test) - out_file = await file.download() + out_file = await file.download_to_memory() try: assert out_file.read_bytes() == self.file_content @@ -112,7 +141,7 @@ class TestFile: out_file.unlink() async def test_download_local_file(self, local_file): - assert await local_file.download() == Path(local_file.file_path) + assert await local_file.download_to_memory() == Path(local_file.file_path) @pytest.mark.parametrize( "custom_path_type", [str, Path], ids=["str custom_path", "pathlib.Path custom_path"] @@ -125,7 +154,7 @@ class TestFile: file_handle, custom_path = mkstemp() custom_path = Path(custom_path) try: - out_file = await file.download(custom_path_type(custom_path)) + out_file = await file.download_to_memory(custom_path_type(custom_path)) assert out_file == custom_path assert out_file.read_bytes() == self.file_content finally: @@ -139,7 +168,7 @@ class TestFile: file_handle, custom_path = mkstemp() custom_path = Path(custom_path) try: - out_file = await local_file.download(custom_path_type(custom_path)) + out_file = await local_file.download_to_memory(custom_path_type(custom_path)) assert out_file == custom_path assert out_file.read_bytes() == self.file_content finally: @@ -153,7 +182,7 @@ class TestFile: file.file_path = None monkeypatch.setattr(file.get_bot().request, "retrieve", test) - out_file = await file.download() + out_file = await file.download_to_memory() assert str(out_file)[-len(file.file_id) :] == file.file_id try: @@ -167,19 +196,15 @@ class TestFile: monkeypatch.setattr(file.get_bot().request, "retrieve", test) with TemporaryFile() as custom_fobj: - out_fobj = await file.download(out=custom_fobj) - assert out_fobj is custom_fobj - - out_fobj.seek(0) - assert out_fobj.read() == self.file_content + await file.download_to_object(out=custom_fobj) + custom_fobj.seek(0) + assert custom_fobj.read() == self.file_content async def test_download_file_obj_local_file(self, local_file): with TemporaryFile() as custom_fobj: - out_fobj = await local_file.download(out=custom_fobj) - assert out_fobj is custom_fobj - - out_fobj.seek(0) - assert out_fobj.read() == self.file_content + await local_file.download_to_object(out=custom_fobj) + custom_fobj.seek(0) + assert custom_fobj.read() == self.file_content async def test_download_bytearray(self, monkeypatch, file): async def test(*args, **kwargs): @@ -210,6 +235,90 @@ class TestFile: assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf + async def test_download_encrypted(self, monkeypatch, bot, encrypted_file): + async def test(*args, **kwargs): + return data_file("image_encrypted.jpg").read_bytes() + + monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) + out_file = await encrypted_file.download_to_memory() + + try: + assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() + finally: + out_file.unlink() + + async def test_download_file_obj_encrypted(self, monkeypatch, encrypted_file): + async def test(*args, **kwargs): + return data_file("image_encrypted.jpg").read_bytes() + + monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) + with TemporaryFile() as custom_fobj: + await encrypted_file.download_to_object(out=custom_fobj) + custom_fobj.seek(0) + assert custom_fobj.read() == data_file("image_decrypted.jpg").read_bytes() + + async def test_download_local_file_encrypted(self, encrypted_local_file): + out_file = await encrypted_local_file.download_to_memory() + try: + assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() + finally: + out_file.unlink() + + @pytest.mark.parametrize( + "custom_path_type", [str, Path], ids=["str custom_path", "pathlib.Path custom_path"] + ) + async def test_download_custom_path_local_file_encrypted( + self, encrypted_local_file, custom_path_type + ): + file_handle, custom_path = mkstemp() + custom_path = Path(custom_path) + try: + out_file = await encrypted_local_file.download_to_memory(custom_path_type(custom_path)) + assert out_file == custom_path + assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() + finally: + os.close(file_handle) + custom_path.unlink() + + async def test_download_file_obj_local_file_encrypted(self, monkeypatch, encrypted_local_file): + async def test(*args, **kwargs): + return data_file("image_encrypted.jpg").read_bytes() + + monkeypatch.setattr(encrypted_local_file.get_bot().request, "retrieve", test) + with TemporaryFile() as custom_fobj: + await encrypted_local_file.download_to_object(out=custom_fobj) + custom_fobj.seek(0) + assert custom_fobj.read() == data_file("image_decrypted.jpg").read_bytes() + + async def test_download_bytearray_encrypted(self, monkeypatch, encrypted_file): + async def test(*args, **kwargs): + return data_file("image_encrypted.jpg").read_bytes() + + monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) + + # Check that a download to a newly allocated bytearray works. + buf = await encrypted_file.download_as_bytearray() + assert buf == bytearray(data_file("image_decrypted.jpg").read_bytes()) + + # Check that a download to a given bytearray works (extends the bytearray). + buf2 = buf[:] + buf3 = await encrypted_file.download_as_bytearray(buf=buf2) + assert buf3 is buf2 + assert buf2[len(buf) :] == buf + assert buf2[: len(buf)] == buf + + async def test_download_bytearray_local_file_encrypted(self, encrypted_local_file): + # Check that a download to a newly allocated bytearray works. + buf = await encrypted_local_file.download_as_bytearray() + assert buf == bytearray(data_file("image_decrypted.jpg").read_bytes()) + + # Check that a download to a given bytearray works (extends the bytearray). + buf2 = buf[:] + buf3 = await encrypted_local_file.download_as_bytearray(buf=buf2) + assert buf3 is buf2 + assert buf2[len(buf) :] == buf + assert buf2[: len(buf)] == buf + def test_equality(self, bot): a = File(self.file_id, self.file_unique_id, bot) b = File("", self.file_unique_id, bot) diff --git a/tests/test_inputfile.py b/tests/test_inputfile.py index adc0575f3..4f8d33a4c 100644 --- a/tests/test_inputfile.py +++ b/tests/test_inputfile.py @@ -145,7 +145,7 @@ class TestInputFile: message = await bot.send_document(chat_id, data_file("text_file.txt").read_bytes()) out = BytesIO() - assert await (await message.document.get_file()).download(out=out) + await (await message.document.get_file()).download_to_object(out=out) out.seek(0) assert out.read().decode("utf-8") == "PTB Rocks! ⅞" @@ -158,7 +158,7 @@ class TestInputFile: ) out = BytesIO() - assert await (await message.document.get_file()).download(out=out) + await (await message.document.get_file()).download_to_object(out=out) out.seek(0) assert out.read().decode("utf-8") == "PTB Rocks! ⅞" diff --git a/tests/test_photo.py b/tests/test_photo.py index ebaff6f33..f478d58a2 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -304,7 +304,7 @@ class TestPhoto: assert new_file.file_unique_id == photo.file_unique_id assert new_file.file_path.startswith("https://") is True - await new_file.download("telegram.jpg") + await new_file.download_to_memory("telegram.jpg") assert path.is_file() diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 3e33cf522..80741f101 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -169,7 +169,7 @@ class TestSticker: assert new_file.file_unique_id == sticker.file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download("telegram.webp") + await new_file.download_to_memory("telegram.webp") assert path.is_file() diff --git a/tests/test_video.py b/tests/test_video.py index 284bbe7b5..c3b7fc099 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -149,7 +149,7 @@ class TestVideo: assert new_file.file_unique_id == video.file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download("telegram.mp4") + await new_file.download_to_memory("telegram.mp4") assert path.is_file() diff --git a/tests/test_videonote.py b/tests/test_videonote.py index d5eb7ac36..ca51fe286 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -133,7 +133,7 @@ class TestVideoNote: assert new_file.file_unique_id == video_note.file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download("telegram2.mp4") + await new_file.download_to_memory("telegram2.mp4") assert path.is_file() diff --git a/tests/test_voice.py b/tests/test_voice.py index e7e0bf61e..d617472db 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -121,7 +121,7 @@ class TestVoice: assert new_file.file_unique_id == voice.file_unique_id assert new_file.file_path.startswith("https://") - await new_file.download("telegram.ogg") + await new_file.download_to_memory("telegram.ogg") assert path.is_file()