Add download_file method

This commit is contained in:
Gabben 2020-05-16 13:41:24 +05:00
parent 84d5e1c7b5
commit d54421c1fb
4 changed files with 168 additions and 1 deletions

View file

@ -1,9 +1,12 @@
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
import aiofiles
from async_lru import alru_cache
from ...utils.mixins import ContextInstanceMixin
@ -167,6 +170,61 @@ class Bot(ContextInstanceMixin["Bot"]):
"""
await self.session.close()
@staticmethod
async def __download_file_binary_io(
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
@staticmethod
async def __download_file(
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`)
:type file_path: str
:param destination: Filename, file path or instance of :class:`io.IOBase`. For e.g. :class:`io.BytesIO`, defaults to None
:type destination: Optional[Union[BinaryIO, pathlib.Path, str]]
:param timeout: Total timeout in seconds, defaults to 30
:type timeout: int
:param chunk_size: File chunks size, defaults to 64 kb
:type chunk_size: int
:param seek: Go to start of file when downloading is finished. Used only for destination with :class:`typing.BinaryIO` type, defaults to True
:type seek: bool
"""
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 __call__(self, method: TelegramMethod[T]) -> T:
"""
Call API method

View file

@ -0,0 +1,57 @@
# How to download file?
## Download file manually
First, you must get the `file_id` of the file you want to download. Information about files sent to the bot is contained in [Message](./types/message.md).
For example, download the document that came to the bot.
```python3
file_id = message.document.file_id
```
Then use the [getFile](./methods/get_file.md) method to get `file_path`.
```python3
file = await bot.get_file(file_id)
file_path = file.file_path
```
After that, use the `download_file` method from the bot object.
### download_file(...)
Download file by file_path to destination.
If you want to automatically create destination (`#!python3 io.BytesIO`) use default
value of destination and handle result of this method.
|Argument|Type|Description|
|---|---|---|
| file_path | `#!python3 str` | File path on Telegram server |
| destination | `#!python3 Optional[Union[BinaryIO, pathlib.Path, str]]` | Filename, file path or instance of `#!python3 io.IOBase`. For e.g. `#!python3 io.BytesIO` (Default: `#!python3 None`) |
| timeout | `#!python3 int` | Total timeout in seconds (Default: `30`) |
| chunk_size | `#!python3 int` | File chunks size (Default: `64 kb`) |
| seek | `#!python3 bool` | Go to start of file when downloading is finished. Used only for destination with `#!python3 typing.BinaryIO` type (Default: `#!python3 True`) |
There are two options where you can download the file: to **disk** or to **binary I/O object**.
### Download file to disk
To download file to disk, you must specify the file name or path where to download the file. In this case, the function will return nothing.
```python3
await bot.download_file(file_path, "text.txt")
```
### Download file to binary I/O object
To download file to binary I/O object, you must specify an object with the `#!python3 typing.BinaryIO` type or use the default (`#!python3 None`) value.
In the first case, the function will return your object:
```python3
my_object = MyBinaryIO()
result: MyBinaryIO = await bot.download_file(file_path, my_object)
# print(result is my_object) # True
```
If you leave the default value, an `#!python3 io.BytesIO` object will be created and returned.
```python3
result: io.BytesIO = await bot.download_file(file_path)
```

View file

@ -233,6 +233,7 @@ nav:
- api/types/game.md
- api/types/callback_game.md
- api/types/game_high_score.md
- api/downloading_files.md
- api/sending_files.md
- Dispatcher:
- dispatcher/index.md

View file

@ -1,4 +1,8 @@
import io
import aiofiles
import pytest
from aresponses import ResponsesMockServer
from aiogram import Bot
from aiogram.api.client.session.aiohttp import AiohttpSession
@ -61,3 +65,50 @@ class TestBot:
mocked_close.assert_awaited()
else:
mocked_close.assert_not_awaited()
@pytest.mark.asyncio
async def test_download_file(self, aresponses: ResponsesMockServer):
aresponses.add(
aresponses.ANY, aresponses.ANY, "get", aresponses.Response(status=200, body=b"\f" * 10)
)
# https://github.com/Tinche/aiofiles#writing-tests-for-aiofiles
aiofiles.threadpool.wrap.register(CoroutineMock)(
lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs)
)
mock_file = CoroutineMock()
bot = Bot("42:TEST")
with patch("aiofiles.threadpool.sync_open", return_value=mock_file):
await bot.download_file("TEST", "file.png")
mock_file.write.assert_called_once_with(b"\f" * 10)
@pytest.mark.asyncio
async def test_download_file_default_destination(self, aresponses: ResponsesMockServer):
bot = Bot("42:TEST")
aresponses.add(
aresponses.ANY, aresponses.ANY, "get", aresponses.Response(status=200, body=b"\f" * 10)
)
result = await bot.download_file("TEST")
assert isinstance(result, io.BytesIO)
assert result.read() == b"\f" * 10
@pytest.mark.asyncio
async def test_download_file_custom_destination(self, aresponses: ResponsesMockServer):
bot = Bot("42:TEST")
aresponses.add(
aresponses.ANY, aresponses.ANY, "get", aresponses.Response(status=200, body=b"\f" * 10)
)
custom = io.BytesIO()
result = await bot.download_file("TEST", custom)
assert isinstance(result, io.BytesIO)
assert result is custom
assert result.read() == b"\f" * 10