├── .bandit ├── .codacy.yaml ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.rst ├── VERSION ├── docs ├── Makefile └── source │ ├── API.rst │ ├── CHANGELOG.rst │ ├── LICENSE.rst │ ├── conf.py │ ├── development.rst │ ├── index.rst │ ├── installation.rst │ └── usage.rst ├── examples ├── .pylintrc ├── hermes_configuration.py └── hermes_listen_for_intent.py ├── pytest.ini ├── requirements ├── development.txt ├── docs.txt ├── install │ ├── all.txt │ ├── common.txt │ ├── hermes.txt │ └── mqtt.txt └── packaging.txt ├── scripts ├── build_package.sh ├── check_examples.sh ├── check_scripts.sh ├── generate_docs.sh ├── run_tests.sh ├── travis │ ├── after_script.sh │ ├── before_script.sh │ └── script.sh └── upload_package.sh ├── setup.py ├── snips.gpg.key ├── src ├── conftest.py └── snipskit │ ├── __init__.py │ ├── apps.py │ ├── components.py │ ├── config.py │ ├── exceptions.py │ ├── hermes │ ├── __init__.py │ ├── apps.py │ ├── components.py │ └── decorators.py │ ├── mqtt │ ├── __init__.py │ ├── apps.py │ ├── client.py │ ├── components.py │ ├── decorators.py │ └── dialogue.py │ ├── services.py │ └── tools.py └── tests ├── __init__.py ├── config ├── test_config_app.py ├── test_config_assistant.py ├── test_config_snips.py └── test_config_snips_mqtt.py ├── conftest.py ├── hermes ├── test_apps_hermes.py ├── test_components_hermes_connection.py └── test_components_hermes_decorators.py ├── mqtt ├── test_apps_mqtt.py ├── test_client.py ├── test_client_integration.py ├── test_components_mqtt_connection.py ├── test_components_mqtt_decorators.py ├── test_components_mqtt_pubsub_integration.py └── test_dialogue.py ├── test_services_integration.py └── test_tools.py /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | skips: B101 # Asserts are used in the tests. 3 | -------------------------------------------------------------------------------- /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'examples/**' 4 | - 'tests/**' 5 | - 'docs/source/conf.py' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | build/ 3 | dist/ 4 | docs/build/ 5 | src/snipskit.egg-info/ 6 | **/__pycache__/ 7 | venv/ 8 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.5 5 | install: 6 | - requirements: requirements/install/all.txt 7 | - requirements: requirements/docs.txt 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | env: 4 | global: 5 | - CC_TEST_REPORTER_ID=b5260482480f0b98b0a18b0e7f7225664abc22d3b23fa3555fdf0de694aee213 6 | - INTEGRATION_TESTS=true 7 | matrix: 8 | - SNIPSKIT_REQUIREMENTS=all 9 | - SNIPSKIT_REQUIREMENTS=mqtt 10 | - SNIPSKIT_REQUIREMENTS=hermes 11 | - SNIPSKIT_REQUIREMENTS=common 12 | 13 | language: python 14 | python: 15 | - 3.5 16 | - 3.6 17 | - 3.7 18 | 19 | before_install: 20 | - sudo apt-get install -y dirmngr apt-transport-https 21 | - sudo bash -c 'echo "deb https://debian.snips.ai/stretch stable main" > /etc/apt/sources.list.d/snips.list' 22 | - sudo apt-key add snips.gpg.key 23 | - sudo apt-get update -qq 24 | - sudo apt-get install -y snips-platform-common snips-dialogue snips-nlu snips-tts mosquitto 25 | 26 | install: 27 | - pip install -r requirements/install/$SNIPSKIT_REQUIREMENTS.txt 28 | - pip install -r requirements/development.txt 29 | - pip install -r requirements/packaging.txt 30 | 31 | before_script: 32 | - source scripts/travis/before_script.sh 33 | 34 | script: 35 | - scripts/travis/script.sh 36 | 37 | after_script: 38 | - scripts/travis/after_script.sh 39 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | See docs/source/CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Koen Vervloesem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | SnipsKit 3 | ######## 4 | 5 | .. image:: https://api.travis-ci.com/koenvervloesem/snipskit.svg?branch=master 6 | :target: https://travis-ci.com/koenvervloesem/snipskit 7 | :alt: Build status 8 | 9 | .. image:: https://api.codeclimate.com/v1/badges/46806611ac7c0e5c1613/maintainability 10 | :target: https://codeclimate.com/github/koenvervloesem/snipskit/maintainability 11 | :alt: Maintainability 12 | 13 | .. image:: https://api.codeclimate.com/v1/badges/46806611ac7c0e5c1613/test_coverage 14 | :target: https://codeclimate.com/github/koenvervloesem/snipskit/test_coverage 15 | :alt: Test coverage 16 | 17 | .. image:: https://api.codacy.com/project/badge/Grade/10e65e471a044d2e9ea0b171626a3333 18 | :target: https://www.codacy.com/app/koenvervloesem/snipskit 19 | :alt: Code quality 20 | 21 | .. image:: https://readthedocs.org/projects/snipskit/badge/?version=latest 22 | :target: https://snipskit.readthedocs.io/en/latest/?badge=latest 23 | :alt: Documentation status 24 | 25 | .. image:: https://img.shields.io/pypi/v/snipskit.svg 26 | :target: https://pypi.python.org/pypi/snipskit 27 | :alt: PyPI package version 28 | 29 | .. image:: https://img.shields.io/pypi/pyversions/snipskit.svg 30 | :target: https://pypi.python.org/pypi/snipskit 31 | :alt: Supported Python versions 32 | 33 | .. image:: https://img.shields.io/github/license/koenvervloesem/snipskit.svg 34 | :target: https://github.com/koenvervloesem/snipskit/blob/master/LICENSE 35 | :alt: License 36 | 37 | .. inclusion-marker-start-intro 38 | 39 | **Important information: Following the acquisition of Snips by Sonos, the Snips Console is not available anymore after January 31 2020. As such, I have archived this project. If you're searching for an alternative to Snips, I believe that** Rhasspy_ **is currently the best choice for an offline open source voice assistant.** 40 | 41 | .. _Rhasspy: https://rhasspy.readthedocs.io/ 42 | 43 | SnipsKit is a Python library with some helper tools to work with the voice assistant Snips_. This can be used by Snips apps or other programs that work with Snips. 44 | 45 | .. _Snips: https://snips.ai/ 46 | 47 | With SnipsKit, you can create Snips apps without having to write much boilerplate code. The simplest example of an app using SnipsKit is the following: 48 | 49 | .. code-block:: python 50 | 51 | from snipskit.hermes.apps import HermesSnipsApp 52 | from snipskit.hermes.decorators import intent 53 | 54 | class SimpleSnipsApp(HermesSnipsApp): 55 | 56 | @intent('User:ExampleIntent') 57 | def example_intent(self, hermes, intent_message): 58 | hermes.publish_end_session(intent_message.session_id, 59 | "I received ExampleIntent") 60 | 61 | if __name__ == "__main__": 62 | SimpleSnipsApp() 63 | 64 | .. end-code-block 65 | 66 | And that's it! No need to connect to an MQTT broker, no need to register callbacks, because the HermesSnipsApp class: 67 | 68 | - reads the MQTT connection settings from the snips.toml file; 69 | - connects to the MQTT broker; 70 | - registers the method with the intent decorator as a callback method for the intent 'User:ExampleIntent'; 71 | - starts the event loop. 72 | 73 | SnipsKit also has decorators for other events, and there's also a class MQTTSnipsApp to listen to MQTT topics directly. Moreover, SnipsKit also gives the app easy access to: 74 | 75 | - the Snips configuration; 76 | - the Hermes or MQTT connection object; 77 | - the assistant's configuration; 78 | - the app's configuration. 79 | 80 | .. warning:: SnipsKit is currently alpha software. Anything may change at any time. The public API should not be considered stable. 81 | 82 | .. note:: This project is not in any way affiliated to the company Snips. 83 | 84 | .. inclusion-marker-end-intro 85 | 86 | ******************* 87 | System requirements 88 | ******************* 89 | 90 | .. inclusion-marker-start-requirements 91 | 92 | SnipsKit is a Python 3-only library, requiring Python 3.5 or higher. It's currently tested on Python 3.5, 3.6 and 3.7. 93 | 94 | .. inclusion-marker-end-requirements 95 | 96 | ************ 97 | Installation 98 | ************ 99 | 100 | .. inclusion-marker-start-installation 101 | 102 | SnipsKit is `packaged on PyPI`_. The latest stable version with all functionality can be installed with the following command: 103 | 104 | .. _`packaged on PyPI`: https://pypi.org/project/snipskit/ 105 | 106 | .. code-block:: sh 107 | 108 | pip3 install snipskit[hermes,mqtt] 109 | 110 | .. inclusion-marker-end-installation 111 | 112 | ************* 113 | Documentation 114 | ************* 115 | 116 | The full documentation can be found on Read the Docs, for both the `stable version`_ and the `development version`_. 117 | 118 | .. _`stable version`: https://snipskit.readthedocs.io/en/stable/ 119 | .. _`development version`: https://snipskit.readthedocs.io/en/latest/ 120 | 121 | ********* 122 | Copyright 123 | ********* 124 | 125 | This library is provided by `Koen Vervloesem`_ as open source software. See LICENSE_ for more information. 126 | 127 | .. _`Koen Vervloesem`: mailto:koen@vervloesem.eu 128 | 129 | .. _LICENSE: LICENSE 130 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.0-dev 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -b coverage 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/source/API.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | API reference 3 | ############# 4 | 5 | This is the API documentation of the SnipsKit package, covering all modules and classes. 6 | 7 | ************* 8 | snipskit.apps 9 | ************* 10 | 11 | .. automodule:: snipskit.apps 12 | 13 | .. autoclass:: snipskit.apps.SnipsAppMixin 14 | :members: 15 | 16 | ******************* 17 | snipskit.components 18 | ******************* 19 | 20 | .. automodule:: snipskit.components 21 | 22 | .. autoclass:: snipskit.components.SnipsComponent 23 | :members: 24 | 25 | *************** 26 | snipskit.config 27 | *************** 28 | 29 | .. automodule:: snipskit.config 30 | 31 | .. autoclass:: snipskit.config.AppConfig 32 | :members: 33 | 34 | .. autoclass:: snipskit.config.AssistantConfig 35 | :members: 36 | 37 | .. autoclass:: snipskit.config.MQTTAuthConfig 38 | :members: 39 | 40 | .. autoclass:: snipskit.config.MQTTConfig 41 | :members: 42 | 43 | .. autoclass:: snipskit.config.MQTTTLSConfig 44 | :members: 45 | 46 | .. autoclass:: snipskit.config.SnipsConfig 47 | :members: 48 | 49 | ******************* 50 | snipskit.exceptions 51 | ******************* 52 | 53 | .. automodule:: snipskit.exceptions 54 | 55 | .. autoexception:: snipskit.exceptions.SnipsKitError 56 | :members: 57 | 58 | .. autoexception:: snipskit.exceptions.AssistantConfigNotFoundError 59 | :members: 60 | 61 | .. autoexception:: snipskit.exceptions.SnipsConfigNotFoundError 62 | :members: 63 | 64 | *************** 65 | snipskit.hermes 66 | *************** 67 | 68 | .. automodule:: snipskit.hermes 69 | 70 | snipskit.hermes.apps 71 | ==================== 72 | 73 | .. automodule:: snipskit.hermes.apps 74 | 75 | .. autoclass:: snipskit.hermes.apps.HermesSnipsApp 76 | :members: 77 | 78 | snipskit.hermes.components 79 | ========================== 80 | 81 | .. automodule:: snipskit.hermes.components 82 | 83 | .. autoclass:: snipskit.hermes.components.HermesSnipsComponent 84 | :members: 85 | 86 | snipskit.hermes.decorators 87 | ========================== 88 | 89 | .. automodule:: snipskit.hermes.decorators 90 | :members: 91 | 92 | ************* 93 | snipskit.mqtt 94 | ************* 95 | 96 | .. automodule:: snipskit.mqtt 97 | 98 | snipskit.mqtt.apps 99 | ================== 100 | 101 | .. automodule:: snipskit.mqtt.apps 102 | 103 | .. autoclass:: snipskit.mqtt.apps.MQTTSnipsApp 104 | :members: 105 | 106 | snipskit.mqtt.client 107 | ==================== 108 | 109 | .. automodule:: snipskit.mqtt.client 110 | :members: 111 | 112 | snipskit.mqtt.components 113 | ======================== 114 | 115 | .. automodule:: snipskit.mqtt.components 116 | 117 | .. autoclass:: snipskit.mqtt.components.MQTTSnipsComponent 118 | :members: 119 | 120 | snipskit.mqtt.decorators 121 | ======================== 122 | 123 | .. automodule:: snipskit.mqtt.decorators 124 | :members: 125 | 126 | snipskit.mqtt.dialogue 127 | ====================== 128 | 129 | .. automodule:: snipskit.mqtt.dialogue 130 | :members: 131 | 132 | ***************** 133 | snipskit.services 134 | ***************** 135 | 136 | .. automodule:: snipskit.services 137 | :members: 138 | 139 | ************** 140 | snipskit.tools 141 | ************** 142 | 143 | .. automodule:: snipskit.tools 144 | :members: 145 | -------------------------------------------------------------------------------- /docs/source/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | Changelog 3 | ######### 4 | 5 | All notable changes to the SnipsKit project are documented in this file. 6 | 7 | The format is based on `Keep a Changelog`_, and this project adheres to the `Semantic Versioning`_ specification with major, minor and patch version. 8 | 9 | Given a version number MAJOR.MINOR.PATCH, this project increments the: 10 | 11 | - MAJOR version when incompatible API changes are made; 12 | - MINOR version when functionality is added in a backwards-compatible manner; 13 | - PATCH version when backwards-compatible bug fixes are made. 14 | 15 | .. warning:: Note that major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable. Before SnipsKit reaches version 1.0.0, the API does not adhere to the above specification, but the minor version is incremented when incompatible API changes are made. 16 | 17 | .. _`Keep a Changelog`: https://keepachangelog.com/en/1.0.0/ 18 | 19 | .. _`Semantic Versioning`: https://semver.org 20 | 21 | ************* 22 | `Unreleased`_ 23 | ************* 24 | 25 | Added 26 | ===== 27 | 28 | Changed 29 | ======= 30 | 31 | Deprecated 32 | ========== 33 | 34 | Removed 35 | ======= 36 | 37 | Fixed 38 | ===== 39 | 40 | Security 41 | ======== 42 | 43 | .. _`Unreleased`: https://github.com/koenvervloesem/snipskit/compare/0.6.0...HEAD 44 | 45 | ********************* 46 | `0.6.0`_ - 2019-04-19 47 | ********************* 48 | 49 | Added 50 | ===== 51 | 52 | - New module :mod:`snipskit.mqtt.client` with helper functions to use the Paho MQTT library with the MQTT broker defined in a :class:`.MQTTConfig` object. 53 | 54 | Changed 55 | ======= 56 | 57 | - Breaking change: Refactored the :class:`.MQTTConfig` object. It now has `auth` (:class:`.MQTTAuthConfig`) and `tls` (:class:`.MQTTTLSConfig`) attributes for the authentication and TLS settings, respectively. 58 | 59 | Fixed 60 | ===== 61 | 62 | - :func:`snipskit.services.is_running` sometimes threw a :exc:`psutil.NoSuchProcess` exception when it tried to get the name of a process that no longer existed. Caught by Travis CI in `job #201.4`_. 63 | 64 | .. _`job #201.4`: https://travis-ci.com/koenvervloesem/snipskit/jobs/192421610 65 | 66 | .. _`0.6.0`: https://github.com/koenvervloesem/snipskit/compare/0.5.4...0.6.0 67 | 68 | ********************* 69 | `0.5.4`_ - 2019-04-11 70 | ********************* 71 | 72 | Added 73 | ===== 74 | 75 | - New function :func:`snipskit.tools.latest_snips_version` that returns the latest version of the Snips platform, as published in the `release notes`_. 76 | - Documented the use of :attr:`.SnipsAppMixin.assistant` to get access to the assistant's configuration outside of an app. 77 | 78 | .. _`release notes`: https://docs.snips.ai/additional-resources/release-notes 79 | 80 | Fixed 81 | ===== 82 | 83 | - Removed '/usr/local/etc/assistant/assistant.json' as a default path for :class:`.AssistantConfig`. This was added erroneously in version 0.5.3. 84 | 85 | .. _`0.5.4`: https://github.com/koenvervloesem/snipskit/compare/0.5.3...0.5.4 86 | 87 | ********************* 88 | `0.5.3`_ - 2019-04-11 89 | ********************* 90 | 91 | Added 92 | ===== 93 | 94 | - New module :mod:`snipskit.services` with functions to check the versions of Snips services, whether a Snips service is installed or running and what the model version of Snips NLU is. 95 | 96 | .. _`0.5.3`: https://github.com/koenvervloesem/snipskit/compare/0.5.2...0.5.3 97 | 98 | Fixed 99 | ===== 100 | 101 | - Added '/usr/local/etc/assistant/assistant.json' as a default path for :class:`.AssistantConfig`. This was meant to fix a bug reported in `issue #4`_. 102 | 103 | .. _`issue #4`: https://github.com/koenvervloesem/snipskit/issues/4 104 | 105 | ********************* 106 | `0.5.2`_ - 2019-04-09 107 | ********************* 108 | 109 | Added 110 | ===== 111 | 112 | - New module :mod:`snipskit.mqtt.dialogue` with helper functions :func:`snipskit.mqtt.dialogue.continue_session` and :func:`snipskit.mqtt.dialogue.end_session` to continue and end a session. 113 | 114 | .. _`0.5.2`: https://github.com/koenvervloesem/snipskit/compare/0.5.1...0.5.2 115 | 116 | ********************* 117 | `0.5.1`_ - 2019-04-09 118 | ********************* 119 | 120 | Fixed 121 | ===== 122 | 123 | - Example code in documentation fixed to use the new callback signature for methods of :class:`.MQTTSnipsComponent`. 124 | - PyPi package was built incorrectly. 125 | 126 | .. _`0.5.1`: https://github.com/koenvervloesem/snipskit/compare/0.5.0...0.5.1 127 | 128 | ********************* 129 | `0.5.0`_ - 2019-04-08 130 | ********************* 131 | 132 | Added 133 | ===== 134 | 135 | - Example code and documentation about accessing the app's configuration, the assistant's configuration and the configuration of Snips. 136 | - Method :meth:`.MQTTSnipsComponent.publish` to publish a payload, optionally encoded as JSON. 137 | 138 | Changed 139 | ======= 140 | 141 | - Breaking change: The callback signature for methods of :class:`.MQTTSnipsComponent` has changed to (self, topic, payload). 142 | - Breaking change: the decorator :func:`.snipskit.mqtt.decorators.topic` now has an optional argument 'json_decode' to decode a JSON payload to a dict, which is True by default. 143 | 144 | .. _`0.5.0`: https://github.com/koenvervloesem/snipskit/compare/0.4.0...0.5.0 145 | 146 | ********************* 147 | `0.4.0`_ - 2019-03-25 148 | ********************* 149 | 150 | Added 151 | ===== 152 | 153 | - Support for Python 3.7. 154 | - Extra documentation about installation and usage. 155 | 156 | Changed 157 | ======= 158 | 159 | - Breaking change: Moved all Hermes Python-related classes to :mod:`snipskit.hermes` submodules and all MQTT-related classes to :mod:`snipskit.mqtt` submodules. 160 | - Breaking change: Class :class:`.SnipsConfig` uses the new class :class:`.MQTTConfig` for its MQTT connection settings so it doesn't depend on Hermes Python. 161 | - Breaking change: Use `pip install snipskit[hermes]` to install the Hermes Python dependency, and `pip install snipskit[mqtt]` to install the Paho MQTT dependency. This way you can use the :mod:`snipskit.hermes` module without pulling in the Paho MQTT dependency, or the :mod:`snipskit.mqtt` module without pulling in the Hermes Python dependency. 162 | 163 | .. _`0.4.0`: https://github.com/koenvervloesem/snipskit/compare/0.3.0...0.4.0 164 | 165 | ********************* 166 | `0.3.0`_ - 2019-03-22 167 | ********************* 168 | 169 | Added 170 | ===== 171 | 172 | - Extra documentation about installation and usage. 173 | - Example code in directory `examples`. 174 | - Script `scripts/check_examples.sh` to check example code with pylint. 175 | 176 | Changed 177 | ======= 178 | 179 | - Breaking change: Refactored :class:`.SnipsAppMixin`. Drop :meth:`.SnipsAppMixin.get_assistant` method, add constructor. 180 | 181 | .. _`0.3.0`: https://github.com/koenvervloesem/snipskit/compare/0.2.0...0.3.0 182 | 183 | ********************* 184 | `0.2.0`_ - 2019-03-17 185 | ********************* 186 | 187 | Added 188 | ===== 189 | 190 | - Changelog. 191 | - Examples in documentation. 192 | 193 | Changed 194 | ======= 195 | 196 | - Breaking change: Divided :mod:`snipskit.decorators` module into two submodules: :mod:`snipskit.decorators.hermes` and :mod:`snipskit.decorators.mqtt`. 197 | 198 | Fixed 199 | ===== 200 | 201 | - Cleaned up API documentation. 202 | 203 | .. _`0.2.0`: https://github.com/koenvervloesem/snipskit/releases/tag/0.2.0 204 | 205 | ****************** 206 | 0.1.0 - 2019-03-16 207 | ****************** 208 | 209 | Added 210 | ===== 211 | 212 | - This is the first version with a 'semi-stable' API. 213 | -------------------------------------------------------------------------------- /docs/source/LICENSE.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | License 3 | ####### 4 | 5 | SnipsKit is distributed published under the following license: 6 | 7 | .. include:: ../../LICENSE 8 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../../src')) 18 | sys.setrecursionlimit(1500) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'SnipsKit' 24 | copyright = '2019, Koen Vervloesem' 25 | author = 'Koen Vervloesem' 26 | 27 | 28 | # The short X.Y version 29 | version = '' 30 | # The full version, including alpha/beta/rc tags 31 | with open('../../VERSION', 'r') as fh: 32 | release = fh.read().strip() 33 | 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | # 39 | # needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.doctest', 47 | 'sphinx.ext.intersphinx', 48 | 'sphinx.ext.todo', 49 | 'sphinx.ext.coverage', 50 | 'sphinx.ext.viewcode', 51 | 'sphinx.ext.githubpages', 52 | 'sphinx.ext.napoleon' 53 | ] 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This pattern also affects html_static_path and html_extra_path. 77 | exclude_patterns = [] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = None 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'sphinx_rtd_theme' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # Custom sidebar templates, must be a dictionary that maps document names 102 | # to template names. 103 | # 104 | # The default sidebars (for documents that don't match any pattern) are 105 | # defined by theme itself. Builtin themes are using these templates by 106 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 107 | # 'searchbox.html']``. 108 | # 109 | # html_sidebars = {} 110 | 111 | 112 | # -- Options for HTMLHelp output --------------------------------------------- 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'SnipsKitdoc' 116 | 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'SnipsKit.tex', 'SnipsKit Documentation', 143 | 'Koen Vervloesem', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output ------------------------------------------ 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'snipskit', 'SnipsKit Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'SnipsKit', 'SnipsKit Documentation', 164 | author, 'SnipsKit', 'One line description of project.', 165 | 'Miscellaneous'), 166 | ] 167 | 168 | 169 | # -- Options for Epub output ------------------------------------------------- 170 | 171 | # Bibliographic Dublin Core info. 172 | epub_title = project 173 | 174 | # The unique identifier of the text. This can be a ISBN number 175 | # or the project homepage. 176 | # 177 | # epub_identifier = '' 178 | 179 | # A unique identification for the text. 180 | # 181 | # epub_uid = '' 182 | 183 | # A list of files that should not be packed into the epub file. 184 | epub_exclude_files = ['search.html'] 185 | 186 | 187 | # -- Extension configuration ------------------------------------------------- 188 | 189 | # -- Options for intersphinx extension --------------------------------------- 190 | 191 | intersphinx_mapping = { 192 | 'hermes_python': ('https://hermespython.readthedocs.io/en/latest/', None), 193 | 'psutil': ('https://psutil.readthedocs.io/en/latest/', None), 194 | 'python': ('https://docs.python.org', None) 195 | } 196 | 197 | # -- Options for todo extension ---------------------------------------------- 198 | 199 | # If true, `todo` and `todoList` produce output, else they produce nothing. 200 | todo_include_todos = True 201 | 202 | # -- Options for Napoleon extension ------------------------------------------ 203 | napoleon_google_docstring = True 204 | napoleon_numpy_docstring = False 205 | napoleon_include_init_with_doc = True 206 | 207 | # -- Options for autodoc extension ------------------------------------------- 208 | autodoc_default_options = { 209 | 'show-inheritance': True 210 | } 211 | -------------------------------------------------------------------------------- /docs/source/development.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | Development 3 | ########### 4 | 5 | You can find the SnipsKit code `on GitHub`_. 6 | 7 | .. _`on GitHub`: https://github.com/koenvervloesem/snipskit 8 | 9 | *********************************** 10 | Set up your development environment 11 | *********************************** 12 | 13 | If you want to start developing on SnipsKit, `fork`_ the repository, clone your fork and install the project's (development) dependencies in a Python virtual environment: 14 | 15 | .. code-block:: sh 16 | 17 | git clone https://github.com//snipskit.git 18 | cd snipskit 19 | python3 -m venv venv 20 | source venv/bin/activate 21 | pip install wheel 22 | pip install -r requirements/install/all.txt 23 | pip install -r requirements/development.txt 24 | 25 | .. _`fork`: https://help.github.com/en/articles/fork-a-repo 26 | 27 | ****************** 28 | Run the unit tests 29 | ****************** 30 | 31 | A good start to check whether your development environment is set up correctly is to run the unit tests: 32 | 33 | .. code-block:: sh 34 | 35 | ./scripts/run_tests.sh 36 | 37 | It's good practice to run the unit tests before and after you work on something. 38 | 39 | ********************* 40 | Development practices 41 | ********************* 42 | 43 | - Before starting significant work, please propose it and discuss it first on the `issue tracker`_ on GitHub. Other people may have suggestions, will want to collaborate and will wish to review your code. 44 | - Please work on one piece of conceptual work at a time. Keep each narrative of work in a different branch. 45 | - As much as possible, have each commit solve one problem. 46 | - A commit must not leave the project in a non-functional state. 47 | - Run the unit tests before you create a commit. 48 | - Treat code, tests and documentation as one. 49 | - Create a `pull request`_ from your fork. 50 | - Investigate the output of the Continuous Integration checks and fix any errors you have introduced (you can find badges with an overview of these checks `on GitHub`_): 51 | 52 | - The Travis CI `build status`_ should be "passing". 53 | - The Code Climate `maintainability`_ should not decrease. 54 | - The Code Climate `test coverage`_ should not decrease. 55 | - The Codacy `code quality`_ should not decrease. 56 | - The `documentation`_ on Read The Docs should be "passing". 57 | 58 | .. _`issue tracker`: https://github.com/koenvervloesem/snipskit/issues 59 | 60 | .. _`pull request`: https://help.github.com/en/articles/creating-a-pull-request-from-a-fork 61 | 62 | .. _`build status`: https://travis-ci.com/koenvervloesem/snipskit 63 | 64 | .. _`maintainability`: https://codeclimate.com/github/koenvervloesem/snipskit/maintainability 65 | 66 | .. _`test coverage`: https://codeclimate.com/github/koenvervloesem/snipskit/test_coverage 67 | 68 | .. _`code quality`: https://www.codacy.com/app/koenvervloesem/snipskit 69 | 70 | .. _`documentation`: https://snipskit.readthedocs.io/en/latest/?badge=latest 71 | 72 | ***************** 73 | Things to work on 74 | ***************** 75 | 76 | Have a look at the issues in the `issue tracker`_, especially the following categories: 77 | 78 | - `help wanted`_: Issues that could use some extra help. 79 | - `good first issue`_: Issues that are good for newcomers to the project. 80 | 81 | .. _`help wanted`: https://github.com/koenvervloesem/snipskit/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 82 | 83 | .. _`good first issue`: https://github.com/koenvervloesem/snipskit/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 84 | 85 | You can also look at the `maintainability`_ issues on Code Climate and the `code quality`_ issues on Codacy. However, note that not all these issues are relevant. 86 | 87 | ************************ 88 | License of contributions 89 | ************************ 90 | 91 | By submitting patches to this project, you agree to allow them to be redistributed under the project's :doc:`LICENSE` according to the normal forms and usages of the open-source community. 92 | 93 | It is your responsibility to make sure you have all the necessary rights to contribute to the project. 94 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | SnipsKit 3 | ######## 4 | 5 | Welcome to SnipsKit's documentation. 6 | 7 | .. include:: ../../README.rst 8 | :start-after: inclusion-marker-start-intro 9 | :end-before: inclusion-marker-end-intro 10 | 11 | ******** 12 | Contents 13 | ******** 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | installation 19 | usage 20 | API 21 | development 22 | LICENSE 23 | CHANGELOG 24 | 25 | ****************** 26 | Indices and tables 27 | ****************** 28 | 29 | - :ref:`genindex` 30 | - :ref:`modindex` 31 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Installation 3 | ############ 4 | 5 | ******************* 6 | System requirements 7 | ******************* 8 | 9 | .. include:: ../../README.rst 10 | :start-after: inclusion-marker-start-requirements 11 | :end-before: inclusion-marker-end-requirements 12 | 13 | SnipsKit requires the following Python packages if you want to use the complete functionality: 14 | 15 | .. literalinclude:: ../../requirements/install/all.txt 16 | 17 | If your platform supports these packages, SnipsKit should also be supported. 18 | 19 | **************** 20 | Install SnipsKit 21 | **************** 22 | 23 | .. include:: ../../README.rst 24 | :start-after: inclusion-marker-start-installation 25 | :end-before: inclusion-marker-end-installation 26 | 27 | This will also install its dependencies: 28 | 29 | .. literalinclude:: ../../requirements/install/all.txt 30 | 31 | However, in many projects you don't need the complete functionality. Then you have the following options to prevent pulling in unnecessary dependencies: 32 | 33 | The hermes module 34 | ================= 35 | 36 | If you don't need the :mod:`snipskit.mqtt` module because you're using the :mod:`snipskit.hermes` module, install the SnipsKit library like this: 37 | 38 | .. code-block:: sh 39 | 40 | pip3 install snipskit[hermes] 41 | 42 | This will install the complete library and the following dependencies: 43 | 44 | .. literalinclude:: ../../requirements/install/hermes.txt 45 | 46 | The mqtt module 47 | =============== 48 | 49 | If you don't need the :mod:`snipskit.hermes` module because you're using the :mod:`snipskit.mqtt` module, install the SnipsKit library like this: 50 | 51 | .. code-block:: sh 52 | 53 | pip3 install snipskit[mqtt] 54 | 55 | This will install the complete library and the following dependencies: 56 | 57 | .. literalinclude:: ../../requirements/install/mqtt.txt 58 | 59 | Only the basic modules 60 | ====================== 61 | 62 | If you only need the basic modules because you don't need the :mod:`snipskit.hermes` and :mod:`snipskit.mqtt` modules, install the SnipsKit library like this: 63 | 64 | .. code-block:: sh 65 | 66 | pip3 install snipskit 67 | 68 | This will install the complete library and the following dependencies: 69 | 70 | .. literalinclude:: ../../requirements/install/common.txt 71 | 72 | ******************** 73 | Virtual environments 74 | ******************** 75 | 76 | It is recommended to use a `virtual environment`_ and activate it before installing SnipsKit in order to manage your project dependencies properly. You can create a virtual environment with the name `venv` in the current directory and activate it like this: 77 | 78 | .. code-block:: sh 79 | 80 | python3 -m venv venv 81 | source venv/bin/activate 82 | 83 | After this, you can install SnipsKit in the virtual environment. 84 | 85 | .. _`virtual environment`: https://docs.python.org/3/library/venv.html 86 | 87 | **************** 88 | Requirements.txt 89 | **************** 90 | 91 | Because the public API of SnipsKit should not be considered stable yet, it's best to define a specific version (major.minor.patch) in the `requirements.txt` file of your project, e.g.: 92 | 93 | .. code-block:: none 94 | 95 | snipskit[hermes]==0.5.3 96 | 97 | Then you can install the specified version with: 98 | 99 | .. code-block:: sh 100 | 101 | pip3 install -r requirements.txt 102 | 103 | Alternatively, you can define a version like: 104 | 105 | .. code-block:: none 106 | 107 | snipskit[hermes]~=0.5.0 108 | 109 | This will install the latest available SnipsKit version compatible with 0.5.0, but not a higher version such as 0.6.0. Because the minor version is incremented when incompatible API changes are made, this prevents your code from breaking because of breaking changes in the SnipsKit API. Have a look at the :doc:`CHANGELOG` for the versions and their changes. 110 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | ##### 2 | Usage 3 | ##### 4 | 5 | The design philosophy of SnipsKit is: 6 | 7 | - It should be easy to create a Snips app with minimal `boilerplate code`_. 8 | - All SnipsKit code should behave by default in a sensible way, tuned for a Snips installation. 9 | 10 | .. _`boilerplate code`: https://en.wikipedia.org/wiki/Boilerplate_code 11 | 12 | ************* 13 | Prerequisites 14 | ************* 15 | 16 | This document presupposes that you: 17 | 18 | - know Python, in particular `Python 3`_; 19 | - have Snips running and have read the `Snips Platform documentation`_; 20 | - know how to interface with the Snips platform using `Hermes Python`_ or MQTT_ (using `Paho MQTT`_). 21 | 22 | .. _Python: https://www.python.org/ 23 | 24 | .. _`Python 3`: https://docs.python.org/3/tutorial/ 25 | 26 | .. _`Snips Platform documentation`: https://docs.snips.ai/ 27 | 28 | .. _`Hermes Python`: https://docs.snips.ai/articles/console/actions/actions/code-your-action/hermes-python 29 | 30 | .. _MQTT: https://docs.snips.ai/reference/hermes 31 | 32 | .. _`Paho MQTT`: https://www.eclipse.org/paho/clients/python/docs/ 33 | 34 | *************************************** 35 | Creating Snips apps using Hermes Python 36 | *************************************** 37 | 38 | You can create a Snips app using Hermes Python by subclassing the :class:`.HermesSnipsApp` class. This is a convenience class that removes a lot of boilerplate code, which it executes behind the curtains without you having to think about it. 39 | 40 | But you still have to know how to use the Hermes Python library, because you'll use it to read slots, publish messages, and so on. Think of the :class:`.HermesSnipsApp` class as an addon that makes your life in the Hermes Python world a little easier. 41 | 42 | Creating a simple Snips app using Hermes Python 43 | =============================================== 44 | 45 | A simple Snips app that listens to a specific intent and then says a message, is created like this: 46 | 47 | .. literalinclude :: ../../examples/hermes_listen_for_intent.py 48 | :caption: Example hermes_listen_for_intent.py 49 | :language: python 50 | :linenos: 51 | :start-at: from snipskit.hermes.apps 52 | :prepend: #!/usr/bin/env python3 53 | 54 | You can download the file `hermes_listen_for_intent.py`_ from our GitHub repository. 55 | 56 | .. _`hermes_listen_for_intent.py`: https://github.com/koenvervloesem/snipskit/blob/master/examples/hermes_listen_for_intent.py 57 | 58 | Let's dissect this code. In line 1, we signal to the shell that this file is to be run by the Python 3 interpreter. In lines 2 and 3 we import the :class:`.HermesSnipsApp` class and the :func:`.intent` decorator_ that we use. 59 | 60 | .. _decorator: https://docs.python.org/3/glossary.html#term-decorator 61 | 62 | Beginning from line 6 we define a class for our Snips App, inheriting from the :class:`.HermesSnipsApp` app. By inheriting from this class, you get a lot of functionality for free, which we'll explain in a minute. 63 | 64 | In line 9, we define a `callback method`_. This method will be called when an intent is recognized. Which intent? This is defined by the decorator :func:`.intent` in line 8. So this line says: "If the intent 'User:ExampleIntent' is recognized, call the method `example_intent`. 65 | 66 | .. _`callback method`: https://en.wikipedia.org/wiki/Callback_(computer_programming) 67 | 68 | Then inside the `example_intent` method, you can use the `hermes` and `intent_message` objects from the Hermes Python library. In this simple case, we end the session by saying a message. 69 | 70 | In line 14, we check if the Python file is run on the commandline. If it is, we create a new object of the class we defined. When we initialize this object, it automatically reads the MQTT connection settings from the snips.toml file, connects to the MQTT broker, registers the method with the :func:`.intent` decorator as a callback method for the intent 'User:ExampleIntent' and starts the event loop so the app starts listening to events from the Snips platform. 71 | 72 | ************************************************************* 73 | Reading the configuration of the app, the assistant and Snips 74 | ************************************************************* 75 | 76 | Each :class:`.HermesSnipsApp` or :class:`.MQTTSnipsApp` object has attributes that give the app access to the app's configuration (:class:`.AppConfig`), the assistant's configuration (:class:`.AssistantConfig`) and the configuration of Snips (:class:`.SnipsConfig`). The following example (for :class:`.HermesSnipsApp`, but it works the same for :class:`.MQTTSnipsApp`) shows the use of these three attributes: 77 | 78 | .. literalinclude :: ../../examples/hermes_configuration.py 79 | :caption: Example hermes_configuration.py 80 | :language: python 81 | :linenos: 82 | :start-at: from snipskit.hermes.apps 83 | :prepend: #!/usr/bin/env python3 84 | 85 | You can download the file `hermes_configuration.py`_ from our GitHub repository. 86 | 87 | .. _`hermes_configuration.py`: https://github.com/koenvervloesem/snipskit/blob/master/examples/hermes_configuration.py 88 | 89 | With `self.config` you get access to this app's configuration as an :class:`.AppConfig` object, which is a subclass of :class:`configparser.ConfigParser`. This example requires you to have a file 'config.ini' in the same directory as the app, with the following content: 90 | 91 | .. code-block:: ini 92 | 93 | [global] 94 | [secret] 95 | switch=light1 96 | 97 | .. note:: To get access to the app configuration, don't forget to add the argument `config=AppConfig()` when initializing your app. If you don't need any app configuration, this argument can be left out. 98 | 99 | With `self.assistant` you get access to the assistant's configuration as an :class:`.AssistantConfig` object, which behaves like a :class:`dict`. This reads the configuration from the assistant's directory, which is normally '/usr/share/snips/assistant/assistant.json' on a Raspbian system. 100 | 101 | And with `self.snips` you get access to the configuration of Snips, which also behaves like a :class:`dict`. This reads the configuration from the Snips configuration file, which is normally '/etc/snips.toml' on a Raspbian system. 102 | 103 | **************************************************** 104 | Reading the assistant's configuration outside an app 105 | **************************************************** 106 | 107 | When you create a :class:`.HermesSnipsApp` or :class:`.MQTTSnipsApp` object, it reads the location of the assistant from 'snips.toml' and creates an :class:`.AssistantConfig` object with the correct path, which gives you access to the assistant's configuration. See the previous section for an example. 108 | 109 | You can also create an :class:`.AssistantConfig` object outside an app object, reading its configuration from a specified file: 110 | 111 | .. code-block:: python 112 | 113 | assistant = AssistantConfig('/opt/assistant/assistant.json') 114 | 115 | The file argument is optional. If you leave it empty, the :class:`.AssistantConfig` object tries to read its configuration from the following files, in this order: 116 | 117 | - /usr/share/snips/assistant/assistant.json 118 | - /usr/local/share/snips/assistant/assistant.json 119 | 120 | Note that the :class:`.AssistantConfig` object doesn't read its location from 'snips.toml' in this case. 121 | 122 | If you want to create an :class:`.AssistantConfig` object outside an app object and initialize it from the location specified in 'snips.toml', you need to use the :attr:`.SnipsAppMixin.assistant` attribute to get an :class:`.AssistantConfig` object with the correct path. 123 | 124 | For instance, this could be interesting if you want to know the language of the user's assistant before initializing your app: 125 | 126 | .. code-block:: python 127 | 128 | language = SnipsAppMixin().assistant['language'] 129 | -------------------------------------------------------------------------------- /examples/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook='import sys; sys.path.append("../src")' 3 | -------------------------------------------------------------------------------- /examples/hermes_configuration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This is an example of a Snips app using the Hermes protocol with the 3 | SnipsKit library. 4 | 5 | This app shows how to read the app's configuration, the assistant's 6 | configuration and the configuration of Snips. This requires a configuration.ini 7 | file in the same directory as the app, with a section `secret` and an option 8 | `switch`. 9 | 10 | You can find the documentation of this library in: 11 | 12 | https://snipskit.readthedocs.io/ 13 | """ 14 | from snipskit.hermes.apps import HermesSnipsApp 15 | from snipskit.config import AppConfig 16 | from snipskit.hermes.decorators import intent 17 | 18 | 19 | class SimpleSnipsApp(HermesSnipsApp): 20 | 21 | @intent('User:TurnOn') 22 | def example_turn_on(self, hermes, intent_message): 23 | switch = self.config['secret']['switch'] 24 | hermes.publish_end_session(intent_message.session_id, 25 | 'You want me to turn on {}'.format(switch)) 26 | 27 | @intent('User:Name') 28 | def example_name(self, hermes, intent_message): 29 | name = self.assistant['name'] 30 | hermes.publish_end_session(intent_message.session_id, 31 | 'My name is {}'.format(name)) 32 | 33 | @intent('User:Master') 34 | def example_master(self, hermes, intent_message): 35 | try: 36 | master = self.snips['snips-audio-server']['bind'].split('@')[0] 37 | except KeyError: 38 | master = 'default' 39 | hermes.publish_end_session(intent_message.session_id, 40 | 'My master site is {}'.format(master)) 41 | 42 | 43 | if __name__ == "__main__": 44 | SimpleSnipsApp(config=AppConfig()) 45 | -------------------------------------------------------------------------------- /examples/hermes_listen_for_intent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This is an example of a Snips app using the Hermes protocol with the 3 | SnipsKit library. 4 | 5 | This app listens for an intent and answers the user. 6 | 7 | You can find the documentation of this library in: 8 | 9 | https://snipskit.readthedocs.io/ 10 | """ 11 | from snipskit.hermes.apps import HermesSnipsApp 12 | from snipskit.hermes.decorators import intent 13 | 14 | 15 | class SimpleSnipsApp(HermesSnipsApp): 16 | 17 | @intent('User:ExampleIntent') 18 | def example_intent(self, hermes, intent_message): 19 | hermes.publish_end_session(intent_message.session_id, 20 | 'I received ExampleIntent') 21 | 22 | 23 | if __name__ == "__main__": 24 | SimpleSnipsApp() 25 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | mock_use_standalone_module = true 3 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | # Documentation 2 | Sphinx 3 | sphinx_rtd_theme 4 | # Tests 5 | bashate 6 | flake8 7 | mock 8 | pylint 9 | pyfakefs 10 | pytest 11 | pytest-cov 12 | pytest-mock 13 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /requirements/install/all.txt: -------------------------------------------------------------------------------- 1 | hermes-python>=0.3.3 2 | paho-mqtt 3 | psutil 4 | toml 5 | -------------------------------------------------------------------------------- /requirements/install/common.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | toml 3 | -------------------------------------------------------------------------------- /requirements/install/hermes.txt: -------------------------------------------------------------------------------- 1 | hermes-python>=0.3.3 2 | psutil 3 | toml 4 | -------------------------------------------------------------------------------- /requirements/install/mqtt.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt 2 | psutil 3 | toml 4 | -------------------------------------------------------------------------------- /requirements/packaging.txt: -------------------------------------------------------------------------------- 1 | twine 2 | wheel 3 | -------------------------------------------------------------------------------- /scripts/build_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 setup.py sdist bdist_wheel 3 | -------------------------------------------------------------------------------- /scripts/check_examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2164 3 | cd examples 4 | 5 | # Extract the example from README.rst 6 | tail -n +"$(grep -n 'code-block:: python' ../README.rst | cut -d: -f1)" ../README.rst > README.example 7 | # The following line needs GNU head. Use tail -r | tail -n +3 | tail -r instead of head -n -2 on BSD systems. 8 | head -n "$(grep -n 'end-code-block' README.example | cut -d: -f1)" README.example | tail -n +3 | head -n -2 | sed 's/^ //' > README.py 9 | 10 | # Check the examples in the examples directory. 11 | pylint -E ./*.py 12 | 13 | # Clean up the files from the extracted code and return the exit code of the pylint command. 14 | eval "rm README.example README.py; exit $?" 15 | -------------------------------------------------------------------------------- /scripts/check_scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | bashate scripts/*.sh scripts/travis/*.sh && shellcheck scripts/*.sh scripts/travis/*.sh 3 | -------------------------------------------------------------------------------- /scripts/generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd docs && make html 3 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pytest --cov src --verbose 3 | -------------------------------------------------------------------------------- /scripts/travis/after_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ev 3 | if [ "$SNIPSKIT_REQUIREMENTS" == "all" ]; then 4 | ./cc-test-reporter after-build --exit-code "$TRAVIS_TEST_RESULT" 5 | fi 6 | -------------------------------------------------------------------------------- /scripts/travis/before_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ev 3 | if [ "$SNIPSKIT_REQUIREMENTS" == "all" ]; then 4 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 5 | chmod +x ./cc-test-reporter 6 | ./cc-test-reporter before-build 7 | fi 8 | -------------------------------------------------------------------------------- /scripts/travis/script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ev 3 | if [ "$SNIPSKIT_REQUIREMENTS" == "all" ]; then 4 | pytest --verbose --cov src --cov-report xml 5 | scripts/check_examples.sh 6 | scripts/check_scripts.sh 7 | scripts/generate_docs.sh 8 | scripts/build_package.sh 9 | else 10 | # Don't test coverage when we only test a part of the modules. 11 | pytest --verbose 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/upload_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 -m twine upload dist/* 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from os.path import basename 3 | from os.path import splitext 4 | 5 | from setuptools import setup, find_packages 6 | 7 | 8 | with open("README.rst", "r") as fh: 9 | long_description = fh.read() 10 | 11 | with open("VERSION", "r") as fh: 12 | version = fh.read().strip() 13 | 14 | with open("requirements/install/common.txt", "r") as fh: 15 | requirements_common = fh.read().splitlines() 16 | 17 | with open("requirements/install/hermes.txt", "r") as fh: 18 | requirements_hermes = fh.read().splitlines() 19 | extra_requirements_hermes = list(set(requirements_hermes) - set(requirements_common)) 20 | 21 | with open("requirements/install/mqtt.txt", "r") as fh: 22 | requirements_mqtt = fh.read().splitlines() 23 | extra_requirements_mqtt = list(set(requirements_mqtt) - set(requirements_common)) 24 | 25 | setup( 26 | name="snipskit", 27 | version=version, 28 | description="A library to help create apps for the voice assistant Snips", 29 | long_description=long_description, 30 | long_description_content_type="text/x-rst", 31 | license="MIT", 32 | author="Koen Vervloesem", 33 | author_email="koen@vervloesem.eu", 34 | url="https://github.com/koenvervloesem/snipskit", 35 | packages=find_packages('src'), 36 | package_dir={'': 'src'}, 37 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 38 | install_requires=requirements_common, 39 | extras_require={'hermes': extra_requirements_hermes, 40 | 'mqtt': extra_requirements_mqtt}, 41 | include_package_data=True, 42 | zip_safe=False, 43 | classifiers=[ 44 | 'Development Status :: 3 - Alpha', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: MIT License', 47 | 'Operating System :: POSIX', 48 | 'Operating System :: Unix', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 3 :: Only', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Programming Language :: Python :: 3.6', 53 | 'Programming Language :: Python :: 3.7', 54 | 'Programming Language :: Python :: Implementation :: CPython', 55 | 'Topic :: Software Development :: Libraries' 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /snips.gpg.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFny/pcBCAC8FFFRvw8UV2/aYMrO3zEeJ3Xq576ja65bZYiyJ7DOKL1ZP/B+ 4 | oltPdEkE1XZ1syPC9N6Gm1zwgD7P1tlGimAOB8g1OdPrdIEbQqFyGaoaNYXTm6aI 5 | 6i6UKc7HEaFeakt68WcieGhQLvaAowBOSIEfDNTOHIR3w4isn8tZ70oaLI6a8o48 6 | C4SHkBGpcwbiE8ihVtKNJIQ5c3L2Nbs+BMve/SOwoz0emTDmb3AHup/QrF6yMOiK 7 | Ex7KIlMhUM9Av2QCxloh7tEjLPkSvkmurdWXNgANdSqcM1NltGHO9ZZKfzwKpico 8 | 3rXZJERP3rY1a8XiJMT843TioWyJVl2Rc+SVABEBAAG0KlNuaXBzIERlYmlhbiBk 9 | aXN0cmlidXRpb24gPGluZnJhQHNuaXBzLmFpPokBVAQTAQgAPhYhBHdE7KEfz2qS 10 | fjjeefcnx3jMsKRVBQJZ8v6XAhsDBQkDwmcABQsJCAcCBhUICQoLAgQWAgMBAh4B 11 | AheAAAoJEPcnx3jMsKRVv9wH/jvwT6sBzTb8fCrIyfdU+qaFGOAngdcwHaMaxEv5 12 | raQgnKtWmGHr18PanJ0EceU4VdJK4D+rQmcbz6NfjRORUj0gFH1Zf9hof21X8fHn 13 | S2GFZ+Ah+geXef0GEFhTKiSwcOTkWHM62fD/pTiNy6MX0TUWHirWMCTw2PmV1Mhu 14 | 8aXNKDURIKWMVn53eMqFS4miLCCVsJTwxV2ibVY3CJCTVXhdkhQcaZmvNN5KHqq8 15 | uNhOwdB7VTuEZ1M//redH3bAjFGYDSvVtlqlBz0WVRTWFvh/WgXLrf1fgagx7iVa 16 | buZWlO4xEMajL1MMLC2TfeS7XAiOEgFSdHc5/LEb7srEd5a5AQ0EWfL+lwEIANFx 17 | zIeRvK2o8JFEfM0Bn5C10HoqPdc1n0sE8R8L7/2Xt3r7LuQAqLEocK1QefcntZis 18 | 52h4xXYosJmaBmBfGt+BzM1OtSRehkRYR0EAVMYGxuUV50TjAGRTyPNJC7f7m5df 19 | qeXvpfy6RzA8gQjt64YjA6g8fzDK6M45qQOl4W2BlWkSQ1X9umGDrQ+CpsCzkE2o 20 | Lvy1vufpjAz5u3KV7X8rAtyDKi4w41obgulWj7JMmZI0fMjnREkUJXYLeYcFdekT 21 | tlFiICBhCeSIu7cOS3cXn3LNjXEGRjaOZKs+EfpRvoNSAt/lD8Vj3yudQCm+ZNhR 22 | f3lhlGcs1jHLA8nX/hEAEQEAAYkBPAQYAQgAJhYhBHdE7KEfz2qSfjjeefcnx3jM 23 | sKRVBQJZ8v6XAhsMBQkDwmcAAAoJEPcnx3jMsKRVpOAIAIxTewR9FpO+M548O2tM 24 | 8/QzII9AlU/Oj159gkxfQQ1UjKo7ZhukaGXxh1z3fKUoud+XFVumI7Fcb/Bf6D3C 25 | Lo3Pe9i0A3H77UdcyJ2CbtzqmKE0Oa+NUKyPv9iEYP08fHnPfsnRRzbcvIoa0PQC 26 | HoQTMAdrbL/2FzSQ0+YnFkxce3JRpPsTyyxibhMaRP5t5V7/4OAyD+n9ezzuapEc 27 | gQfeQuRh3m1j1Mwb0XwHrpmb10JD1dKmk1+NxZ9U6yFmuOaOziFBMiv4e35huKqR 28 | osfDRlPtM+4OhC9ZoOio3f+Dq+hVsqcBOSrdcInHK+fs1835aztwB8lTrmG8ofgp 29 | +SQ= 30 | =3KTG 31 | -----END PGP PUBLIC KEY BLOCK----- 32 | -------------------------------------------------------------------------------- /src/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koenvervloesem/snipskit/442aa70d423f8371de5f3557b8e2fc6a7c6e19bf/src/conftest.py -------------------------------------------------------------------------------- /src/snipskit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koenvervloesem/snipskit/442aa70d423f8371de5f3557b8e2fc6a7c6e19bf/src/snipskit/__init__.py -------------------------------------------------------------------------------- /src/snipskit/apps.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create Snips apps. 2 | 3 | You can create a Snips app in two ways: 4 | 5 | - By subclassing :class:`snipskit.hermes.apps.HermesSnipsApp`: This creates a 6 | Snips app using the Hermes Python library. 7 | - By subclassing :class:`snipskit.mqtt.apps.MQTTSnipsApp`: This creates a Snips 8 | app using the MQTT protocol directly. 9 | 10 | :class:`.HermesSnipsApp` is a subclass of :class:`.HermesSnipsComponent` and 11 | adds `assistant` and `config` attributes for access to the assistant's 12 | configuration and the app's configuration, respectively. 13 | 14 | :class:`.MQTTSnipsApp` is a subclass of :class:`.MQTTSnipsComponent` and adds 15 | `assistant` and `config` attributes for access to the assistant's configuration 16 | and the app's configuration, respectively. 17 | 18 | Both classes include the :class:`.SnipsAppMixin` mixin of this module to read 19 | the assistant's configuration from the location defined in snips.toml. 20 | 21 | .. note:: 22 | These classes require you to have a Snips assistant installed. 23 | 24 | If you don't need access to the assistant's configuration nor an 25 | app-specific configuration, you can create a subclass of 26 | :class:`.SnipsComponent`. 27 | 28 | .. note:: 29 | If you only need access to the Snips configuration and the assistant's 30 | configuration without the need to connect to the MQTT broker, you can use 31 | the :class:`.SnipsAppMixin` class. 32 | """ 33 | 34 | from pathlib import Path 35 | 36 | from snipskit.config import AssistantConfig, SnipsConfig 37 | 38 | 39 | class SnipsAppMixin: 40 | """A `mixin`_ for classes that should have access to a Snips app's 41 | configuration, the Snips assistant's configuration and the Snips 42 | configuration. 43 | 44 | The classes :class:`.HermesSnipsApp` and :class:`.MQTTSnipsApp` include 45 | this mixin, primarily to avoid code duplication for reading the assistant's 46 | configuration from the location defined in snips.toml. 47 | 48 | You can also subclass this mixin for easy access to the Snips configuration 49 | and the assistant's configuration without the need to connect to the MQTT 50 | broker. 51 | 52 | Attributes: 53 | assistant (:class:`.AssistantConfig`): The assistant configuration. Its 54 | location is read from the Snips configuration file. 55 | config (:class:`.AppConfig`): The app configuration. 56 | snips (:class:`.SnipsConfig`): The Snips configuration. 57 | 58 | .. _`mixin`: https://en.wikipedia.org/wiki/Mixin 59 | """ 60 | 61 | def __init__(self, snips=None, config=None): 62 | """Initialize the mixin by setting the :attr:`config`, :attr:`snips` 63 | and :attr:`assistant` attributes. 64 | 65 | To initialize the :attr:`assistant` attribute, the location of the 66 | assistant is read from the Snips configuration file. If the location is 67 | not specified there, a default :class:`.AssistantConfig` object is 68 | created. 69 | 70 | Args: 71 | snips (:class:`.SnipsConfig`, optional): a Snips configuration. 72 | If the argument is not specified, a default 73 | :class:`.SnipsConfig` object is created for a locally installed 74 | instance of Snips. 75 | 76 | config (:class:`.AppConfig`, optional): an app configuration. If 77 | the argument is not specified, the app has no configuration. 78 | 79 | .. versionadded:: 0.3.0 80 | """ 81 | self.config = config 82 | 83 | if not snips: 84 | snips = SnipsConfig() 85 | self.snips = snips 86 | 87 | try: 88 | assistant_directory = snips['snips-common']['assistant'] 89 | assistant_file = Path(assistant_directory) / 'assistant.json' 90 | self.assistant = AssistantConfig(assistant_file) 91 | except KeyError: 92 | self.assistant = AssistantConfig() 93 | -------------------------------------------------------------------------------- /src/snipskit/components.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create components to communicate with Snips. 2 | 3 | A Snips component (a subclass of :class:`.SnipsComponent`) can communicate with 4 | Snips services. There are two subclasses of :class:`.SnipsComponent` in other 5 | modules: 6 | 7 | - :class:`snipskit.hermes.components.HermesSnipsComponent`: a Snips component 8 | using the Hermes Python library; 9 | - :class:`snipskit.mqtt.components.MQTTSnipsComponent`: a Snips component using 10 | the MQTT protocol directly. 11 | 12 | .. note:: 13 | If you want to create a Snips app with access to an assistant's 14 | configuration and a configuration for the app, you need to instantiate a 15 | :class:`.HermesSnipsApp` or :class:`.MQTTSnipsApp` object, which is a 16 | subclass of :class:`.HermesSnipsComponent` or :class:`.MQTTSnipsComponent` 17 | respectively, adding `assistant` and `config` attributes. See the module 18 | :mod:`snipskit.apps`. 19 | """ 20 | 21 | from abc import ABCMeta, abstractmethod 22 | 23 | from snipskit.config import SnipsConfig 24 | 25 | 26 | class SnipsComponent(metaclass=ABCMeta): 27 | """Connect with a Snips instance and give access to a Snips configuration. 28 | 29 | This is an `abstract base class`_. You don't instantiate an object of this 30 | class, but an object of one of its subclasses: 31 | :class:`.HermesSnipsComponent` or :class:`.MQTTSnipsComponent` 32 | 33 | .. _`abstract base class`: https://docs.python.org/3/glossary.html#term-abstract-base-class 34 | 35 | Attributes: 36 | snips (:class:`.SnipsConfig`): The Snips configuration. 37 | """ 38 | 39 | def __init__(self, snips=None): 40 | """Initialize a Snips component. 41 | 42 | Args: 43 | snips (:class:`.SnipsConfig`, optional): a Snips configuration. 44 | If the argument is not specified, a default 45 | :class:`.SnipsConfig` object is created for a locally installed 46 | instance of Snips. 47 | """ 48 | if not snips: 49 | snips = SnipsConfig() 50 | self.snips = snips 51 | 52 | self._connect() 53 | self.initialize() 54 | self._start() 55 | 56 | @abstractmethod 57 | def _connect(self): 58 | """Connect with Snips. 59 | 60 | This method should be implemented in a subclass of 61 | :class:`.SnipsComponent`. 62 | """ 63 | 64 | def initialize(self): 65 | """If you have to initialize a component in your subclass of 66 | :class:`.SnipsComponent`, add your code in this method. It will be 67 | called between connecting to Snips and starting the event loop. 68 | """ 69 | 70 | @abstractmethod 71 | def _start(self): 72 | """Connect with Snips. 73 | 74 | This method should be implemented in a subclass of 75 | :class:`.SnipsComponent`. 76 | """ 77 | -------------------------------------------------------------------------------- /src/snipskit/config.py: -------------------------------------------------------------------------------- 1 | """This module gives a way to access the configuration of a locally installed 2 | instance of Snips, a Snips assistant and a Snips skill. 3 | 4 | Classes: 5 | 6 | - :class:`.AppConfig`: Gives access to the configuration of a Snips app, 7 | stored in an INI file. 8 | - :class:`.AssistantConfig`: Gives access to the configuration of a Snips 9 | assistant, stored in a JSON file. 10 | - :class:`.MQTTAuthConfig`: Represents the authentication settings for a 11 | connection to an MQTT broker. 12 | - :class:`.MQTTConfig`: Represents the configuration for a connection to an 13 | MQTT broker. 14 | - :class:`.MQTTTLSConfig`: Represents the TLS settings for a connection to an 15 | MQTT broker. 16 | - :class:`.SnipsConfig`: Gives access to the configuration of a locally 17 | installed instance of Snips, stored in a TOML file. 18 | """ 19 | 20 | from collections import UserDict 21 | from configparser import ConfigParser 22 | import json 23 | from pathlib import Path 24 | 25 | from snipskit.exceptions import AssistantConfigNotFoundError, \ 26 | SnipsConfigNotFoundError 27 | from snipskit.tools import find_path 28 | import toml 29 | 30 | SEARCH_PATH_SNIPS = ['/etc/snips.toml', '/usr/local/etc/snips.toml'] 31 | SEARCH_PATH_ASSISTANT = ['/usr/share/snips/assistant/assistant.json', 32 | '/usr/local/share/snips/assistant/assistant.json'] 33 | 34 | DEFAULT_BROKER = 'localhost:1883' 35 | 36 | 37 | class AppConfig(ConfigParser): 38 | """This class gives access to the configuration of a Snips app as a 39 | :class:`configparser.ConfigParser` object. 40 | 41 | Attributes: 42 | filename (str): The filename of the configuration file. 43 | 44 | Example: 45 | >>> config = AppConfig() # Use default file config.ini 46 | >>> config['secret']['api-key'] 47 | 'foobar' 48 | >>> config['secret']['api-key'] = 'barfoo' 49 | >>> config.write() 50 | """ 51 | 52 | def __init__(self, filename=None): 53 | """Initialize an :class:`.AppConfig` object. 54 | 55 | Args: 56 | filename (optional): A filename for the configuration file. If the 57 | filename is not specified, the default filename 'config.ini' 58 | in the current directory is chosen. 59 | """ 60 | 61 | ConfigParser.__init__(self) 62 | 63 | if not filename: 64 | filename = 'config.ini' 65 | 66 | self.filename = filename 67 | 68 | config_path = Path(filename) 69 | 70 | with config_path.open('rt') as config: 71 | self.read_file(config) 72 | 73 | def write(self, *args, **kwargs): 74 | """Write the current configuration to the app's configuration file. 75 | 76 | If this method is called without any arguments, the configuration is 77 | written to the :attr:`filename` attribute of this object. 78 | 79 | If this method is called with any arguments, they are forwarded to the 80 | :meth:`configparser.ConfigParser.write` method of its superclass. 81 | """ 82 | if len(args) + len(kwargs): 83 | super().write(*args, **kwargs) 84 | else: 85 | with Path(self.filename).open('wt') as config: 86 | super().write(config) 87 | 88 | 89 | class AssistantConfig(UserDict): 90 | """This class gives access to the configuration of a Snips assistant as a 91 | :class:`dict`. 92 | 93 | Attributes: 94 | filename (str): The filename of the configuration file. 95 | 96 | Example: 97 | >>> assistant = AssistantConfig('/opt/assistant/assistant.json') 98 | >>> assistant['language'] 99 | 'en' 100 | """ 101 | 102 | def __init__(self, filename=None): 103 | """Initialize an :class:`.AssistantConfig` object. 104 | 105 | Args: 106 | filename (str, optional): The path of the assistant's configuration 107 | file. 108 | 109 | If the argument is not specified, the configuration file is 110 | searched for in the following locations, in this order: 111 | 112 | - /usr/share/snips/assistant/assistant.json 113 | - /usr/local/share/snips/assistant/assistant.json 114 | 115 | Raises: 116 | :exc:`FileNotFoundError`: If the specified filename doesn't exist. 117 | 118 | :exc:`.AssistantConfigNotFoundError`: If there's no assistant 119 | configuration found in the search path. 120 | 121 | :exc:`json.JSONDecodeError`: If the assistant's configuration 122 | file doesn't have a valid JSON syntax. 123 | 124 | Examples: 125 | >>> assistant = AssistantConfig() # default configuration 126 | >>> assistant2 = AssistantConfig('/opt/assistant/assistant.json') 127 | """ 128 | if filename: 129 | self.filename = filename 130 | assistant_file = Path(filename) 131 | else: 132 | self.filename = find_path(SEARCH_PATH_ASSISTANT) 133 | 134 | if not self.filename: 135 | raise AssistantConfigNotFoundError() 136 | 137 | assistant_file = Path(self.filename) 138 | 139 | # Open the assistant's file. This raises FileNotFoundError if the 140 | # file doesn't exist. 141 | with assistant_file.open('rt') as json_file: 142 | # Create a dict with our configuration. 143 | # This raises JSONDecodeError if the file doesn't have a 144 | # valid JSON syntax. 145 | UserDict.__init__(self, json.load(json_file)) 146 | 147 | 148 | class MQTTAuthConfig: 149 | """This class represents the authentication settings for a connection to an 150 | MQTT broker. 151 | 152 | .. versionadded:: 0.6.0 153 | 154 | Attributes: 155 | username (str): The username to authenticate to the MQTT broker. `None` 156 | if there's no authentication. 157 | password (str): The password to authenticate to the MQTT broker. Can be 158 | `None`. 159 | """ 160 | 161 | def __init__(self, username=None, password=None): 162 | """Initialize a :class:`.MQTTAuthConfig` object. 163 | 164 | Args: 165 | username (str, optional): The username to authenticate to the MQTT 166 | broker. `None` if there's no authentication. 167 | password (str, optional): The password to authenticate to the MQTT 168 | broker. Can be `None`. 169 | 170 | All arguments are optional. 171 | """ 172 | self.username = username 173 | self.password = password 174 | 175 | @property 176 | def enabled(self): 177 | """Check whether authentication is enabled. 178 | 179 | Returns: 180 | bool: True if the username is not `None`. 181 | """ 182 | return self.username is not None 183 | 184 | 185 | class MQTTTLSConfig: 186 | """This class represents the TLS settings for a connection to an MQTT 187 | broker. 188 | 189 | .. versionadded:: 0.6.0 190 | 191 | Attributes: 192 | hostname (str, optional): The TLS hostname of the MQTT broker. 193 | `None` if no TLS is used. 194 | ca_file (str, optional): Path to the Certificate Authority file. 195 | Can be `None`. 196 | ca_path (str, optional): Path to the Certificate Authority files. 197 | Can be `None`. 198 | client_key (str, optional): Path to the private key file. Can be 199 | `None`. 200 | client_cert (str, optional): Path to the client certificate file. 201 | Can be `None`. 202 | disable_root_store (bool, optional): Whether the TLS root store is 203 | disabled. 204 | """ 205 | 206 | def __init__(self, hostname=None, ca_file=None, ca_path=None, 207 | client_key=None, client_cert=None, disable_root_store=False): 208 | """Initialize a :class:`.MQTTTLSConfig` object. 209 | 210 | Args: 211 | hostname (str, optional): The TLS hostname of the MQTT broker. 212 | `None` if no TLS is used. 213 | ca_file (str, optional): Path to the Certificate Authority 214 | file. Can be `None`. 215 | ca_path (str, optional): Path to the Certificate Authority 216 | files. Can be `None`. 217 | client_key (str, optional): Path to the private key file. Can 218 | be `None`. 219 | client_cert (str, optional): Path to the client certificate 220 | file. Can be `None`. 221 | disable_root_store (bool, optional): Whether the TLS root store 222 | is disabled. Defaults to `False`. 223 | 224 | All arguments are optional. 225 | """ 226 | self.hostname = hostname 227 | self.ca_file = ca_file 228 | self.ca_path = ca_path 229 | self.client_key = client_key 230 | self.client_cert = client_cert 231 | self.disable_root_store = disable_root_store 232 | 233 | @property 234 | def enabled(self): 235 | """Check whether TLS is enabled. 236 | 237 | Returns: 238 | bool: True if the hostname is not `None`. 239 | """ 240 | return self.hostname is not None 241 | 242 | 243 | class MQTTConfig: 244 | """This class represents the configuration for a connection to an 245 | MQTT broker. 246 | 247 | .. versionadded:: 0.4.0 248 | 249 | Attributes: 250 | broker_address (str, optional): The address of the MQTT broker, in the 251 | form 'host:port'. 252 | auth (:class:`.MQTTAuthConfig`, optional): The authentication 253 | settings (username and password) for the MQTT broker. 254 | tls (:class:`.MQTTTLSConfig`, optional): The TLS settings for the MQTT 255 | broker. 256 | 257 | """ 258 | def __init__(self, broker_address='localhost:1883', auth=None, tls=None): 259 | """Initialize a :class:`.MQTTConfig` object. 260 | 261 | Args: 262 | broker_address (str, optional): The address of the MQTT broker, in 263 | the form 'host:port'. 264 | auth (:class:`.MQTTAuthConfig`, optional): The authentication 265 | settings (username and password) for the MQTT broker. Defaults 266 | to a default :class:`.MQTTAuthConfig` object. 267 | tls (:class:`.MQTTTLSConfig`, optional): The TLS settings for the 268 | MQTT broker. Defaults to a default :class:`.MQTTTLSConfig` 269 | object. 270 | 271 | All arguments are optional. 272 | """ 273 | self.broker_address = broker_address 274 | 275 | if auth is None: 276 | self.auth = MQTTAuthConfig() 277 | else: 278 | self.auth = auth 279 | 280 | if tls is None: 281 | self.tls = MQTTTLSConfig() 282 | else: 283 | self.tls = tls 284 | 285 | 286 | class SnipsConfig(UserDict): 287 | """This class gives access to a snips.toml configuration file as a 288 | :class:`dict`. 289 | 290 | Attributes: 291 | filename (str): The filename of the configuration file. 292 | mqtt (:class:`.MQTTConfig`): The MQTT options of the Snips 293 | configuration. 294 | 295 | Example: 296 | >>> snips = SnipsConfig() 297 | >>> snips['snips-hotword']['audio'] 298 | ['default@mqtt', 'bedroom@mqtt'] 299 | """ 300 | 301 | def __init__(self, filename=None): 302 | """Initialize a :class:`.SnipsConfig` object. 303 | 304 | The :attr:`mqtt` attribute is initialized with the MQTT connection 305 | settings from the configuration file, or the default value 306 | 'localhost:1883' for the broker address if the settings are not 307 | specified. 308 | 309 | Args: 310 | filename (str, optional): The full path of the config file. If 311 | the argument is not specified, the file snips.toml is searched 312 | for in the following locations, in this order: 313 | 314 | - /etc/snips.toml 315 | - /usr/local/etc/snips.toml 316 | 317 | Raises: 318 | :exc:`FileNotFoundError`: If :attr:`filename` is specified but 319 | doesn't exist. 320 | 321 | :exc:`.SnipsConfigNotFoundError`: If there's no snips.toml found 322 | in the search path. 323 | 324 | :exc:`TomlDecodeError`: If :attr:`filename` doesn't have a valid 325 | TOML syntax. 326 | 327 | Examples: 328 | >>> snips = SnipsConfig() # Tries to find snips.toml. 329 | >>> snips_local = SnipsConfig('/usr/local/etc/snips.toml') 330 | """ 331 | if filename: 332 | if not Path(filename).is_file(): 333 | raise FileNotFoundError('{} not found'.format(filename)) 334 | self.filename = filename 335 | else: 336 | self.filename = find_path(SEARCH_PATH_SNIPS) 337 | if not self.filename: 338 | raise SnipsConfigNotFoundError() 339 | 340 | # Create a dict with our configuration. 341 | # This raises TomlDecodeError if the file doesn't have a valid TOML 342 | # syntax. 343 | UserDict.__init__(self, toml.load(self.filename)) 344 | 345 | # Now find all the MQTT options in the configuration file and use 346 | # sensible defaults for options that aren't specified. 347 | try: 348 | # Basic MQTT connection settings. 349 | broker_address = self['snips-common'].get('mqtt', DEFAULT_BROKER) 350 | 351 | # MQTT authentication 352 | username = self['snips-common'].get('mqtt_username', None) 353 | password = self['snips-common'].get('mqtt_password', None) 354 | 355 | # MQTT TLS configuration 356 | tls_hostname = self['snips-common'].get('mqtt_tls_hostname', None) 357 | tls_ca_file = self['snips-common'].get('mqtt_tls_cafile', None) 358 | tls_ca_path = self['snips-common'].get('mqtt_tls_capath', None) 359 | tls_client_key = self['snips-common'].get('mqtt_tls_client_key', 360 | None) 361 | tls_client_cert = self['snips-common'].get('mqtt_tls_client_cert', 362 | None) 363 | tls_disable_root_store = self['snips-common'].get('mqtt_tls_disable_root_store', 364 | False) 365 | 366 | # Store the MQTT connection settings in an MQTTConfig object. 367 | self.mqtt = MQTTConfig(broker_address, 368 | MQTTAuthConfig(username, password), 369 | MQTTTLSConfig(tls_hostname, tls_ca_file, 370 | tls_ca_path, tls_client_key, 371 | tls_client_cert, 372 | tls_disable_root_store)) 373 | except KeyError: 374 | # The 'snips-common' section isn't in the configuration file, so we 375 | # use a sensible default: 'localhost:1883'. 376 | self.mqtt = MQTTConfig() 377 | -------------------------------------------------------------------------------- /src/snipskit/exceptions.py: -------------------------------------------------------------------------------- 1 | """This module contains exceptions defined for the SnipsKit library.""" 2 | 3 | 4 | class SnipsKitError(Exception): 5 | """Base class for exceptions raised by SnipsKit code. 6 | 7 | By catching this exception type, you catch all exceptions that are 8 | defined by the SnipsKit library.""" 9 | 10 | 11 | class AssistantConfigNotFoundError(SnipsKitError): 12 | """Raised when the assistant's configuration is not found in the search 13 | path. 14 | """ 15 | 16 | 17 | class SnipsConfigNotFoundError(SnipsKitError): 18 | """Raised when there's no snips.toml found in the search path.""" 19 | -------------------------------------------------------------------------------- /src/snipskit/hermes/__init__.py: -------------------------------------------------------------------------------- 1 | """This module contains classes to create components that communicate with 2 | Snips services using the Hermes Python library. 3 | """ 4 | -------------------------------------------------------------------------------- /src/snipskit/hermes/apps.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create Snips apps using the Hermes Python 2 | library. 3 | 4 | Example: 5 | 6 | .. code-block:: python 7 | 8 | from snipskit.hermes.apps import HermesSnipsApp 9 | from snipskit.hermes.decorators import intent 10 | 11 | 12 | class SimpleSnipsApp(HermesSnipsApp): 13 | 14 | def initialize(self): 15 | print('App initialized') 16 | 17 | @intent('User:ExampleIntent') 18 | def example_intent(self, hermes, intent_message): 19 | print('I received intent "User:ExampleIntent"') 20 | """ 21 | 22 | from snipskit.apps import SnipsAppMixin 23 | from snipskit.hermes.components import HermesSnipsComponent 24 | 25 | 26 | class HermesSnipsApp(SnipsAppMixin, HermesSnipsComponent): 27 | """A Snips app using the Hermes Python library. 28 | 29 | Attributes: 30 | assistant (:class:`.AssistantConfig`): The assistant configuration. Its 31 | location is read from the Snips configuration file and otherwise 32 | a default location is used. 33 | config (:class:`.AppConfig`): The app configuration. 34 | snips (:class:`.SnipsConfig`): The Snips configuration. 35 | hermes (:class:`hermes_python.hermes.Hermes`): The Hermes object. 36 | 37 | """ 38 | 39 | def __init__(self, snips=None, config=None): 40 | """Initialize a Snips app using the Hermes protocol. 41 | 42 | Args: 43 | snips (:class:`.SnipsConfig`, optional): a Snips configuration. 44 | If the argument is not specified, a default 45 | :class:`.SnipsConfig` object is created for a locally installed 46 | instance of Snips. 47 | 48 | config (:class:`.AppConfig`, optional): an app configuration. If 49 | the argument is not specified, the app has no configuration. 50 | 51 | """ 52 | SnipsAppMixin.__init__(self, snips, config) 53 | HermesSnipsComponent.__init__(self, snips) 54 | -------------------------------------------------------------------------------- /src/snipskit/hermes/components.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create components to communicate with Snips 2 | using the Hermes Python library. 3 | 4 | .. note:: 5 | If you want to create a Snips app with access to an assistant's 6 | configuration and a configuration for the app, you need to instantiate a 7 | :class:`.HermesSnipsApp` object, which is a subclass of 8 | :class:`.HermesSnipsComponent` and adds `assistant` and `config` attributes. 9 | 10 | Example: 11 | 12 | .. code-block:: python 13 | 14 | from snipskit.hermes.components import HermesSnipsComponent 15 | from snipskit.hermes.decorators import intent 16 | 17 | 18 | class SimpleSnipsComponent(HermesSnipsComponent): 19 | 20 | def initialize(self): 21 | print('Component initialized') 22 | 23 | @intent('User:ExampleIntent') 24 | def example_intent(self, hermes, intent_message): 25 | print('I received intent "User:ExampleIntent"') 26 | """ 27 | 28 | from hermes_python.hermes import Hermes 29 | from hermes_python.ontology import MqttOptions 30 | from snipskit.components import SnipsComponent 31 | 32 | 33 | class HermesSnipsComponent(SnipsComponent): 34 | """A Snips component using the Hermes Python library. 35 | 36 | Attributes: 37 | snips (:class:`.SnipsConfig`): The Snips configuration. 38 | hermes (:class:`hermes_python.hermes.Hermes`): The Hermes object. 39 | 40 | """ 41 | 42 | def _connect(self): 43 | """Connect with the MQTT broker referenced in the snips configuration 44 | file. 45 | """ 46 | mqtt_options = self.snips.mqtt 47 | self.hermes = Hermes(mqtt_options=MqttOptions(mqtt_options.broker_address, 48 | mqtt_options.auth.username, 49 | mqtt_options.auth.password, 50 | mqtt_options.tls.hostname, 51 | mqtt_options.tls.ca_file, 52 | mqtt_options.tls.ca_path, 53 | mqtt_options.tls.client_key, 54 | mqtt_options.tls.client_cert, 55 | mqtt_options.tls.disable_root_store)) 56 | self.hermes.connect() 57 | self._register_callbacks() 58 | 59 | def _start(self): 60 | """Start the event loop to the Hermes object so the component 61 | starts listening to events and the callback methods are called. 62 | """ 63 | self.hermes.loop_forever() 64 | 65 | def _register_callbacks(self): 66 | """Subscribe to the Hermes events we're interested in. 67 | 68 | Each method with an attribute set by a decorator is registered as a 69 | callback for the corresponding event. 70 | """ 71 | for name in dir(self): 72 | callable_name = getattr(self, name) 73 | 74 | # If we have given the method a subscribe_method attribute by one 75 | # of the decorators. 76 | if hasattr(callable_name, 'subscribe_method'): 77 | subscribe_method = getattr(callable_name, 'subscribe_method') 78 | # If we have given the method a subscribe_parameter attribute 79 | # by one of the decorators. 80 | if hasattr(callable_name, 'subscribe_parameter'): 81 | subscribe_parameter = getattr(callable_name, 82 | 'subscribe_parameter') 83 | # Register callable_name as a callback with 84 | # subscribe_method and subscribe_parameter as a parameter. 85 | getattr(self.hermes, subscribe_method)(subscribe_parameter, 86 | callable_name) 87 | else: 88 | # Register callable_name as a callback with 89 | # subscribe_method. 90 | getattr(self.hermes, subscribe_method)(callable_name) 91 | -------------------------------------------------------------------------------- /src/snipskit/hermes/decorators.py: -------------------------------------------------------------------------------- 1 | """This module contains decorators_ to apply to methods of a 2 | :class:`.HermesSnipsComponent` object. 3 | 4 | .. _decorators: https://docs.python.org/3/glossary.html#term-decorator 5 | 6 | By applying one of these decorators to a method of a 7 | :class:`.HermesSnipsComponent` object, this method is registered as a callback 8 | to the corresponding event. When the event fires (e.g. an intent happens), the 9 | method is called. 10 | 11 | Example: 12 | 13 | .. code-block:: python 14 | 15 | from snipskit.hermes.apps import HermesSnipsApp 16 | from snipskit.hermes.decorators import intent 17 | 18 | class SimpleSnipsApp(HermesSnipsApp): 19 | 20 | @intent('User:ExampleIntent') 21 | def example_intent(self, hermes, intent_message): 22 | print('I received intent "User:ExampleIntent"') 23 | """ 24 | 25 | 26 | def intent(intent_name): 27 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 28 | to register it as a callback to be triggered when the intent `intent_name` 29 | is recognized. 30 | 31 | Args: 32 | intent_name (str): The intent you want to subscribe to. 33 | 34 | """ 35 | def inner(method): 36 | """The method to apply the decorator to.""" 37 | method.subscribe_method = 'subscribe_intent' 38 | method.subscribe_parameter = intent_name 39 | return method 40 | return inner 41 | 42 | 43 | def intent_not_recognized(method): 44 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 45 | to register it as a callback to be triggered when the dialogue manager 46 | doesn't recognize an intent. 47 | """ 48 | method.subscribe_method = 'subscribe_intent_not_recognized' 49 | return method 50 | 51 | 52 | def intents(method): 53 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 54 | to register it as a callback to be triggered everytime an intent is 55 | recognized. 56 | """ 57 | method.subscribe_method = 'subscribe_intents' 58 | return method 59 | 60 | 61 | def session_ended(method): 62 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 63 | to register it as a callback to be triggered when the dialogue manager ends 64 | a session. 65 | """ 66 | method.subscribe_method = 'subscribe_session_ended' 67 | return method 68 | 69 | 70 | def session_queued(method): 71 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 72 | to register it as a callback to be triggered when the dialogue manager 73 | queues the current session. 74 | """ 75 | method.subscribe_method = 'subscribe_session_queued' 76 | return method 77 | 78 | 79 | def session_started(method): 80 | """Apply this decorator to a method of class :class:`.HermesSnipsComponent` 81 | to register it as a callback to be triggered when the dialogue manager 82 | queues starts a new session. 83 | """ 84 | method.subscribe_method = 'subscribe_session_started' 85 | return method 86 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | """This module contains classes to create components that communicate with 2 | Snips services using the MQTT protocol directly. 3 | """ 4 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/apps.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create Snips apps using the MQTT protocol 2 | directly. 3 | 4 | Example: 5 | 6 | .. code-block:: python 7 | 8 | from snipskit.mqtt.apps import MQTTSnipsApp 9 | from snipskit.mqtt.decorators import topic 10 | 11 | 12 | class SimpleSnipsApp(MQTTSnipsApp): 13 | 14 | def initialize(self): 15 | print('App initialized') 16 | 17 | @topic('hermes/hotword/toggleOn') 18 | def hotword_on(self, topic, payload): 19 | print('Hotword on {} is toggled on.'.format(payload['siteId'])) 20 | """ 21 | 22 | from snipskit.apps import SnipsAppMixin 23 | from snipskit.mqtt.components import MQTTSnipsComponent 24 | 25 | 26 | class MQTTSnipsApp(SnipsAppMixin, MQTTSnipsComponent): 27 | """A Snips app using the MQTT protocol directly. 28 | 29 | Attributes: 30 | assistant (:class:`.AssistantConfig`): The assistant configuration. Its 31 | location is read from the Snips configuration file and otherwise a 32 | default location is used. 33 | config (:class:`.AppConfig`): The app configuration. 34 | snips (:class:`.SnipsConfig`): The Snips configuration. 35 | mqtt (`paho.mqtt.client.Client`_): The MQTT client object. 36 | 37 | .. _`paho.mqtt.client.Client`: https://www.eclipse.org/paho/clients/python/docs/#client 38 | 39 | """ 40 | 41 | def __init__(self, snips=None, config=None): 42 | """Initialize a Snips app using the MQTT protocol. 43 | 44 | Args: 45 | snips (:class:`.SnipsConfig`, optional): a Snips configuration. 46 | If the argument is not specified, a default 47 | :class:`.SnipsConfig` object is created for a locally installed 48 | instance of Snips. 49 | 50 | config (:class:`.AppConfig`, optional): an app configuration. If 51 | the argument is not specified, the app has no configuration. 52 | 53 | """ 54 | SnipsAppMixin.__init__(self, snips, config) 55 | MQTTSnipsComponent.__init__(self, snips) 56 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/client.py: -------------------------------------------------------------------------------- 1 | """This module contains helper functions to use the Paho MQTT library with the 2 | MQTT broker defined in a :class:`.MQTTConfig` object. 3 | """ 4 | import json 5 | 6 | from paho.mqtt.publish import single 7 | 8 | 9 | def auth_params(mqtt_config): 10 | """Return the authentication parameters from a :class:`.MQTTConfig` 11 | object. 12 | 13 | Args: 14 | mqtt_config (:class:`.MQTTConfig`): The MQTT connection settings. 15 | 16 | Returns: 17 | dict: A dict {'username': username, 'password': password} with the 18 | authentication parameters, or None if no authentication is used. 19 | 20 | .. versionadded:: 0.6.0 21 | """ 22 | # Set up a dict containing authentication parameters for the MQTT client. 23 | if mqtt_config.auth.username: 24 | # The password can be None. 25 | return {'username': mqtt_config.auth.username, 26 | 'password': mqtt_config.auth.password} 27 | # Or use no authentication. 28 | else: 29 | return None 30 | 31 | 32 | def host_port(mqtt_config): 33 | """Return the host and port from a :class:`.MQTTConfig` object. 34 | 35 | Args: 36 | mqtt_config (:class:`.MQTTConfig`): The MQTT connection settings. 37 | 38 | Returns: 39 | (str, int): A tuple with the host and port defined in the MQTT 40 | connection settings. 41 | 42 | .. versionadded:: 0.6.0 43 | """ 44 | host_port = mqtt_config.broker_address.split(':') 45 | 46 | if mqtt_config.tls.hostname: 47 | host = mqtt_config.tls.hostname 48 | else: 49 | host = host_port[0] 50 | 51 | port = int(host_port[1]) 52 | 53 | return (host, port) 54 | 55 | 56 | def tls_params(mqtt_config): 57 | """Return the TLS configuration parameters from a :class:`.MQTTConfig` 58 | object. 59 | 60 | Args: 61 | mqtt_config (:class:`.MQTTConfig`): The MQTT connection settings. 62 | 63 | Returns: 64 | dict: A dict {'ca_certs': ca_certs, 'certfile': certfile, 65 | 'keyfile': keyfile} with the TLS configuration parameters, or None if 66 | no TLS connection is used. 67 | 68 | .. versionadded:: 0.6.0 69 | """ 70 | # Set up a dict containing TLS configuration parameters for the MQTT 71 | # client. 72 | if mqtt_config.tls.hostname: 73 | return {'ca_certs': mqtt_config.tls.ca_file, 74 | 'certfile': mqtt_config.tls.client_cert, 75 | 'keyfile': mqtt_config.tls.client_key} 76 | # Or don't use TLS. 77 | else: 78 | return None 79 | 80 | 81 | def connect(client, mqtt_config, keepalive=60, bind_address=''): 82 | """Connect to an MQTT broker with the MQTT connection settings defined in 83 | an :class:`.MQTTConfig` object. 84 | 85 | Args: 86 | client (`paho.mqtt.client.Client`_): The MQTT client object. 87 | mqtt_config (:class:`.MQTTConfig`): The MQTT connection settings. 88 | keepalive (int, optional): The maximum period in seconds allowed 89 | between communications with the broker. Defaults to 60. 90 | bind_address (str, optional): The IP address of a local network 91 | interface to bind this client to, assuming multiple interfaces 92 | exist. Defaults to ''. 93 | 94 | .. _`paho.mqtt.client.Client`: https://www.eclipse.org/paho/clients/python/docs/#client 95 | 96 | .. versionadded:: 0.6.0 97 | """ 98 | host, port = host_port(mqtt_config) 99 | 100 | # Set up MQTT authentication. 101 | auth = auth_params(mqtt_config) 102 | if auth: 103 | client.username_pw_set(auth['username'], auth['password']) 104 | 105 | # Set up an MQTT TLS connection. 106 | tls = tls_params(mqtt_config) 107 | if tls: 108 | client.tls_set(ca_certs=tls['ca_certs'], 109 | certfile=tls['certfile'], 110 | keyfile=tls['keyfile']) 111 | 112 | client.connect(host, port, keepalive, bind_address) 113 | 114 | 115 | def publish_single(mqtt_config, topic, payload=None, json_encode=True): 116 | """Publish a single message to the MQTT broker with the connection settings 117 | defined in an :class:`.MQTTConfig` object, and then disconnect cleanly. 118 | 119 | .. note:: The Paho MQTT library supports many more arguments when 120 | publishing a single message. Other arguments than `topic` and `payload` 121 | are not supported by this helper function: it’s aimed at just the 122 | simplest use cases. 123 | 124 | Args: 125 | mqtt_config (:class:`.MQTTConfig`): The MQTT connection settings. 126 | topic (str): The topic string to which the payload will be published. 127 | payload (str, optional): The payload to be published. If '' or None, a 128 | zero length payload will be published. 129 | json_encode (bool, optional): Whether or not the payload is a dict 130 | that will be encoded as a JSON string. The default value is 131 | True. Set this to False if you want to publish a binary payload 132 | as-is. 133 | 134 | .. versionadded:: 0.6.0 135 | """ 136 | host, port = host_port(mqtt_config) 137 | auth = auth_params(mqtt_config) 138 | tls = tls_params(mqtt_config) 139 | 140 | if json_encode: 141 | payload = json.dumps(payload) 142 | 143 | single(topic, payload, hostname=host, port=port, auth=auth, tls=tls) 144 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/components.py: -------------------------------------------------------------------------------- 1 | """This module contains a class to create components to communicate with Snips 2 | using the MQTT protocol directly. 3 | 4 | .. note:: 5 | If you want to create a Snips app with access to an assistant's 6 | configuration and a configuration for the app, you need to instantiate a 7 | :class:`.MQTTSnipsApp` object, which is a subclass of 8 | :class:`.MQTTSnipsComponent` and adds `assistant` and `config` attributes. 9 | 10 | Example: 11 | 12 | .. code-block:: python 13 | 14 | from snipskit.mqtt.components import MQTTSnipsComponent 15 | from snipskit.mqtt.decorators import topic 16 | 17 | 18 | class SimpleSnipsComponent(MQTTSnipsComponent): 19 | 20 | def initialize(self): 21 | print('Component initialized') 22 | 23 | @topic('hermes/hotword/toggleOn') 24 | def hotword_on(self, topic, payload): 25 | print('Hotword on {} is toggled on.'.format(payload['siteId'])) 26 | """ 27 | import json 28 | 29 | from paho.mqtt.client import Client 30 | from snipskit.components import SnipsComponent 31 | from snipskit.mqtt.client import connect 32 | 33 | 34 | class MQTTSnipsComponent(SnipsComponent): 35 | """A Snips component using the MQTT protocol directly. 36 | 37 | Attributes: 38 | snips (:class:`.SnipsConfig`): The Snips configuration. 39 | mqtt (`paho.mqtt.client.Client`_): The MQTT client object. 40 | 41 | .. _`paho.mqtt.client.Client`: https://www.eclipse.org/paho/clients/python/docs/#client 42 | """ 43 | 44 | def _connect(self): 45 | """Connect with the MQTT broker referenced in the Snips configuration 46 | file. 47 | """ 48 | self.mqtt = Client() 49 | self.mqtt.on_connect = self._subscribe_topics 50 | connect(self.mqtt, self.snips.mqtt) 51 | 52 | def _start(self): 53 | """Start the event loop to the MQTT broker so the component starts 54 | listening to MQTT topics and the callback methods are called. 55 | """ 56 | self.mqtt.loop_forever() 57 | 58 | def _subscribe_topics(self, client, userdata, flags, connection_result): 59 | """Subscribe to the MQTT topics we're interested in. 60 | 61 | Each method with an attribute set by a 62 | :func:`snipskit.decorators.mqtt.topic` decorator is registered as a 63 | callback for the corresponding topic. 64 | """ 65 | for name in dir(self): 66 | callable_name = getattr(self, name) 67 | if hasattr(callable_name, 'topic'): 68 | self.mqtt.subscribe(getattr(callable_name, 'topic')) 69 | self.mqtt.message_callback_add(getattr(callable_name, 'topic'), 70 | callable_name) 71 | 72 | def publish(self, topic, payload, json_encode=True): 73 | """Publish a payload on an MQTT topic on the MQTT broker of this object. 74 | 75 | Args: 76 | topic (str): The MQTT topic to publish the payload on. 77 | payload (str): The payload to publish. 78 | json_encode (bool, optional): Whether or not the payload is a dict 79 | that will be encoded as a JSON string. The default value is 80 | True. Set this to False if you want to publish a binary payload 81 | as-is. 82 | 83 | Returns: 84 | :class:`paho.mqtt.MQTTMessageInfo`: Information about the 85 | publication of the message. 86 | 87 | .. versionadded:: 0.5.0 88 | """ 89 | if json_encode: 90 | payload = json.dumps(payload) 91 | 92 | return self.mqtt.publish(topic, payload) 93 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/decorators.py: -------------------------------------------------------------------------------- 1 | """This module contains decorators_ to apply to methods of a 2 | :class:`.MQTTSnipsComponent` object. 3 | 4 | .. _decorators: https://docs.python.org/3/glossary.html#term-decorator 5 | 6 | By applying one of these decorators to a method of a 7 | :class:`.MQTTSnipsComponent` object, this method is registered as a callback to 8 | the corresponding event. When the event fires (e.g. an MQTT topic is 9 | published), the method is called. 10 | 11 | Example: 12 | 13 | .. code-block:: python 14 | 15 | from snipskit.mqtt.apps import MQTTSnipsApp 16 | from snipskit.mqtt.decorators import topic 17 | 18 | class SimpleSnipsApp(MQTTSnipsApp): 19 | 20 | @topic('hermes/hotword/toggleOn') 21 | def hotword_on(self, topic, payload): 22 | print('Hotword on {} is toggled on.'.format(payload['siteId'])) 23 | """ 24 | 25 | import json 26 | 27 | 28 | def topic(topic_name, json_decode=True): 29 | """Apply this decorator to a method of class :class:`.MQTTSnipsComponent` 30 | to register it as a callback to be triggered when the MQTT topic 31 | `topic_name` is published. 32 | 33 | The callback needs to have the following signature: 34 | 35 | method(self, topic, payload) 36 | 37 | Args: 38 | topic_name (str): The MQTT topic you want to subscribe to. 39 | json_decode (bool, optional): Whether or not the payload will be 40 | decoded as JSON to a dict. The default value is True. Set this to 41 | False if you want to subscribe to a topic with a binary payload. 42 | """ 43 | def wrapper(method): 44 | def wrapped(self, client, userdata, msg): 45 | """This is the callback with the signature that Paho MQTT expects. 46 | """ 47 | if json_decode: 48 | payload = json.loads(msg.payload.decode('utf-8')) 49 | else: 50 | payload = msg.payload 51 | 52 | # This is the callback with the signature that SnipsKit expects. 53 | method(self, msg.topic, payload) 54 | 55 | wrapped.topic = topic_name 56 | return wrapped 57 | return wrapper 58 | -------------------------------------------------------------------------------- /src/snipskit/mqtt/dialogue.py: -------------------------------------------------------------------------------- 1 | """This module contains some helper functions to work with MQTT messages using 2 | the `Snips dialogue API`_. 3 | 4 | .. _`Snips dialogue API`: https://docs.snips.ai/reference/dialogue 5 | """ 6 | 7 | DM_CONTINUE_SESSION = 'hermes/dialogueManager/continueSession' 8 | DM_END_SESSION = 'hermes/dialogueManager/endSession' 9 | 10 | 11 | def continue_session(session_id, text): 12 | """Return a tuple with a topic and payload for a `continueSession`_ message 13 | for the specified session ID and text. 14 | 15 | .. _`continueSession`: https://docs.snips.ai/reference/dialogue#continue-session 16 | 17 | Args: 18 | session_id (str): The session Id of the message. 19 | text (str): The text to say before continuing the session. 20 | 21 | Returns: 22 | (str, dict): A tuple of the topic and the payload to call 23 | :meth:`.MQTTSnipsComponent.publish` with. 24 | 25 | .. note:: The payload of a continueSession message can be much more 26 | complex. Other keys than sessionId and text are not supported by this 27 | helper function: it's aimed at just the simplest use cases. 28 | 29 | Example: 30 | You would use this function like this in a callback method of an 31 | :class:`.MQTTSnipsApp` object: 32 | 33 | >>> self.publish(*continue_session('mySessionId', 'myText')) 34 | 35 | This is equivalent to the much more wordy: 36 | 37 | >>> self.publish('hermes/dialogueManager/continueSession', 38 | {'sessionId': 'mySessionId', 39 | 'text': 'myText'}) 40 | 41 | .. versionadded:: 0.5.2 42 | """ 43 | return (DM_CONTINUE_SESSION, {'sessionId': session_id, 'text': text}) 44 | 45 | 46 | def end_session(session_id, text=None): 47 | """Return a tuple with a topic and payload for an `endSession`_ message for 48 | the specified session ID and text. 49 | 50 | .. _`endSession`: https://docs.snips.ai/reference/dialogue#end-session 51 | 52 | Args: 53 | session_id (str): The session Id of the message. 54 | text (str, optional): The text to say before ending the session. If 55 | this is None, the session is ended immediately after publishing 56 | this message. 57 | 58 | Returns: 59 | (str, dict): A tuple of the topic and the payload to call 60 | :meth:`.MQTTSnipsComponent.publish` with. 61 | 62 | Example: 63 | You would use this function like this in a callback method of an 64 | :class:`.MQTTSnipsApp` object: 65 | 66 | >>> self.publish(*end_session('mySessionId', 'myText')) 67 | 68 | This is equivalent to the much more wordy: 69 | 70 | >>> self.publish('hermes/dialogueManager/endSession', 71 | {'sessionId': 'mySessionId', 72 | 'text': 'myText'}) 73 | 74 | .. versionadded:: 0.5.2 75 | """ 76 | if text: 77 | payload = {'sessionId': session_id, 'text': text} 78 | else: 79 | payload = {'sessionId': session_id} 80 | 81 | return (DM_END_SESSION, payload) 82 | -------------------------------------------------------------------------------- /src/snipskit/services.py: -------------------------------------------------------------------------------- 1 | """This module contains some functions related to Snips services.""" 2 | 3 | from psutil import process_iter, NoSuchProcess 4 | import re 5 | from subprocess import check_output 6 | 7 | SNIPS_SERVICES = ['snips-analytics', 'snips-asr', 'snips-asr-google', 8 | 'snips-audio-server', 'snips-dialogue', 'snips-hotword', 9 | 'snips-injection', 'snips-nlu', 'snips-skill-server', 10 | 'snips-tts'] 11 | VERSION_FLAG = '--version' 12 | 13 | 14 | def _state(state_function): 15 | """Return a dict with the state of all Snips Services. 16 | 17 | Args: 18 | state_function: A function that returns a state for a Snips service. 19 | 20 | Returns: 21 | dict: A dict with all Snips services as keys and their state as value. 22 | """ 23 | states = [state_function(service) for service in SNIPS_SERVICES] 24 | return dict(zip(SNIPS_SERVICES, states)) 25 | 26 | 27 | def _version_output(service): 28 | """Return the output of the command `service` with the argument 29 | '--version'. 30 | 31 | Args: 32 | service (str): The service to check the version of. 33 | 34 | Returns: 35 | str: The output of the command `service` with the argument 36 | '--version', or an empty string if the command is not installed. 37 | 38 | Example: 39 | 40 | >>> _version_output('snips-nlu') 41 | 'snips-nlu 1.1.2 (0.62.3) [model_version: 0.19.0]' 42 | """ 43 | try: 44 | version_output = check_output([service, 45 | VERSION_FLAG]).decode('utf-8').strip() 46 | 47 | except FileNotFoundError: 48 | version_output = '' 49 | 50 | return version_output 51 | 52 | 53 | def is_installed(service): 54 | """Check whether the Snips service `service` is installed. 55 | 56 | Args: 57 | service (str): The Snips service to check. 58 | 59 | Returns: 60 | bool: True if the service is installed; False otherwise. 61 | 62 | Example: 63 | 64 | >>> is_installed('snips-nlu') 65 | True 66 | 67 | .. versionadded:: 0.5.3 68 | """ 69 | return bool(_version_output(service)) 70 | 71 | 72 | def is_running(service): 73 | """Check whether the Snips service `service` is running. 74 | 75 | Args: 76 | service (str): The Snips service to check. 77 | 78 | Returns: 79 | bool: True if the service is running; False otherwise. 80 | 81 | Example: 82 | 83 | >>> is_running('snips-nlu') 84 | True 85 | 86 | .. versionadded:: 0.5.3 87 | """ 88 | service_found = False 89 | for process in process_iter(): 90 | try: 91 | if service == process.name(): 92 | service_found = True 93 | break 94 | except NoSuchProcess: # Happens when the process no longer exists. 95 | pass 96 | 97 | return service_found 98 | 99 | 100 | def model_version(): 101 | """Return the model version of Snips NLU. 102 | 103 | Returns: 104 | str: The model version of Snips NLU, or an empty string if snips-nlu 105 | is not installed. 106 | 107 | Example: 108 | 109 | >>> model_version() 110 | '0.19.0' 111 | 112 | .. versionadded:: 0.5.3 113 | """ 114 | version_output = _version_output('snips-nlu') 115 | try: 116 | model_version = re.search(r'\[model_version: (.*)\]', 117 | version_output).group(1) 118 | except (AttributeError, IndexError): 119 | model_version = '' 120 | 121 | return model_version 122 | 123 | 124 | def installed(): 125 | """Return a dict with the installation state of all Snips services. 126 | 127 | Returns: 128 | dict: A dict with all Snips services as keys and their installation 129 | state (True or False) as value. 130 | 131 | .. versionadded:: 0.5.3 132 | """ 133 | return _state(is_installed) 134 | 135 | 136 | def running(): 137 | """Return a dict with the running state of all Snips services. 138 | 139 | Returns: 140 | dict: A dict with all Snips services as keys and their running state 141 | (True or False) as value. 142 | 143 | .. versionadded:: 0.5.3 144 | """ 145 | return _state(is_running) 146 | 147 | 148 | def versions(): 149 | """Return a dict with the version numbers of all Snips services. 150 | 151 | Returns: 152 | dict: A dict with all Snips services as keys and their version numbers 153 | as value. Services that are not installed have an empty string as their 154 | value. 155 | 156 | .. versionadded:: 0.5.3 157 | """ 158 | return _state(version) 159 | 160 | 161 | def version(service=None): 162 | """Return the version number of a Snips service or the Snips platform. 163 | 164 | If the `service` argument is empty, this returns the minimum value of the 165 | version numbers of all installed Snips services. 166 | 167 | Args: 168 | service (str, optional): The Snips service to check. 169 | 170 | Returns: 171 | str: The version number of the Snips service or an empty string if the 172 | service is not installed. If no `service` argument is given: the 173 | version of the Snips platform or an empty string if no Snips services 174 | are installed. 175 | 176 | Examples: 177 | 178 | >>> version() 179 | '1.1.2' 180 | >>> version('snips-nlu') 181 | '1.1.2' 182 | 183 | .. versionadded:: 0.5.3 184 | """ 185 | if service: 186 | try: 187 | return _version_output(service).split()[1] 188 | except IndexError: 189 | # The version output is empty, so the service is not installed. 190 | return '' 191 | else: 192 | # Filter the empty versions and then compute the minimum value. 193 | return min([version for version in versions().values() if version]) 194 | -------------------------------------------------------------------------------- /src/snipskit/tools.py: -------------------------------------------------------------------------------- 1 | """This module contains some useful tools for the snipskit library.""" 2 | 3 | from pathlib import Path 4 | import re 5 | from urllib.request import urlopen 6 | 7 | # Workaround for occasional errors when downloading the release notes. 8 | import http.client 9 | http.client._MAXHEADERS = 1000 10 | 11 | _RELEASE_NOTES_URL = 'https://docs.snips.ai/additional-resources/release-notes' 12 | _LATEST_VERSION_REGEX = r'Platform Update (\d*\.\d*\.\d*)\s' 13 | 14 | 15 | def find_path(paths): 16 | """Given a search path of files or directories with absolute paths, find 17 | the first existing path. 18 | 19 | Args: 20 | paths (list): A list of strings with absolute paths. 21 | 22 | Returns: 23 | str: The first path in the list `paths` that exists, or `None` if 24 | none of the paths exist. 25 | 26 | Example: 27 | The following example works if the file system has a file 28 | /usr/local/etc/snips.toml (e.g. on macOS with Snips installed): 29 | 30 | >>> find_path(['/etc/snips.toml', '/usr/local/etc/snips.toml']) 31 | '/usr/local/etc/snips.toml' 32 | """ 33 | for name in paths: 34 | path = Path(name) 35 | if path.exists(): 36 | return str(path.resolve()) 37 | 38 | # If none of the paths in the search path are found in the file system, 39 | # return None. 40 | return None 41 | 42 | 43 | def latest_snips_version(): 44 | """Return the latest version of Snips, as published in the release notes. 45 | 46 | Returns: 47 | str: The latest version of Snips. 48 | 49 | Raises: 50 | urllib.error.URLError: When the function runs into a problem 51 | downloading the release notes. 52 | 53 | .. versionadded:: 0.5.4 54 | """ 55 | url = urlopen(_RELEASE_NOTES_URL) 56 | release_notes = url.read().decode('utf-8') 57 | versions = re.findall(_LATEST_VERSION_REGEX, release_notes) 58 | return max(versions) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koenvervloesem/snipskit/442aa70d423f8371de5f3557b8e2fc6a7c6e19bf/tests/__init__.py -------------------------------------------------------------------------------- /tests/config/test_config_app.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.config.AppConfig` class.""" 2 | 3 | from snipskit.config import AppConfig 4 | 5 | 6 | def test_app_config_default(fs): 7 | """Test whether a default `AppConfig` object is initialized 8 | correctly. 9 | """ 10 | config_file = 'config.ini' 11 | fs.create_file(config_file, contents='[secret]\n' 12 | 'api-key=foobar\n') 13 | 14 | app_config = AppConfig() 15 | assert app_config.filename == config_file 16 | assert app_config['secret']['api-key'] == 'foobar' 17 | 18 | 19 | def test_app_config_path(fs): 20 | """Test whether an `AppConfig` object is initialized with the correct 21 | filename. 22 | """ 23 | config_file = 'foobar.ini' 24 | fs.create_file(config_file, contents='[secret]\n' 25 | 'api-key=foobar\n') 26 | 27 | app_config = AppConfig(config_file) 28 | assert app_config.filename == config_file 29 | assert app_config['secret']['api-key'] == 'foobar' 30 | 31 | 32 | def test_app_config_write(fs): 33 | """Test whether we can write an `AppConfig` object. 34 | """ 35 | config_file = 'config.ini' 36 | fs.create_file(config_file, contents='[secret]\n' 37 | 'api-key=foobar\n') 38 | 39 | app_config = AppConfig() 40 | app_config['secret']['api-key'] = 'barfoo' 41 | 42 | assert app_config['secret']['api-key'] == 'barfoo' 43 | 44 | app_config.write() 45 | 46 | app_config2 = AppConfig() 47 | assert app_config2['secret']['api-key'] == 'barfoo' 48 | 49 | 50 | def test_app_config_write_with_filename(fs): 51 | """Test whether we can write an `AppConfig` object with a filename 52 | argument. 53 | """ 54 | config_file = 'config.ini' 55 | fs.create_file(config_file, contents='[secret]\n' 56 | 'api-key=foobar\n') 57 | 58 | app_config = AppConfig() 59 | app_config['secret']['api-key'] = 'barfoo' 60 | 61 | with open('config.ini', 'wt') as config: 62 | app_config.write(config) 63 | 64 | app_config2 = AppConfig() 65 | assert app_config2['secret']['api-key'] == 'barfoo' 66 | -------------------------------------------------------------------------------- /tests/config/test_config_assistant.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.config.AssistantConfig` class.""" 2 | 3 | from json import JSONDecodeError 4 | 5 | import pytest 6 | from snipskit.config import AssistantConfig 7 | from snipskit.exceptions import AssistantConfigNotFoundError 8 | 9 | 10 | def test_assistant_config_default(fs): 11 | """Test whether a default `AssistantConfig` object is initialized 12 | correctly. 13 | """ 14 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 15 | fs.create_file(assistant_file, contents='{"language": "en"}') 16 | 17 | assistant_config = AssistantConfig() 18 | assert assistant_config.filename == assistant_file 19 | assert assistant_config['language'] == 'en' 20 | 21 | 22 | def test_assistant_config_with_filename(fs): 23 | """Test whether an `AssistantConfig` object is initialized correctly with a 24 | filename argument. 25 | """ 26 | assistant_file = '/opt/assistant/assistant.json' 27 | fs.create_file(assistant_file, contents='{"language": "en"}') 28 | 29 | assistant_config = AssistantConfig(assistant_file) 30 | assert assistant_config.filename == assistant_file 31 | assert assistant_config['language'] == 'en' 32 | 33 | 34 | def test_assistant_config_key_not_found(fs): 35 | """Test whether accessing a key that doesn't exist in an `AssistantConfig` 36 | object raises a `KeyError`. 37 | """ 38 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 39 | fs.create_file(assistant_file, contents='{"language": "en"}') 40 | 41 | assistant_config = AssistantConfig() 42 | with pytest.raises(KeyError): 43 | assistant_config['name'] 44 | 45 | 46 | def test_assistant_config_broken_json(fs): 47 | """Test whether an `AssistantConfig` object raises `JSONDecodeError` when a 48 | broken JSON file is read. 49 | """ 50 | assistant_file = '/usr/share/snips/assistant/assistant.json' 51 | fs.create_file(assistant_file, contents='{"language": "en", }') 52 | 53 | with pytest.raises(JSONDecodeError): 54 | assistant_config = AssistantConfig() 55 | 56 | 57 | def test_assistant_config_file_not_found(fs): 58 | """Test whether an `AssistantConfig` object raises `FileNotFoundError` when 59 | the specified assistant configuration file doesn't exist. 60 | """ 61 | with pytest.raises(FileNotFoundError): 62 | assistant_config = AssistantConfig('/opt/assistant/assistant.json') 63 | 64 | 65 | def test_assistant_config_no_config_file(fs): 66 | """Test whether an `AssistantConfig` object raises 67 | `AssistantConfigNotFoundError` when there's no assistant configuration 68 | found in the search path. 69 | """ 70 | with pytest.raises(AssistantConfigNotFoundError): 71 | assistant_config = AssistantConfig() 72 | -------------------------------------------------------------------------------- /tests/config/test_config_snips.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.config.SnipsConfig` class.""" 2 | 3 | import pytest 4 | from snipskit.config import SnipsConfig 5 | from snipskit.exceptions import SnipsConfigNotFoundError 6 | from toml import TomlDecodeError 7 | 8 | 9 | def test_snips_config_default(fs): 10 | """Test whether a default `SnipsConfig` object is initialized correctly.""" 11 | config_file = '/usr/local/etc/snips.toml' 12 | fs.create_file(config_file, 13 | contents='[snips-hotword]\n' 14 | 'audio = ["+@mqtt"]\n') 15 | 16 | snips_config = SnipsConfig() 17 | assert snips_config.filename == config_file 18 | assert snips_config['snips-hotword']['audio'] == ["+@mqtt"] 19 | 20 | 21 | def test_snips_config_with_filename(fs): 22 | """Test whether a `SnipsConfig` object is initialized correctly with a 23 | filename argument.""" 24 | config_file = '/usr/local/etc/snips.toml' 25 | fs.create_file(config_file, 26 | contents='[snips-hotword]\n' 27 | 'audio = ["+@mqtt"]\n') 28 | 29 | snips_config = SnipsConfig(config_file) 30 | assert snips_config.filename == config_file 31 | assert snips_config['snips-hotword']['audio'] == ["+@mqtt"] 32 | 33 | 34 | def test_snips_config_key_not_found(fs): 35 | """Test whether accessing a key that doesn't exist in a `SnipsConfig` 36 | object raises a `KeyError`. 37 | """ 38 | config_file = '/usr/local/etc/snips.toml' 39 | fs.create_file(config_file, 40 | contents='[snips-hotword]\n' 41 | 'audio = ["+@mqtt"]\n') 42 | 43 | snips_config = SnipsConfig() 44 | with pytest.raises(KeyError): 45 | snips_config['snips-hotword']['model'] 46 | 47 | 48 | def test_snips_config_broken_toml(fs): 49 | """Test whether a `SnipsConfig` object raises `TomlDecodeError` when a 50 | broken TOML file is read. 51 | """ 52 | config_file = '/etc/snips.toml' 53 | fs.create_file(config_file, 54 | contents='[snips-hotword\n' 55 | 'audio = ["+@mqtt"]\n') 56 | 57 | with pytest.raises(TomlDecodeError): 58 | snips_config = SnipsConfig() 59 | 60 | 61 | def test_snips_config_file_not_found(fs): 62 | """Test whether a `SnipsConfig` object raises `FileNotFoundError` when the 63 | specified file doesn't exist. 64 | """ 65 | with pytest.raises(FileNotFoundError): 66 | snips_config = SnipsConfig('/etc/snips.toml') 67 | 68 | 69 | def test_snips_config_no_config_file(fs): 70 | """Test whether a `SnipsConfig` object raises `SnipsConfigNotFoundError` 71 | when there's no snips.toml found in the search path. 72 | """ 73 | with pytest.raises(SnipsConfigNotFoundError): 74 | snips_config = SnipsConfig() 75 | -------------------------------------------------------------------------------- /tests/config/test_config_snips_mqtt.py: -------------------------------------------------------------------------------- 1 | """Tests for the MQTT connection settings of the `snipskit.config.SnipsConfig` 2 | class. 3 | """ 4 | 5 | from snipskit.config import SnipsConfig 6 | 7 | 8 | def test_snips_config_mqtt_default(fs): 9 | """Test whether a `SnipsConfig` object with default MQTT connection 10 | settings is initialized correctly. 11 | """ 12 | config_file = '/etc/snips.toml' 13 | fs.create_file(config_file, 14 | contents='[snips-common]\n') 15 | 16 | snips_config = SnipsConfig() 17 | assert snips_config.mqtt.broker_address == 'localhost:1883' 18 | assert snips_config.mqtt.auth.username is None 19 | assert snips_config.mqtt.auth.password is None 20 | assert snips_config.mqtt.auth.enabled is False 21 | assert snips_config.mqtt.tls.hostname is None 22 | assert snips_config.mqtt.tls.ca_file is None 23 | assert snips_config.mqtt.tls.ca_path is None 24 | assert snips_config.mqtt.tls.client_key is None 25 | assert snips_config.mqtt.tls.client_cert is None 26 | assert snips_config.mqtt.tls.disable_root_store is False 27 | assert snips_config.mqtt.tls.enabled is False 28 | 29 | 30 | def test_snips_config_mqtt_hostname(fs): 31 | """Test whether a `SnipsConfig` object with specified MQTT broker address 32 | is initialized correctly. 33 | """ 34 | config_file = '/etc/snips.toml' 35 | fs.create_file(config_file, 36 | contents='[snips-common]\n' 37 | 'mqtt="mqtt.example.com:8883"\n') 38 | 39 | snips_config = SnipsConfig() 40 | assert snips_config.mqtt.broker_address == 'mqtt.example.com:8883' 41 | assert snips_config.mqtt.auth.username is None 42 | assert snips_config.mqtt.auth.password is None 43 | assert snips_config.mqtt.auth.enabled is False 44 | assert snips_config.mqtt.tls.hostname is None 45 | assert snips_config.mqtt.tls.ca_file is None 46 | assert snips_config.mqtt.tls.ca_path is None 47 | assert snips_config.mqtt.tls.client_key is None 48 | assert snips_config.mqtt.tls.client_cert is None 49 | assert snips_config.mqtt.tls.disable_root_store is False 50 | assert snips_config.mqtt.tls.enabled is False 51 | 52 | 53 | def test_snips_config_mqtt_hostname_authentication(fs): 54 | """Test whether a `SnipsConfig` object with specified MQTT broker address 55 | and authentication is initialized correctly. 56 | """ 57 | config_file = '/etc/snips.toml' 58 | fs.create_file(config_file, 59 | contents='[snips-common]\n' 60 | 'mqtt="mqtt.example.com:8883"\n' 61 | 'mqtt_username="foobar"\n' 62 | 'mqtt_password="secretpassword"\n') 63 | 64 | snips_config = SnipsConfig() 65 | assert snips_config.mqtt.broker_address == 'mqtt.example.com:8883' 66 | assert snips_config.mqtt.auth.username == 'foobar' 67 | assert snips_config.mqtt.auth.password == 'secretpassword' 68 | assert snips_config.mqtt.auth.enabled is True 69 | assert snips_config.mqtt.tls.hostname is None 70 | assert snips_config.mqtt.tls.ca_file is None 71 | assert snips_config.mqtt.tls.ca_path is None 72 | assert snips_config.mqtt.tls.client_key is None 73 | assert snips_config.mqtt.tls.client_cert is None 74 | assert snips_config.mqtt.tls.disable_root_store is False 75 | assert snips_config.mqtt.tls.enabled is False 76 | 77 | 78 | def test_snips_config_mqtt_tls(fs): 79 | """Test whether a `SnipsConfig` object with specified MQTT broker address 80 | and authentication and TLS settings is initialized correctly. 81 | """ 82 | config_file = '/etc/snips.toml' 83 | fs.create_file(config_file, 84 | contents='[snips-common]\n' 85 | 'mqtt="mqtt.example.com:4883"\n' 86 | 'mqtt_username="foobar"\n' 87 | 'mqtt_password="secretpassword"\n' 88 | 'mqtt_tls_hostname="mqtt.example.com"\n' 89 | 'mqtt_tls_cafile="/etc/ssl/certs/ca-certificates.crt"\n') 90 | 91 | snips_config = SnipsConfig() 92 | assert snips_config.mqtt.broker_address == 'mqtt.example.com:4883' 93 | assert snips_config.mqtt.auth.username == 'foobar' 94 | assert snips_config.mqtt.auth.password == 'secretpassword' 95 | assert snips_config.mqtt.auth.enabled is True 96 | assert snips_config.mqtt.tls.hostname == 'mqtt.example.com' 97 | assert snips_config.mqtt.tls.ca_file == '/etc/ssl/certs/ca-certificates.crt' 98 | assert snips_config.mqtt.tls.ca_path is None 99 | assert snips_config.mqtt.tls.client_key is None 100 | assert snips_config.mqtt.tls.client_cert is None 101 | assert snips_config.mqtt.tls.disable_root_store is False 102 | assert snips_config.mqtt.tls.enabled is True 103 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import time 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture 10 | def mqtt_server(): 11 | print('Starting MQTT server') 12 | mosquitto = subprocess.Popen('mosquitto') 13 | time.sleep(1) # Let's wait a bit before it's started 14 | yield mosquitto 15 | print('Tearing down MQTT server') 16 | mosquitto.kill() 17 | 18 | 19 | try: 20 | # This environment variable is used by Travis CI to define which 21 | # dependencies are installed. Pytest uses it to define which modules 22 | # are checked. 23 | requirements = os.environ['SNIPSKIT_REQUIREMENTS'] 24 | 25 | if requirements == 'common': 26 | collect_ignore = ['mqtt', 'hermes'] 27 | elif requirements == 'mqtt': 28 | collect_ignore = ['hermes'] 29 | elif requirements == 'hermes': 30 | collect_ignore = ['mqtt'] 31 | elif requirements == 'all': 32 | # Run all the tests 33 | pass 34 | else: 35 | sys.exit('Unkown value for SNIPSKIT_REQUIREMENTS environment variable: {}'.format(requirements)) 36 | except KeyError: 37 | # Run all the tests 38 | pass 39 | -------------------------------------------------------------------------------- /tests/hermes/test_apps_hermes.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.apps.hermes.HermesSnipsApp` class.""" 2 | 3 | from snipskit.hermes.apps import HermesSnipsApp 4 | from snipskit.config import AppConfig, SnipsConfig 5 | 6 | 7 | class SimpleHermesApp(HermesSnipsApp): 8 | """A simple Snips app using Hermes to test.""" 9 | 10 | def initialize(self): 11 | pass 12 | 13 | 14 | def test_snips_app_hermes_default(fs, mocker): 15 | """Test whether a `HermesSnipsApp` object with the default parameters is 16 | set up correctly. 17 | """ 18 | 19 | config_file = '/etc/snips.toml' 20 | fs.create_file(config_file, contents='[snips-common]\n') 21 | 22 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 23 | fs.create_file(assistant_file, contents='{"language": "en"}') 24 | 25 | mocker.patch('hermes_python.hermes.Hermes.connect') 26 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 27 | mocker.spy(SimpleHermesApp, 'initialize') 28 | 29 | app = SimpleHermesApp() 30 | 31 | # Check Snips configuration 32 | assert app.snips.mqtt.broker_address == 'localhost:1883' 33 | 34 | # Check assistant configuration 35 | assert app.assistant['language'] == 'en' 36 | 37 | # Check there's no app configuration 38 | assert app.config is None 39 | 40 | # Check MQTT connection 41 | assert app.hermes.mqtt_options.broker_address == app.snips.mqtt.broker_address 42 | assert app.hermes.loop_forever.call_count == 1 43 | 44 | # Check whether `initialize()` method is called. 45 | assert app.initialize.call_count == 1 46 | 47 | 48 | def test_snips_app_hermes_default_with_assistant_path(fs, mocker): 49 | """Test whether a `HermesSnipsApp` object with the default parameters and 50 | an assistant configuration path in snips.toml is set up correctly. 51 | """ 52 | 53 | config_file = '/etc/snips.toml' 54 | fs.create_file(config_file, contents='[snips-common]\n' 55 | 'assistant = "/opt/assistant"\n') 56 | 57 | assistant_file = '/opt/assistant/assistant.json' 58 | fs.create_file(assistant_file, contents='{"language": "en"}') 59 | 60 | mocker.patch('hermes_python.hermes.Hermes.connect') 61 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 62 | mocker.spy(SimpleHermesApp, 'initialize') 63 | 64 | app = SimpleHermesApp() 65 | 66 | # Check Snips configuration 67 | assert app.snips.mqtt.broker_address == 'localhost:1883' 68 | 69 | # Check assistant configuration 70 | assert app.assistant['language'] == 'en' 71 | 72 | # Check there's no app configuration 73 | assert app.config is None 74 | 75 | # Check MQTT connection 76 | assert app.hermes.mqtt_options.broker_address == app.snips.mqtt.broker_address 77 | assert app.hermes.loop_forever.call_count == 1 78 | 79 | # Check whether `initialize()` method is called. 80 | assert app.initialize.call_count == 1 81 | 82 | 83 | def test_snips_app_hermes_snips_config(fs, mocker): 84 | """Test whether a `HermesSnipsapp` object with a SnipsConfig parameter is 85 | set up correctly. 86 | """ 87 | 88 | config_file = '/opt/snips.toml' 89 | fs.create_file(config_file, contents='[snips-common]\n' 90 | 'mqtt = "mqtt.example.com:1883"\n') 91 | 92 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 93 | fs.create_file(assistant_file, contents='{"language": "en"}') 94 | 95 | mocker.patch('hermes_python.hermes.Hermes.connect') 96 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 97 | mocker.spy(SimpleHermesApp, 'initialize') 98 | 99 | snips_config = SnipsConfig(config_file) 100 | app = SimpleHermesApp(snips=snips_config) 101 | 102 | # Check Snips configuration 103 | assert app.snips == snips_config 104 | assert app.snips.mqtt.broker_address == 'mqtt.example.com:1883' 105 | 106 | # Check assistant configuration 107 | assert app.assistant['language'] == 'en' 108 | 109 | # Check there's no app configuration 110 | assert app.config is None 111 | 112 | # Check MQTT connection 113 | assert app.hermes.mqtt_options.broker_address == app.snips.mqtt.broker_address 114 | assert app.hermes.loop_forever.call_count == 1 115 | 116 | # Check whether `initialize()` method is called. 117 | assert app.initialize.call_count == 1 118 | 119 | 120 | def test_snips_app_hermes_config(fs, mocker): 121 | """Test whether a `HermesSnipsApp` object with an app configuration is set 122 | up correctly. 123 | """ 124 | 125 | config_file = '/etc/snips.toml' 126 | fs.create_file(config_file, contents='[snips-common]\n') 127 | 128 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 129 | fs.create_file(assistant_file, contents='{"language": "en"}') 130 | 131 | app_config_file = 'config.ini' 132 | fs.create_file(app_config_file, contents='[secret]\n' 133 | 'api-key=foobar\n') 134 | 135 | mocker.patch('hermes_python.hermes.Hermes.connect') 136 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 137 | mocker.spy(SimpleHermesApp, 'initialize') 138 | 139 | app_config = AppConfig() 140 | app = SimpleHermesApp(config=app_config) 141 | 142 | # Check Snips configuration 143 | assert app.snips.mqtt.broker_address == 'localhost:1883' 144 | 145 | # Check assistant configuration 146 | assert app.assistant['language'] == 'en' 147 | 148 | # Check the app configuration 149 | assert app.config == app_config 150 | assert app.config.filename == app_config_file 151 | assert app.config['secret']['api-key'] == 'foobar' 152 | 153 | # Check MQTT connection 154 | assert app.hermes.mqtt_options.broker_address == app.snips.mqtt.broker_address 155 | assert app.hermes.loop_forever.call_count == 1 156 | 157 | # Check whether `initialize()` method is called. 158 | assert app.initialize.call_count == 1 159 | -------------------------------------------------------------------------------- /tests/hermes/test_components_hermes_connection.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.components.HermesSnipsComponent` class.""" 2 | 3 | from snipskit.hermes.components import HermesSnipsComponent 4 | from snipskit.config import SnipsConfig 5 | 6 | 7 | class SimpleHermesComponent(HermesSnipsComponent): 8 | """A simple Snips component using Hermes to test.""" 9 | 10 | def initialize(self): 11 | pass 12 | 13 | 14 | def test_snips_component_hermes_connection_default(fs, mocker): 15 | """Test whether a `HermesSnipsComponent` object with the default MQTT 16 | connection settings sets up its `Hermes` object correctly. 17 | """ 18 | 19 | config_file = '/etc/snips.toml' 20 | fs.create_file(config_file, contents='[snips-common]\n') 21 | 22 | mocker.patch('hermes_python.hermes.Hermes.connect') 23 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 24 | mocker.spy(SimpleHermesComponent, 'initialize') 25 | 26 | component = SimpleHermesComponent() 27 | 28 | # Check configuration 29 | assert component.snips.mqtt.broker_address == 'localhost:1883' 30 | 31 | # Check MQTT connection 32 | assert component.hermes.mqtt_options.broker_address == component.snips.mqtt.broker_address 33 | assert component.hermes.loop_forever.call_count == 1 34 | 35 | # Check whether `initialize()` method is called. 36 | assert component.initialize.call_count == 1 37 | 38 | 39 | def test_snips_component_hermes_with_snips_config(fs, mocker): 40 | """Test whether a `HermesSnipsComponent` object with a `SnipsConfig` object 41 | passed to `__init__` uses the connection settings from the specified file. 42 | """ 43 | 44 | config_file = 'snips.toml' 45 | fs.create_file(config_file, contents='[snips-common]\n' 46 | 'mqtt = "mqtt.example.com:1883"\n') 47 | 48 | mocker.patch('hermes_python.hermes.Hermes.connect') 49 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 50 | mocker.spy(SimpleHermesComponent, 'initialize') 51 | 52 | snips_config = SnipsConfig(config_file) 53 | component = SimpleHermesComponent(snips_config) 54 | 55 | # Check configuration 56 | assert component.snips == snips_config 57 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:1883' 58 | 59 | # Check MQTT connection 60 | assert component.hermes.mqtt_options.broker_address == component.snips.mqtt.broker_address 61 | assert component.hermes.loop_forever.call_count == 1 62 | 63 | # Check whether `initialize()` method is called. 64 | assert component.initialize.call_count == 1 65 | 66 | # Check whether `initialize()` method is called. 67 | assert component.initialize.call_count == 1 68 | 69 | 70 | def test_snips_component_hermes_connection_with_authentication(fs, mocker): 71 | """Test whether a `HermesSnipsComponent` object with MQTT authentication 72 | settings sets up its `Hermes` object correctly. 73 | """ 74 | 75 | config_file = '/etc/snips.toml' 76 | fs.create_file(config_file, contents='[snips-common]\n' 77 | 'mqtt = "mqtt.example.com:8883"\n' 78 | 'mqtt_username = "foobar"\n' 79 | 'mqtt_password = "secretpassword"\n') 80 | 81 | mocker.patch('hermes_python.hermes.Hermes.connect') 82 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 83 | mocker.spy(SimpleHermesComponent, 'initialize') 84 | 85 | component = SimpleHermesComponent() 86 | 87 | # Check configuration 88 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:8883' 89 | assert component.snips.mqtt.auth.username == 'foobar' 90 | assert component.snips.mqtt.auth.password == 'secretpassword' 91 | 92 | # Check MQTT connection 93 | assert component.hermes.mqtt_options.broker_address == component.snips.mqtt.broker_address 94 | assert component.hermes.mqtt_options.username == component.snips.mqtt.auth.username 95 | assert component.hermes.mqtt_options.password == component.snips.mqtt.auth.password 96 | assert component.hermes.loop_forever.call_count == 1 97 | 98 | # Check whether `initialize()` method is called. 99 | assert component.initialize.call_count == 1 100 | 101 | 102 | def test_snips_component_hermes_connection_with_tls_and_authentication(fs, mocker): 103 | """Test whether a `HermesSnipsComponent` object with TLS and MQTT 104 | authentication settings sets up its `Hermes` object correctly. 105 | """ 106 | 107 | config_file = '/etc/snips.toml' 108 | fs.create_file(config_file, 109 | contents='[snips-common]\n' 110 | 'mqtt = "mqtt.example.com:4883"\n' 111 | 'mqtt_username = "foobar"\n' 112 | 'mqtt_password = "secretpassword"\n' 113 | 'mqtt_tls_hostname="mqtt.example.com"\n' 114 | 'mqtt_tls_cafile="/etc/ssl/certs/ca-certificates.crt"\n') 115 | 116 | mocker.patch('hermes_python.hermes.Hermes.connect') 117 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 118 | mocker.spy(SimpleHermesComponent, 'initialize') 119 | 120 | component = SimpleHermesComponent() 121 | 122 | # Check configuration 123 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:4883' 124 | assert component.snips.mqtt.auth.username == 'foobar' 125 | assert component.snips.mqtt.auth.password == 'secretpassword' 126 | assert component.snips.mqtt.tls.hostname == 'mqtt.example.com' 127 | assert component.snips.mqtt.tls.ca_file == '/etc/ssl/certs/ca-certificates.crt' 128 | 129 | # Check MQTT connection 130 | assert component.hermes.mqtt_options.broker_address == component.snips.mqtt.broker_address 131 | assert component.hermes.mqtt_options.username == component.snips.mqtt.auth.username 132 | assert component.hermes.mqtt_options.password == component.snips.mqtt.auth.password 133 | assert component.hermes.mqtt_options.tls_hostname == component.snips.mqtt.tls.hostname 134 | assert component.hermes.mqtt_options.tls_ca_file == component.snips.mqtt.tls.ca_file 135 | assert component.hermes.loop_forever.call_count == 1 136 | 137 | # Check whether `initialize()` method is called. 138 | assert component.initialize.call_count == 1 139 | -------------------------------------------------------------------------------- /tests/hermes/test_components_hermes_decorators.py: -------------------------------------------------------------------------------- 1 | """Tests for the decorators for the `snipskit.components.HermesSnipsComponent` 2 | class. 3 | """ 4 | 5 | from snipskit.hermes.components import HermesSnipsComponent 6 | from snipskit.hermes.decorators import intent, intent_not_recognized, \ 7 | intents, session_ended, session_queued, session_started 8 | 9 | 10 | class DecoratedHermesComponent(HermesSnipsComponent): 11 | 12 | @intent('koan:Intent1') 13 | def callback_intent1(self, hermes, intent_message): 14 | pass 15 | 16 | @intent_not_recognized 17 | def callback_intent_not_recognized(self, hermes, intent_message): 18 | pass 19 | 20 | @intents 21 | def callback_intents(self, hermes, intent_message): 22 | pass 23 | 24 | @session_ended 25 | def callback_session_ended(self, hermes, session_ended_message): 26 | pass 27 | 28 | @session_queued 29 | def callback_session_queued(self, hermes, session_queued_message): 30 | pass 31 | 32 | @session_started 33 | def callback_session_started(self, hermes, session_started_message): 34 | pass 35 | 36 | 37 | def test_snips_component_hermes_decorators(fs, mocker): 38 | """Test whether a `HermesSnipsComponent` object with callbacks using 39 | decorators is initialized correctly. 40 | """ 41 | 42 | config_file = '/etc/snips.toml' 43 | fs.create_file(config_file, contents='[snips-common]\n') 44 | 45 | mocker.patch('hermes_python.hermes.Hermes.connect') 46 | mocker.patch('hermes_python.hermes.Hermes.loop_forever') 47 | mocker.patch('hermes_python.hermes.Hermes.subscribe_intent') 48 | mocker.patch('hermes_python.hermes.Hermes.subscribe_intent_not_recognized') 49 | mocker.patch('hermes_python.hermes.Hermes.subscribe_intents') 50 | mocker.patch('hermes_python.hermes.Hermes.subscribe_session_ended') 51 | mocker.patch('hermes_python.hermes.Hermes.subscribe_session_queued') 52 | mocker.patch('hermes_python.hermes.Hermes.subscribe_session_started') 53 | 54 | component = DecoratedHermesComponent() 55 | 56 | assert component.callback_intent1.subscribe_method == 'subscribe_intent' 57 | assert component.callback_intent1.subscribe_parameter == 'koan:Intent1' 58 | component.hermes.subscribe_intent.assert_called_once_with('koan:Intent1', 59 | component.callback_intent1) 60 | 61 | assert component.callback_intent_not_recognized.subscribe_method == 'subscribe_intent_not_recognized' 62 | component.hermes.subscribe_intent_not_recognized.assert_called_once_with(component.callback_intent_not_recognized) 63 | 64 | assert component.callback_intents.subscribe_method == 'subscribe_intents' 65 | component.hermes.subscribe_intents.assert_called_once_with(component.callback_intents) 66 | 67 | assert component.callback_session_ended.subscribe_method == 'subscribe_session_ended' 68 | component.hermes.subscribe_session_ended.assert_called_once_with(component.callback_session_ended) 69 | 70 | assert component.callback_session_queued.subscribe_method == 'subscribe_session_queued' 71 | component.hermes.subscribe_session_queued.assert_called_once_with(component.callback_session_queued) 72 | 73 | assert component.callback_session_started.subscribe_method == 'subscribe_session_started' 74 | component.hermes.subscribe_session_started.assert_called_once_with(component.callback_session_started) 75 | -------------------------------------------------------------------------------- /tests/mqtt/test_apps_mqtt.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.apps.MQTTSnipsApp` class.""" 2 | 3 | from snipskit.mqtt.apps import MQTTSnipsApp 4 | from snipskit.config import AppConfig, SnipsConfig 5 | 6 | 7 | class SimpleMQTTApp(MQTTSnipsApp): 8 | """A simple Snips app using MQTT directly to test.""" 9 | 10 | def initialize(self): 11 | pass 12 | 13 | 14 | def test_snips_app_mqtt_default(fs, mocker): 15 | """Test whether a `MQTTSnipsApp` object with the default parameters is set 16 | up correctly. 17 | """ 18 | 19 | config_file = '/etc/snips.toml' 20 | fs.create_file(config_file, contents='[snips-common]\n') 21 | 22 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 23 | fs.create_file(assistant_file, contents='{"language": "en"}') 24 | 25 | mocker.patch('paho.mqtt.client.Client.connect') 26 | mocker.patch('paho.mqtt.client.Client.loop_forever') 27 | mocker.patch('paho.mqtt.client.Client.tls_set') 28 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 29 | mocker.patch.object(SimpleMQTTApp, 'initialize') 30 | 31 | app = SimpleMQTTApp() 32 | 33 | # Check Snips configuration 34 | assert app.snips.mqtt.broker_address == 'localhost:1883' 35 | 36 | # Check assistant configuration 37 | assert app.assistant['language'] == 'en' 38 | 39 | # Check there's no app configuration 40 | assert app.config is None 41 | 42 | # Check MQTT connection 43 | assert app.mqtt.username_pw_set.call_count == 0 44 | assert app.mqtt.tls_set.call_count == 0 45 | assert app.mqtt.loop_forever.call_count == 1 46 | app.mqtt.connect.assert_called_once_with('localhost', 1883, 60, '') 47 | 48 | # Check whether `initialize()` method is called. 49 | assert app.initialize.call_count == 1 50 | 51 | 52 | def test_snips_app_mqtt_default_with_assistant_path(fs, mocker): 53 | """Test whether a `MQTTSnipsApp` object with the default parameters and an 54 | assistant configuration path in snips.toml is set up correctly. 55 | """ 56 | 57 | config_file = '/etc/snips.toml' 58 | fs.create_file(config_file, contents='[snips-common]\n' 59 | 'assistant = "/opt/assistant"\n') 60 | 61 | assistant_file = '/opt/assistant/assistant.json' 62 | fs.create_file(assistant_file, contents='{"language": "en"}') 63 | 64 | mocker.patch('paho.mqtt.client.Client.connect') 65 | mocker.patch('paho.mqtt.client.Client.loop_forever') 66 | mocker.patch('paho.mqtt.client.Client.tls_set') 67 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 68 | mocker.patch.object(SimpleMQTTApp, 'initialize') 69 | 70 | app = SimpleMQTTApp() 71 | 72 | # Check Snips configuration 73 | assert app.snips.mqtt.broker_address == 'localhost:1883' 74 | 75 | # Check assistant configuration 76 | assert app.assistant['language'] == 'en' 77 | 78 | # Check there's no app configuration 79 | assert app.config is None 80 | 81 | # Check MQTT connection 82 | assert app.mqtt.username_pw_set.call_count == 0 83 | assert app.mqtt.tls_set.call_count == 0 84 | assert app.mqtt.loop_forever.call_count == 1 85 | app.mqtt.connect.assert_called_once_with('localhost', 1883, 60, '') 86 | 87 | # Check whether `initialize()` method is called. 88 | assert app.initialize.call_count == 1 89 | 90 | 91 | def test_snips_app_mqtt_snips_config(fs, mocker): 92 | """Test whether a `MQTTSnipsApp` object with a SnipsConfig parameter is 93 | set up correctly. 94 | """ 95 | 96 | config_file = '/opt/snips.toml' 97 | fs.create_file(config_file, contents='[snips-common]\n' 98 | 'mqtt = "mqtt.example.com:1883"\n') 99 | 100 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 101 | fs.create_file(assistant_file, contents='{"language": "en"}') 102 | 103 | mocker.patch('paho.mqtt.client.Client.connect') 104 | mocker.patch('paho.mqtt.client.Client.loop_forever') 105 | mocker.patch('paho.mqtt.client.Client.tls_set') 106 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 107 | mocker.patch.object(SimpleMQTTApp, 'initialize') 108 | 109 | snips_config = SnipsConfig(config_file) 110 | app = SimpleMQTTApp(snips=snips_config) 111 | 112 | # Check Snips configuration 113 | assert app.snips == snips_config 114 | assert app.snips.mqtt.broker_address == 'mqtt.example.com:1883' 115 | 116 | # Check assistant configuration 117 | assert app.assistant['language'] == 'en' 118 | 119 | # Check there's no app configuration 120 | assert app.config is None 121 | 122 | # Check MQTT connection 123 | assert app.mqtt.username_pw_set.call_count == 0 124 | assert app.mqtt.tls_set.call_count == 0 125 | assert app.mqtt.loop_forever.call_count == 1 126 | app.mqtt.connect.assert_called_once_with('mqtt.example.com', 1883, 60, '') 127 | 128 | # Check whether `initialize()` method is called. 129 | assert app.initialize.call_count == 1 130 | 131 | 132 | def test_snips_app_mqtt_config(fs, mocker): 133 | """Test whether a `MQTTSnipsApp` object with an app configuration is set 134 | up correctly. 135 | """ 136 | 137 | config_file = '/etc/snips.toml' 138 | fs.create_file(config_file, contents='[snips-common]\n') 139 | 140 | assistant_file = '/usr/local/share/snips/assistant/assistant.json' 141 | fs.create_file(assistant_file, contents='{"language": "en"}') 142 | 143 | app_config_file = 'config.ini' 144 | fs.create_file(app_config_file, contents='[secret]\n' 145 | 'api-key=foobar\n') 146 | 147 | mocker.patch('paho.mqtt.client.Client.connect') 148 | mocker.patch('paho.mqtt.client.Client.loop_forever') 149 | mocker.patch('paho.mqtt.client.Client.tls_set') 150 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 151 | mocker.patch.object(SimpleMQTTApp, 'initialize') 152 | 153 | app_config = AppConfig() 154 | app = SimpleMQTTApp(config=app_config) 155 | 156 | # Check Snips configuration 157 | assert app.snips.mqtt.broker_address == 'localhost:1883' 158 | 159 | # Check assistant configuration 160 | assert app.assistant['language'] == 'en' 161 | 162 | # Check the app configuration 163 | assert app.config == app_config 164 | assert app.config.filename == app_config_file 165 | assert app.config['secret']['api-key'] == 'foobar' 166 | 167 | # Check MQTT connection 168 | assert app.mqtt.username_pw_set.call_count == 0 169 | assert app.mqtt.tls_set.call_count == 0 170 | assert app.mqtt.loop_forever.call_count == 1 171 | app.mqtt.connect.assert_called_once_with('localhost', 1883, 60, '') 172 | 173 | # Check whether `initialize()` method is called. 174 | assert app.initialize.call_count == 1 175 | -------------------------------------------------------------------------------- /tests/mqtt/test_client.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the helper functions of :mod:`snipskit.mqtt.client`. 2 | """ 3 | 4 | from snipskit.config import MQTTAuthConfig, MQTTConfig, MQTTTLSConfig 5 | from snipskit.mqtt.client import auth_params, host_port, tls_params 6 | 7 | 8 | # Test auth_params 9 | def test_client_auth_params(): 10 | config = MQTTConfig(auth=MQTTAuthConfig(username='foo', 11 | password='bar')) 12 | auth = auth_params(config) 13 | 14 | assert auth['username'] == 'foo' 15 | assert auth['password'] == 'bar' 16 | 17 | 18 | def test_client_auth_params_without_password(): 19 | config = MQTTConfig(auth=MQTTAuthConfig(username='foo')) 20 | auth = auth_params(config) 21 | 22 | assert auth['username'] == 'foo' 23 | assert auth['password'] is None 24 | 25 | 26 | def test_client_auth_params_without_auth(): 27 | config = MQTTConfig() 28 | auth = auth_params(config) 29 | 30 | assert auth is None 31 | 32 | 33 | # Test host_port 34 | def test_client_host_port(): 35 | config = MQTTConfig() 36 | host, port = host_port(config) 37 | 38 | assert host == 'localhost' 39 | assert port == 1883 40 | 41 | 42 | def test_client_host_port_tls(): 43 | config = MQTTConfig(tls=MQTTTLSConfig(hostname='example.com')) 44 | host, port = host_port(config) 45 | 46 | assert host == 'example.com' 47 | assert port == 1883 48 | 49 | 50 | # Test tls_params 51 | def test_client_tls_params(): 52 | config = MQTTConfig(tls=MQTTTLSConfig(hostname='example.com', 53 | ca_file='foo', 54 | client_key='bar', 55 | client_cert='foobar')) 56 | tls = tls_params(config) 57 | 58 | assert tls['ca_certs'] == 'foo' 59 | assert tls['keyfile'] == 'bar' 60 | assert tls['certfile'] == 'foobar' 61 | 62 | 63 | def test_client_tls_params_without_tls(): 64 | config = MQTTConfig() 65 | tls = tls_params(config) 66 | 67 | assert tls is None 68 | 69 | -------------------------------------------------------------------------------- /tests/mqtt/test_client_integration.py: -------------------------------------------------------------------------------- 1 | """Integration test for the :mod:`snipskit.mqtt.client` module. 2 | 3 | This needs mosquitto running on localhost:1883. 4 | """ 5 | import json 6 | import os 7 | import subprocess 8 | import re 9 | import threading 10 | import time 11 | 12 | import paho.mqtt.subscribe as subscribe 13 | import pytest 14 | 15 | from snipskit.config import MQTTConfig 16 | from snipskit.mqtt.client import publish_single 17 | 18 | # Only run these tests if the environment variable INTEGRATION_TESTS is set. 19 | pytestmark = pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS'), 20 | reason='Integration test') 21 | # Delay between subscribing and publishing an MQTT message. 22 | DELAY=1 23 | 24 | 25 | def test_client_publish_single_json(mqtt_server): 26 | 27 | config = MQTTConfig() 28 | 29 | def publish_test(): 30 | publish_single(config, 'snipskit-test/topic', {'foo': 'bar', 31 | 'foobar': True}) 32 | 33 | threading.Timer(DELAY, publish_test).start() 34 | 35 | message = subscribe.simple('snipskit-test/topic') 36 | assert json.loads(message.payload.decode('utf-8')) == {'foo': 'bar', 37 | 'foobar': True} 38 | 39 | 40 | def test_client_publish_single_binary(mqtt_server): 41 | 42 | config = MQTTConfig() 43 | 44 | def publish_test(): 45 | publish_single(config, 'snipskit-test/topic', 'foobar', 46 | json_encode=False) 47 | 48 | threading.Timer(DELAY, publish_test).start() 49 | 50 | message = subscribe.simple('snipskit-test/topic') 51 | assert message.payload.decode('utf-8') == 'foobar' 52 | -------------------------------------------------------------------------------- /tests/mqtt/test_components_mqtt_connection.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.components.MQTTSnipsComponent` class.""" 2 | 3 | from snipskit.mqtt.components import MQTTSnipsComponent 4 | from snipskit.config import SnipsConfig 5 | 6 | 7 | class SimpleMQTTComponent(MQTTSnipsComponent): 8 | """A simple Snips component using MQTT directly to test.""" 9 | 10 | def initialize(self): 11 | pass 12 | 13 | 14 | def test_snips_component_mqtt_connection_default(fs, mocker): 15 | """Test whether a `MQTTSnipsComponent` object with the default MQTT 16 | connection settings connects to the MQTT broker correctly. 17 | """ 18 | 19 | config_file = '/etc/snips.toml' 20 | fs.create_file(config_file, contents='[snips-common]\n') 21 | 22 | mocker.patch('paho.mqtt.client.Client.connect') 23 | mocker.patch('paho.mqtt.client.Client.loop_forever') 24 | mocker.patch('paho.mqtt.client.Client.tls_set') 25 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 26 | mocker.patch.object(SimpleMQTTComponent, 'initialize') 27 | 28 | component = SimpleMQTTComponent() 29 | 30 | # Check configuration 31 | assert component.snips.mqtt.broker_address == 'localhost:1883' 32 | 33 | # Check MQTT connection 34 | assert component.mqtt.username_pw_set.call_count == 0 35 | assert component.mqtt.tls_set.call_count == 0 36 | assert component.mqtt.loop_forever.call_count == 1 37 | component.mqtt.connect.assert_called_once_with('localhost', 1883, 60, '') 38 | 39 | # Check whether `initialize()` method is called. 40 | assert component.initialize.call_count == 1 41 | 42 | 43 | def test_snips_component_mqtt_with_snips_config(fs, mocker): 44 | """Test whether a `MQTTSnipsComponent` object with a `SnipsConfig` object 45 | passed to `__init__` uses the connection settings from the specified file. 46 | """ 47 | 48 | config_file = 'snips.toml' 49 | fs.create_file(config_file, contents='[snips-common]\n' 50 | 'mqtt = "mqtt.example.com:1883"\n') 51 | 52 | mocker.patch('paho.mqtt.client.Client.connect') 53 | mocker.patch('paho.mqtt.client.Client.loop_forever') 54 | mocker.patch('paho.mqtt.client.Client.tls_set') 55 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 56 | mocker.patch.object(SimpleMQTTComponent, 'initialize') 57 | 58 | snips_config = SnipsConfig(config_file) 59 | component = SimpleMQTTComponent(snips_config) 60 | 61 | # Check configuration 62 | assert component.snips == snips_config 63 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:1883' 64 | 65 | # Check MQTT connection 66 | assert component.mqtt.username_pw_set.call_count == 0 67 | assert component.mqtt.tls_set.call_count == 0 68 | assert component.mqtt.loop_forever.call_count == 1 69 | component.mqtt.connect.assert_called_once_with('mqtt.example.com', 1883, 70 | 60, '') 71 | 72 | # Check whether `initialize()` method is called. 73 | assert component.initialize.call_count == 1 74 | 75 | 76 | def test_snips_component_mqtt_connection_with_authentication(fs, mocker): 77 | """Test whether a `MQTTSnipsComponent` object with MQTT authentication 78 | connects to the MQTT broker correctly. 79 | """ 80 | 81 | config_file = '/etc/snips.toml' 82 | fs.create_file(config_file, contents='[snips-common]\n' 83 | 'mqtt = "mqtt.example.com:8883"\n' 84 | 'mqtt_username = "foobar"\n' 85 | 'mqtt_password = "secretpassword"\n') 86 | 87 | mocker.patch('paho.mqtt.client.Client.connect') 88 | mocker.patch('paho.mqtt.client.Client.loop_forever') 89 | mocker.patch('paho.mqtt.client.Client.tls_set') 90 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 91 | mocker.patch.object(SimpleMQTTComponent, 'initialize') 92 | 93 | component = SimpleMQTTComponent() 94 | 95 | # Check configuration 96 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:8883' 97 | assert component.snips.mqtt.auth.username == 'foobar' 98 | assert component.snips.mqtt.auth.password == 'secretpassword' 99 | 100 | # Check MQTT connection 101 | component.mqtt.username_pw_set.assert_called_once_with('foobar', 102 | 'secretpassword') 103 | assert component.mqtt.tls_set.call_count == 0 104 | assert component.mqtt.loop_forever.call_count == 1 105 | component.mqtt.connect.assert_called_once_with('mqtt.example.com', 8883, 106 | 60, '') 107 | 108 | # Check whether `initialize()` method is called. 109 | assert component.initialize.call_count == 1 110 | 111 | 112 | def test_snips_component_mqtt_connection_with_tls_and_authentication(fs, mocker): 113 | """Test whether a `MQTTSnipsComponent` object with TLS and MQTT 114 | authentication connects to the MQTT broker correctly. 115 | """ 116 | 117 | config_file = '/etc/snips.toml' 118 | fs.create_file(config_file, 119 | contents='[snips-common]\n' 120 | 'mqtt = "mqtt.example.com:4883"\n' 121 | 'mqtt_username = "foobar"\n' 122 | 'mqtt_password = "secretpassword"\n' 123 | 'mqtt_tls_hostname="mqtt.example.com"\n' 124 | 'mqtt_tls_cafile="/etc/ssl/certs/ca-certificates.crt"\n') 125 | 126 | mocker.patch('paho.mqtt.client.Client.connect') 127 | mocker.patch('paho.mqtt.client.Client.loop_forever') 128 | mocker.patch('paho.mqtt.client.Client.tls_set') 129 | mocker.patch('paho.mqtt.client.Client.username_pw_set') 130 | mocker.patch.object(SimpleMQTTComponent, 'initialize') 131 | 132 | component = SimpleMQTTComponent() 133 | 134 | # Check configuration 135 | assert component.snips.mqtt.broker_address == 'mqtt.example.com:4883' 136 | assert component.snips.mqtt.auth.username == 'foobar' 137 | assert component.snips.mqtt.auth.password == 'secretpassword' 138 | assert component.snips.mqtt.tls.hostname == 'mqtt.example.com' 139 | assert component.snips.mqtt.tls.ca_file == '/etc/ssl/certs/ca-certificates.crt' 140 | 141 | # Check MQTT connection 142 | component.mqtt.username_pw_set.assert_called_once_with('foobar', 143 | 'secretpassword') 144 | component.mqtt.tls_set.assert_called_once_with(ca_certs='/etc/ssl/certs/ca-certificates.crt', 145 | certfile=None, 146 | keyfile=None) 147 | assert component.mqtt.loop_forever.call_count == 1 148 | component.mqtt.connect.assert_called_once_with('mqtt.example.com', 4883, 149 | 60, '') 150 | 151 | # Check whether `initialize()` method is called. 152 | assert component.initialize.call_count == 1 153 | -------------------------------------------------------------------------------- /tests/mqtt/test_components_mqtt_decorators.py: -------------------------------------------------------------------------------- 1 | """Tests for the decorators for the `snipskit.components.MQTTSnipsComponent` 2 | class. 3 | """ 4 | 5 | from snipskit.mqtt.components import MQTTSnipsComponent 6 | from snipskit.mqtt.decorators import topic 7 | 8 | 9 | class DecoratedMQTTComponent(MQTTSnipsComponent): 10 | """A simple Snips component using MQTT directly to test.""" 11 | 12 | def initialize(self): 13 | pass 14 | 15 | def do_something(self): 16 | pass 17 | 18 | @topic('hermes/intent/#') 19 | def handle_intents(self, topic, payload): 20 | pass 21 | 22 | 23 | def test_snips_component_mqtt_decorators(fs, mocker): 24 | """Test whether a `MQTTSnipsComponent` object with callbacks using the 25 | @topic decorator is initialized correctly. 26 | """ 27 | 28 | config_file = '/etc/snips.toml' 29 | fs.create_file(config_file, contents='[snips-common]\n') 30 | 31 | mocker.patch('paho.mqtt.client.Client.connect') 32 | mocker.patch('paho.mqtt.client.Client.loop_forever') 33 | mocker.patch('paho.mqtt.client.Client.subscribe') 34 | mocker.patch('paho.mqtt.client.Client.message_callback_add') 35 | mocker.spy(DecoratedMQTTComponent, '_subscribe_topics') 36 | 37 | component = DecoratedMQTTComponent() 38 | 39 | # Check if the callback method has the attribute `topic` and other methods 40 | # don't. 41 | assert component.handle_intents.topic == 'hermes/intent/#' 42 | assert not hasattr(component.do_something, 'topic') 43 | 44 | # Simulate the call of `_subscribe_topics` when the client connects to MQTT 45 | component._subscribe_topics(None, None, None, None) 46 | # Check whether the right callback is called. 47 | assert component._subscribe_topics.call_count == 1 48 | component.mqtt.subscribe.assert_called_once_with('hermes/intent/#') 49 | component.mqtt.message_callback_add.assert_called_once_with('hermes/intent/#', 50 | component.handle_intents) 51 | -------------------------------------------------------------------------------- /tests/mqtt/test_components_mqtt_pubsub_integration.py: -------------------------------------------------------------------------------- 1 | """Integration test for subscribing and publishing to MQTT topic in the 2 | :class:`snipskit.components.MQTTSnipsComponent` class. 3 | 4 | This needs mosquitto running on localhost:1883. 5 | """ 6 | import json 7 | import os 8 | import subprocess 9 | import re 10 | import threading 11 | import time 12 | 13 | import paho.mqtt.publish as publish 14 | import paho.mqtt.subscribe as subscribe 15 | import pytest 16 | 17 | from snipskit.mqtt.components import MQTTSnipsComponent 18 | from snipskit.mqtt.decorators import topic 19 | 20 | # Only run these tests if the environment variable INTEGRATION_TESTS is set. 21 | pytestmark = pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS'), 22 | reason='Integration test') 23 | # Delay between subscribing and publishing an MQTT message. 24 | DELAY=1 25 | 26 | 27 | class DecoratedMQTTComponentPubSub(MQTTSnipsComponent): 28 | """A simple Snips component using MQTT directly to test.""" 29 | 30 | @topic('hermes/hotword/+/detected') 31 | def handle_hotword(self, topic, payload): 32 | hotword = re.search('^hermes/hotword/(.*)/detected$', topic).group(1) 33 | siteId = payload['siteId'] 34 | result_sentence = 'I detected the hotword {} on site ID {}.'.format(hotword, siteId) 35 | self.publish('hermes-test/tts/say', {'siteId': siteId, 'text': result_sentence}) 36 | 37 | @topic('hermes/audioServer/+/playBytes/+', json_decode=False) 38 | def handle_audio(self, topic, payload): 39 | parsed_topic = re.search('^hermes/audioServer/(.*)/playBytes/(.*)$', topic) 40 | siteId = parsed_topic.group(1) 41 | requestId = parsed_topic.group(2) 42 | result_sentence = 'I detected audio with request ID {} and payload {} on site ID {}.'.format(requestId, payload.decode('utf-8'), siteId) 43 | self.publish('hermes-test/tts/say', {'siteId': siteId, 'text': result_sentence}) 44 | 45 | 46 | def test_snips_component_mqtt_pubsub(mqtt_server): 47 | """Test whether a :class:`MQTTSnipsComponent` object executes the right 48 | callback after a topic it's subscribed to gets published on the MQTT bus 49 | and publishes the right payload. 50 | """ 51 | 52 | def publish_audio(): 53 | publish.single('hermes/audioServer/default/playBytes/1234', 'foobar') 54 | 55 | def publish_hotword(): 56 | publish.single('hermes/hotword/hey_snips/detected', '{"siteId": "default"}') 57 | 58 | # Test handle_hotword method: JSON payload 59 | threading.Thread(target=DecoratedMQTTComponentPubSub, daemon=True).start() 60 | 61 | threading.Timer(DELAY, publish_hotword).start() 62 | 63 | message = subscribe.simple('hermes-test/tts/say') 64 | assert json.loads(message.payload.decode('utf-8')) == {'siteId': 'default', 65 | 'text': 'I detected the hotword hey_snips on site ID default.'} 66 | 67 | # Test handle_audio method: 'Binary' payload (just a string here) 68 | threading.Timer(DELAY, publish_audio).start() 69 | 70 | message = subscribe.simple('hermes-test/tts/say') 71 | assert json.loads(message.payload.decode('utf-8')) == {'siteId': 'default', 72 | 'text': 'I detected audio with request ID 1234 and payload foobar on site ID default.'} 73 | -------------------------------------------------------------------------------- /tests/mqtt/test_dialogue.py: -------------------------------------------------------------------------------- 1 | """Tests for the dialogue helper functions of :mod:`snipskit.mqtt.dialogue`. 2 | """ 3 | 4 | from snipskit.mqtt.components import MQTTSnipsComponent 5 | from snipskit.mqtt.dialogue import DM_CONTINUE_SESSION, DM_END_SESSION,\ 6 | continue_session, end_session 7 | 8 | 9 | class DialogueMQTTComponent(MQTTSnipsComponent): 10 | """A simple Snips component using MQTT directly to test.""" 11 | 12 | def initialize(self): 13 | pass 14 | 15 | 16 | def test_dialogue_continue_session(fs, mocker): 17 | """Test whether the :func:`snipskit.mqtt.dialogue.continue_session` 18 | function works correctly with :meth:`.MQTTSnipsComponent.publish`. 19 | """ 20 | 21 | config_file = '/etc/snips.toml' 22 | fs.create_file(config_file, contents='[snips-common]\n') 23 | 24 | mocker.patch('paho.mqtt.client.Client.connect') 25 | mocker.patch('paho.mqtt.client.Client.loop_forever') 26 | mocker.patch('paho.mqtt.client.Client.subscribe') 27 | mocker.patch('paho.mqtt.client.Client.message_callback_add') 28 | mocker.spy(DialogueMQTTComponent, 'publish') 29 | 30 | component = DialogueMQTTComponent() 31 | 32 | component.publish(*continue_session('testSessionId', 'testText')) 33 | 34 | component.publish.assert_called_once_with(component, 35 | DM_CONTINUE_SESSION, 36 | {'sessionId': 'testSessionId', 37 | 'text': 'testText'}) 38 | 39 | def test_dialogue_end_session(fs, mocker): 40 | """Test whether the :func:`snipskit.mqtt.dialogue.end_session` function 41 | works correctly with :meth:`.MQTTSnipsComponent.publish`. 42 | """ 43 | 44 | config_file = '/etc/snips.toml' 45 | fs.create_file(config_file, contents='[snips-common]\n') 46 | 47 | mocker.patch('paho.mqtt.client.Client.connect') 48 | mocker.patch('paho.mqtt.client.Client.loop_forever') 49 | mocker.patch('paho.mqtt.client.Client.subscribe') 50 | mocker.patch('paho.mqtt.client.Client.message_callback_add') 51 | mocker.spy(DialogueMQTTComponent, 'publish') 52 | 53 | component = DialogueMQTTComponent() 54 | 55 | component.publish(*end_session('testSessionId', 'testText')) 56 | 57 | component.publish.assert_called_once_with(component, 58 | DM_END_SESSION, 59 | {'sessionId': 'testSessionId', 60 | 'text': 'testText'}) 61 | 62 | def test_dialogue_end_session_with_empty_text(fs, mocker): 63 | """Test whether the :func:`snipskit.mqtt.dialogue.end_session` function 64 | works correctly with :meth:`.MQTTSnipsComponent.publish` when the text is 65 | empty. 66 | """ 67 | 68 | config_file = '/etc/snips.toml' 69 | fs.create_file(config_file, contents='[snips-common]\n') 70 | 71 | mocker.patch('paho.mqtt.client.Client.connect') 72 | mocker.patch('paho.mqtt.client.Client.loop_forever') 73 | mocker.patch('paho.mqtt.client.Client.subscribe') 74 | mocker.patch('paho.mqtt.client.Client.message_callback_add') 75 | mocker.spy(DialogueMQTTComponent, 'publish') 76 | 77 | component = DialogueMQTTComponent() 78 | 79 | component.publish(*end_session('testSessionId')) 80 | 81 | component.publish.assert_called_once_with(component, 82 | DM_END_SESSION, 83 | {'sessionId': 'testSessionId'}) 84 | -------------------------------------------------------------------------------- /tests/test_services_integration.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the `snipskit.services` module. 2 | 3 | This needs snips-dialogue and snips-nlu running, as well as mosquitto listening 4 | on localhost:1883. 5 | """ 6 | import os 7 | import subprocess 8 | import time 9 | 10 | import pytest 11 | 12 | from snipskit.services import _version_output, is_installed, is_running, \ 13 | model_version, installed, running, versions, version 14 | 15 | # Only run these tests if the environment variable INTEGRATION_TESTS is set. 16 | pytestmark = pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS'), 17 | reason='Integration test') 18 | 19 | @pytest.fixture 20 | def snips_tts(mqtt_server): 21 | print('Starting Snips TTS') 22 | snips_tts = subprocess.Popen('snips-tts') 23 | time.sleep(1) # Let's wait a bit before it's started 24 | yield snips_tts 25 | print('Tearing down Snips TTS') 26 | snips_tts.kill() 27 | 28 | def test_version_output(): 29 | """Test whether the `_version_output` function returns the right result.""" 30 | 31 | # These services are installed 32 | assert _version_output('snips-dialogue') == 'snips-dialogue 1.1.2 (0.62.3)' 33 | assert _version_output('snips-nlu') == 'snips-nlu 1.1.2 (0.62.3) [model_version: 0.19.0]' 34 | assert _version_output('snips-tts') == 'snips-tts 1.1.2 (0.62.3)' 35 | 36 | # These services are not installed 37 | assert _version_output('snips-asr') == '' 38 | assert _version_output('snips-audio-server') == '' 39 | 40 | def test_is_installed(): 41 | """Test whether the `is_installed` function returns the right result.""" 42 | 43 | # These services are installed 44 | assert is_installed('snips-dialogue') 45 | assert is_installed('snips-nlu') 46 | assert is_installed('snips-tts') 47 | 48 | # These services are not installed 49 | assert not is_installed('snips-asr') 50 | assert not is_installed('snips-audio-server') 51 | 52 | def test_is_running(snips_tts): 53 | """Test whether the `is_running` function returns the right result.""" 54 | 55 | # This service is running 56 | assert is_running('snips-tts') 57 | 58 | # These services are not running 59 | assert not is_running('snips-asr') 60 | assert not is_running('snips-audio-server') 61 | assert not is_running('snips-dialogue') 62 | assert not is_running('snips-nlu') 63 | 64 | def test_model_version(): 65 | """Test whether the `model_version` function returns the right result.""" 66 | 67 | assert model_version() == '0.19.0' 68 | 69 | def test_installed(): 70 | """test whether the `installed` function returns the right result. 71 | """ 72 | assert installed() == {'snips-analytics': False, 73 | 'snips-asr': False, 74 | 'snips-asr-google': False, 75 | 'snips-audio-server': False, 76 | 'snips-dialogue': True, 77 | 'snips-hotword': False, 78 | 'snips-injection': False, 79 | 'snips-nlu': True, 80 | 'snips-skill-server': False, 81 | 'snips-tts': True} 82 | 83 | def test_running(snips_tts): 84 | """Test whether the `running` function returns the right result. 85 | """ 86 | assert running() == {'snips-analytics': False, 87 | 'snips-asr': False, 88 | 'snips-asr-google': False, 89 | 'snips-audio-server': False, 90 | 'snips-dialogue': False, 91 | 'snips-hotword': False, 92 | 'snips-injection': False, 93 | 'snips-nlu': False, 94 | 'snips-skill-server': False, 95 | 'snips-tts': True} 96 | 97 | def test_versions(): 98 | """test whether the `versions` function returns the right result. 99 | """ 100 | assert versions() == {'snips-analytics': '', 101 | 'snips-asr': '', 102 | 'snips-asr-google': '', 103 | 'snips-audio-server': '', 104 | 'snips-dialogue': '1.1.2', 105 | 'snips-hotword': '', 106 | 'snips-injection': '', 107 | 'snips-nlu': '1.1.2', 108 | 'snips-skill-server': '', 109 | 'snips-tts': '1.1.2'} 110 | 111 | def test_version(): 112 | """Test whether the `version` function returns the right result.""" 113 | 114 | assert version('snips-asr') == '' 115 | assert version('snips-tts') == '1.1.2' 116 | assert version() == '1.1.2' 117 | 118 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | """Tests for the `snipskit.tools` module.""" 2 | 3 | import pytest 4 | from snipskit.tools import find_path, latest_snips_version 5 | 6 | # Variables for some file paths we test.""" 7 | etc = '/etc/snips.toml' 8 | usr = '/usr/local/etc/snips.toml' 9 | 10 | # A list of the scenarios we want to test for the `find_path` function. 11 | # 12 | # Each item in the list is a tuple: 13 | # (files in the file system, search path for the function, expected result) 14 | test_data_search_path = [ 15 | ([], [], None), 16 | ([], [etc], None), 17 | ([], [etc, usr], None), 18 | ([etc], [], None), 19 | ([etc], [etc], etc), 20 | ([etc], [etc, usr], etc), 21 | ([usr], [], None), 22 | ([usr], [etc], None), 23 | ([usr], [etc, usr], usr), 24 | ([etc, usr], [], None), 25 | ([etc, usr], [etc], etc), 26 | ([etc, usr], [etc, usr], etc) 27 | ] 28 | 29 | 30 | @pytest.mark.parametrize("file_system,files,expected", test_data_search_path) 31 | def test_find_path(file_system, files, expected, fs): 32 | """Test whether the `find_path` function returns the right result 33 | in a couple of scenarios.""" 34 | for filename in file_system: 35 | fs.create_file(filename) 36 | 37 | assert find_path(files) == expected 38 | 39 | def test_latest_snips_version(): 40 | """Test whether the `latest_snips_version` function returns the current 41 | latest version of Snips. 42 | 43 | Because a real test would just reimplement the function, we compare to a 44 | hardcoded string. Update this string when a new version of Snips is 45 | published.""" 46 | 47 | assert latest_snips_version() == '1.1.2' 48 | --------------------------------------------------------------------------------