From 05c78770b56e0e4039ff832cd527043ed3ab3369 Mon Sep 17 00:00:00 2001 From: Alexander Samoylenko Date: Thu, 11 Nov 2021 14:52:35 +0300 Subject: [PATCH] feat: Add SmartApps main functionality --- botx/__init__.py | 3 + botx/bots/mixins/collecting/system_events.py | 24 +++ botx/bots/mixins/requests/mixin.py | 2 + botx/bots/mixins/requests/smartapps.py | 62 ++++++++ botx/clients/methods/v3/smartapps/__init__.py | 1 + .../methods/v3/smartapps/smartapp_event.py | 38 +++++ .../v3/smartapps/smartapp_notification.py | 25 ++++ .../collectors/mixins/system_events.py | 26 ++++ botx/models/enums.py | 3 + botx/models/events.py | 22 ++- botx/models/messages/incoming_message.py | 8 +- botx/models/messages/message.py | 7 +- botx/models/smartapps.py | 116 ++++++++++++++ botx/testing/botx_mock/asgi/application.py | 4 + botx/testing/botx_mock/asgi/routes/chats.py | 4 +- .../botx_mock/asgi/routes/smartapps.py | 41 +++++ botx/testing/botx_mock/wsgi/application.py | 4 + botx/testing/botx_mock/wsgi/routes/chats.py | 4 +- .../botx_mock/wsgi/routes/smartapps.py | 45 ++++++ docs/changelog.md | 7 + setup.cfg | 21 +-- tests/fixtures/smartapps.py | 34 +++++ .../test_requests/test_smartapps.py | 64 ++++++++ .../test_base/test_empty_error_handlers.py | 2 + .../test_smartapps/test_smartapp_event.py | 42 ++++++ .../test_smartapp_notification.py | 36 +++++ .../test_decorators/test_system_event.py | 2 + tests/test_models/test_smartapps.py | 141 ++++++++++++++++++ 28 files changed, 770 insertions(+), 18 deletions(-) create mode 100644 botx/bots/mixins/requests/smartapps.py create mode 100644 botx/clients/methods/v3/smartapps/__init__.py create mode 100644 botx/clients/methods/v3/smartapps/smartapp_event.py create mode 100644 botx/clients/methods/v3/smartapps/smartapp_notification.py create mode 100644 botx/models/smartapps.py create mode 100644 botx/testing/botx_mock/asgi/routes/smartapps.py create mode 100644 botx/testing/botx_mock/wsgi/routes/smartapps.py create mode 100644 tests/fixtures/smartapps.py create mode 100644 tests/test_bots/test_mixins/test_requests/test_smartapps.py create mode 100644 tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_event.py create mode 100644 tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_notification.py create mode 100644 tests/test_models/test_smartapps.py diff --git a/botx/__init__.py b/botx/__init__.py index 762dde66..0bdf1196 100644 --- a/botx/__init__.py +++ b/botx/__init__.py @@ -51,6 +51,7 @@ from botx.models.messages.sending.message import SendingMessage from botx.models.messages.sending.options import MessageOptions, NotificationOptions from botx.models.messages.sending.payload import MessagePayload, UpdatePayload +from botx.models.smartapps import SendingSmartAppEvent, SendingSmartAppNotification from botx.models.status import StatusRecipient from botx.models.stickers import ( Pagination, @@ -146,6 +147,8 @@ "StickerPackList", "StickerFromPack", "StickerPackPreview", + "SendingSmartAppEvent", + "SendingSmartAppNotification", # testing "TestClient", "MessageBuilder", diff --git a/botx/bots/mixins/collecting/system_events.py b/botx/bots/mixins/collecting/system_events.py index 510ae496..59c8b3af 100644 --- a/botx/bots/mixins/collecting/system_events.py +++ b/botx/bots/mixins/collecting/system_events.py @@ -212,3 +212,27 @@ def cts_logout( dependencies=dependencies, dependency_overrides_provider=dependency_overrides_provider, ) + + def smartapp_event( + self, + handler: Optional[Callable] = None, + *, + dependencies: Optional[Sequence[Depends]] = None, + dependency_overrides_provider: Any = None, + ) -> Callable: + """Register handler for `smartapp_event` event. + + Arguments: + handler: callable that will be used for executing handler. + dependencies: sequence of dependencies that should be executed before + handler. + dependency_overrides_provider: mock of callable for handler. + + Returns: + Passed in `handler` callable. + """ + return self.collector.smartapp_event( + handler=handler, + dependencies=dependencies, + dependency_overrides_provider=dependency_overrides_provider, + ) diff --git a/botx/bots/mixins/requests/mixin.py b/botx/bots/mixins/requests/mixin.py index 2410f7f1..95d2015e 100644 --- a/botx/bots/mixins/requests/mixin.py +++ b/botx/bots/mixins/requests/mixin.py @@ -12,6 +12,7 @@ files, internal_bot_notification, notification, + smartapps, stickers, users, ) @@ -48,6 +49,7 @@ class BotXRequestsMixin( # noqa: WPS215 users.UsersRequestsMixin, internal_bot_notification.InternalBotNotificationRequestsMixin, files.FilesRequestsMixin, + smartapps.SmartAppMixin, stickers.StickersMixin, ): """Mixin that defines methods for communicating with BotX API.""" diff --git a/botx/bots/mixins/requests/smartapps.py b/botx/bots/mixins/requests/smartapps.py new file mode 100644 index 00000000..69e2ac27 --- /dev/null +++ b/botx/bots/mixins/requests/smartapps.py @@ -0,0 +1,62 @@ +"""Mixin for shortcut for smartapp.""" + +from uuid import UUID + +from botx.bots.mixins.requests.call_protocol import BotXMethodCallProtocol +from botx.clients.methods.v3.smartapps.smartapp_event import SmartAppEvent +from botx.clients.methods.v3.smartapps.smartapp_notification import SmartAppNotification +from botx.models.messages.sending.credentials import SendingCredentials +from botx.models.smartapps import SendingSmartAppEvent, SendingSmartAppNotification + + +class SmartAppMixin: + """Mixin for shortcut for smartapp methods.""" + + async def send_smartapp_event( + self: BotXMethodCallProtocol, + credentials: SendingCredentials, + smartapp_event: SendingSmartAppEvent, + ) -> UUID: + """Send smartapp event into chat. + + Arguments: + credentials: credentials for making request. + smartapp_event: SmartpApp event. + + Returns: + ID sent message. + """ + return await self.call_method( + SmartAppEvent( + ref=smartapp_event.ref, + smartapp_id=smartapp_event.smartapp_id, + data=smartapp_event.data, + opts=smartapp_event.opts, + smartapp_api_version=smartapp_event.smartapp_api_version, + group_chat_id=smartapp_event.group_chat_id, + files=smartapp_event.files, + async_files=smartapp_event.async_files, + ), + credentials=credentials, + ) + + async def send_smartapp_notification( + self: BotXMethodCallProtocol, + credentials: SendingCredentials, + smartapp_notification: SendingSmartAppNotification, + ) -> None: + """Send smartapp notification into chat. + + Arguments: + credentials: credentials for making request. + smartapp_notification: Smartapp notification. + """ + await self.call_method( + SmartAppNotification( + group_chat_id=smartapp_notification.group_chat_id, + smartapp_counter=smartapp_notification.smartapp_counter, + opts=smartapp_notification.opts, + smartapp_api_version=smartapp_notification.smartapp_api_version, + ), + credentials=credentials, + ) diff --git a/botx/clients/methods/v3/smartapps/__init__.py b/botx/clients/methods/v3/smartapps/__init__.py new file mode 100644 index 00000000..1e1bdd78 --- /dev/null +++ b/botx/clients/methods/v3/smartapps/__init__.py @@ -0,0 +1 @@ +"""Definition for smartapp methods.""" diff --git a/botx/clients/methods/v3/smartapps/smartapp_event.py b/botx/clients/methods/v3/smartapps/smartapp_event.py new file mode 100644 index 00000000..c4a0e1c0 --- /dev/null +++ b/botx/clients/methods/v3/smartapps/smartapp_event.py @@ -0,0 +1,38 @@ +"""Method for sending smartapp event.""" +from typing import Any, Dict, List, Optional +from uuid import UUID + +from botx.clients.methods.base import AuthorizedBotXMethod +from botx.models.files import File, MetaFile + + +class SmartAppEvent(AuthorizedBotXMethod[str]): + """Method for sending smartapp events.""" + + __url__ = "/api/v3/botx/smartapps/event" + __method__ = "POST" + __returning__ = str + + #: unique request id + ref: Optional[UUID] = None + + #: smartapp id + smartapp_id: UUID + + #: event data + data: Dict[str, Any] # noqa: WPS110 + + #: event options + opts: Dict[str, Any] = {} + + #: version of protocol smartapp <-> bot + smartapp_api_version: int + + #: smartapp chat + group_chat_id: Optional[UUID] + + #: files + files: List[File] = [] + + #: file's meta to upload + async_files: List[MetaFile] = [] diff --git a/botx/clients/methods/v3/smartapps/smartapp_notification.py b/botx/clients/methods/v3/smartapps/smartapp_notification.py new file mode 100644 index 00000000..574beddb --- /dev/null +++ b/botx/clients/methods/v3/smartapps/smartapp_notification.py @@ -0,0 +1,25 @@ +"""Method for sending smartapp event.""" +from typing import Any, Dict, Optional +from uuid import UUID + +from botx.clients.methods.base import AuthorizedBotXMethod + + +class SmartAppNotification(AuthorizedBotXMethod[str]): + """Method for sending smartapp notifications.""" + + __url__ = "/api/v3/botx/smartapps/notification" + __method__ = "POST" + __returning__ = str + + #: smartapp chat + group_chat_id: Optional[UUID] + + #: unread notifications count + smartapp_counter: int + + #: event options + opts: Dict[str, Any] = {} + + #: version of protocol smartapp <-> bot + smartapp_api_version: int diff --git a/botx/collecting/collectors/mixins/system_events.py b/botx/collecting/collectors/mixins/system_events.py index 13a6f7ec..d8c79e33 100644 --- a/botx/collecting/collectors/mixins/system_events.py +++ b/botx/collecting/collectors/mixins/system_events.py @@ -261,3 +261,29 @@ def cts_logout( dependencies=dependencies, dependency_overrides_provider=dependency_overrides_provider, ) + + def smartapp_event( + self, + handler: Optional[Callable] = None, + *, + dependencies: Optional[Sequence[Depends]] = None, + dependency_overrides_provider: Any = None, + ) -> Callable: + """Register handler for `smartapp_event` event. + + Arguments: + handler: callable that will be used for executing handler. + dependencies: sequence of dependencies that should be executed before + handler. + dependency_overrides_provider: mock of callable for handler. + + Returns: + Passed in `handler` callable. + """ + return self.system_event( + handler=handler, + event=SystemEvents.smartapp_event, + name=SystemEvents.smartapp_event.value, + dependencies=dependencies, + dependency_overrides_provider=dependency_overrides_provider, + ) diff --git a/botx/models/enums.py b/botx/models/enums.py index 31d47abc..f395b451 100644 --- a/botx/models/enums.py +++ b/botx/models/enums.py @@ -32,6 +32,9 @@ class SystemEvents(Enum): #: `system:cts_logout` event. cts_logout = "system:cts_logout" + #: `system:smartapp_event` event. + smartapp_event = "system:smartapp_event" + #: `file_transfer` message. file_transfer = "file_transfer" diff --git a/botx/models/events.py b/botx/models/events.py index 1847de51..4234508a 100644 --- a/botx/models/events.py +++ b/botx/models/events.py @@ -1,7 +1,7 @@ """Definition of different schemas for system events.""" from types import MappingProxyType -from typing import Any, Dict, List, Mapping, Type +from typing import Any, Dict, List, Mapping, Optional, Type from uuid import UUID from botx.clients.types.message_payload import InternalBotNotificationPayload @@ -80,6 +80,25 @@ class CTSLogoutEvent(BotXBaseModel): cts_id: UUID +class SmartAppEvent(BotXBaseModel): + """Shape for `system:smartapp_event` event data.""" + + #: unique request id + ref: Optional[UUID] = None + + #: smartapp id + smartapp_id: UUID + + #: event data + data: Dict[str, Any] # noqa: WPS110 + + #: event options + opts: Dict[str, Any] = {} + + #: version of protocol smartapp <-> bot + smartapp_api_version: int + + # dict for validating shape for different events EVENTS_SHAPE_MAP: Mapping[SystemEvents, Type[BotXBaseModel]] = MappingProxyType( { @@ -90,5 +109,6 @@ class CTSLogoutEvent(BotXBaseModel): SystemEvents.internal_bot_notification: InternalBotNotificationEvent, SystemEvents.cts_login: CTSLoginEvent, SystemEvents.cts_logout: CTSLogoutEvent, + SystemEvents.smartapp_event: SmartAppEvent, }, ) diff --git a/botx/models/messages/incoming_message.py b/botx/models/messages/incoming_message.py index 0c80d087..3b8dbefb 100644 --- a/botx/models/messages/incoming_message.py +++ b/botx/models/messages/incoming_message.py @@ -1,6 +1,6 @@ """Definition of messages received by bot or sent by it.""" -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from uuid import UUID from pydantic import BaseConfig, BaseModel, Field, validator @@ -9,7 +9,7 @@ from botx.models.attachments import AttachList from botx.models.entities import EntityList from botx.models.enums import ChatTypes, ClientPlatformEnum, CommandTypes -from botx.models.files import File +from botx.models.files import File, MetaFile CommandDataType = Union[ events.ChatCreatedEvent, @@ -19,6 +19,7 @@ events.InternalBotNotificationEvent, events.CTSLoginEvent, events.CTSLogoutEvent, + events.SmartAppEvent, Dict[str, Any], ] @@ -161,6 +162,9 @@ class IncomingMessage(BaseModel): #: file attached to message. file: Optional[File] = None + #: meta info for downloading files + async_files: List[MetaFile] = Field(default_factory=list) + #: information about user from which message was received. user: Sender = Field(..., alias="from") diff --git a/botx/models/messages/message.py b/botx/models/messages/message.py index 3fa7b257..88e98aec 100644 --- a/botx/models/messages/message.py +++ b/botx/models/messages/message.py @@ -2,7 +2,7 @@ from __future__ import annotations from functools import partial -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, List, Optional, Type from uuid import UUID from botx.bots import bots @@ -11,7 +11,7 @@ from botx.models.datastructures import State from botx.models.entities import EntityList from botx.models.enums import CommandTypes -from botx.models.files import File +from botx.models.files import File, MetaFile from botx.models.messages.incoming_message import Command, IncomingMessage, Sender from botx.models.messages.sending.credentials import SendingCredentials @@ -73,6 +73,9 @@ class Message: #: file from message. file: Optional[File] = _message_proxy_property() + #: Meta for download files. + async_files: List[MetaFile] = _message_proxy_property() + #: attachment from message v4+ attachments: AttachList = _message_proxy_property() diff --git a/botx/models/smartapps.py b/botx/models/smartapps.py new file mode 100644 index 00000000..45169b4e --- /dev/null +++ b/botx/models/smartapps.py @@ -0,0 +1,116 @@ +"""Definition of smartapp object.""" + +from typing import Any, BinaryIO, Dict, List, Optional, TextIO, Union +from uuid import UUID + +from botx.models.base import BotXBaseModel +from botx.models.files import File, MetaFile +from botx.models.messages.message import Message + + +class SendingSmartAppEvent(BotXBaseModel): + """SmartApp event with data.""" + + #: unique request id + ref: Optional[UUID] = None + + #: smartapp id + smartapp_id: UUID + + #: event data + data: Dict[str, Any] # noqa: WPS110 + + #: event options + opts: Dict[str, Any] = {} + + #: version of protocol smartapp <-> bot + smartapp_api_version: int + + #: smartapp chat + group_chat_id: Optional[UUID] + + #: files + files: List[File] = [] + + #: file's meta to upload + async_files: List[MetaFile] = [] + + @classmethod + def from_message( + cls, + data: Dict[str, Any], # noqa: WPS110 + message: Message, + ) -> "SendingSmartAppEvent": + """Build smartapp event from message. + + Arguments: + data: smartapp's data. + message: incoming message. + + Returns: + Built smartapp event. + """ + return cls( + ref=message.data["ref"], + smartapp_id=message.data["smartapp_id"], + data=data, + opts=message.data["opts"], + smartapp_api_version=message.data["smartapp_api_version"], + group_chat_id=message.group_chat_id, + ) + + def add_file( + self, + file: Union[TextIO, BinaryIO, File], + filename: Optional[str] = None, + ) -> None: + """Attach file to smartapp. + + Arguments: + file: file that should be attached to the message. + filename: name for file that will be used if if can not be retrieved from + file. + """ + if isinstance(file, File): + file.file_name = filename or file.file_name + self.files.append(file) + else: + self.files.append(File.from_file(file, filename=filename)) + + +class SendingSmartAppNotification(BotXBaseModel): + """SmartApp notification with counter.""" + + #: smartapp chat + group_chat_id: Optional[UUID] + + #: unread notifications count + smartapp_counter: int + + #: event options + opts: Dict[str, Any] = {} + + #: version of protocol smartapp <-> bot + smartapp_api_version: int + + @classmethod + def from_message( + cls, + smartapp_counter: int, + message: Message, + ) -> "SendingSmartAppNotification": + """Build smartapp notification from message. + + Arguments: + smartapp_counter: smartapp notification counter. + message: incoming message. + + Returns: + Built smartapp notification. + """ + return cls( + smartapp_counter=smartapp_counter, + opts=message.data["opts"], + smartapp_api_version=message.data["smartapp_api_version"], + group_chat_id=message.group_chat_id, + ) diff --git a/botx/testing/botx_mock/asgi/application.py b/botx/testing/botx_mock/asgi/application.py index 5f4faf1d..836d730d 100644 --- a/botx/testing/botx_mock/asgi/application.py +++ b/botx/testing/botx_mock/asgi/application.py @@ -16,6 +16,7 @@ files, notification, notifications, + smartapps, stickers, users, ) @@ -63,6 +64,9 @@ stickers.post_delete_sticker_from_sticker_pack, stickers.post_create_sticker_pack, stickers.post_edit_sticker_pack, + # smartapps + smartapps.post_smartapp_event, + smartapps.post_smartapp_notification, ) diff --git a/botx/testing/botx_mock/asgi/routes/chats.py b/botx/testing/botx_mock/asgi/routes/chats.py index 814af0eb..05fd03a5 100644 --- a/botx/testing/botx_mock/asgi/routes/chats.py +++ b/botx/testing/botx_mock/asgi/routes/chats.py @@ -5,8 +5,8 @@ from starlette import requests, responses from botx.clients.methods.base import APIResponse -from botx.clients.methods.v3.chats import ( # noqa: WPS235 - add_admin_role, +from botx.clients.methods.v3.chats import add_admin_role # noqa: WPS235 +from botx.clients.methods.v3.chats import ( add_user, chat_list, create, diff --git a/botx/testing/botx_mock/asgi/routes/smartapps.py b/botx/testing/botx_mock/asgi/routes/smartapps.py new file mode 100644 index 00000000..94201969 --- /dev/null +++ b/botx/testing/botx_mock/asgi/routes/smartapps.py @@ -0,0 +1,41 @@ +"""Endpoints for smartapps.""" + +from starlette.requests import Request +from starlette.responses import Response + +from botx.clients.methods.base import APIResponse +from botx.clients.methods.v3.smartapps.smartapp_event import SmartAppEvent +from botx.clients.methods.v3.smartapps.smartapp_notification import SmartAppNotification +from botx.testing.botx_mock.asgi.messages import add_message_to_collection +from botx.testing.botx_mock.asgi.responses import PydanticResponse +from botx.testing.botx_mock.binders import bind_implementation_to_method + + +@bind_implementation_to_method(SmartAppEvent) +async def post_smartapp_event(request: Request) -> Response: + """Handle pushed smartapp event request. + + Arguments: + request: HTTP request from Starlette. + + Returns: + Response with sync_id of pushed message. + """ + payload = SmartAppEvent.parse_obj(await request.json()) + add_message_to_collection(request, payload) + return PydanticResponse(APIResponse[str](result="smartapp_event_pushed")) + + +@bind_implementation_to_method(SmartAppNotification) +async def post_smartapp_notification(request: Request) -> Response: + """Handle pushed smartapp notification request. + + Arguments: + request: HTTP request from Starlette. + + Returns: + Response with sync_id of pushed message. + """ + payload = SmartAppNotification.parse_obj(await request.json()) + add_message_to_collection(request, payload) + return PydanticResponse(APIResponse[str](result="smartapp_notification_pushed")) diff --git a/botx/testing/botx_mock/wsgi/application.py b/botx/testing/botx_mock/wsgi/application.py index b69bc942..91fe6b36 100644 --- a/botx/testing/botx_mock/wsgi/application.py +++ b/botx/testing/botx_mock/wsgi/application.py @@ -14,6 +14,7 @@ files, notification, notifications, + smartapps, stickers, users, ) @@ -60,6 +61,9 @@ stickers.delete_sticker_from_sticker_pack, stickers.post_create_sticker_pack, stickers.post_edit_sticker_pack, + # smartapps + smartapps.post_smartapp_event, + smartapps.post_smartapp_notification, ) diff --git a/botx/testing/botx_mock/wsgi/routes/chats.py b/botx/testing/botx_mock/wsgi/routes/chats.py index 5f882f33..f0935f28 100644 --- a/botx/testing/botx_mock/wsgi/routes/chats.py +++ b/botx/testing/botx_mock/wsgi/routes/chats.py @@ -5,8 +5,8 @@ from molten import Request, RequestData, Response, Settings from botx.clients.methods.base import APIResponse -from botx.clients.methods.v3.chats import ( # noqa: WPS235 - add_admin_role, +from botx.clients.methods.v3.chats import add_admin_role # noqa: WPS235 +from botx.clients.methods.v3.chats import ( add_user, chat_list, create, diff --git a/botx/testing/botx_mock/wsgi/routes/smartapps.py b/botx/testing/botx_mock/wsgi/routes/smartapps.py new file mode 100644 index 00000000..81827dc7 --- /dev/null +++ b/botx/testing/botx_mock/wsgi/routes/smartapps.py @@ -0,0 +1,45 @@ +"""Endpoints for smartapps.""" + +from molten import RequestData, Response, Settings + +from botx.clients.methods.base import APIResponse +from botx.clients.methods.v3.smartapps.smartapp_event import SmartAppEvent +from botx.clients.methods.v3.smartapps.smartapp_notification import SmartAppNotification +from botx.testing.botx_mock.binders import bind_implementation_to_method +from botx.testing.botx_mock.wsgi.messages import add_message_to_collection +from botx.testing.botx_mock.wsgi.responses import PydanticResponse + + +@bind_implementation_to_method(SmartAppEvent) +def post_smartapp_event(request_data: RequestData, settings: Settings) -> Response: + """Handle pushed smartapp event request. + + Arguments: + request_data: parsed json data from request. + settings: application settings with storage. + + Returns: + Response with sync_id of pushed message. + """ + payload = SmartAppEvent.parse_obj(request_data) + add_message_to_collection(settings, payload) + return PydanticResponse(APIResponse[str](result="smartapp_event_pushed")) + + +@bind_implementation_to_method(SmartAppNotification) +def post_smartapp_notification( + request_data: RequestData, + settings: Settings, +) -> Response: + """Handle pushed smartapp notification request. + + Arguments: + request_data: parsed json data from request. + settings: application settings with storage. + + Returns: + Response with sync_id of pushed message. + """ + payload = SmartAppNotification.parse_obj(request_data) + add_message_to_collection(settings, payload) + return PydanticResponse(APIResponse[str](result="smartapp_notification_pushed")) diff --git a/docs/changelog.md b/docs/changelog.md index 29c99979..d207ca3f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,10 @@ +## 0.28.0 (Nov 11, 2021) + +### Added + +* SmartApps main functionality. + + ## 0.27.0 (Nov 8, 2021) ### Added diff --git a/setup.cfg b/setup.cfg index fa1533bf..7639e7a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,13 +21,6 @@ strictness = long [tool:pytest] -# Timeout for tests, so they can not take longer -# than this amount of seconds. -# You should adjust this value to be as low as possible. -# Configuration: -# https://pypi.org/project/pytest-timeout/ -timeout = 5 - # Directories that are not visited by pytest collector: norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ @@ -35,7 +28,7 @@ norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ # so you can see whether it gives you any performance gain, or just gives # you an overhead. See `docs/template/development-process.rst`. addopts = - --strict + --strict-markers --tb=short --cov=botx --cov=tests @@ -79,6 +72,7 @@ disallow_untyped_defs = True strict_optional = True warn_redundant_casts = True warn_unused_ignores = True +show_error_codes = True [pydantic-mypy] @@ -173,8 +167,17 @@ per-file-ignores = # many chat methods botx/bots/mixins/requests/chats.py: WPS201, WPS214 - # too many module members botx/exceptions.py: WPS202 + botx/models/events.py: WPS202 + + # too many module members + # too many imported names from a module + # found overused expression + botx/bots/mixins/requests/mixin.py: WPS235 + botx/testing/botx_mock/asgi/application.py: WPS235 + botx/testing/botx_mock/asgi/routes/chats.py: WPS202,WPS204,WPS235 + botx/testing/botx_mock/wsgi/application.py: WPS235 + botx/testing/botx_mock/wsgi/routes/chats.py: WPS202,WPS204,WPS235 # Disable some checks: ignore = diff --git a/tests/fixtures/smartapps.py b/tests/fixtures/smartapps.py new file mode 100644 index 00000000..00763dae --- /dev/null +++ b/tests/fixtures/smartapps.py @@ -0,0 +1,34 @@ +from typing import Dict +from uuid import UUID, uuid4 + +import pytest + + +@pytest.fixture() +def smartapp_api_version() -> int: + return 1 + + +@pytest.fixture() +def smartapp_counter() -> int: + return 42 + + +@pytest.fixture() +def smartapp_id() -> UUID: + return uuid4() + + +@pytest.fixture() +def group_chat_id() -> UUID: + return uuid4() + + +@pytest.fixture() +def ref() -> UUID: + return uuid4() + + +@pytest.fixture() +def smartapp_data() -> Dict[str, str]: + return {"key": "value"} diff --git a/tests/test_bots/test_mixins/test_requests/test_smartapps.py b/tests/test_bots/test_mixins/test_requests/test_smartapps.py new file mode 100644 index 00000000..1cde5887 --- /dev/null +++ b/tests/test_bots/test_mixins/test_requests/test_smartapps.py @@ -0,0 +1,64 @@ +from typing import Any, Dict +from uuid import UUID + +import pytest + +from botx import Message, TestClient +from botx.clients.methods.v3.smartapps.smartapp_event import SmartAppEvent +from botx.clients.methods.v3.smartapps.smartapp_notification import SmartAppNotification +from botx.models.smartapps import SendingSmartAppEvent + +pytestmark = pytest.mark.asyncio +pytest_plugins = ("tests.test_clients.fixtures", "tests.fixtures.smartapps") + + +async def test_smartapp_event( + client: TestClient, + message: Message, + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +): + await client.bot.send_smartapp_event( + credentials=message.credentials, + smartapp_event=SendingSmartAppEvent( + ref=ref, + smartapp_id=smartapp_id, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + data=smartapp_data, + ), + ) + + assert client.requests[0] == SmartAppEvent( + ref=ref, + smartapp_id=smartapp_id, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + data=smartapp_data, + ) + + +async def test_smartapp_notification( + client: TestClient, + message: Message, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_counter: int, +): + await client.bot.send_smartapp_notification( + credentials=message.credentials, + smartapp_notification=SmartAppNotification( + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + smartapp_counter=smartapp_counter, + ), + ) + + assert client.requests[0] == SmartAppNotification( + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + smartapp_counter=smartapp_counter, + ) diff --git a/tests/test_clients/test_methods/test_base/test_empty_error_handlers.py b/tests/test_clients/test_methods/test_base/test_empty_error_handlers.py index 89d3b0c5..5106ff1d 100644 --- a/tests/test_clients/test_methods/test_base/test_empty_error_handlers.py +++ b/tests/test_clients/test_methods/test_base/test_empty_error_handlers.py @@ -2,6 +2,8 @@ class TestMethod(BotXMethod): + __test__ = False + __url__ = "/path/to/example" __method__ = "GET" __returning__ = str diff --git a/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_event.py b/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_event.py new file mode 100644 index 00000000..decc32ee --- /dev/null +++ b/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_event.py @@ -0,0 +1,42 @@ +from typing import Any, Dict, Union +from uuid import UUID + +import pytest + +from botx import AsyncClient, Client, TestClient +from botx.clients.methods.v3.smartapps.smartapp_event import SmartAppEvent +from botx.concurrency import callable_to_coroutine + +pytestmark = pytest.mark.asyncio +pytest_plugins = ("tests.test_clients.fixtures", "tests.fixtures.smartapps") + + +async def test_smartapp_event( + client: TestClient, + requests_client: Union[AsyncClient, Client], + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +) -> None: + method = SmartAppEvent( + host="example.com", # type: ignore [call-arg] + ref=ref, + smartapp_id=smartapp_id, + data=smartapp_data, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + ) + + request = requests_client.build_request(method) + assert await callable_to_coroutine(requests_client.execute, request) + + assert isinstance(client.requests[0], SmartAppEvent) + smartapp_event = client.requests[0] + + assert smartapp_event.ref == ref + assert smartapp_event.smartapp_id == smartapp_id + assert smartapp_event.smartapp_api_version == smartapp_api_version + assert smartapp_event.group_chat_id == group_chat_id + assert smartapp_event.data == smartapp_data diff --git a/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_notification.py b/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_notification.py new file mode 100644 index 00000000..33ec0d25 --- /dev/null +++ b/tests/test_clients/test_methods/test_v3/test_smartapps/test_smartapp_notification.py @@ -0,0 +1,36 @@ +from typing import Union +from uuid import UUID + +import pytest + +from botx import AsyncClient, Client, TestClient +from botx.clients.methods.v3.smartapps.smartapp_notification import SmartAppNotification +from botx.concurrency import callable_to_coroutine + +pytestmark = pytest.mark.asyncio +pytest_plugins = ("tests.test_clients.fixtures", "tests.fixtures.smartapps") + + +async def test_smartapp_event( + client: TestClient, + requests_client: Union[AsyncClient, Client], + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_counter: int, +) -> None: + method = SmartAppNotification( + host="example.com", # type: ignore [call-arg] + group_chat_id=group_chat_id, + smartapp_counter=smartapp_counter, + smartapp_api_version=smartapp_api_version, + ) + + request = requests_client.build_request(method) + assert await callable_to_coroutine(requests_client.execute, request) + + assert isinstance(client.requests[0], SmartAppNotification) + client.requests[0] + + assert client.requests[0].smartapp_counter == smartapp_counter + assert client.requests[0].smartapp_api_version == smartapp_api_version + assert client.requests[0].group_chat_id == group_chat_id diff --git a/tests/test_collecting/test_collector/test_decorators/test_system_event.py b/tests/test_collecting/test_collector/test_decorators/test_system_event.py index bf6234f5..c72edcba 100644 --- a/tests/test_collecting/test_collector/test_decorators/test_system_event.py +++ b/tests/test_collecting/test_collector/test_decorators/test_system_event.py @@ -19,6 +19,7 @@ def test_registration_handler_for_several_system_events( SystemEvents.internal_bot_notification, SystemEvents.cts_login, SystemEvents.cts_logout, + SystemEvents.smartapp_event, } collector = collector_cls() collector.system_event( @@ -39,6 +40,7 @@ def test_registration_handler_for_several_system_events( SystemEvents.left_from_chat, SystemEvents.cts_login, SystemEvents.cts_logout, + SystemEvents.smartapp_event, ], ) def test_defining_system_handler_in_collector_as_decorator( diff --git a/tests/test_models/test_smartapps.py b/tests/test_models/test_smartapps.py new file mode 100644 index 00000000..ff210fcc --- /dev/null +++ b/tests/test_models/test_smartapps.py @@ -0,0 +1,141 @@ +from io import BytesIO +from typing import Any, Dict +from uuid import UUID + +from botx import File, Message +from botx.models.smartapps import SendingSmartAppEvent, SendingSmartAppNotification + +pytest_plugins = ("tests.test_clients.fixtures", "tests.fixtures.smartapps") + + +def test_sending_smartapp_event( + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +): + sending_smartapp = SendingSmartAppEvent( + ref=ref, + smartapp_id=smartapp_id, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + data=smartapp_data, + ) + + assert sending_smartapp.ref == ref + assert sending_smartapp.smartapp_id == smartapp_id + assert sending_smartapp.smartapp_api_version == smartapp_api_version + assert sending_smartapp.group_chat_id == group_chat_id + assert sending_smartapp.data == smartapp_data + + +def test_sending_smartapp_notification( + ref: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_counter: int, +): + sending_smartapp = SendingSmartAppNotification( + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + smartapp_counter=smartapp_counter, + ) + + assert sending_smartapp.smartapp_api_version == smartapp_api_version + assert sending_smartapp.group_chat_id == group_chat_id + assert sending_smartapp.smartapp_counter == smartapp_counter + + +def test_sending_smartapp_notification_from_message( + message: Message, + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_counter: int, +): + message.incoming_message.command.data_dict[ + "smartapp_api_version" + ] = smartapp_api_version + message.incoming_message.command.data_dict["opts"] = {} + message.group_chat_id = group_chat_id + + sending_smartapp = SendingSmartAppNotification.from_message( + smartapp_counter=smartapp_counter, + message=message, + ) + + assert sending_smartapp.smartapp_api_version == smartapp_api_version + assert sending_smartapp.group_chat_id == group_chat_id + assert sending_smartapp.smartapp_counter == smartapp_counter + + +def test_sending_smartapp_event_from_message( + message: Message, + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +): + message.incoming_message.command.data_dict[ + "smartapp_api_version" + ] = smartapp_api_version + message.incoming_message.command.data_dict["opts"] = {} + message.incoming_message.command.data_dict["smartapp_id"] = smartapp_id + message.incoming_message.command.data_dict["ref"] = ref + + message.group_chat_id = group_chat_id + + sending_smartapp = SendingSmartAppEvent.from_message( + data=smartapp_data, + message=message, + ) + + assert sending_smartapp.smartapp_api_version == smartapp_api_version + assert sending_smartapp.group_chat_id == group_chat_id + assert sending_smartapp.data == smartapp_data + + +def test_sending_smartapp_event_add_botx_file( + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +): + sending_smartapp = SendingSmartAppEvent( + ref=ref, + smartapp_id=smartapp_id, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + data=smartapp_data, + ) + + file = File.from_string(b"data", filename="file.txt") + sending_smartapp.add_file(file) + + assert sending_smartapp.files == [file] + + +def test_sending_smartapp_event_add_file( + ref: UUID, + smartapp_id: UUID, + smartapp_api_version: int, + group_chat_id: UUID, + smartapp_data: Dict[str, Any], +): + sending_smartapp = SendingSmartAppEvent( + ref=ref, + smartapp_id=smartapp_id, + smartapp_api_version=smartapp_api_version, + group_chat_id=group_chat_id, + data=smartapp_data, + ) + + file_data = b"data" + file = File.from_string(file_data, filename="file.txt") + sending_smartapp.add_file(BytesIO(file_data), file.file_name) + + assert sending_smartapp.files == [file]