From 6eda0da706ea73922488e05d35f7c0f4bc432d8d Mon Sep 17 00:00:00 2001 From: Michael Kryukov Date: Sun, 8 Sep 2024 16:43:51 +0300 Subject: [PATCH] feat: bumped api_version for vkontakte, provided more typing for methods (should fix #93) --- .github/workflows/check.yml | 2 +- CHANGELOG.md | 7 ++++ example/plugins/documents.py | 18 ++++---- kutana/backends/vkontakte/base.py | 7 ++-- kutana/context.py | 2 +- kutana/decorators.py | 19 ++++++--- kutana/handler.py | 6 +-- kutana/kutana.py | 18 +++----- kutana/plugin.py | 66 +++++++++++++++++++----------- tests/test_vkontakte_longpoll.json | 26 ++++++------ 10 files changed, 100 insertions(+), 71 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f69c461..9c8b41c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,7 +14,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.10", "3.11"] + python-version: ["3.8", "3.11", '3.12'] defaults: run: shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1b249..120ce94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ > Changes to public API are marked as `^`. Possible changes > to public API are marked as `^?`. +- v6.0.1 + - Changes + - (VKontakte) Bumped default `api_version` to `5.199`. + - Updated version of python for tests and lintings. + - Refactor + - (Core) Provided more typing for some of the plugin's methods. + - v6.0.0 - Notes - This version focuses on removing features that are too hard diff --git a/example/plugins/documents.py b/example/plugins/documents.py index c298f92..3079f68 100644 --- a/example/plugins/documents.py +++ b/example/plugins/documents.py @@ -25,6 +25,15 @@ async def _(msg, ctx): await ctx.reply("Document", attachments=[doc]) + # Audio message + with open(get_path(__file__, "assets/audio.ogg"), "rb") as fh: + audio_message = Attachment( + kind=AttachmentKind.VOICE, + content=("audio.ogg", fh.read()), + ) + + await ctx.reply("Audio message", attachments=[audio_message]) + # Video with open(get_path(__file__, "assets/video.mp4"), "rb") as fh: video = Attachment( @@ -34,12 +43,3 @@ async def _(msg, ctx): ) await ctx.reply("Video", attachments=[video]) - - # Audio message - with open(get_path(__file__, "assets/audio.ogg"), "rb") as fh: - audio_message = Attachment( - kind=AttachmentKind.VOICE, - content=("audio.ogg", fh.read()), - ) - - await ctx.reply("Audio message", attachments=[audio_message]) diff --git a/kutana/backends/vkontakte/base.py b/kutana/backends/vkontakte/base.py index 154c399..601c09f 100644 --- a/kutana/backends/vkontakte/base.py +++ b/kutana/backends/vkontakte/base.py @@ -65,7 +65,7 @@ def __init__( self, token, requests_per_second=19, - api_version="5.131", + api_version="5.199", api_url="https://api.vk.com", ): if not token: @@ -224,9 +224,8 @@ def _make_update(self, raw_update): ) async def _update_group_data(self): - groups = await self._direct_request("groups.getById", {"fields": "screen_name"}) - - self.group = groups[0] + data = await self._direct_request("groups.getById", {"fields": "screen_name"}) + self.group = data["groups"][0] async def on_start(self, app): await self._update_group_data() diff --git a/kutana/context.py b/kutana/context.py index 0ac2a89..4ced271 100644 --- a/kutana/context.py +++ b/kutana/context.py @@ -79,7 +79,7 @@ async def reply(self, text=None, attachments=None, **kwargs): ) -def split_large_text(text, length=4096): +def split_large_text(text: str, length=4096): """ Split text into chunks with specified length. diff --git a/kutana/decorators.py b/kutana/decorators.py index 2d7512b..0eab4e8 100644 --- a/kutana/decorators.py +++ b/kutana/decorators.py @@ -1,4 +1,5 @@ from functools import wraps +from typing import Callable, List from .backend import Backend from .context import Context @@ -24,7 +25,7 @@ async def wrapper(message: Message, context: Context): return decorator -def expect_backend(expected_identity): +def expect_backend(expected_identity: str): """ Return decorators that skips all updates acquired from backends with identity different from specified one. @@ -42,7 +43,11 @@ async def wrapper(message: Message, context: Context): return decorator -async def _get_user_statuses_vk(backend, chat_id, user_id): +async def _get_user_statuses_vk( + backend: Backend, + chat_id: str, + user_id: str, +) -> List[str]: members_response = await backend.request( "messages.getConversationMembers", {"peer_id": chat_id, "fields": ""}, @@ -63,7 +68,11 @@ async def _get_user_statuses_vk(backend, chat_id, user_id): return [] -async def _get_user_statuses_tg(backend, chat_id, user_id): +async def _get_user_statuses_tg( + backend: Backend, + chat_id: str, + user_id: str, +) -> List[str]: chat_administrators = await backend.request( "getChatAdministrators", {"chat_id": chat_id}, @@ -83,7 +92,7 @@ async def _get_user_statuses_tg(backend, chat_id, user_id): return [] -async def _get_user_statuses(backend: Backend, chat_id, user_id): +async def _get_user_statuses(backend: Backend, chat_id: str, user_id: str): if backend.get_identity() == "vk": return await _get_user_statuses_vk(backend, chat_id, user_id) @@ -93,7 +102,7 @@ async def _get_user_statuses(backend: Backend, chat_id, user_id): raise NotImplementedError() -def _expect_sender_status(func, expected_status): +def _expect_sender_status(func: Callable, expected_status: str): @expect_recipient_kind(RecipientKind.GROUP_CHAT) @wraps(func) async def wrapper(message: Message, context: Context): diff --git a/kutana/handler.py b/kutana/handler.py index 0510cba..0032ce8 100644 --- a/kutana/handler.py +++ b/kutana/handler.py @@ -1,4 +1,4 @@ -class Symbol: +class HandledResultSymbol: def __init__(self, name: str): self.name = name @@ -6,5 +6,5 @@ def __repr__(self): return f"" -PROCESSED = Symbol("PROCESSED") -SKIPPED = Symbol("SKIPPED") +PROCESSED = HandledResultSymbol("PROCESSED") +SKIPPED = HandledResultSymbol("SKIPPED") diff --git a/kutana/kutana.py b/kutana/kutana.py index 57af6d9..0605248 100644 --- a/kutana/kutana.py +++ b/kutana/kutana.py @@ -1,7 +1,7 @@ import asyncio import logging from itertools import groupby -from typing import Dict, List +from typing import Callable, Dict, List from .backend import Backend from .context import Context @@ -43,7 +43,7 @@ def __init__( self._storages: Dict[str, Storage] = {"default": MemoryStorage()} self._root_router: Router - self._hooks = { + self._hooks: Dict[str, List[Callable]] = { "start": [], "exception": [], "completion": [], @@ -79,7 +79,7 @@ def _prepare_routers(self): self._root_router = root_router - async def _handle_event(self, event, *args, **kwargs): + async def _handle_event(self, event: str, *args, **kwargs): for handler in self._hooks[event]: try: await handler(*args, **kwargs) @@ -96,11 +96,8 @@ def add_storage(self, name, storage): def storages(self): return self._storages - def add_plugin(self, plugin): + def add_plugin(self, plugin: Plugin): """Add plugin to the application.""" - if not isinstance(plugin, Plugin): - raise ValueError(f"Provided value is not a plugin: {plugin}") - if plugin in self._plugins: raise RuntimeError("Plugin already added") @@ -113,11 +110,8 @@ def add_plugin(self, plugin): def plugins(self): return self._plugins - def add_backend(self, backend): + def add_backend(self, backend: Backend): """Add backend to the application.""" - if not isinstance(backend, Backend): - raise ValueError(f"Provided value is not a backend: {backend}") - if backend in self._backends: raise RuntimeError("Backend already added") @@ -174,7 +168,7 @@ async def _run(self): self.stop() raise - async def _handle_update(self, context): + async def _handle_update(self, context: Context): logging.debug("Processing update %s", context.update) try: diff --git a/kutana/plugin.py b/kutana/plugin.py index f04a034..8591747 100644 --- a/kutana/plugin.py +++ b/kutana/plugin.py @@ -1,13 +1,16 @@ import functools import re -from typing import Any, List +from typing import Any, Awaitable, Callable, Dict, List, Optional, Union from .backends.vkontakte import VkontaktePluginExtension -from .handler import SKIPPED +from .context import Context +from .handler import SKIPPED, HandledResultSymbol from .router import AttachmentsRouter, CommandsRouter, ListRouter, Router from .storage import Document from .update import Message +HandlerType = Callable[[Message, Context], Awaitable[Optional[HandledResultSymbol]]] + class Plugin: """ @@ -32,14 +35,6 @@ def __init__(self, name: str, **kwargs): # Setup extensions self.vk = VkontaktePluginExtension(self) - def __getattr__(self, name): - """Defined for typing""" - return super().__getattribute__(name) - - def __setattr__(self, name, value): - """Defined for typing""" - return super().__setattr__(name, value) - def on_start(self): """ Return decorator for registering coroutines that will be called @@ -91,8 +86,8 @@ def decorator(coro): def on_commands( self, - commands, - priority=0, + commands: List[str], + priority: int = 0, ): """ Return decorator for registering handler that will be called @@ -112,7 +107,7 @@ def on_commands( other handlers are not executed further. """ - def decorator(coro): + def decorator(coro: HandlerType): router = CommandsRouter(priority=priority) for command in commands: router.add_handler(command, coro) @@ -123,7 +118,11 @@ def decorator(coro): return decorator - def on_match(self, patterns, priority=0): + def on_match( + self, + patterns: List[Union[str, re.Pattern[str]]], + priority: int = 0, + ): """ Return decorator for registering handler that will be called when incoming update is a message and it's message matches any @@ -168,8 +167,8 @@ async def _wrapper(update, ctx): def on_attachments( self, - kinds, - priority=0, + kinds: List[str], + priority: int = 0, ): """ Return decorator for registering handler that will be called @@ -180,7 +179,7 @@ def on_attachments( about 'priority' and return values. """ - def decorator(coro): + def decorator(coro: HandlerType): router = AttachmentsRouter(priority=priority) for kind in kinds: router.add_handler(kind, coro) @@ -191,7 +190,10 @@ def decorator(coro): return decorator - def on_messages(self, priority=-1): + def on_messages( + self, + priority: int = -1, + ): """ Return decorator for registering handler that will be called when incoming update is a message. Handler will always be @@ -202,7 +204,7 @@ def on_messages(self, priority=-1): about 'priority' and return values. """ - def decorator(coro): + def decorator(coro: HandlerType): router = ListRouter(priority=priority) @functools.wraps(coro) @@ -219,7 +221,10 @@ async def _wrapper(update, context): return decorator - def on_updates(self, priority=0): + def on_updates( + self, + priority: int = 0, + ): """ Return decorator for registering handler that will be always called (for messages and not messages). @@ -228,7 +233,7 @@ def on_updates(self, priority=0): about 'priority' and return values. """ - def decorator(coro): + def decorator(coro: HandlerType): router = ListRouter(priority=priority) router.add_handler(coro) @@ -238,7 +243,12 @@ def decorator(coro): return decorator - def with_storage(self, check_sender=None, check_recipient=None, storage="default"): + def with_storage( + self, + check_sender: Optional[Dict] = None, + check_recipient: Optional[Dict] = None, + storage: str = "default", + ): """ This decorator allow plugins to implicitly require access to database. Context is populated with the following fields: @@ -276,12 +286,12 @@ def with_storage(self, check_sender=None, check_recipient=None, storage="default context.sender.update_and_save(field2="value2") # update data and store it in database """ - def _perform_check(data, check): + def _perform_check(data: Document, check: Optional[Dict]): """Return true if handler should be called.""" return not check or all(data.get(k) == v for k, v in check.items()) - def decorator(coro): + def decorator(coro: HandlerType): @functools.wraps(coro) async def wrapper(update, context): context.storage = self.app.storages[storage] @@ -315,3 +325,11 @@ async def wrapper(update, context): return wrapper return decorator + + def __getattr__(self, name: str): + """Defined for typing""" + return super().__getattribute__(name) + + def __setattr__(self, name: str, value): + """Defined for typing""" + return super().__setattr__(name, value) diff --git a/tests/test_vkontakte_longpoll.json b/tests/test_vkontakte_longpoll.json index 9539f7d..8171ba8 100644 --- a/tests/test_vkontakte_longpoll.json +++ b/tests/test_vkontakte_longpoll.json @@ -3,18 +3,20 @@ "url": "https://api.vk.com/method/groups.getById", "data": { "fields": "screen_name" }, "response": { - "response": [ - { - "id": 654327632, - "name": "jahskdkafsdhj", - "screen_name": "jahskdkafsdhj", - "is_closed": 1, - "type": "group", - "photo_50": "https://sun1-22.userapi.com/s/v1/if1/ajsdhakjhdskjahdhjgasdjhagsd.jpg?size=50x50&quality=96&crop=394,0,1076,1076&ava=1", - "photo_100": "https://sun1-22.userapi.com/s/v1/if1/adsgajdhkajdshajdgjahldjhgjahksd.jpg?size=100x100&quality=96&crop=394,0,1076,1076&ava=1", - "photo_200": "https://sun1-22.userapi.com/s/v1/if1/asdjghjaklkdjhjasgkdlkjhagsydku.jpg?size=200x200&quality=96&crop=394,0,1076,1076&ava=1" - } - ] + "response": { + "groups": [ + { + "id": 654327632, + "name": "jahskdkafsdhj", + "screen_name": "jahskdkafsdhj", + "is_closed": 1, + "type": "group", + "photo_50": "https://sun1-22.userapi.com/s/v1/if1/ajsdhakjhdskjahdhjgasdjhagsd.jpg?size=50x50&quality=96&crop=394,0,1076,1076&ava=1", + "photo_100": "https://sun1-22.userapi.com/s/v1/if1/adsgajdhkajdshajdgjahldjhgjahksd.jpg?size=100x100&quality=96&crop=394,0,1076,1076&ava=1", + "photo_200": "https://sun1-22.userapi.com/s/v1/if1/asdjghjaklkdjhjasgkdlkjhagsydku.jpg?size=200x200&quality=96&crop=394,0,1076,1076&ava=1" + } + ] + } } }, {