From 1c168e979270e05f9c36e2a560e84880eda75562 Mon Sep 17 00:00:00 2001 From: Ivan Kudinov Date: Thu, 14 Mar 2024 10:52:00 +0300 Subject: [PATCH 1/4] feat: Add notification v5 metthod --- pybotx/bot/bot.py | 54 ++++++++++++ .../smartapp_counter_notification.py | 84 +++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 pybotx/client/smartapps_api/smartapp_counter_notification.py diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 7705121d..ac7d6b3c 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -131,6 +131,10 @@ BotXAPISmartAppCustomNotificationRequestPayload, SmartAppCustomNotificationMethod, ) +from pybotx.client.smartapps_api.smartapp_counter_notification import ( + BotXAPISmartAppCounterNotificationRequestPayload, + SmartAppCounterNotificationMethod, +) from pybotx.client.smartapps_api.smartapp_event import ( BotXAPISmartAppEventRequestPayload, SmartAppEventMethod, @@ -1573,6 +1577,56 @@ async def send_smartapp_custom_notification( return botx_api_sync_id.to_domain() + async def send_smartapp_counter_notification( + self, + *, + bot_id: UUID, + group_chat_id: UUID, + title: str, + body: str, + unread_counter: int, + meta: Missing[Dict[str, Any]] = Undefined, + wait_callback: bool = True, + callback_timeout: Optional[float] = None, + ) -> UUID: + """Send SmartApp notification with counter. + + :param bot_id: Bot which should perform the request. + :param group_chat_id: Target chat id. + :param title: Notification title. + :param body: Notification body. + :param meta: Meta information. + :param unread_counter: Counter information. + :param wait_callback: Block method call until callback received. + :param callback_timeout: Callback timeout in seconds (or `None` for + endless waiting). + + :return: Notification sync_id. + """ + + method = SmartAppCounterNotificationMethod( + bot_id, + self._httpx_client, + self._bot_accounts_storage, + self._callbacks_manager, + ) + payload = BotXAPISmartAppCounterNotificationRequestPayload.from_domain( + group_chat_id=group_chat_id, + title=title, + body=body, + unread_counter=unread_counter, + meta=meta, + ) + + botx_api_sync_id = await method.execute( + payload, + wait_callback, + callback_timeout, + self._default_callback_timeout, + ) + + return botx_api_sync_id.to_domain() + # - Stickers API - async def create_sticker_pack( self, diff --git a/pybotx/client/smartapps_api/smartapp_counter_notification.py b/pybotx/client/smartapps_api/smartapp_counter_notification.py new file mode 100644 index 00000000..6dedd3f9 --- /dev/null +++ b/pybotx/client/smartapps_api/smartapp_counter_notification.py @@ -0,0 +1,84 @@ +from typing import Any, Dict, Literal, Optional +from uuid import UUID + +from pybotx.client.authorized_botx_method import AuthorizedBotXMethod +from pybotx.missing import Missing +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel + + +class BotXAPISmartAppCounterNotificationNestedPayload(UnverifiedPayloadBaseModel): + title: str + body: str + unread_counter: int + + +class BotXAPISmartAppCounterNotificationRequestPayload(UnverifiedPayloadBaseModel): + group_chat_id: UUID + payload: BotXAPISmartAppCounterNotificationNestedPayload + meta: Missing[Dict[str, Any]] + + @classmethod + def from_domain( + cls, + group_chat_id: UUID, + title: str, + body: str, + unread_counter: int, + meta: Missing[Dict[str, Any]], + ) -> "BotXAPISmartAppCounterNotificationRequestPayload": + return cls( + group_chat_id=group_chat_id, + payload=BotXAPISmartAppCounterNotificationNestedPayload( + title=title, + body=body, + unread_counter=unread_counter, + ), + meta=meta, + ) + + +class BotXAPISyncIdResult(VerifiedPayloadBaseModel): + sync_id: UUID + + +class BotXAPISmartAppCounterNotificationResponsePayload(VerifiedPayloadBaseModel): + status: Literal["ok"] + result: BotXAPISyncIdResult + + def to_domain(self) -> UUID: + return self.result.sync_id + + +class SmartAppCounterNotificationMethod(AuthorizedBotXMethod): + error_callback_handlers = { + **AuthorizedBotXMethod.error_callback_handlers, + } + + async def execute( + self, + payload: BotXAPISmartAppCounterNotificationRequestPayload, + wait_callback: bool, + callback_timeout: Optional[float], + default_callback_timeout: float, + ) -> BotXAPISmartAppCounterNotificationResponsePayload: + path = "/api/v5/botx/smartapps/notification" + + response = await self._botx_method_call( + "POST", + self._build_url(path), + json=payload.jsonable_dict(), + ) + + api_model = self._verify_and_extract_api_model( + BotXAPISmartAppCounterNotificationResponsePayload, + response, + ) + + await self._process_callback( + api_model.result.sync_id, + wait_callback, + callback_timeout, + default_callback_timeout, + ) + + return api_model diff --git a/pyproject.toml b/pyproject.toml index 8f9f7f06..ba889071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.63.0" +version = "0.63.1" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", From 7c56640ce86a1d05fb9ed943a5180db090cf3d4e Mon Sep 17 00:00:00 2001 From: "denis.postnykh" Date: Thu, 14 Mar 2024 11:36:42 +0300 Subject: [PATCH 2/4] chore: Up project version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba889071..9bd73bbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.63.1" +version = "0.64.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", From 43996ec79eb6f0406475a503943d7d978611a243 Mon Sep 17 00:00:00 2001 From: "denis.postnykh" Date: Thu, 14 Mar 2024 11:38:53 +0300 Subject: [PATCH 3/4] fix: Lint --- pybotx/bot/bot.py | 8 ++++---- setup.cfg | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index ac7d6b3c..8f004370 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -127,14 +127,14 @@ BotXAPIRefreshAccessTokenRequestPayload, RefreshAccessTokenMethod, ) -from pybotx.client.smartapps_api.smartapp_custom_notification import ( - BotXAPISmartAppCustomNotificationRequestPayload, - SmartAppCustomNotificationMethod, -) from pybotx.client.smartapps_api.smartapp_counter_notification import ( BotXAPISmartAppCounterNotificationRequestPayload, SmartAppCounterNotificationMethod, ) +from pybotx.client.smartapps_api.smartapp_custom_notification import ( + BotXAPISmartAppCustomNotificationRequestPayload, + SmartAppCustomNotificationMethod, +) from pybotx.client.smartapps_api.smartapp_event import ( BotXAPISmartAppEventRequestPayload, SmartAppEventMethod, diff --git a/setup.cfg b/setup.cfg index 25f477c2..c959b4d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ per-file-ignores = pybotx/bot/handler_collector.py:WPS437, pybotx/client/notifications_api/internal_bot_notification.py:WPS202, pybotx/client/smartapps_api/smartapp_custom_notification.py:WPS118, + pybotx/client/smartapps_api/smartapp_counter_notification.py:WPS118, # Complex model converting pybotx/models/message/incoming_message.py:WPS232, # WPS reacts at using `}` in f-strings From fb825eb773264d15ea27790ad199b172c4d33271 Mon Sep 17 00:00:00 2001 From: "denis.postnykh" Date: Thu, 14 Mar 2024 11:39:47 +0300 Subject: [PATCH 4/4] tests: Cover smartapp counter notification method --- .../test_smartapp_counter_notification.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/client/smartapps_api/test_smartapp_counter_notification.py diff --git a/tests/client/smartapps_api/test_smartapp_counter_notification.py b/tests/client/smartapps_api/test_smartapp_counter_notification.py new file mode 100644 index 00000000..94334a6a --- /dev/null +++ b/tests/client/smartapps_api/test_smartapp_counter_notification.py @@ -0,0 +1,80 @@ +import asyncio +from http import HTTPStatus +from uuid import UUID + +import httpx +import pytest +from respx.router import MockRouter + +from pybotx import Bot, BotAccountWithSecret, HandlerCollector, lifespan_wrapper + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +@pytest.mark.parametrize( + argnames="unread_counter", + argvalues=[-1, 0, 1], + ids=["counter below zero", "zero counter", "counter above zero"], +) +async def test__send_smartapp_counter_notification__succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, + unread_counter: int, +) -> None: + # - Arrange - + endpoint = respx_mock.post( + f"https://{host}/api/v5/botx/smartapps/notification", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "group_chat_id": "705df263-6bfd-536a-9d51-13524afaab5c", + "payload": { + "title": "test", + "body": "test", + "unread_counter": unread_counter, + }, + "meta": {"message": "ping"}, + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.ACCEPTED, + json={ + "status": "ok", + "result": {"sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3"}, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + task = asyncio.create_task( + bot.send_smartapp_counter_notification( + bot_id=bot_id, + group_chat_id=UUID("705df263-6bfd-536a-9d51-13524afaab5c"), + title="test", + body="test", + unread_counter=unread_counter, + meta={"message": "ping"}, + ), + ) + await asyncio.sleep(0) # Return control to event loop + + await bot.set_raw_botx_method_result( + { + "status": "ok", + "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", + "result": {}, + }, + verify_request=False, + ) + + # - Assert - + assert await task == UUID("21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3") + assert endpoint.called