Skip to content

Commit

Permalink
Feature/upload smartapp file (#394)
Browse files Browse the repository at this point in the history
* feat: add upload_smartapp_file method

* test: upload_smartapp_file method
  • Loading branch information
Kiruha01 committed Feb 20, 2023
1 parent b57caa1 commit e69e650
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 1 deletion.
29 changes: 29 additions & 0 deletions pybotx/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
BotXAPISmartAppsListRequestPayload,
SmartAppsListMethod,
)
from pybotx.client.smartapps_api.upload_file import (
UploadFileMethod as SmartappsUploadFileMethod,
)
from pybotx.client.stickers_api.add_sticker import (
AddStickerMethod,
BotXAPIAddStickerRequestPayload,
Expand Down Expand Up @@ -1352,6 +1355,32 @@ async def get_smartapps_list(

return botx_api_smartapps_list.to_domain()

async def upload_static_file(
self,
*,
bot_id: UUID,
async_buffer: AsyncBufferReadable,
filename: str,
) -> str:
"""Upload static file to file service.
:param bot_id: Bot which should perform the request.
:param async_buffer: Buffer to read uploaded file.
:param filename: File name.
:return: file link.
"""

method = SmartappsUploadFileMethod(
bot_id,
self._httpx_client,
self._bot_accounts_storage,
)

botx_api_static_file = await method.execute(async_buffer, filename)

return botx_api_static_file.to_domain()

# - Stickers API -
async def create_sticker_pack(
self,
Expand Down
4 changes: 4 additions & 0 deletions pybotx/client/exceptions/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ class FileDeletedError(BaseClientError):

class FileMetadataNotFound(BaseClientError):
"""Can't find file metadata."""


class FileTypeNotAllowed(BaseClientError):
"""File type is not allowed."""
54 changes: 54 additions & 0 deletions pybotx/client/smartapps_api/upload_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import tempfile
from typing import Literal

from pybotx.async_buffer import AsyncBufferReadable
from pybotx.client.authorized_botx_method import AuthorizedBotXMethod
from pybotx.client.botx_method import response_exception_thrower
from pybotx.client.exceptions.files import FileTypeNotAllowed
from pybotx.constants import CHUNK_SIZE
from pybotx.models.api_base import VerifiedPayloadBaseModel


class BotXAPIUploadFileResult(VerifiedPayloadBaseModel):
link: str


class BotXAPIUploadFileResponsePayload(VerifiedPayloadBaseModel):
result: BotXAPIUploadFileResult
status: Literal["ok"]

def to_domain(self) -> str:
return self.result.link


class UploadFileMethod(AuthorizedBotXMethod):
status_handlers = {
**AuthorizedBotXMethod.status_handlers,
400: response_exception_thrower(FileTypeNotAllowed),
}

async def execute(
self,
async_buffer: AsyncBufferReadable,
filename: str,
) -> BotXAPIUploadFileResponsePayload:
path = "/api/v3/botx/smartapps/upload_file"

with tempfile.SpooledTemporaryFile(max_size=CHUNK_SIZE) as tmp_file:
chunk = await async_buffer.read(CHUNK_SIZE)
while chunk:
tmp_file.write(chunk)
chunk = await async_buffer.read(CHUNK_SIZE)

tmp_file.seek(0)

response = await self._botx_method_call(
"POST",
self._build_url(path),
files={"content": (filename, tmp_file)},
)

return self._verify_and_extract_api_model(
BotXAPIUploadFileResponsePayload,
response,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pybotx"
version = "0.54.0"
version = "0.55.0"
description = "A python library for interacting with eXpress BotX API"
authors = [
"Sidnev Nikolay <[email protected]>",
Expand Down
106 changes: 106 additions & 0 deletions tests/client/smartapps_api/test_smartapp_upload_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from http import HTTPStatus
from uuid import UUID

import httpx
import pytest
from aiofiles.tempfile import NamedTemporaryFile
from respx.router import MockRouter

from pybotx import Bot, BotAccountWithSecret, HandlerCollector, lifespan_wrapper
from pybotx.client.exceptions.files import FileTypeNotAllowed

pytestmark = [
pytest.mark.asyncio,
pytest.mark.mock_authorization,
pytest.mark.usefixtures("respx_mock"),
]


async def test__upload_static_file__wrong_file_type(
respx_mock: MockRouter,
host: str,
bot_id: UUID,
bot_account: BotAccountWithSecret,
async_buffer: NamedTemporaryFile,
) -> None:
# - Arrange -
await async_buffer.write(b"Hello, world!\n")
await async_buffer.seek(0)

endpoint = respx_mock.post(
f"https://{host}/api/v3/botx/smartapps/upload_file",
# TODO: check data too, when files pattern will be ready
# https://github.com/lundberg/respx/issues/115
headers={"Authorization": "Bearer token"},
).mock(
return_value=httpx.Response(
HTTPStatus.BAD_REQUEST,
json={
"status": "error",
"reason": "invalid_extension",
"errors": [],
"error_data": {
"allowed": ["jpg", "jpeg", "gif", "png", "svg", "tiff"],
"error_descrion": "txt extension isn't allowed for unencrypted smartapp files",
},
},
),
)

built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account])

# - Act -
async with lifespan_wrapper(built_bot) as bot:
with pytest.raises(FileTypeNotAllowed) as exc:
await bot.upload_static_file(
bot_id=bot_id,
async_buffer=async_buffer,
filename="test.txt",
)

# - Assert -
assert endpoint.called
assert "txt" in str(exc.value)


async def test__upload_static_file__succeed(
respx_mock: MockRouter,
host: str,
bot_id: UUID,
bot_account: BotAccountWithSecret,
async_buffer: NamedTemporaryFile,
) -> None:
# - Arrange -
await async_buffer.write(b"Hello, world!\n")
await async_buffer.seek(0)

endpoint = respx_mock.post(
f"https://{host}/api/v3/botx/smartapps/upload_file",
# TODO: check data too, when files pattern will be ready
# https://github.com/lundberg/respx/issues/115
headers={"Authorization": "Bearer token"},
).mock(
return_value=httpx.Response(
HTTPStatus.OK,
json={
"status": "ok",
"result": {
"link": "https://link.to/file",
},
},
),
)

built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account])

# - Act -
async with lifespan_wrapper(built_bot) as bot:
smartapp_file_link = await bot.upload_static_file(
bot_id=bot_id,
async_buffer=async_buffer,
filename="test.png",
)

# - Assert -
assert endpoint.called
assert smartapp_file_link == "https://link.to/file"

0 comments on commit e69e650

Please sign in to comment.