diff --git a/README.md b/README.md index 0e5ca12..e18e99e 100644 --- a/README.md +++ b/README.md @@ -25,34 +25,28 @@ python -m pip install kutana ``` ### Usage -- Create `Kutana` engine and add controllers. You can use shortcuts like `VKKutana` for adding and registering controllers, callbacks and other usefull functions. -- You can set settings in `Kutana.settings`. -- Add or create plugins other files and register them in executor. You can import plugin from files with function `load_plugins`. Files should be a valid python modules with available `plugin` field with your plugin (`Plugin`). +- Create `Kutana` engine and add controllers. +- Register your plugins in the executor. You can import plugin from folders with function `load_plugins`. Files should be a valid python modules with available `plugin` field with your plugin (`Plugin`). - Start engine. Example `run.py` ```py -from kutana import Kutana, VKKutana, load_plugins +from Kutana import * -# Creation -kutana = VKKutana(configuration="configuration.json") +# Create engine +kutana = Kutana() -# Settings -kutana.settings["MOTD_MESSAGE"] = "Greetings, traveler." +# Add VKController to engine +kutana.add_controller( + VKController(load_configuration("vk_token", "configuration.json")) +) +# configuration.json: +# {"vk_token": "API token here"} -# Create simple plugin -plugin = Plugin() - -@self.plugin.on_text("hi", "hi!") -async def greet(message, attachments, env, extenv): - await env.reply("Hello!") - -kutana.executor.register_plugins(plugin) - -# Load plugins from folder +# Load and register plugins kutana.executor.register_plugins(*load_plugins("plugins/")) -# Start kutana +# Run engine kutana.run() ``` @@ -60,13 +54,11 @@ Example `plugins/echo.py` ```py from kutana import Plugin -plugin = Plugin() - -plugin.name = "Echo" +plugin = Plugin(name="Echo") @plugin.on_startswith_text("echo") -async def on_message(message, attachments, env, extenv): - await env.reply("{}!".format(env.body)) +async def on_echo(message, attachments, env): + await env.reply("{}".format(env.body)) ``` ### Available controllers @@ -77,9 +69,6 @@ Task|Priority ---|--- Find and fix all current bugs | high Find and fix grammar and semantic errors | high -Create proper documentation | medium -Adding tests | medium -Add module to PyPi | low Developing plugins | very low ### Authors diff --git a/configuration.json.example b/configuration.json.example index ec5b9cd..b4ce3c1 100644 --- a/configuration.json.example +++ b/configuration.json.example @@ -1,3 +1,3 @@ { - "token": "" + "vk_token": "" } diff --git a/configuration_test.json.example b/configuration_test.json.example index 9719451..903480b 100644 --- a/configuration_test.json.example +++ b/configuration_test.json.example @@ -1,4 +1,4 @@ { - "token": "", - "utoken": "" -} \ No newline at end of file + "vk_token": "", + "vk_utoken": "" +} diff --git a/docs/controller.rst b/docs/controller.rst new file mode 100644 index 0000000..d72b28f --- /dev/null +++ b/docs/controller.rst @@ -0,0 +1,32 @@ +Controller +========== + +Description +^^^^^^^^^^^ + +Controllers are responsible for receiving updates and sending responses. +Often adding new source (like vk.com) requires creation of two things: +controller and normalizer. + +- Controller just exchanges messages, oversees over this exchange and + provides means to interact with service to callbacks throught environment. +- Normalizer turns updates from raw data to instances of classes + :class:`.Message` and :class:`.Attachment` if possible. These objects are + then passed to plugins. They should only create instances of messages + without editing environment. + +These files are placed in folders "kutana/controllers" and +"kutana/plugins/converters". + +Custom controller +^^^^^^^^^^^^^^^^^ + +You can simply fill required methods from :class:`kutana.BasicController`. +Use :class:`.DumpingController` as example and it's files in folders +"controllers" and "converters" with its names. + +.. autoclass:: kutana.BasicController + :members: + +.. note:: + You don't have to create your own controllers if you don't want to. diff --git a/docs/controllers.rst b/docs/controllers.rst deleted file mode 100644 index 8ed794a..0000000 --- a/docs/controllers.rst +++ /dev/null @@ -1,28 +0,0 @@ -Controllers -=========== - -Controllers are responsible for receiving updates and sending responses. -Often adding new source (like vk.com) requires creation of three things: -controller, environment and normalizer. - -- Controller just exchanges messages and oversees over this exhange. -- Environment provides convinient methods for performing different - operations like sending messages and uploading files. -- Normalizer turns updates from raw data to instances of classes :class:`.Message` - and :class:`.Attachment` if possible. Theese objects are then passed to plugins. - -Theese files are placed in folders /environments, /controllers and /plguins/norm. - -Custom controller -***************** - -You can simply fill required methods from :class:`.BasicController`. -Use :class:`.DumpingController` as example and it's files in folders -`/environments`, `/controllers` and `/plguins/norm` with similar names. - -.. autoclass:: kutana.BasicController - :members: - -If you need to transfer some data or methods to callbacks -(plugin's callbacks too) - it is recommended to use the environment -dictionaries. An example of this can be seen in `environments/vk.py`. \ No newline at end of file diff --git a/docs/executor.rst b/docs/executor.rst new file mode 100644 index 0000000..17f07ed --- /dev/null +++ b/docs/executor.rst @@ -0,0 +1,46 @@ +Executor +======== + +Description +^^^^^^^^^^^ + +This class collects coroutines for updates processing and calls +them with the appropriate data. These coroutines receive the update itself +and the dictionary-like environment with data that could be written +there by previous and read by next coroutine calls. You can register +These callbacks with executor's :func:`subscribe` as method or decorator. +Environment has "ctrl_type" set to the type of update's controller +(VKontakte, Telegram, etc.). + +.. code-block:: python + + @kutana.executor.subscribe() + async def prc(update, eenv): + # eenv stands for "executor's environment" + await eenv.reply("Ого, эхо!") + +Same as + +.. code-block:: python + + async def prc(update, eenv): + await env.reply("Ого, эхо!") + + kutana.executor.subscribe(prc) + +All coroutine callbacks will be called in order they were added until +one of the coroutines returns **"DONE"** or none coroutines left. + +You can register callbacks on exceptions in normal callbacks like that. +Exception accesible from eenv.exception in error callbacks. + +.. code-block:: python + + async def prc(update, eenv): + await env.reply("Error happened c:") + + kutana.executor.subscribe(prc, error=True) + +.. note:: + You can't and shouldn't create or use your own executors. It's just + convenient collector and executor of callbacks. diff --git a/docs/executors.rst b/docs/executors.rst deleted file mode 100644 index df05598..0000000 --- a/docs/executors.rst +++ /dev/null @@ -1,36 +0,0 @@ -Executors -========= - -This class collects coroutines for updates processing and calls -them with the appropriate data. These coroutines receive the type of -update's controller (VKontakte, Telegram, etc.), the update itself -and the dictionary-like environment with data that could be written -there by previous and read by next coroutine calls. You can register -theese callbacks with executor's :func:`subscribe` as method or decorator. - -.. code-block:: python - - @kutana.executor.subscribe() - async def prc(controller_type, update, env): - await env.reply("Ого, эхо!") - -Same as - -.. code-block:: python - - async def prc(controller_type, update, env): - await env.reply("Ого, эхо!") - - kutana.executor.subscribe(prc) - -All coroutine callbacks will be called in order they were added until -one of the coroutines returns **"DONE"** or none coroutines left. - -You can register callbacks on errors like that. - -.. code-block:: python - - async def prc(controller_type, update, env): - await env.reply("Error happened c:") - - kutana.executor.subscribe(prc, error=True) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 928c709..ebf27e7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,28 +1,26 @@ Welcome to Kutana! -================================== +================== .. image:: _static/kutana-logo-512.png :alt: Kutana + :scale: 65% -The engine for developing bots for soc. networks, +The engine for developing bots for soc. networks, instant messengers and other systems. .. note:: - We apologize in advance for errors and omissions in this documentation. + We apologize in advance for errors and omissions in this documentation. If you can help improve the documentation corrections or modifications, we will be very grateful. Usage ***** -- Create :class:`.Kutana` engine and add controllers. You can use - shortcuts like :class:`VKKutana ` for adding and registering controllers, - callbacks and other usefull functions. -- You can set settings in `Kutana.settings`. -- Add or create plugins other files and register them in executor. You - can import plugin from files with function `load_plugins`. Files - should be a valid python modules with available `plugin` field with - your plugin (`Plugin`). +- Create :class:`.Kutana` engine and add controllers. +- Register your plugins in the executor. You can import + plugin from folders with function `load_plugins`. Files + should be a valid python modules with available `plugin` + field with your plugin (`Plugin`). - Start engine. Example @@ -32,54 +30,53 @@ Example "run.py" .. code-block:: python - from kutana import Kutana, VKKutana, load_plugins + from kutana import Kutana, VKController, \ + load_plugins, load_configuration - # Creation - kutana = VKKutana(configuration="configuration.json") + # Create engine + kutana = Kutana() - # Settings - kutana.settings["MOTD_MESSAGE"] = "Greetings, traveler." + # Add VKController to engine + kutana.add_controller( + VKController( + load_configuration( + "vk_token", "configuration.json" + ) + ) + ) - # Create simple plugin - plugin = Plugin() + # Load and register plugins + kutana.executor.register_plugins( + *load_plugins("example/plugins/") + ) - @self.plugin.on_text("hi", "hi!") - async def greet(message, attachments, env, extenv): - await env.reply("Hello!") - - kutana.executor.register_plugins(plugin) - - # Load plugins from folder - kutana.executor.register_plugins(*load_plugins("plugins/")) - - # Start kutana + # Run engine kutana.run() + Example "plugins/echo.py" .. code-block:: python from kutana import Plugin - plugin = Plugin() - - plugin.name = "Echo" + plugin = Plugin(name="Echo") @plugin.on_startswith_text("echo") - async def on_message(message, attachments, env, extenv): - await env.reply("{}!".format(env.body)) + async def on_echo(message, attachments, env): + await env.reply("{}".format(env.body)) .. toctree:: :maxdepth: 2 - :caption: Docs + :caption: Elements - plugins - controllers + plugin + controller special-updates - executors + executor .. toctree:: :maxdepth: 1 - :caption: Source + :caption: Reference - src/modules \ No newline at end of file + src/modules diff --git a/docs/plugin.rst b/docs/plugin.rst new file mode 100644 index 0000000..7b962b2 --- /dev/null +++ b/docs/plugin.rst @@ -0,0 +1,115 @@ +Plugin +====== + +Description +^^^^^^^^^^^ + +Plugins allows you to work with incoming updates while abstracting +as much as possible from every concrete controller. You still can +use any specific feature, but you have to know what these features +are. You can specify any amount of callbacks that will process updates +inside of plugin. + +Available decorators +^^^^^^^^^^^^^^^^^^^^ + +.. automethod:: kutana.Plugin.on_text + +.. automethod:: kutana.Plugin.on_has_text + +.. automethod:: kutana.Plugin.on_startswith_text + +.. automethod:: kutana.Plugin.on_regexp_text + +.. automethod:: kutana.Plugin.on_attachment + +All methods above decorates callback which should look like that: + +.. code-block:: python + + async def on_message(message, attachments, env): + # `message` is instance of Message with text, + # attachments and update information. + + # `attachments` is tuple of instances of Attachment + # and update information. + + # `env` is a dictionary (:class:`.objdict`) with data + # accessible from callbacks of current plugin. + # `env`.`eenv` is a dictionary (:class:`.objdict`) with + # data accessible from callbacks of current executor. + + # if you return None or return "DONE", update will + # be considered successfully processed and its + # processing will stop. You can return anything but + # "DONE (for example - "GOON") if you want the update to + # be processed further. + + pass + +.. automethod:: kutana.Plugin.on_startup + +.. automethod:: kutana.Plugin.on_raw + +.. automethod:: kutana.Plugin.on_dispose + +.. note:: + If any callback returns "DONE", no other callback will process this + update any further. You can return anything but "DONE (for example - "GOON") + if you want update to be processed further. + +Available fields +^^^^^^^^^^^^^^^^ + +- **order** - you can manipulate order in which plugins process updates. + Lower value - earlier this plugin will get to process update. This + works only when using default `load_plugins` function and only inside + a single call of `load_plugins`. You should put often used + plugins closer to a beginning as much as possible. + +See :ref:`special_updates` for special updates. + +Examples +^^^^^^^^ + +Simple "echo.py" +**************** + +.. code-block:: python + + from kutana import Plugin + + plugin = Plugin(name="Echo") + + @plugin.on_startswith_text("echo") + async def on_echo(message, attachments, env): + await env.reply("{}".format(env.body)) + +Not quite simple "lister.py" +**************************** + +.. code-block:: python + + from kutana import Plugin + + plugin = Plugin(name="Plugins") + + @plugin.on_startup() + async def on_startup(kutana, update): + plugin.plugins = [] # create list in plugins's memory + + # check all callback owners (possible plugins) + for pl in update["callbacks_owners"]: + + # check if we're working with plugin + if isinstance(pl, Plugin): + + # save plugin to list + plugin.plugins.append(pl.name) + + @plugin.on_startswith_text("list") + async def on_list(message, attachments, env): + # reply with list of plugins' names + await env.reply( + "Plugins:\n" + " | ".join(plugin.plugins) + ) diff --git a/docs/plugins.rst b/docs/plugins.rst deleted file mode 100644 index ca4b576..0000000 --- a/docs/plugins.rst +++ /dev/null @@ -1,105 +0,0 @@ -Plugins -======= - -Available methods of :class:`.Plugin` for subscribing to specific updates. - -- **on_text(text, ...)** - is triggered when the message and any of the - specified text are fully matched. -- **on_has_text(text, ...)** - is triggered when the message contains any - of the specified texts. -- **on_startswith_text(text, ...)** - is triggered when the message starts - with any of the specified texts. -- **on_regexp_text(regexp, flags = 0)** - is triggered when the message - matches the specified regular expression. -- **on_attachment(type, ...)** - is triggered when the message has - attachments of the specified type (if no types specified, - then any attachments). - -All methods above decorates callback which should look like that: - -.. code-block:: python - - async def on_message(message, attachments, env, extenv): - # `message` is instance of Message with text, - # attachmnets and update information. - - # `attachments` is tuple of instances of Attachment - # and update information. - - # `env` is a dictionary (:class:`.objdict`) with data - # accesible from callbacks of current plugin. - - # `extenv` is a dictionary (:class:`.objdict`) with - # data accesible from callbacks of current executor. - - pass - -- **on_startup()** - is triggered at the startup of kutana. Decorated - coroutine receives kutana object and some information in update. See - below for example. -- **on_raw()** - is triggered every time when update can't be turned - into `Message` or `Attachment` object. Arguments `env`, `extenv` - and raw `update` is passed to callback. If callback returns "DONE", no - other callback will process this update any further. -- **on_dispose()** - is triggered when everything is going to shutdown. - -Available fields of :class:`.Plugin`. - -- **order** - you can manipulate order in which plugins process updates. - Lower value - earlier this plugin will get to process update. This - works only when using default `load_plugins` function and only inside - a single call of `load_plugins`. You should put frequently used - plugins closer to a beginning as much as possible. - -See :ref:`special_updates` for special updates. - -Examples -******** - -Simple "echo.py" - -.. code-block:: python - - from kutana import Plugin - - plugin = Plugin() - - plugin.name = "Echo" - - @plugin.on_startswith_text("echo") - async def on_message(message, attachments, env, extenv): - await env.reply("{}!".format(env.body)) - -Not quite simple "lister.py" - -.. code-block:: python - - from kutana import Plugin, load_plugins - - plugin = Plugin() - - plugin.name = "Plugins Lister" - - @plugin.on_startup() - async def on_startup(kutana, update): - plugin.plugins = [] # create list in plugins's memory - - # check all callback owners (possible plugins) - for pl in update["callbacks_owners"]: - - # check if we're working with plugin - if isinstance(pl, Plugin): - - # save plugin to list - plugin.plugins.append(pl.name) - - # read setting from kutana or use default - plugin.bot_name = kutana.settings.get("bot_name", "noname") - - @plugin.on_startswith_text("list") - async def on_message(message, attachments, env, extenv): - # create answer with list of plugins' names and bot name - await env.reply( - "Bot with name \"{}\" has:\n".format(plugin.bot_name) + - "; ".join(plugin.plugins) - ) diff --git a/docs/special-updates.rst b/docs/special-updates.rst index 6895052..70997e0 100644 --- a/docs/special-updates.rst +++ b/docs/special-updates.rst @@ -6,17 +6,18 @@ Special updates Startup ^^^^^^^ -When kutana starts, it sends update with some data and controller_type "kutana" to executors. Update example: +When kutana starts, it sends update with some data +(ctrl_type is "kutana") to executors. Update example: .. code-block:: python { - # kutana object + # kutana object "kutana": self, # update type - "update_type": "startup", + "update_type": "startup", - # list of registered callbacks owners + # list of registered callbacks owners "callbacks_owners": self.executor.callbacks_owners - } \ No newline at end of file + } diff --git a/docs/src/kutana.controllers.rst b/docs/src/kutana.controllers.rst index 2411591..36c6ac9 100644 --- a/docs/src/kutana.controllers.rst +++ b/docs/src/kutana.controllers.rst @@ -1,6 +1,13 @@ kutana.controllers package ========================== +Subpackages +----------- + +.. toctree:: + + kutana.controllers.vk + Submodules ---------- @@ -20,14 +27,6 @@ kutana.controllers.dumping module :undoc-members: :show-inheritance: -kutana.controllers.vk module ----------------------------- - -.. automodule:: kutana.controllers.vk - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/docs/src/kutana.controllers.vk.rst b/docs/src/kutana.controllers.vk.rst new file mode 100644 index 0000000..aed916e --- /dev/null +++ b/docs/src/kutana.controllers.vk.rst @@ -0,0 +1,30 @@ +kutana.controllers.vk package +============================= + +Submodules +---------- + +kutana.controllers.vk.vk module +------------------------------- + +.. automodule:: kutana.controllers.vk.vk + :members: + :undoc-members: + :show-inheritance: + +kutana.controllers.vk.vk\_helpers module +---------------------------------------- + +.. automodule:: kutana.controllers.vk.vk_helpers + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: kutana.controllers.vk + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/src/kutana.environments.rst b/docs/src/kutana.environments.rst deleted file mode 100644 index f919ee5..0000000 --- a/docs/src/kutana.environments.rst +++ /dev/null @@ -1,30 +0,0 @@ -kutana.environments package -=========================== - -Submodules ----------- - -kutana.environments.vk module ------------------------------ - -.. automodule:: kutana.environments.vk - :members: - :undoc-members: - :show-inheritance: - -kutana.environments.vk\_helpers module --------------------------------------- - -.. automodule:: kutana.environments.vk_helpers - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: kutana.environments - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/src/kutana.rst b/docs/src/kutana.rst index 82d2943..c71ae7c 100644 --- a/docs/src/kutana.rst +++ b/docs/src/kutana.rst @@ -7,7 +7,6 @@ Subpackages .. toctree:: kutana.controllers - kutana.environments kutana.plugins kutana.tools @@ -46,14 +45,6 @@ kutana.logger module :undoc-members: :show-inheritance: -kutana.shortcuts module ------------------------ - -.. automodule:: kutana.shortcuts - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/example/plugins/echo.py b/example/plugins/echo.py index 0bec7b7..7558486 100644 --- a/example/plugins/echo.py +++ b/example/plugins/echo.py @@ -1,12 +1,10 @@ from kutana import Plugin -plugin = Plugin() - -plugin.name = "Echo" +plugin = Plugin(name="Grand echo") @plugin.on_startswith_text("echo") -async def on_message(message, attachments, env, extenv): - a_image = await env.upload_photo("test/author.png") - a_doc = await env.upload_doc("test/girl.ogg", doctype="audio_message", filename="file.ogg") +async def on_echo(message, attachments, env): + a_image = await env.upload_photo("test/test_assets/author.png") + a_doc = await env.upload_doc("test/test_assets/girl.ogg", doctype="audio_message", filename="file.ogg") - await env.reply("{}!".format(env.body), attachment=(a_image, a_doc)) \ No newline at end of file + await env.reply("{}!".format(env.body), attachment=(a_image, a_doc)) diff --git a/example/plugins/list.py b/example/plugins/list.py index 6915ee2..f4686f8 100644 --- a/example/plugins/list.py +++ b/example/plugins/list.py @@ -1,8 +1,6 @@ from kutana import Plugin -plugin = Plugin() - -plugin.name = "Plugins Lister" +plugin = Plugin(name="Plugins") @plugin.on_startup() async def on_startup(kutana, update): @@ -12,11 +10,9 @@ async def on_startup(kutana, update): if isinstance(pl, Plugin): plugin.plugins.append(pl.name) - plugin.bot_name = kutana.settings.get("bot_name", "noname") - @plugin.on_startswith_text("list") -async def on_message(message, attachments, env, extenv): +async def on_list(message, attachments, env): await env.reply( - "Bot with name \"{}\" has:\n".format(plugin.bot_name) + - "; ".join(plugin.plugins) + "Plugins:\n" + + " | ".join(plugin.plugins) ) diff --git a/example/run.py b/example/run.py index 8f39268..f2c5bab 100644 --- a/example/run.py +++ b/example/run.py @@ -1,14 +1,15 @@ -from kutana import VKKutana, load_plugins +from kutana import Kutana, VKController, load_plugins, load_configuration -# Create engine with VK throught shortcut -kutana = VKKutana(configuration="configuration.json") +# Create engine +kutana = Kutana() -# Set your settings -kutana.settings["bot_name"] = "V" -kutana.settings["path_to_plugins"] = "example/plugins/" +# Add VKController to engine +kutana.add_controller( + VKController(load_configuration("vk_token", "configuration.json")) +) # Load and register plugins -kutana.executor.register_plugins(*load_plugins(kutana.settings.path_to_plugins)) +kutana.executor.register_plugins(*load_plugins("example/plugins/")) # Run engine kutana.run() diff --git a/kutana/__init__.py b/kutana/__init__.py index 51f4fef..50ac324 100644 --- a/kutana/__init__.py +++ b/kutana/__init__.py @@ -7,10 +7,8 @@ from kutana.controllers.vk import * # lgtm [py/polluting-import] from kutana.controllers.dumping import * # lgtm [py/polluting-import] -from kutana.environments.vk import * # lgtm [py/polluting-import] from kutana.tools.structures import * # lgtm [py/polluting-import] from kutana.tools.functions import * # lgtm [py/polluting-import] -from kutana.shortcuts import * # lgtm [py/polluting-import] name = "kutana" -__version__ = "0.0.5" +__version__ = "0.1.0" diff --git a/kutana/controllers/basiccontroller.py b/kutana/controllers/basiccontroller.py index 764253c..fb524dc 100644 --- a/kutana/controllers/basiccontroller.py +++ b/kutana/controllers/basiccontroller.py @@ -1,5 +1,10 @@ class BasicController: # pragma: no cover - TYPE = "basic" + type = "basic" + + async def setup_env(self, update, eenv): + """Installs wanted methods and values for update into eenv.""" + + raise NotImplementedError async def create_tasks(self, ensure_future): """Create tasks to be executed in background in Kutana.""" @@ -12,6 +17,6 @@ async def create_receiver(self): raise NotImplementedError async def dispose(self): - """Free used resourses.""" + """Free used resources.""" raise NotImplementedError diff --git a/kutana/controllers/dumping.py b/kutana/controllers/dumping.py index 17e5711..2a4d6da 100644 --- a/kutana/controllers/dumping.py +++ b/kutana/controllers/dumping.py @@ -5,12 +5,18 @@ class DumpingController(BasicController): """Shoots target texts once.""" - TYPE = "dumping" + type = "dumping" def __init__(self, *texts): self.die = False self.queue = list(texts) + async def async_print(self, *args, **kwargs): + print(*args, **kwargs) + + async def setup_env(self, update, eenv): + eenv["reply"] = self.async_print + async def create_tasks(self, ensure_future): return [] diff --git a/kutana/controllers/vk/__init__.py b/kutana/controllers/vk/__init__.py new file mode 100644 index 0000000..d828548 --- /dev/null +++ b/kutana/controllers/vk/__init__.py @@ -0,0 +1 @@ +from kutana.controllers.vk.vk import * # lgtm [py/polluting-import] diff --git a/kutana/controllers/vk.py b/kutana/controllers/vk/vk.py similarity index 84% rename from kutana/controllers/vk.py rename to kutana/controllers/vk/vk.py index 180f8b7..88acbeb 100644 --- a/kutana/controllers/vk.py +++ b/kutana/controllers/vk/vk.py @@ -1,4 +1,7 @@ +from kutana.controllers.vk.vk_helpers import \ + upload_doc_class, upload_photo_class, reply_concrete_class from kutana.controllers.basiccontroller import BasicController +from kutana.plugins.data import Message, Attachment from collections import namedtuple from kutana.logger import logger import asyncio @@ -28,7 +31,7 @@ class VKController(BasicController): groups.setLongPollSettings with argument `longpoll_settings`. """ - TYPE = "vk" + type = "vk" def __init__(self, token, longpoll_settings=None): if not token: @@ -132,6 +135,52 @@ async def request(self, method, **kwargs): execute_errors="" ) + async def generic_answer(self, message, peer_id, attachment=None, + sticker_id=None, payload=None, keyboard=None): + """Send message to target peer_id wiith parameters.""" + + if isinstance(attachment, Attachment): + attachment = [attachment] + + if isinstance(attachment, (list, tuple)): + new_attachment = "" + + for a in attachment: + if isinstance(a, Attachment): + new_attachment += \ + "{}{}_{}".format(a.type, a.owner_id, a.id) + \ + ("_" + a.access_key if a.access_key else "") + + else: + new_attachment += str(a) + + new_attachment += "," + + attachment = new_attachment + + return await self.request( + "messages.send", + message=message, + peer_id=peer_id, + attachment=attachment, + sticker_id=sticker_id, + payload=sticker_id, + keyboard=keyboard + )\ + + async def setup_env(self, update, eenv): + peer_id = update["object"].get("peer_id") + + if update["type"] == "message_new": + eenv["reply"] = reply_concrete_class(self, peer_id) + + eenv["send_msg"] = self.generic_answer + + eenv["upload_photo"] = upload_photo_class(self, peer_id) + eenv["upload_doc"] = upload_doc_class(self, peer_id) + + eenv["request"] = self.request + async def create_tasks(self, ensure_future): async def execute_loop(): while self.running: @@ -196,7 +245,7 @@ async def clean_up(): try: req.set_result(res) - except asyncio.InvalidStateError: # pragma: no cover + except asyncio.InvalidStateError: pass await ensure_future(clean_up()) @@ -271,7 +320,7 @@ async def create_receiver(self): async def update_longpoll_data(): longpoll = await self.raw_request("groups.getLongPollServer", group_id=self.group_id) - if longpoll.error: # pragma: no cover + if longpoll.error: raise ValueError( "Couldn't get longpoll information\n{}" .format( @@ -294,14 +343,14 @@ async def receiver(): )) as resp: try: response = await resp.json() - except Exception: # pragma: no cover + except Exception: return [] if "ts" in response: self.longpoll["ts"] = response["ts"] if "failed" in response: - if response["failed"] in (2, 3, 4): # pragma: no cover + if response["failed"] in (2, 3, 4): await update_longpoll_data() return diff --git a/kutana/environments/vk_helpers.py b/kutana/controllers/vk/vk_helpers.py similarity index 64% rename from kutana/environments/vk_helpers.py rename to kutana/controllers/vk/vk_helpers.py index c0bce1a..3d8b0e7 100644 --- a/kutana/environments/vk_helpers.py +++ b/kutana/controllers/vk/vk_helpers.py @@ -3,18 +3,18 @@ import json -async def upload_file_to_vk(controller, upload_url, data): - upload_result_resp = await controller.session.post( +async def upload_file_to_vk(ctrl, upload_url, data): + upload_result_resp = await ctrl.session.post( upload_url, data=data ) if not upload_result_resp: - return None # pragma: no cover + return None upload_result_text = await upload_result_resp.text() if not upload_result_text: - return None # pragma: no cover + return None try: upload_result = json.loads(upload_result_text) @@ -23,19 +23,37 @@ async def upload_file_to_vk(controller, upload_url, data): raise Exception except Exception: - return None # pragma: no cover + return None return upload_result +class reply_concrete_class(): + """Class-method for replying to messages.""" + + def __init__(self, ctrl, peer_id): + self.ctrl = ctrl + self.peer_id = peer_id + + async def __call__(self, message, attachment=None, sticker_id=None, payload=None, keyboard=None): + return await self.ctrl.generic_answer( + message, + self.peer_id, + attachment, + sticker_id, + payload, + keyboard + ) + + class upload_doc_class(): """Class-method for uploading documents. Pass peer_id=False to upload with docs.getWallUploadServer. """ - def __init__(self, controller, peer_id): - self.controller = controller + def __init__(self, ctrl, peer_id): + self.ctrl = ctrl self.peer_id = peer_id async def __call__(self, file, peer_id=None, group_id=None, @@ -51,14 +69,14 @@ async def __call__(self, file, peer_id=None, group_id=None, file = o.read() if peer_id: - upload_data = await self.controller.request( + upload_data = await self.ctrl.request( "docs.getMessagesUploadServer", peer_id=peer_id, type=doctype ) else: - upload_data = await self.controller.request( + upload_data = await self.ctrl.request( "docs.getWallUploadServer", - group_id=group_id or self.controller.group_id + group_id=group_id or self.ctrl.group_id ) if "upload_url" not in upload_data.response: @@ -69,17 +87,17 @@ async def __call__(self, file, peer_id=None, group_id=None, data = aiohttp.FormData() data.add_field("file", file, filename=filename) - upload_result = await upload_file_to_vk(self.controller, upload_url, data) + upload_result = await upload_file_to_vk(self.ctrl, upload_url, data) if not upload_result: - return None # pragma: no cover + return None - attachments = await self.controller.request( + attachments = await self.ctrl.request( "docs.save", **upload_result ) if not attachments.response: - return None # pragma: no cover + return None return convert_to_attachment( attachments.response[0], "doc" @@ -89,8 +107,8 @@ async def __call__(self, file, peer_id=None, group_id=None, class upload_photo_class(): """Class-method for uploading documents.""" - def __init__(self, controller, peer_id): - self.controller = controller + def __init__(self, ctrl, peer_id): + self.ctrl = ctrl self.peer_id = peer_id async def __call__(self, file, peer_id=None): @@ -101,29 +119,29 @@ async def __call__(self, file, peer_id=None): with open(file, "rb") as o: file = o.read() - upload_data = await self.controller.request( + upload_data = await self.ctrl.request( "photos.getMessagesUploadServer", peer_id=peer_id ) if "upload_url" not in upload_data.response: - return None # pragma: no cover + return None upload_url = upload_data.response["upload_url"] data = aiohttp.FormData() data.add_field("photo", file, filename="image.png") - upload_result = await upload_file_to_vk(self.controller, upload_url, data) + upload_result = await upload_file_to_vk(self.ctrl, upload_url, data) if not upload_result: - return None # pragma: no cover + return None - attachments = await self.controller.request( + attachments = await self.ctrl.request( "photos.saveMessagesPhoto", **upload_result ) if not attachments.response: - return None # pragma: no cover + return None return convert_to_attachment( attachments.response[0], "photo" diff --git a/kutana/environments/__init__.py b/kutana/environments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kutana/environments/vk.py b/kutana/environments/vk.py deleted file mode 100644 index 625f01b..0000000 --- a/kutana/environments/vk.py +++ /dev/null @@ -1,94 +0,0 @@ -from kutana.executor import Executor -from kutana.plugins.data import Attachment -from kutana.environments.vk_helpers import upload_doc_class, upload_photo_class -from kutana.controllers.vk import VKController -import json - - -def create_vk_env(token=None, configuration=None): - """Create controller and executor for working with vk.com""" - - if isinstance(configuration, str): - with open(configuration) as o: - configuration = json.load(o) - - elif hasattr(configuration, 'read'): - configuration = json.load(o) - - if isinstance(configuration, dict): - if token is None: - token = configuration["token"] - - if not token: - raise ValueError("No token.") - - controller = VKController(token) - - executor = Executor() - - async def generic_answer(message, peer_id, attachment=None, sticker_id=None, payload=None, keyboard=None): - if isinstance(attachment, Attachment): - attachment = [attachment] - - if isinstance(attachment, (list, tuple)): - new_attachment = "" - - for a in attachment: - if isinstance(a, Attachment): - new_attachment += \ - "{}{}_{}".format(a.type, a.owner_id, a.id) + \ - ("_" + a.access_key if a.access_key else "") - - else: - new_attachment += str(a) - - new_attachment += "," - - attachment = new_attachment - - return await controller.request( - "messages.send", - message=message, - peer_id=peer_id, - attachment=attachment, - sticker_id=sticker_id, - payload=sticker_id, - keyboard=keyboard - ) - - async def build_vk_environment(controller_type, update, env): - if controller_type != "vk": - return - - if update["type"] == "message_new": - async def concrete_answer(message, attachment=None, sticker_id=None, payload=None, keyboard=None): - return await generic_answer( - message, - update["object"]["peer_id"], - attachment, - sticker_id, - payload, - keyboard - ) - - env["reply"] = concrete_answer - - env["send_msg"] = generic_answer - - env["upload_photo"] = upload_photo_class(controller, update["object"].get("peer_id")) - env["upload_doc"] = upload_doc_class(controller, update["object"].get("peer_id")) - - env["request"] = controller.request - - executor.register(build_vk_environment) - - async def prc_err(controller_type, update, env): - if update["type"] == "message_new": - await env.reply("Произошла ошибка! Приносим свои извинения.") - - executor.register(prc_err, error=True) - - return { - "controller": controller, - "executor": executor, - } diff --git a/kutana/executor.py b/kutana/executor.py index 2c47d9e..ddaf432 100644 --- a/kutana/executor.py +++ b/kutana/executor.py @@ -24,13 +24,11 @@ def register_plugins(self, *plugins): """Register callbacks from plugins.""" for plugin in plugins: - if hasattr(plugin, "on_message"): - self.register(plugin.on_message) + if hasattr(plugin, "proc_update"): + self.register(plugin.proc_update) - if hasattr(plugin, "on_error"): - self.register(plugin.on_error, error=True) - - plugin.executor = self + if hasattr(plugin, "proc_error"): # pragma: no cover + self.register(plugin.proc_error, error=True) def register(self, *callbacks, error=False): """Register callbacks.""" @@ -52,14 +50,12 @@ def _register(coroutine): return _register - async def __call__(self, controller_type, update): + async def __call__(self, update, eenv): """Process update from controller.""" - env = objdict() - try: for cb in self.callbacks: - comm = await cb(controller_type, update, env) + comm = await cb(update, eenv) if comm == "DONE": break @@ -67,20 +63,24 @@ async def __call__(self, controller_type, update): except Exception as e: logger.exception( "\"{}::{}\"on update {} from {}".format( - sys.exc_info()[0].__name__, e, update, controller_type + sys.exc_info()[0].__name__, e, update, eenv.ctrl_type ) ) - env["exception"] = e + eenv["exception"] = e + + if not self.error_callbacks: + if "reply" in eenv: + return await eenv.reply("Произошла ошибка! Приносим свои извинения.") for cb in self.error_callbacks: - comm = await cb(controller_type, update, env) + comm = await cb(update, eenv) if comm == "DONE": break async def dispose(self): - """Free resourses and prepare for shutdown.""" + """Free resources and prepare for shutdown.""" for callback_owner in self.callbacks_owners: if hasattr(callback_owner, "dispose"): diff --git a/kutana/kutana.py b/kutana/kutana.py index 1c60e82..3f0bee7 100644 --- a/kutana/kutana.py +++ b/kutana/kutana.py @@ -18,39 +18,39 @@ def __init__(self, executor=None, loop=None): self.tasks = [] self.gathered_loops = None - self.settings = objdict() - def add_controller(self, controller): """Adding controller to engine.""" self.controllers.append(controller) - def apply_environment(self, environment): - """Shortcut for adding controller and updating executor.""" + async def process_update(self, ctrl, update): + """Prepare environment and process update from controller.""" - self.add_controller(environment["controller"]) - self.executor.add_callbacks_from(environment["executor"]) + eenv = objdict(ctrl_type=ctrl.type) - async def shedule_update_process(self, controller_type, update): - """Shedule update to be processed.""" + await ctrl.setup_env(update, eenv) - await self.ensure_future(self.executor(controller_type, update)) + await self.executor(update, eenv) async def ensure_future(self, awaitable): """Shurtcut for asyncio.ensure_loop with curretn loop.""" await asyncio.ensure_future(awaitable, loop=self.loop) - async def loop_for_controller(self, controller): + async def loop_for_controller(self, ctrl): """Receive and process updated from target controller.""" - receiver = await controller.create_receiver() + receiver = await ctrl.create_receiver() while self.running: for update in await receiver(): - await self.shedule_update_process(controller.TYPE, update) + await self.ensure_future( + self.process_update( + ctrl, update + ) + ) - await asyncio.sleep(0.05) + await asyncio.sleep(0.051) def run(self): """Start engine.""" @@ -69,12 +69,12 @@ def run(self): for awaitable in awaitables: self.loops.append(awaitable()) - self.loop.run_until_complete(self.shedule_update_process( - "kutana", + self.loop.run_until_complete(self.executor( { "kutana": self, "update_type": "startup", "callbacks_owners": self.executor.callbacks_owners - } + }, + objdict(ctrl_type="kutana") )) try: @@ -87,7 +87,7 @@ def run(self): self.loop.run_until_complete(self.dispose()) async def dispose(self): - """Free resourses and prepare for shutdown.""" + """Free resources and prepare for shutdown.""" await self.executor.dispose() diff --git a/kutana/plugins/converters/__init__.py b/kutana/plugins/converters/__init__.py index 4540c1e..f11e106 100644 --- a/kutana/plugins/converters/__init__.py +++ b/kutana/plugins/converters/__init__.py @@ -1,13 +1,14 @@ from kutana.plugins.converters import vk, dumping -def get_convert_to_message(controller_type): - if controller_type == "vk": + +def get_converter(ctrl_type): + if ctrl_type == "vk": return vk.convert_to_message - elif controller_type == "dumping": + elif ctrl_type == "dumping": return dumping.convert_to_message else: raise RuntimeError("No converter for controller type {}".format( - controller_type + ctrl_type )) diff --git a/kutana/plugins/converters/dumping.py b/kutana/plugins/converters/dumping.py index 338787c..eb3a2f1 100644 --- a/kutana/plugins/converters/dumping.py +++ b/kutana/plugins/converters/dumping.py @@ -1,15 +1,7 @@ from kutana.plugins.data import Message -async def convert_to_message(arguments, update, env, extenv): - arguments["message"] = Message( - update, - (), - "PC", - "KUTANA", - update +async def convert_to_message(update, env): + return Message( + update, (), "U", "KU", update ) - - arguments["attachments"] = arguments["message"].attachments - - env["reply"] = print diff --git a/kutana/plugins/converters/vk.py b/kutana/plugins/converters/vk.py index 7acd2b8..1c7a431 100644 --- a/kutana/plugins/converters/vk.py +++ b/kutana/plugins/converters/vk.py @@ -1,6 +1,7 @@ from kutana.plugins.data import Message, Attachment import re + def convert_to_attachment(attachment, attachment_type=None): if "type" in attachment and attachment["type"] in attachment: body = attachment[attachment["type"]] @@ -29,11 +30,11 @@ def convert_to_attachment(attachment, attachment_type=None): naive_cache = {} -async def resolveScreenName(screen_name, extenv): # pragma: no cover +async def resolveScreenName(screen_name, eenv): # pragma: no cover if screen_name in naive_cache: return naive_cache[screen_name] - result = await extenv.request( + result = await eenv.request( "utils.resolveScreenName", screen_name=screen_name ) @@ -43,9 +44,9 @@ async def resolveScreenName(screen_name, extenv): # pragma: no cover return result -async def convert_to_message(arguments, update, env, extenv): +async def convert_to_message(update, eenv): if update["type"] != "message_new": - return True + return None obj = update["object"] @@ -56,7 +57,7 @@ async def convert_to_message(arguments, update, env, extenv): new_text = "" for m in re.finditer(r"\[(.+?)\|.+?\]", text): - resp = await resolveScreenName(m.group(1), extenv) + resp = await resolveScreenName(m.group(1), eenv) new_text += text[cursor : m.start()] @@ -71,15 +72,10 @@ async def convert_to_message(arguments, update, env, extenv): text = new_text.lstrip() - arguments["message"] = Message( + return Message( text, tuple(convert_to_attachment(a) for a in obj["attachments"]), obj.get("from_id"), obj.get("peer_id"), update ) - - arguments["attachments"] = arguments["message"].attachments - - for w in ("reply", "send_msg", "request", "upload_photo", "upload_doc"): - env[w] = extenv[w] diff --git a/kutana/plugins/plugin.py b/kutana/plugins/plugin.py index 78b30fc..65baa09 100644 --- a/kutana/plugins/plugin.py +++ b/kutana/plugins/plugin.py @@ -1,60 +1,70 @@ -from kutana.plugins.converters import get_convert_to_message +from kutana.plugins.converters import get_converter from kutana.tools.structures import objdict +import shlex import re class Plugin(): """Class for craeting extensions for kutana engine.""" - def __init__(self): - self.callbacks = [] - self.callbacks_raw = [] - self.callbacks_dispose = [] - self.callback_startup = None + def __init__(self, **kwargs): + self._callbacks = [] + self._callbacks_raw = [] + self._callbacks_dispose = [] + self._callback_startup = None self.order = 50 - self.executor = None + for k, v in kwargs.items(): + setattr(self, k, v) - async def on_message(self, controller_type, update, extenv): + @staticmethod + def done_if_none(value): + """Return "DONE" if value is None. Otherwise return value.""" + if value is None: + return "DONE" + + return value + + async def proc_update(self, update, eenv): """Method for processing updates.""" - if controller_type == "kutana": + if eenv.ctrl_type == "kutana": if update["update_type"] == "startup": - if self.callback_startup: - await self.callback_startup(update["kutana"], update) + if self._callback_startup: + await self._callback_startup(update["kutana"], update) return - env = objdict() - extenv.controller_type = controller_type + env = objdict(eenv=eenv, **eenv) - arguments = { - "message": "", - "attachments": [], - "env": env, - "extenv": extenv - } + if eenv.get("_cached_message"): + message = eenv["_cached_message"] - convert_to_message = get_convert_to_message(controller_type) + else: + message = await get_converter(eenv.ctrl_type)(update, eenv) - isNotMessageOrAttachment = await convert_to_message( - arguments, - update, - env, - extenv - ) + eenv["_cached_message"] = message - if isNotMessageOrAttachment: - del arguments["message"] - del arguments["attachments"] + if message is None: + if not self._callbacks_raw: + return - arguments["update"] = update + arguments = { + "env": env, + "update": update + } - callbacks = self.callbacks_raw + callbacks = self._callbacks_raw else: - callbacks = self.callbacks + arguments = { + "env": env, + "message": message, + "attachments": message.attachments + } + + callbacks = self._callbacks for callback in callbacks: comm = await callback(**arguments) @@ -63,16 +73,16 @@ async def on_message(self, controller_type, update, extenv): return "DONE" async def dispose(self): - """Free resourses and prepare for shutdown.""" + """Free resources and prepare for shutdown.""" - for callback in self.callbacks_dispose: + for callback in self._callbacks_dispose: await callback() def add_callbacks(self, *callbacks): """Add callbacks for processing updates.""" for callback in callbacks: - self.callbacks.append(callback) + self._callbacks.append(callback) def on_dispose(self): """Returns decorator for adding callbacks which is triggered when @@ -80,7 +90,7 @@ def on_dispose(self): """ def decorator(coro): - self.callbacks_dispose.append(coro) + self._callbacks_dispose.append(coro) return coro @@ -93,7 +103,7 @@ def on_startup(self): """ def decorator(coro): - self.callback_startup = coro + self._callback_startup = coro return coro @@ -102,11 +112,12 @@ def decorator(coro): def on_raw(self): """Returns decorator for adding callbacks which is triggered every time when update can't be turned into `Message` or - `Attachment` object. Raw update is passed to callback. + `Attachment` object. Arguments `env` and raw `update` + is passed to callback. """ def decorator(coro): - self.callbacks_raw.append(coro) + self._callbacks_raw.append(coro) return coro @@ -122,9 +133,10 @@ def decorator(coro): async def wrapper(*args, **kwargs): if kwargs["message"].text.strip().lower() in check_texts: - await coro(*args, **kwargs) + comm = self.done_if_none(await coro(*args, **kwargs)) - return "DONE" + if comm == "DONE": + return "DONE" self.add_callbacks(wrapper) @@ -135,6 +147,10 @@ async def wrapper(*args, **kwargs): def on_has_text(self, *texts): """Returns decorator for adding callbacks which is triggered when the message contains any of the specified texts. + + Fills env for callback with: + + - "found_text" - text found in message. """ def decorator(coro): @@ -143,10 +159,16 @@ def decorator(coro): async def wrapper(*args, **kwargs): check_text = kwargs["message"].text.strip().lower() - if any(text in check_text for text in check_texts): - await coro(*args, **kwargs) + for text in check_texts: + if text not in check_text: + continue - return "DONE" + kwargs["env"]["found_text"] = text + + comm = self.done_if_none(await coro(*args, **kwargs)) + + if comm == "DONE": + return "DONE" self.add_callbacks(wrapper) @@ -157,6 +179,12 @@ async def wrapper(*args, **kwargs): def on_startswith_text(self, *texts): """Returns decorator for adding callbacks which is triggered when the message starts with any of the specified texts. + + Fills env for callback with: + + - "body" - text without prefix. + - "args" - text without prefix splitted in bash-like style. + - "prefix" - prefix. """ def decorator(coro): @@ -176,11 +204,13 @@ async def wrapper(*args, **kwargs): return kwargs["env"]["body"] = kwargs["message"].text[len(search_result):].strip() + kwargs["env"]["args"] = shlex.split(kwargs["env"]["body"]) kwargs["env"]["prefix"] = kwargs["message"].text[:len(search_result)].strip() - await coro(*args, **kwargs) + comm = self.done_if_none(await coro(*args, **kwargs)) - return "DONE" + if comm == "DONE": + return "DONE" self.add_callbacks(wrapper) @@ -191,6 +221,10 @@ async def wrapper(*args, **kwargs): def on_regexp_text(self, regexp, flags=0): """Returns decorator for adding callbacks which is triggered when the message matches the specified regular expression. + + Fills env for callback with: + + - "match" - match. """ if isinstance(regexp, str): @@ -208,9 +242,10 @@ async def wrapper(*args, **kwargs): kwargs["env"]["match"] = match - await coro(*args, **kwargs) + comm = self.done_if_none(await coro(*args, **kwargs)) - return "DONE" + if comm == "DONE": + return "DONE" self.add_callbacks(wrapper) @@ -236,9 +271,10 @@ async def wrapper(*args, **kwargs): else: return - await coro(*args, **kwargs) + comm = self.done_if_none(await coro(*args, **kwargs)) - return "DONE" + if comm == "DONE": + return "DONE" self.add_callbacks(wrapper) diff --git a/kutana/shortcuts.py b/kutana/shortcuts.py deleted file mode 100644 index 945b812..0000000 --- a/kutana/shortcuts.py +++ /dev/null @@ -1,16 +0,0 @@ -from kutana.kutana import Kutana -from kutana.environments.vk import create_vk_env - - -def VKKutana(token=None, configuration=None): - """Shortcut for creating kutana object and applying VK environment.""" - - # Create engine - kutana = Kutana() - - # Create VK controller and environment - kutana.apply_environment( - create_vk_env(token=token, configuration=configuration) - ) - - return kutana diff --git a/kutana/tools/functions.py b/kutana/tools/functions.py index ba10c49..3d07122 100644 --- a/kutana/tools/functions.py +++ b/kutana/tools/functions.py @@ -1,8 +1,16 @@ from kutana.logger import logger import importlib.util +import json import os +def load_configuration(target, path): + with open(path, "r") as fh: + config = json.load(fh) + + return config.get(target) + + def import_plugin(name, path): """Import plugin from specified path with specified name.""" diff --git a/setup.py b/setup.py index aaebc70..a7daf91 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ import sys -VERSION = "0.0.5" +VERSION = "0.1.0" class VerifyVersionCommand(install): diff --git a/test/benchmarks.py b/test/benchmarks.py index d4ff895..81e4c05 100644 --- a/test/benchmarks.py +++ b/test/benchmarks.py @@ -9,7 +9,7 @@ class TestTiming(KutanaTest): def test_exec_time(self): - self.target = [";)", "echo message"] * 1000 + self.target = ["message", "echo message"] * 5000 stime = time.time() @@ -23,17 +23,16 @@ async def on_echo(message, env, **kwargs): async def on_regexp(message, env, **kwargs): self.actual.append(env.match.group(0)) - plugin.on_regexp_text(r";\)")(on_regexp) - + plugin.on_regexp_text(r"message")(on_regexp) stime = time.time() print("\nTIME TEST 1: ~{} ( {} )".format( - (time.time() - stime) / 2000, + (time.time() - stime) / 10000, time.time() - stime )) - self.assertLess(time.time() - stime, 0.5) + self.assertLess(time.time() - stime, 1.5) def test_raw_exec_time(self): self.target = ["message"] * 10000 @@ -44,7 +43,6 @@ async def on_any(message, env, **kwargs): plugin.on_has_text()(on_any) - stime = time.time() print("\nTIME TEST 2: ~{} ( {} )".format( @@ -52,7 +50,7 @@ async def on_any(message, env, **kwargs): time.time() - stime )) - self.assertLess(time.time() - stime, 2) + self.assertLess(time.time() - stime, 1.5) if __name__ == '__main__': diff --git a/test/author.png b/test/test_assets/author.png similarity index 100% rename from test/author.png rename to test/test_assets/author.png diff --git a/test/girl.ogg b/test/test_assets/girl.ogg similarity index 100% rename from test/girl.ogg rename to test/test_assets/girl.ogg diff --git a/test/test_assets/sample.json b/test/test_assets/sample.json new file mode 100644 index 0000000..1824c5c --- /dev/null +++ b/test/test_assets/sample.json @@ -0,0 +1,4 @@ +{ + "key": "value", + "key2": {"keynkey": "hvalue"} +} diff --git a/test/test_controller_vk.py b/test/test_controller_vk.py index 55cdebb..4dc6eae 100644 --- a/test/test_controller_vk.py +++ b/test/test_controller_vk.py @@ -1,5 +1,5 @@ from kutana import Kutana, VKController, ExitException, Plugin, \ - logger, create_vk_env + logger import unittest import requests import time @@ -10,34 +10,34 @@ logger.setLevel(40) # ERROR LEVEL -# Theese tests requires internet and token of VK user account as well as VK group. +# These tests requires internet and token of VK user account as well as VK group. # You can set these values through environment variables: # TEST_TOKEN and TEST_UTOKEN. # # Alternatively you can use json file `configuration_test.json` with format like this: # { # "token": "токен группы", -# "utoken": "токен пользователя. который модет писать в группу" +# "utoken": "токен пользователя, который может писать в группу" # } class TestControllerVk(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): try: with open("configuration_test.json") as o: - self.conf = json.load(o) + cls.conf = json.load(o) except FileNotFoundError: - self.conf = { - "token": os.environ.get("TEST_TOKEN", ""), - "utoken": os.environ.get("TEST_UTOKEN", ""), + cls.conf = { + "vk_token": os.environ.get("TEST_TOKEN", ""), + "vk_utoken": os.environ.get("TEST_UTOKEN", ""), } - self.messages_to_delete = set() - this_case = self + cls.messages_to_delete = set() - if not self.conf["token"] or not self.conf["utoken"]: - self.skipTest("No authorization found for tests.") + if not cls.conf["vk_token"] or not cls.conf["vk_utoken"]: + raise unittest.SkipTest("No authorization found for this tests.") async def create_receiver(self): actual_reci = await self.original_create_receiver() @@ -50,12 +50,19 @@ async def reci(): empty_if_done.pop(0) - this_case.messages_to_delete.add( + cls.ureq( + "messages.setActivity", + type="typing", + peer_id=-self.group_id + ) + + cls.messages_to_delete.add( str( - this_case.ureq( + cls.ureq( "messages.send", message="echo message", - peer_id=-self.group_id + peer_id=-self.group_id, + attachment= "photo-164328508_456239017," * 2 ) ) ) @@ -67,13 +74,17 @@ async def reci(): VKController.original_create_receiver = VKController.create_receiver VKController.create_receiver = create_receiver - self.kutana = Kutana() + cls.kutana = Kutana() - self.kutana.apply_environment( - create_vk_env(token=self.conf["token"]) + cls.kutana.add_controller( + VKController( + token=cls.conf["vk_token"], + longpoll_settings={"message_typing_state": 1} + ) ) - def ureq(self, method, **kwargs): + @classmethod + def ureq(cls, method, **kwargs): data = {"v": "5.80"} for k, v in kwargs.items(): @@ -83,7 +94,7 @@ def ureq(self, method, **kwargs): response = requests.post( "https://api.vk.com/method/{}?access_token={}".format( method, - self.conf["utoken"] + cls.conf["vk_utoken"] ), data=data ).json() @@ -92,22 +103,52 @@ def ureq(self, method, **kwargs): return response["response"] - def tearDown(self): + @classmethod + def tearDownClass(cls): VKController.create_receiver = VKController.original_create_receiver - self.ureq("messages.delete", message_ids=",".join(self.messages_to_delete), delete_for_all=1) + def tearDown(self): + if self.messages_to_delete: + self.ureq("messages.delete", message_ids=",".join(self.messages_to_delete), delete_for_all=1) + + self.messages_to_delete.clear() + + def test_exceptions(self): + with self.assertRaises(ValueError): + VKController("") - def test_controller_vk(self): + with self.assertRaises(RuntimeError): + self.kutana.loop.run_until_complete( + VKController("token").raw_request("any.method") + ) + + def test_vk_full(self): plugin = Plugin() self.called = False + self.called_on_attachment = False + + async def on_attachment(*args, **kwargs): + self.called_on_attachment = True + return "GOON" - async def on_regexp(message, env, **kwargs): + plugin.on_attachment("photo")(on_attachment) + + async def on_regexp(message, attachments, env, **kwargs): + # Test receiving self.assertEqual(env.match.group(1), "message") self.assertEqual(env.match.group(0), "echo message") - a_image = await env.upload_photo("test/author.png") - a_audio = await env.upload_doc("test/girl.ogg", doctype="audio_message", filename="file.ogg") + self.assertEqual(message.attachments, attachments) + self.assertEqual(len(attachments), 2) + + self.assertTrue(attachments[0].link) + self.assertTrue(attachments[1].link) + + # Test sending + a_image = await env.upload_photo("test/test_assets/author.png") + a_image = await env.upload_photo("test/test_assets/author.png", peer_id=False) + a_audio = await env.upload_doc("test/test_assets/girl.ogg", doctype="audio_message", filename="file.ogg") self.assertTrue(a_image.id) self.assertTrue(a_audio.id) @@ -120,12 +161,24 @@ async def on_regexp(message, env, **kwargs): self.assertTrue(resp.response) + # Test failed request + resp = await env.request("wrong.method") + + self.assertTrue(resp.error) + self.assertFalse(resp.response) + self.called = True plugin.on_regexp_text(r"echo (.+)")(on_regexp) + async def on_raw(*args, **kwargs): + return "GOON" + + plugin.on_raw()(on_raw) + self.kutana.executor.register_plugins(plugin) self.kutana.run() self.assertTrue(self.called) + self.assertTrue(self.called_on_attachment) diff --git a/test/test_executors.py b/test/test_executors.py index 5936a8a..6e8805f 100644 --- a/test/test_executors.py +++ b/test/test_executors.py @@ -1,3 +1,4 @@ +from kutana import Executor, get_converter from test_framework import KutanaTest import logging @@ -6,81 +7,138 @@ class TestExecutors(KutanaTest): + def test_get_converter(self): + self.assertIsNotNone(get_converter("vk")) + self.assertIsNotNone(get_converter("dumping")) + + with self.assertRaises(RuntimeError): + get_converter("") + def test_just_dumping(self): - self.target = ["message"] * 10 + self.target = ["message"] * 5 with self.dumping_controller(self.target): - async def new_update(controller_type, update, env): - if controller_type == "dumping": + async def new_update(update, eenv): + if eenv.ctrl_type == "dumping": self.actual.append(update) else: - self.assertEqual(controller_type, "kutana") + self.assertEqual(eenv.ctrl_type, "kutana") self.kutana.executor.register(new_update) def test_exception(self): self.called = 0 - with self.dumping_controller(["message"] * 10): + with self.dumping_controller(["message"] * 5): - async def new_update(controller_type, update, env): - if controller_type == "dumping": + async def new_update(update, eenv): + if eenv.ctrl_type == "dumping": raise Exception self.kutana.executor.register(new_update) - async def new_error(controller_type, update, env): + async def new_error(update, eenv): + self.assertTrue(eenv.exception) + self.called += 1 + return "DONE" + + async def new_error_no(update, eenv): + self.assertTrue(False) + self.kutana.executor.register(new_error, error=True) + self.kutana.executor.register(new_error_no, error=True) + + self.assertEqual(self.called, 5) + + def test_default_exception_handle(self): + self.called = 0 + + async def my_faked_reply(mes): + self.assertEqual(mes, "Произошла ошибка! Приносим свои извинения.") + self.called += 1 + + with self.dumping_controller(["message"] * 5): + + async def new_update(update, eenv): + if eenv.ctrl_type == "dumping": + eenv.reply = my_faked_reply + raise Exception + + self.kutana.executor.register(new_update) + + self.assertEqual(self.called, 5) - self.assertEqual(self.called, 10) def test_two_dumping(self): - self.target = ["message"] * 10 + self.target = ["message"] * 5 with self.dumping_controller(self.target): with self.dumping_controller(self.target): self.target *= 2 - async def new_update(controller_type, update, env): - if controller_type == "dumping": + async def new_update(update, eenv): + if eenv.ctrl_type == "dumping": self.actual.append(update) else: - self.assertEqual(controller_type, "kutana") + self.assertEqual(eenv.ctrl_type, "kutana") self.kutana.executor.register(new_update) def test_two_callbacks_and_two_dumpings(self): - self.target = ["message"] * 10 + self.target = ["message"] * 5 with self.dumping_controller(self.target): with self.dumping_controller(self.target): self.target *= 4 - async def new_update_1(controller_type, update, env): - if controller_type == "dumping": + async def new_update_1(update, eenv): + if eenv.ctrl_type == "dumping": self.actual.append(update) - async def new_update_2(controller_type, update, env): - if controller_type == "dumping": + async def new_update_2(update, eenv): + if eenv.ctrl_type == "dumping": self.actual.append(update) self.kutana.executor.register(new_update_1) self.kutana.executor.register(new_update_2) def test_decorate_or_call(self): - self.target = ["message"] * 10 + self.target = ["message"] * 5 with self.dumping_controller(self.target): self.target *= 2 @self.kutana.executor.register() - async def new_update(controller_type, update, env): - if controller_type == "dumping": + async def new_update(update, eenv): + if eenv.ctrl_type == "dumping": self.actual.append(update) self.kutana.executor.register(new_update) + + def test_merge_executors(self): + exec1 = Executor() + exec2 = Executor() + + async def cor1(*args, **kwargs): + pass + + async def cor2(*args, **kwargs): + pass + + exec1.register(cor1) + exec2.register(cor2) + + exec1.callbacks_owners.append(1) + exec2.callbacks_owners.append(2) + + exec1.add_callbacks_from(exec2) + + self.assertEqual(exec1.callbacks, [cor1, cor2]) + self.assertEqual(exec1.error_callbacks, []) + self.assertEqual(exec1.callbacks_owners, [1, 2]) + diff --git a/test/test_miscellaneous.py b/test/test_miscellaneous.py index 2896e97..31caf10 100644 --- a/test/test_miscellaneous.py +++ b/test/test_miscellaneous.py @@ -1,5 +1,5 @@ -from kutana import VKKutana, VKResponse, Executor, load_plugins, objdict, \ - icedict, create_vk_env +from kutana import Kutana, VKResponse, Executor, load_plugins, objdict, \ + icedict, load_configuration import kutana.plugins.converters.vk as converters_vk import unittest import asyncio @@ -57,27 +57,24 @@ def test_functions(self): executor.register_plugins(*loaded_plugins) loop = asyncio.get_event_loop() - loop.run_until_complete(executor("dumping", "message")) + loop.run_until_complete( + executor( + "message", objdict(ctrl_type="dumping") + ) + ) self.assertEqual(loaded_plugins[0].memory, "message") - def test_create_vk_env(self): - create_vk_env(token="token") - - with self.assertRaises(ValueError): - create_vk_env(configuration="configuration.json.example") + def test_load_configuration(self): + value = load_configuration("key", "test/test_assets/sample.json") - with self.assertRaises(ValueError): - create_vk_env(token="") + self.assertEqual(value, "value") - def test_vk_shortcut(self): - vkkutana = VKKutana(token="token") + value = load_configuration("key2", "test/test_assets/sample.json") - self.assertIsNotNone(vkkutana) + self.assertEqual(value, {"keynkey": "hvalue"}) def test_vk_conversation(self): - arguments = {} - async def fake_resolveScreenName(*args, **kwargs): return VKResponse(False, "", "", {"object_id": 1}, "") @@ -86,41 +83,37 @@ async def fake_resolveScreenName(*args, **kwargs): loop = asyncio.get_event_loop() - loop.run_until_complete( + message = loop.run_until_complete( converters_vk.convert_to_message( - arguments, {"object": {"date": 1, "random_id": 0, "fwd_messages": [], "important": False, "peer_id": 1, "text": "echo [club1|\u0421\u043e] 123", "attachments": [], "conversation_message_id": 1411, "out": 0, "from_id": 1, "id": 0, "is_hidden": False}, "group_id": 1, "type": "message_new"}, - {}, {w: 1 for w in ("reply", "send_msg", "request", "upload_photo", "upload_doc")} ) ) - self.assertEqual(arguments["message"].text, "echo 123") - self.assertEqual(arguments["attachments"], ()) + self.assertEqual(message.text, "echo 123") + self.assertEqual(message.attachments, ()) - loop.run_until_complete( + message = loop.run_until_complete( converters_vk.convert_to_message( - arguments, {"object": {"date": 1, "random_id": 0, "fwd_messages": [], "important": False, "peer_id": 1, "text": "echo [club1|\u0421\u043e] 123", "attachments": [], "conversation_message_id": 1411, "out": 0, "from_id": 1, "id": 0, "is_hidden": False}, "group_id": 2, "type": "message_new"}, - {}, {w: 1 for w in ("reply", "send_msg", "request", "upload_photo", "upload_doc")} ) ) - self.assertEqual(arguments["message"].text, "echo [club1|\u0421\u043e] 123") - self.assertEqual(arguments["attachments"], ()) + self.assertEqual(message.text, "echo [club1|\u0421\u043e] 123") + self.assertEqual(message.attachments, ()) converters_vk.resolveScreenName = resolveScreenName diff --git a/test/test_plugins.py b/test/test_plugins.py index d55b313..5c9d6ad 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -1,12 +1,21 @@ +from kutana import Plugin from test_framework import KutanaTest import re class TestPlugins(KutanaTest): + def test_plugin_creation(self): + plugin = Plugin(name="Name", cmds=["cmd1", "cmd2"]) + + self.assertEqual(plugin.name, "Name") # pylint: disable=E1101 + self.assertEqual(plugin.cmds, ["cmd1", "cmd2"]) # pylint: disable=E1101 + + plugin = Plugin(order=5) + def test_echo_plugin(self): - queue = ["message", "echo message", "echonotecho"] * 10 + queue = ["message", "echo message", "echonotecho"] * 5 - self.target = ["message"] * 10 + self.target = ["message"] * 5 with self.dumping_controller(queue) as plugin: async def on_echo(message, env, **kwargs): @@ -14,7 +23,7 @@ async def on_echo(message, env, **kwargs): plugin.on_startswith_text("echo ", "echo\n")(on_echo) - def test_plugin_on_raw(self): + def test_plugin_on_startup(self): self.called = 0 with self.dumping_controller("message") as plugin: @@ -25,11 +34,84 @@ async def on_startup(message, env, **kwargs): self.assertEqual(self.called, 1) + def test_args_on_startswith_text(self): + queue = ["pr a b c", "pr a \"b c\"", "pr a \"\\\"\" b c"] + + queue_answer = [ + ["a", "b", "c"], ["a", "b c"], ["a", "\"", "b", "c"] + ] + + with self.dumping_controller(queue) as plugin: + async def on_startswith_text(message, env, **kwargs): + self.assertEqual(env.args, queue_answer.pop(0)) + + plugin.on_startswith_text("pr")(on_startswith_text) + + self.assertFalse(queue_answer) + + def test_plugins_callbacks_done(self): + self.counter = 0 + + with self.dumping_controller(["123"] * 5) as plugin: + + async def on_has_text(message, **kwargs): + self.counter += 1 + + return "DONE" + + plugin.on_has_text("123")(on_has_text) + + async def on_has_text_2(message, **kwargs): + self.counter += 1 + + plugin.on_has_text("123")(on_has_text_2) + + self.assertEqual(self.counter, 5) + + def test_plugins_callbacks_not_done(self): + self.counter = 0 + + with self.dumping_controller(["123"] * 5) as plugin: + + async def on_has_text(message, **kwargs): + self.counter += 1 + + return "GOON" + + plugin.on_has_text("123")(on_has_text) + + async def on_has_text_2(message, **kwargs): + self.counter += 1 + + plugin.on_has_text("123")(on_has_text_2) + + self.assertEqual(self.counter, 10) + + def test_multiple_plugins(self): + self.counter = 0 + + with self.dumping_controller(["msg"] * 2): + self.plugins.append(Plugin()) + self.plugins.append(Plugin()) + + async def on_has_text(message, env, **kwargs): + self.counter += 1 + + self.assertNotIn("key", env) + env["key"] = "value" + + return "GOON" + + for pl in self.plugins: + pl.on_has_text()(on_has_text) + + self.assertEqual(self.counter, 6) + def test_plugin_callbacks(self): self.disposed = 0 self.counter = 0 - with self.dumping_controller(["123"] * 10 + ["321"]) as plugin: + with self.dumping_controller(["123"] * 5 + ["321"]) as plugin: async def on_123(message, **kwargs): self.assertEqual(message.text, "123") self.counter += 1 @@ -55,15 +137,15 @@ async def on_dispose(): plugin.on_dispose()(on_dispose) - self.assertEqual(self.counter, 11) + self.assertEqual(self.counter, 6) self.assertEqual(self.disposed, 1) def test_environment_reply(self): - self.target = ["echo 123"] * 10 + ["ECHO 123"] + self.target = ["echo 123"] with self.dumping_controller(self.target) as plugin: async def on_echo(message, env, **kwargs): - self.assertEqual(env.reply, print) + self.assertIsNotNone(env.reply) self.actual.append(message.text) plugin.on_startswith_text("echo")(on_echo) @@ -76,7 +158,7 @@ def test_plugin_onstar(self): async def no_trigger(message, **kwargs): self.assertTrue(False) - plugin.on_attachment()(no_trigger) + plugin.on_attachment("photo", "audio")(no_trigger) async def zero_trigger(message, **kwargs):