Download feature and URLInputFile (#332)

* Fix How to upload docs

* Rename BaseBot to Bot

* Add download_file method

* Add download method

* Add URLInputFile

* Add Downloadable to __init__ and __all__

* Fix ImportError for Python 3.7

* Related pages

* Improving docs

* Some speed

* staticmethod to classmethod
This commit is contained in:
Gabben 2020-05-27 03:25:13 +05:00 committed by GitHub
parent 28382ebf5f
commit de3c5c1a8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 460 additions and 89 deletions

View file

@ -1,9 +1,22 @@
from __future__ import annotations
import datetime
import io
import pathlib
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, List, Optional, TypeVar, Union
from typing import (
Any,
AsyncGenerator,
AsyncIterator,
BinaryIO,
List,
Optional,
TypeVar,
Union,
cast,
)
import aiofiles
from async_lru import alru_cache
from ...utils.mixins import ContextInstanceMixin
@ -86,6 +99,7 @@ from ..types import (
Chat,
ChatMember,
ChatPermissions,
Downloadable,
File,
ForceReply,
GameHighScore,
@ -167,6 +181,93 @@ class Bot(ContextInstanceMixin["Bot"]):
"""
await self.session.close()
@classmethod
async def __download_file_binary_io(
cls, destination: BinaryIO, seek: bool, stream: AsyncGenerator[bytes, None]
) -> BinaryIO:
async for chunk in stream:
destination.write(chunk)
destination.flush()
if seek is True:
destination.seek(0)
return destination
@classmethod
async def __download_file(
cls, destination: Union[str, pathlib.Path], stream: AsyncGenerator[bytes, None]
) -> None:
async with aiofiles.open(destination, "wb") as f:
async for chunk in stream:
await f.write(chunk)
async def download_file(
self,
file_path: str,
destination: Optional[Union[BinaryIO, pathlib.Path, str]] = None,
timeout: int = 30,
chunk_size: int = 65536,
seek: bool = True,
) -> Optional[BinaryIO]:
"""
Download file by file_path to destination.
If you want to automatically create destination (:class:`io.BytesIO`) use default
value of destination and handle result of this method.
:param file_path: File path on Telegram server (You can get it from :obj:`aiogram.types.File`)
:param destination: Filename, file path or instance of :class:`io.IOBase`. For e.g. :class:`io.BytesIO`, defaults to None
:param timeout: Total timeout in seconds, defaults to 30
:param chunk_size: File chunks size, defaults to 64 kb
:param seek: Go to start of file when downloading is finished. Used only for destination with :class:`typing.BinaryIO` type, defaults to True
"""
if destination is None:
destination = io.BytesIO()
url = self.session.api.file_url(self.__token, file_path)
stream = self.session.stream_content(url=url, timeout=timeout, chunk_size=chunk_size)
if isinstance(destination, (str, pathlib.Path)):
return await self.__download_file(destination=destination, stream=stream)
else:
return await self.__download_file_binary_io(
destination=destination, seek=seek, stream=stream
)
async def download(
self,
file: Union[str, Downloadable],
destination: Optional[Union[BinaryIO, pathlib.Path, str]] = None,
timeout: int = 30,
chunk_size: int = 65536,
seek: bool = True,
) -> Optional[BinaryIO]:
"""
Download file by file_id or Downloadable object to destination.
If you want to automatically create destination (:class:`io.BytesIO`) use default
value of destination and handle result of this method.
:param file: file_id or Downloadable object
:param destination: Filename, file path or instance of :class:`io.IOBase`. For e.g. :class:`io.BytesIO`, defaults to None
:param timeout: Total timeout in seconds, defaults to 30
:param chunk_size: File chunks size, defaults to 64 kb
:param seek: Go to start of file when downloading is finished. Used only for destination with :class:`typing.BinaryIO` type, defaults to True
"""
if isinstance(file, str):
file_id = file
else:
file_id = getattr(file, "file_id", None)
if file_id is None:
raise TypeError("file can only be of the string or Downloadable type")
_file = await self.get_file(file_id)
# https://github.com/aiogram/aiogram/pull/282/files#r394110017
file_path = cast(str, _file.file_path)
return await self.download_file(
file_path, destination=destination, timeout=timeout, chunk_size=chunk_size, seek=seek
)
async def __call__(self, method: TelegramMethod[T]) -> T:
"""
Call API method

View file

@ -12,6 +12,7 @@ from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
from .dice import Dice, DiceEmoji
from .document import Document
from .downloadable import Downloadable
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
from .file import File
@ -43,7 +44,7 @@ from .inline_query_result_venue import InlineQueryResultVenue
from .inline_query_result_video import InlineQueryResultVideo
from .inline_query_result_voice import InlineQueryResultVoice
from .input_contact_message_content import InputContactMessageContent
from .input_file import BufferedInputFile, FSInputFile, InputFile
from .input_file import BufferedInputFile, FSInputFile, InputFile, URLInputFile
from .input_location_message_content import InputLocationMessageContent
from .input_media import InputMedia
from .input_media_animation import InputMediaAnimation
@ -101,8 +102,10 @@ from .webhook_info import WebhookInfo
__all__ = (
"TelegramObject",
"Downloadable",
"BufferedInputFile",
"FSInputFile",
"URLInputFile",
"Update",
"WebhookInfo",
"User",

View file

@ -0,0 +1,5 @@
from typing_extensions import Protocol
class Downloadable(Protocol):
file_id: str

View file

@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import AsyncGenerator, AsyncIterator, Iterator, Optional, Union
import aiofiles as aiofiles
import aiofiles
DEFAULT_CHUNK_SIZE = 64 * 1024 # 64 kb
@ -82,3 +82,28 @@ class FSInputFile(InputFile):
while chunk:
yield chunk
chunk = await f.read(chunk_size)
class URLInputFile(InputFile):
def __init__(
self,
url: str,
filename: Optional[str] = None,
chunk_size: int = DEFAULT_CHUNK_SIZE,
timeout: int = 30,
):
super().__init__(filename=filename, chunk_size=chunk_size)
self.url = url
self.timeout = timeout
async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
from aiogram.api.client.bot import Bot
bot = Bot.get_current(no_error=False)
stream = bot.session.stream_content(
url=self.url, timeout=self.timeout, chunk_size=self.chunk_size
)
async for chunk in stream:
yield chunk