mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
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:
parent
28382ebf5f
commit
de3c5c1a8d
40 changed files with 460 additions and 89 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
5
aiogram/api/types/downloadable.py
Normal file
5
aiogram/api/types/downloadable.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from typing_extensions import Protocol
|
||||
|
||||
|
||||
class Downloadable(Protocol):
|
||||
file_id: str
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue