diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index fd947add..3e5d975e 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -1,6 +1,16 @@ from asyncio import Task from types import SimpleNamespace -from typing import Any, AsyncIterable, Dict, Iterator, List, Optional, Sequence, Union +from typing import ( + Any, + AsyncIterable, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, +) from uuid import UUID import httpx @@ -96,6 +106,10 @@ BotXAPISmartAppNotificationRequestPayload, SmartAppNotificationMethod, ) +from pybotx.client.smartapps_api.smartapps_list import ( + BotXAPISmartAppsListRequestPayload, + SmartAppsListMethod, +) from pybotx.client.stickers_api.add_sticker import ( AddStickerMethod, BotXAPIAddStickerRequestPayload, @@ -168,6 +182,7 @@ from pybotx.models.message.outgoing_message import OutgoingMessage from pybotx.models.message.reply_message import ReplyMessage from pybotx.models.method_callbacks import BotXMethodCallback +from pybotx.models.smartapps import SmartApp from pybotx.models.status import ( BotAPIStatusRecipient, BotMenu, @@ -1262,6 +1277,31 @@ async def send_smartapp_notification( await method.execute(payload) + async def get_smartapps_list( + self, + *, + bot_id: UUID, + version: Missing[int] = Undefined, + ) -> Tuple[List[SmartApp], int]: + """Get list of SmartApps on the current CTS. + + :param bot_id: Bot which should perform the request. + :param version: Specific list version. + + :return: List of SmartApps, list version. + """ + + method = SmartAppsListMethod( + bot_id, + self._httpx_client, + self._bot_accounts_storage, + ) + payload = BotXAPISmartAppsListRequestPayload.from_domain(version=version) + + botx_api_smartapps_list = await method.execute(payload) + + return botx_api_smartapps_list.to_domain() + # - Stickers API - async def create_sticker_pack( self, diff --git a/pybotx/client/smartapps_api/smartapps_list.py b/pybotx/client/smartapps_api/smartapps_list.py new file mode 100644 index 00000000..0097f4be --- /dev/null +++ b/pybotx/client/smartapps_api/smartapps_list.py @@ -0,0 +1,77 @@ +from typing import List, Literal, Optional, Tuple +from uuid import UUID + +from pybotx.client.authorized_botx_method import AuthorizedBotXMethod +from pybotx.missing import Missing, Undefined +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pybotx.models.smartapps import SmartApp + + +class BotXAPISmartAppsListRequestPayload(UnverifiedPayloadBaseModel): + version: Missing[int] = Undefined + + @classmethod + def from_domain( + cls, + version: Missing[int] = Undefined, + ) -> "BotXAPISmartAppsListRequestPayload": + return cls(version=version) + + +class BotXAPISmartAppEntity(VerifiedPayloadBaseModel): + app_id: str + enabled: bool + id: UUID + name: str + avatar: Optional[str] = None + avatar_preview: Optional[str] = None + + +class BotXAPISmartAppsListResult(VerifiedPayloadBaseModel): + phonebook_version: int + smartapps: List[BotXAPISmartAppEntity] + + +class BotXAPISmartAppsListResponsePayload(VerifiedPayloadBaseModel): + result: BotXAPISmartAppsListResult + status: Literal["ok"] + + def to_domain(self) -> Tuple[List[SmartApp], int]: + smartapps_list = [ + SmartApp( + app_id=smartapp.app_id, + enabled=smartapp.enabled, + id=smartapp.id, + name=smartapp.name, + avatar=smartapp.avatar, + avatar_preview=smartapp.avatar_preview, + ) + for smartapp in self.result.smartapps + ] + return ( + smartapps_list, + self.result.phonebook_version, + ) + + +class SmartAppsListMethod(AuthorizedBotXMethod): + status_handlers = { + **AuthorizedBotXMethod.status_handlers, + } + + async def execute( + self, + payload: BotXAPISmartAppsListRequestPayload, + ) -> BotXAPISmartAppsListResponsePayload: + path = "/api/v3/botx/smartapps/list" + + response = await self._botx_method_call( + "GET", + self._build_url(path), + params=payload.jsonable_dict(), + ) + + return self._verify_and_extract_api_model( + BotXAPISmartAppsListResponsePayload, + response, + ) diff --git a/pybotx/models/smartapps.py b/pybotx/models/smartapps.py new file mode 100644 index 00000000..75b10cc0 --- /dev/null +++ b/pybotx/models/smartapps.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import Optional +from uuid import UUID + + +@dataclass +class SmartApp: + """SmartApp from list of SmartApps. + + Attributes: + app_id: Readable SmartApp id. + enabled: Is the SmartApp enabled or not. + id: SmartApp uuid. + name: SmartApp name. + avatar: SmartApp avatar url. + avatar_preview: SmartApp avatar preview url. + """ + + app_id: str + enabled: bool + id: UUID + name: str + avatar: Optional[str] = None + avatar_preview: Optional[str] = None diff --git a/pyproject.toml b/pyproject.toml index 582edf8b..49fbc4cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.46.0" +version = "0.47.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", diff --git a/tests/client/smartapps_api/test_smartapps_list.py b/tests/client/smartapps_api/test_smartapps_list.py new file mode 100644 index 00000000..8c955a65 --- /dev/null +++ b/tests/client/smartapps_api/test_smartapps_list.py @@ -0,0 +1,71 @@ +from http import HTTPStatus +from uuid import UUID + +import httpx +import pytest +from respx.router import MockRouter + +from pybotx import Bot, HandlerCollector, lifespan_wrapper +from pybotx.models.bot_account import BotAccountWithSecret +from pybotx.models.smartapps import SmartApp + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +async def test__smartapps_list__succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + endpoint = respx_mock.get( + f"https://{host}/api/v3/botx/smartapps/list", + headers={"Authorization": "Bearer token"}, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json={ + "result": { + "phonebook_version": 1, + "smartapps": [ + { + "app_id": "amazing_smartapp", + "avatar": "https://cts.example.com/uploads/profile_avatar/foo", + "avatar_preview": "https://cts.example.com/uploads/profile_avatar/bar", + "enabled": True, + "id": "dc4acaf2-310f-4b0f-aec7-253b9def42ac", + "name": "Amazing SmartApp", + }, + ], + }, + "status": "ok", + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + smartapps_list, version = await bot.get_smartapps_list( + bot_id=bot_id, + ) + + # - Assert - + assert endpoint.called + assert smartapps_list == [ + SmartApp( + app_id="amazing_smartapp", + avatar="https://cts.example.com/uploads/profile_avatar/foo", + avatar_preview="https://cts.example.com/uploads/profile_avatar/bar", + enabled=True, + id=UUID("dc4acaf2-310f-4b0f-aec7-253b9def42ac"), + name="Amazing SmartApp", + ), + ] + assert version == 1