diff --git a/CHANGELOG.md b/CHANGELOG.md index 3442545d..6a831fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +v0.4.0 / 2016-12-28 +=================== +- Add resources directory (neuron, stt, tts, (trigger)) +- Install community modules from git url +- Fix API (strict slashes + included brains) +- starter kits (FR - EN) +- Add support Ubuntu 14.04 +- Fix neurotransmitter (multiple synapses call) +- Neurotransmitter improvements (pass arguments to called synapse) +- Split Core Neurons and Community Neurons +- update some pip dependencies +- Add Russian snowboy model for каллиопа + v0.3.0 / 2016-12-7 ================= - add unit tests for core & neurons @@ -21,4 +34,4 @@ v0.2 / 2016-11-21 v0.1 / 2016-11-02 ================= -- Initial Release \ No newline at end of file +- Initial Release diff --git a/Docs/contributing.md b/Docs/contributing.md index 9e4ae688..54b908c2 100644 --- a/Docs/contributing.md +++ b/Docs/contributing.md @@ -1,120 +1,32 @@ # Contributing -Kalliope needs the community to improve its Core features and to create new Neurons. Let's join us ! +Kalliope needs the community to improve its Core features and to create new Neurons, STTs, TTSs. Let's join us ! +[Here is the Todo list](https://trello.com/b/H0TecLSi/kalliopecore) if you are looking for some ideas. ## Core The community can contribute to the Core of Kalliope by providing some new features. -#### How to contribute +**How to contribute** 1. Fork it! -2. Create your feature branch: `git checkout -b my-new-feature` -3. Commit your changes: `git commit -am 'Add some feature'` -4. Push to the branch: `git push origin my-new-feature` -5. Submit a pull request :D +1. Checkout the dev branch `git checkout dev` +1. Create your feature branch: `git checkout -b my-new-feature` +1. Commit your changes: `git commit -am 'Add some feature'` +1. Push to the branch: `git push origin my-new-feature` +1. Submit a pull request in the **dev** branch +## Community module (Neuron, STT, TTS) -## Neurons +Kalliope comes with a community [installation command](kalliope_cli.md). Your can create a module in you own git repo that will be available to all Kalliope users. -Kalliope modularity is fully based on Neuron so the community can contribute by adding their own. -Neurons are independent projects so they can be developed under a github project. Anyone can clone them, place them under the neurons repository and reuse them. +See the dedicated documentation depending on the type of module you want to code. +- create a [community neuron](contributing/contribute_neuron.md) +- create a [community STT](contributing/contribute_stt.md) +- create a [community TTS](contributing/contribute_tts.md) -Creating a new Neuron must follow some rules: -##### Repository Structure -1. The Neuron repository name is in __lowercase__. -1. The Neuron repository must be added under the __neurons__ repository coming from the Core. -1. Under the Neuron repository, the Neuron has a __README.md file__ describing the Neuron following this structure: - - Neuron name: - - Synopsis: Description of the Neuron - - Options: A table of the incoming parameters managed by the Neuron. - - Return Values: A table of the returned values which can be catched by the *say_template attribute*. - - Synapses example: An example of how to use the Neuron inside a Synapse. - - Notes: Something which needs to be add. -1. Under the Neuron repository, include a __Tests repository__ to manage the test of the Neuron. - - -##### Code -1. Under the Neuron repository, the Neuron file name .py is also in __lowercase__. -1. The Neuron must be coded in __Python 2.7__. -1. Under the Neuron repository, include the __init__.py file which contains: *from neuron import Neuron* (/!\ respect the Case) -1. Inside the Neuron file, the Neuron Class name is in __uppercase__. -1. The Neuron __inherits from the NeuronModule__ coming from the Core. - - ``` - from core.NeuronModule import NeuronModule - class Say(NeuronModule): - ``` - - -1. The Neuron has a constructor __init__ which is the entry point. -The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the brain file or in the signal. -1. The Neuron must refer to its __parent structure__ in the init by calling the super of NeuronModule. - - ``` - def __init__(self, **kwargs): - super(Say, self).__init__(**kwargs) - ``` - -1. You must run unit tests with success before sending a pull request. Add new tests that cover the code you want to publish. - ``` - cd /path/to/kalliope - python -m unittest discover - ``` - -1. (*optionnal-> good practice*) The Neuron can implement a __private method _is_parameters_ok(self)__ which checks if entries are ok. *return: true if parameters are ok, raise an exception otherwise* -1. (*optionnal-> good practice*) The Neuron can __import and raise exceptions__ coming from NeuronModule: - - MissingParameterException: *Some Neuron parameters are missing.* - - InvalidParameterException: *Some Neuron parameters are invalid.* - -1. The Neuron can use a __self.say(message) method__ to speak out some return values using the *say_template* attribute in the brain file. -the message variable must be a Dict of variable:values where variables can be defined as output. - -1. Example of neuron structure - ``` - myneuron/ - ├── __init__.py - ├── myneuron.py - ├── README.md - └── tests - ├── __init__.py - └── test_myneuron.py - ``` - -1. Example of neuron code - ``` - class Myneuron(NeuronModule): - def __init__(self, **kwargs): - super(Myneuron, self).__init__(**kwargs) - # the args from the neuron configuration - self.arg1 = kwargs.get('arg1', None) - self.arg2 = kwargs.get('arg2', None) - - # check if parameters have been provided - if self._is_parameters_ok(): - # ------------------- - # do amazing code - # ------------------- - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: MissingParameterException - """ - if self.arg1 is None: - raise MissingParameterException("You must specify a arg1") - if not isinstance(self.arg2, int): - raise MissingParameterException("arg2 must be an integer") - return True - ``` - -##### Constraints - -1. The Neuron must (as much as possible) ensure the i18n. This means that they should __not manage a specific language__ inside its own logic. -Only [Synapse](brain.md) by the use of [Order](signals.md) must interact with the languages. This allow a Neuron to by reused by anyone, speaking any language. +## Constraints 1. Respect [PEP 257](https://www.python.org/dev/peps/pep-0257/) -- Docstring conventions. For each class or method add a description with summary, input parameter, returned parameter, type of parameter ``` @@ -131,17 +43,11 @@ We recommend the usage of an IDE like [Pycharm](https://www.jetbrains.com/pychar ##### Limitations -1. The management of incoming variable from the signal order when they are __numbers or float are not efficient__. (Thanks to @thebao for pointing this out!) +1. The management of incoming variable from the signal order when they are __numbers or float are not efficient__. - Because of the differences between the STTs outputs: some are returning word some numbers (two != 2). - Because of the i18n, we are not able to know if a variable should be interpreted in english, french, spanish, etc ... ("two" != "deux" != "dos") -## STT, TTS, Trigger - -They are managed like Neurons, you can follow the same process to develop your own ! - ## Share it -*Incoming* - -We are maintening a list of all the Neurons available from the community, let us know +We are maintening a list of all the Neurons available from the community, let us know you've developed your own by opening [an issue](../../issues) with the link of your neuron or send a pull request to update the [neuron list](neuron_list.md) directly. diff --git a/Docs/contributing/contribute_neuron.md b/Docs/contributing/contribute_neuron.md new file mode 100644 index 00000000..54dbae12 --- /dev/null +++ b/Docs/contributing/contribute_neuron.md @@ -0,0 +1,104 @@ +# Contributing: Create a neuron + +Neurons are independent projects so they can be developed under a github project. Anyone can clone them, place them under the neurons repository and reuse them. + +Creating a new Neuron must follow some rules: + +## Repository Structure +1. The Neuron repository name is in __lowercase__. +1. Under the Neuron repository, the Neuron has a __README.md file__ describing the Neuron following this structure. You can get a [template here](neuron_template.md): + - Neuron name: + - Installation: The CLI command used to install the neuron + - Synopsis: Description of the Neuron + - Options: A table of the incoming parameters managed by the Neuron. + - Return Values: A table of the returned values which can be catched by the *say_template attribute*. + - Synapses example: An example of how to use the Neuron inside a Synapse. + - Notes: Something which needs to be add. +1. Under the Neuron repository, include a __Tests repository__ to manage the test of the Neuron. +1. Under the neuron repository, a [dna.yml file](dna.md) must be added that contains information about the neuron. type = "neuron" +1. Under the neuron repository, a [install.yml file](installation_file.md) must be added that contains the installation process. + + +## Code +1. Under the Neuron repository, the Neuron file name .py is also in __lowercase__. +1. The Neuron must be coded in __Python 2.7__. +1. Under the Neuron repository, include the __init__.py file which contains: *from neuron import Neuron* (/!\ respect the Case) +1. Inside the Neuron file, the Neuron Class name is in __uppercase__. +1. The Neuron __inherits from the NeuronModule__ coming from the Core. + + ``` + from core.NeuronModule import NeuronModule + class Say(NeuronModule): + ``` + + +1. The Neuron has a constructor __init__ which is the entry point. +The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the brain file or in the signal. +1. The Neuron must refer to its __parent structure__ in the init by calling the super of NeuronModule. + + ``` + def __init__(self, **kwargs): + super(Say, self).__init__(**kwargs) + ``` + +1. You must run unit tests with success before sending a pull request. Add new tests that cover the code you want to publish. + ``` + cd /path/to/kalliope + python -m unittest discover + ``` + +1. (*optionnal-> good practice*) The Neuron can implement a __private method _is_parameters_ok(self)__ which checks if entries are ok. *return: true if parameters are ok, raise an exception otherwise* +1. (*optionnal-> good practice*) The Neuron can __import and raise exceptions__ coming from NeuronModule: + - MissingParameterException: *Some Neuron parameters are missing.* + - InvalidParameterException: *Some Neuron parameters are invalid.* + +1. The Neuron can use a __self.say(message) method__ to speak out some return values using the *say_template* attribute in the brain file. +the message variable must be a Dict of variable:values where variables can be defined as output. + +1. The Neuron must (as much as possible) ensure the i18n. This means that they should __not manage a specific language__ inside its own logic. +Only [Synapse](brain.md) by the use of [Order](signals.md) must interact with the languages. This allow a Neuron to by reused by anyone, speaking any language. + + +## Code example + +Example of neuron structure +``` +myneuron/ +├── __init__.py +├── myneuron.py +├── dna.yml +├── install.yml +├── README.md +└── tests + ├── __init__.py + └── test_myneuron.py +``` + +Example of neuron code +``` +class Myneuron(NeuronModule): +def __init__(self, **kwargs): + super(Myneuron, self).__init__(**kwargs) + # the args from the neuron configuration + self.arg1 = kwargs.get('arg1', None) + self.arg2 = kwargs.get('arg2', None) + + # check if parameters have been provided + if self._is_parameters_ok(): + # ------------------- + # do amazing code + # ------------------- + +def _is_parameters_ok(self): + """ + Check if received parameters are ok to perform operations in the neuron + :return: true if parameters are ok, raise an exception otherwise + + .. raises:: MissingParameterException + """ + if self.arg1 is None: + raise MissingParameterException("You must specify a arg1") + if not isinstance(self.arg2, int): + raise MissingParameterException("arg2 must be an integer") + return True +``` diff --git a/Docs/contributing/contribute_stt.md b/Docs/contributing/contribute_stt.md new file mode 100644 index 00000000..9a681e25 --- /dev/null +++ b/Docs/contributing/contribute_stt.md @@ -0,0 +1,77 @@ +# Contributing: Create a STT + +[STT](../stt.md) are independent projects so they can be developed under a github project. +Anyone can clone them and place them under the STT repository and reuse them or directly [install](../stt.md) them + +Creating a new STT must follow some rules: + +## Repository Structure +1. The STT repository name is in __lowercase__. +1. Under the STT repository, the STT has a __README.md file__ describing the STT following this structure. You can get a [template here](stt_template.md): + - STT name: + - Installation: The CLI command used to install the STT + - Synopsis: Description of the STT + - Options: A table of the incoming parameters managed by the STT. + - Notes: Something which needs to be add. + - Licence: The licence you want to use +1. Under the STT repository, a [dna.yml file](dna.md) must be added that contains information about the STT. type = "stt" +1. Under the STT repository, a [install.yml file](installation_file.md) must be added that contains the installation process. + + +## Code +1. Under the STT repository, the STT file name .py is also in __lowercase__. +1. The STT must be coded in __Python 2.7__. +1. Under the STT repository, include the __init__.py file which contains: *from stt import STT* (/!\ respect the Case) +1. Inside the STT file, the STT Class name is in __uppercase__. +1. The STT __inherits from the OrderListener__ coming from the Core. + + ``` + from kalliope.core.OrderListener import OrderListener + class Google(OrderListener): + ``` + +1. The STT has a constructor __init__ which is the entry point. +The constructor has an incoming callback to call once we get the text. +The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the settings file. +1. The STT must init itself first. +1. Attach the incoming callback to the self.attribute. +1. Obtain audio from the microphone in the constructor. (Note : we mostly use the [speech_recognition library](https://pypi.python.org/pypi/SpeechRecognition/)) +1. Once you get the text back, let give it to the callback + + ``` + def __init__(self, callback=None, **kwargs): + OrderListener.__init__(self) + self.callback = callback + # ------------------- + # do amazing code + # ------------------- + self.callback(audio_to_text) + ``` + + + +## Code example + +Example of STT structure +``` +mystt/ +├── __init__.py +├── mystt.py +├── dna.yml +├── install.yml +└── README.md +``` + +Example of STT code +``` +class Mystt(OrderListener): +def __init__(self, callback=None, **kwargs): + OrderListener.__init__(self) + self.callback = callback + # ------------------- + # - get the microphone audio + # - do amazing code to retrieve the text + # - call the callback giving it the result text -> self.callback(audio_to_text) + # ------------------- + +``` diff --git a/Docs/contributing/contribute_tts.md b/Docs/contributing/contribute_tts.md new file mode 100644 index 00000000..1aa14ae4 --- /dev/null +++ b/Docs/contributing/contribute_tts.md @@ -0,0 +1,90 @@ +# Contributing: Create a TTS + +[TTS](../tts.md) are independent projects so they can be developed under a github project. +Anyone can clone them and place them under the TTS repository and reuse them or directly [install](../tts.md) them + +Creating a new TTS must follow some rules: + +## Repository Structure +1. The TTS repository name is in __lowercase__. +1. Under the TTS repository, the TTS has a __README.md file__ describing the TTS following this structure. You can get a [template here](tts_template.md): + - TTS name: + - Installation: The CLI command used to install the TTS + - Synopsis: Description of the TTS + - Options: A table of the incoming parameters managed by the TTS. + - Notes: Something which needs to be add. + - Licence: The licence you want to use +1. Under the TTS repository, a [dna.yml file](dna.md) must be added that contains information about the TTS. type = "tts" +1. Under the TTS repository, a [install.yml file](installation_file.md) must be added that contains the installation process. + + +## Code +1. Under the TTS repository, the TTS file name .py is also in __lowercase__. +1. The TTS must be coded in __Python 2.7__. +1. Under the TTS repository, include the __init__.py file which contains: *from tts import TTS* (/!\ respect the Case) +1. Inside the TTS file, the TTS Class name is in __uppercase__. +1. The TTS __inherits from the TTSModule__ coming from the Core. + + ``` + from kalliope.core.TTS.TTSModule import TTSModule + class Pico2wave(TTSModule): + ``` + + +1. The TTS has a constructor __init__ which is the entry point. +The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the settings file. +1. The TTS must refer to its __parent structure__ in the init by calling the super of TTSModule. + + ``` + def __init__(self, **kwargs): + super(Pico2wave, self).__init__(**kwargs) + ``` + +1. The TTS __must__ implements a method _say(self, words) which must run call a method coming from the mother Class self.generate_and_play(words, callback). +1. Implement a callback in a separate method to run the audio. +This callback is in charge to get the sound and save it on the disk. You can use our lib "FileManager.write_in_file(file_path, content)" +1. The module must use `self.file_path` from the mother class to know the full path where to save the generated audio file. The file path is handled by the core in order to allow caching. +1. The generated audio file must be supported by Mplayer. Try to play a generated file with `mplayer /path/to/the/generated_audio_file` + +## Code example + +Example of TTS structure +``` +mytts/ +├── __init__.py +├── mytts.py +├── dna.yml +├── install.yml +└── README.md +``` + +Example of TTS code +``` +class Mytts(TTSModule): +def __init__(self, **kwargs): + super(Mytts, self).__init__(**kwargs) + # the args from the tts configuration + self.arg1 = kwargs.get('arg1', None) + self.arg2 = kwargs.get('arg2', None) + + def say(self, words): + """ + :param words: The sentence to say + """ + + self.generate_and_play(words, self._generate_audio_file) + + def _generate_audio_file(self): + """ + Generic method used as a Callback in TTSModule + - must provided the audio file and write it on the disk in self.file_path + + .. raises:: FailToLoadSoundFile + """ + # ------------------- + # - do amazing code to get the sound + # - save it to the disk using self.file_path + # - Attach the sound file path to the attribute : self.file_path = audio_file_path ! + # ------------------- + +``` diff --git a/Docs/contributing/dna.md b/Docs/contributing/dna.md new file mode 100644 index 00000000..3f827805 --- /dev/null +++ b/Docs/contributing/dna.md @@ -0,0 +1,30 @@ +# dna.yml file + +The dna file is the descriptor of your module. +This file has a yaml syntax and must be present to allow Kalliope to install it from the [CLI](../kalliope_cli.md). + +## DNA parameters + +| parameter | type | required | default | choices | comment | +|----------------------------|--------|----------|---------|------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| name | string | yes | | | Lowercase. It will be the name of the folder installed in ressources_dir for the target type of resource | +| type | string | yes | | neuron, stt, tts | The type of resource. This will be used by Kalliope install process to place the resource in the right directory set in resources_dir | +| author | string | no | | | String that contain info about the author of the modul like a name or a github profile page | +| kalliope_supported_version | list | yes | | 0.4.0 | list of kalliope version the module support. E.g `- 0.4.0` | +| tags | list | no | | | list of tags that can help to categorize the module. E.g: "email", "social network", "search engine" | + +## DNA file examples + +A dna file for a neuron +``` +name: "wikipedia_searcher" +type: "neuron" +author: "The dream team of Kalliope project" + +kalliope_supported_version: + - 0.4.0 + +tags: + - "search engine" + - "wiki" +``` diff --git a/Docs/contributing/installation_file.md b/Docs/contributing/installation_file.md new file mode 100644 index 00000000..b9cac044 --- /dev/null +++ b/Docs/contributing/installation_file.md @@ -0,0 +1,53 @@ +# install.yml file + +The installation file, called install.yml, must be placed at the root of a repository. This one will be read by Kalliope in order to install your module from the [command line](../kalliope_cli.md) by any Kalliope user. + +## How to create an install.yml file + +The module installation process is based on the Ansible program. Ansible is an IT orchestrator. It means it can help you to perform configuration management, application deployment or task automation. + +The `install.yml` file must contains what we called a Playbook in the Ansible world. +A playbook is like a recipe or an instructions manual which tells Ansible what to do against an host. In our case, the host will be the local machine of the current user who asked Kalliope to install the module. + +Let's see a basic playbook, the one used by the neuron [wikipedia_searcher](https://github.com/kalliope-project/kalliope_neuron_wikipedia) + +``` +- name: Kalliope wikipedia_searcher neuron install + hosts: localhost + gather_facts: no + connection: local + become: true + + tasks: + - name: "Install pip dependencies" + pip: + name: wikipedia + version: 1.4.0 +``` + +As the file is a **playbook**, it can contains multiple **play**. That's why the file start with a "-", the yaml syntax to define a list of element. In this example, our playbook contains only one play. + +The first element is the `name`. It can be anything you want. Here we've set what the play do. + +The `hosts` parameter is, like the name sugest us, to design on which host we want to apply our configuration. In the context of a Kalliope module installation, it will always be **localhost**. + +By default, ansible call a module to [gather useful variables](http://docs.ansible.com/ansible/setup_module.html) about remote hosts that can be used in playbooks. +In this example, we don't need it and so we disable the `gather_facts` feature in order to win a couple seconds during the installation process. + +In most of case, our play will need to apply admin operations. In this case, installing a python lib. So we set `become` to true to be allowed to install our lib as root user. + +The next part is `tasks`. This key must contains a list of task to apply on the target system. + +The only task we've added here is based on the [pip Ansible module](http://docs.ansible.com/ansible/pip_module.html). + +Ansible comes with a lot of modules, see the [complete list here](http://docs.ansible.com/ansible/modules_by_category.html). + +Here is an example which use the [apt module](http://docs.ansible.com/ansible/apt_module.html) to install Debian packages +``` +tasks: + - name: Install packages + apt: name={{ item }} update_cache=yes + with_items: + - flac + - mplayer +``` diff --git a/Docs/neuron_template.md b/Docs/contributing/neuron_template.md similarity index 92% rename from Docs/neuron_template.md rename to Docs/contributing/neuron_template.md index 61d5f452..10524775 100644 --- a/Docs/neuron_template.md +++ b/Docs/contributing/neuron_template.md @@ -4,6 +4,11 @@ Little description of what the neuron does. +## Installation +``` +kalliope install --git-url https://github.com/my_user/my_neuron.git +``` + ## Options (usage of a [table generator](http://www.tablesgenerator.com/markdown_tables) is recommended) @@ -54,3 +59,7 @@ This is a var {{ var }} ## Notes > **Note:** This is an important note concerning the neuron + +## Licence + +Here define or link the licence you want to use. diff --git a/Docs/contributing/stt_template.md b/Docs/contributing/stt_template.md new file mode 100644 index 00000000..21b6e696 --- /dev/null +++ b/Docs/contributing/stt_template.md @@ -0,0 +1,28 @@ +# Name of the STT + +## Synopsis + +Description of your STT + +## Installation + + kalliope install --git-url https://github.com/username/mystt.git + +## Options + +(usage of a [table generator](http://www.tablesgenerator.com/markdown_tables) is recommended) + +| parameter | required | default | choices | comments | +|------------------|----------|-------------------------------|-----------------------------------|------------------------------| +| parameter_name_1 | yes | | | description of the parameter | +| parameter_name_2 | no | | possible_value_1,possible_value_2 | description of the parameter | +| parameter_name_3 | yes | default_value_if_not_provided | | description of the parameter | + + +## Notes + +> **Note:** This is an important note concerning the neuron + +## Licence + +Here define or link the licence you want to use. diff --git a/Docs/installation.md b/Docs/installation.md index b06ea5b3..ae8208b6 100644 --- a/Docs/installation.md +++ b/Docs/installation.md @@ -4,7 +4,8 @@ Please follow the right link bellow to install requirements depending on your target environment: - [Raspbian (Raspberry Pi 2 & 3)](installation/raspbian_jessie.md) -- [Ubuntu 14.04/16.04](installation/ubuntu_16.04.md) +- [Ubuntu 14.04](installation/ubuntu_14.04.md) +- [Ubuntu 16.04](installation/ubuntu_16.04.md) - [Debian Jessie](installation/debian_jessie.md) ## Installation diff --git a/Docs/installation/ubuntu_14.04.md b/Docs/installation/ubuntu_14.04.md new file mode 100644 index 00000000..338c35e9 --- /dev/null +++ b/Docs/installation/ubuntu_14.04.md @@ -0,0 +1,10 @@ +# Kalliope requirements for Ubuntu 14.04 + +## Debian packages requirements + +Install some required system libraries and softwares: + +``` +sudo apt-get update +sudo apt-get install git python-pip python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev libjack0 libjack-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer +``` diff --git a/Docs/installation/ubuntu_16.04.md b/Docs/installation/ubuntu_16.04.md index aee99bc0..e2489545 100644 --- a/Docs/installation/ubuntu_16.04.md +++ b/Docs/installation/ubuntu_16.04.md @@ -1,4 +1,4 @@ -# Kalliope requirements for Ubuntu 14.04/16.04 +# Kalliope requirements for Ubuntu 16.04 ## Debian packages requirements diff --git a/Docs/kalliope_cli.md b/Docs/kalliope_cli.md index c3ace26c..9b73e912 100644 --- a/Docs/kalliope_cli.md +++ b/Docs/kalliope_cli.md @@ -32,10 +32,35 @@ Example of use kalliope gui ``` +### install +Install a community module. You must set an install type option. Currently the only available option is `--git-url`. + +Syntax +``` +kalliope install --git-url +``` + +Example of use +``` +kalliope install --git-url https://github.com/kalliope-project/kalliope_neuron_wikipedia.git +``` + ## OPTIONS Commands can be completed by the following options: +### -v or --version +Display the current isntalled version of Kalliope. + +Example of use +``` +kalliope --version +``` + +``` +kalliope -v +``` + ### --run-synapse SYNAPSE_NAME Run a specific synapse from the brain file. @@ -67,4 +92,9 @@ Show debug output in the console Example of use ``` kalliope start --debug -``` \ No newline at end of file +``` + +### --git-url + +Used by the `install` argument to specify the URL of a git repository of the module to install. + diff --git a/Docs/neuron_list.md b/Docs/neuron_list.md index 279b5c79..28df9ab1 100644 --- a/Docs/neuron_list.md +++ b/Docs/neuron_list.md @@ -2,22 +2,33 @@ A neuron is a module that will perform some actions attached to an order. You can use it in your synapses. See the [complete neuron documentation](neurons.md) for more information. -| Name | Description | -|---------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| [ansible_playbook](../kalliope/neurons/ansible_playbook/) | Run an ansible playbook | -| [gmail_checker](../kalliope/neurons/gmail_checker/) | Get the number of unread email and their subjects from a gmail account | -| [kill_switch](../kalliope/neurons/kill_switch/) | Stop Kalliope process | -| [neurotransmitter](../kalliope/neurons/neurotransmitter/) | Link synapse together | -| [push_message](../kalliope/neurons/push_message/) | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser | -| [rss_reader](../kalliope/neurons/rss_reader/) | get rss feed from website | -| [say](../kalliope/neurons/say/) | Make Kalliope talk by using TTS | -| [script](../kalliope/neurons/script/) | Run an executable script | -| [shell](../kalliope/neurons/shell/) | Run a shell command | -| [sleep](../kalliope/neurons/sleep/) | Make Kalliope sleep for a while before continuing | -| [systemdate](../kalliope/neurons/systemdate/) | Give the local system date and time | -| [tasker_autoremote](../kalliope/neurons/tasker_autoremote/) | Send a message to Android tasker app | -| [twitter](../kalliope/neurons/twitter/) | Send a Twit from kalliope | -| [uri](../kalliope/neurons/uri/) | Interacts with HTTP and HTTPS web services. | -| [wake_on_lan](../kalliope/neurons/wake_on_lan/) | Wake on lan a computer | -| [wikipedia_searcher](../kalliope/neurons/wikipedia_searcher/) | Search for a page on Wikipedia | +## Core neuron +| Name | Description | +|-----------------------------------------------------------|---------------------------------------------------| +| [ansible_playbook](../kalliope/neurons/ansible_playbook/) | Run an ansible playbook | +| [kill_switch](../kalliope/neurons/kill_switch/) | Stop Kalliope process | +| [neurotransmitter](../kalliope/neurons/neurotransmitter/) | Link synapse together | +| [say](../kalliope/neurons/say/) | Make Kalliope talk by using TTS | +| [script](../kalliope/neurons/script/) | Run an executable script | +| [shell](../kalliope/neurons/shell/) | Run a shell command | +| [sleep](../kalliope/neurons/sleep/) | Make Kalliope sleep for a while before continuing | +| [systemdate](../kalliope/neurons/systemdate/) | Give the local system date and time | +| [uri](../kalliope/neurons/uri/) | Interacts with HTTP and HTTPS web services. | + +## Community neuron + +| Name | Description | +|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| [gmail_checker](https://github.com/kalliope-project/kalliope_neuron_gmail) | Get the number of unread email and their subjects from a gmail account | +| [openweathermap](https://github.com/kalliope-project/kalliope_neuron_openweathermap) | Get the weather of a location | +| [pushetta](https://github.com/kalliope-project/kalliope_neuron_pushetta) | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser | +| [rss_reader](https://github.com/kalliope-project/kalliope_neuron_rss_reader) | get rss feed from website | +| [tasker](https://github.com/kalliope-project/kalliope_neuron_tasker) | Send a message to Android tasker app | +| [twitter](https://github.com/kalliope-project/kalliope_neuron_twitter) | Send a Twit from kalliope | +| [wake_on_lan](https://github.com/kalliope-project/kalliope_neuron_wake_on_lan) | Wake on lan a computer | +| [wikipedia](https://github.com/kalliope-project/kalliope_neuron_wikipedia) | Search for a page on Wikipedia | + +Wanna add your neuron in the list? Open [an issue](../../issues) with the link of your neuron or send a pull request to update the list directly. + +To know how to install a community neuron, read the "Installation" section of the [neuron documentation](neurons.md). diff --git a/Docs/neurons.md b/Docs/neurons.md index b254a620..6bbec1e6 100644 --- a/Docs/neurons.md +++ b/Docs/neurons.md @@ -3,6 +3,22 @@ A neuron is a plugin that performs a specific action. You use it to create a synapse. You can add as many neurons as you want to a synapse. The neurons are executed one by one when the input order is triggered. +## Installation + +Core neurons are already packaged with the installation of kalliope an can be used out of the box. Community neuron need to be installed manually. + +Use the CLI +``` +kalliope install --git-url +``` + +E.g: +``` +kalliope install --git-url https://github.com/kalliope-project/kalliope_neuron_wikipedia.git +``` + +You may be prompted to type your `sudo` password during the process. You can see the list of [available neuron here](neuron_list.md) + ## Usage Neurons are declared in the `neurons` section of a synapse in your brain file. The `neurons` section is a list (because it starts with a "-") which contains neuron modules names @@ -22,6 +38,8 @@ neurons: ``` > **note:** parameters are indented with two spaces bellow the neuron's name following the YAML syntax. +> **note:** Kalliope will try to load the neuron from your resources directory, then from core neuron packages. + To know the list of required parameters, check of documentation of the neuron. Full list of [available neuron here](neuron_list.md) diff --git a/Docs/settings.md b/Docs/settings.md index afd89808..2e9adc56 100644 --- a/Docs/settings.md +++ b/Docs/settings.md @@ -225,5 +225,24 @@ E.g default_synapse: "Default-response" ``` +## Resources directory + +The resources directory is the path where Kalliope will try to load community modules like Neurons, STTs or TTSs. +Set a valid path is required if you want to install community neuron. The path can be relative or absolute. + +``` +resource_directory: + resource_name: "path" +``` + +E.g +``` +resource_directory: + neuron: "resources/neurons" + stt: "resources/stt" + tts: "resources/tts" + trigger: "/full/path/to/trigger" +``` + ## Next: configure the brain of Kalliope Now your settings are ok, you can start creating the [brain](brain.md) of your assistant. diff --git a/Docs/signals.md b/Docs/signals.md index 82e2d1e6..bf3891bc 100644 --- a/Docs/signals.md +++ b/Docs/signals.md @@ -28,6 +28,9 @@ signals: > **Important note:** SST engines can misunderstand what you say, or translate your sentence into text containing some spelling mistakes. For example, if you say "Kalliope please do this", the SST engine can return "caliope please do this". So, to be sure that your speaking order will be correctly caught and executed, we recommend you to test your STT engine by using the [Kalliope GUI](kalliope_cli.md) and check the returned text for the given order. +> **Important note:** STT engines don't know the context. Sometime they will return an unexpected word. +For example, "the operation to perform is 2 minus 2" can return "two", "too", "to" or "2" in english. + > **Important note:** Kalliope will try to match the order in each synapse of its brain. So, if an order of one synapse is included in another order of another synapse, then both synapses tasks will be started by Kalliope. > For example, you have "test my umbrella" in a synapse A and "test" in a synapse B. When you'll say "test my umbrella", both synapse A and B diff --git a/Docs/stt.md b/Docs/stt.md index 63113d10..37659b7d 100644 --- a/Docs/stt.md +++ b/Docs/stt.md @@ -7,7 +7,6 @@ Each STT has a specific configuration and supports multiple languages. The configuration of each STT you use must appear in the [settings.yml](settings.md) file. - ## Settings The setting.yml defines the STT you want to use by default @@ -26,7 +25,8 @@ speech_to_text: ``` Sometime, an API key will be necessary to use an engine. Click on a TTS engine link in the `Current Available STT` section to know which parameter are required. -## Current Available STT +## Current CORE Available STT +Core STTs are already packaged with the installation of Kalliope an can be used out of the box. - [apiai](../kalliope/stt/apiai/README.md) - [bing](../kalliope/stt/bing/README.md) @@ -34,6 +34,17 @@ Sometime, an API key will be necessary to use an engine. Click on a TTS engine l - [houndify](../kalliope/stt/houndify/README.md) - [witai](../kalliope/stt/wit/README.md) +## STT Community Installation + +Community STTs need to be installed manually. + +Use the CLI +``` +kalliope install --git-url +``` + +You may be prompted to type your `sudo` password during the process. You can see the list of [available STT here](stt_list.md) + ## Full Example In the settings.yml file : diff --git a/Docs/stt_list.md b/Docs/stt_list.md new file mode 100644 index 00000000..6d345c77 --- /dev/null +++ b/Docs/stt_list.md @@ -0,0 +1,28 @@ +# List of available STT + +A stt is a module that Kalliope will use to speak out. You can define them in your [settings.yml file](settings.md). +See the [complete STT documentation](stt.md) for more information. + +## Core STT +Core STTs are already packaged with the installation of Kalliope an can be used out of the box. + +| Name | Description | +|----------|------------------------------------------------| +| api.ai | [apiai](../kalliope/stt/apiai/README.md) | +| bing | [bing](../kalliope/stt/bing/README.md) | +| Google | [google](../kalliope/stt/google/README.md) | +| Houndify | [houndify](../kalliope/stt/houndify/README.md) | +| wit.ai | [wit.ai](../kalliope/stt/wit/README.md) | + + +## Community STT +Community STTs need to be installed manually. + +To know how to install a community STT, read the "Installation" section of the [STT documentation](stt.md). + +| Name | Description | +|----------|------------------------------------------------| + +Wanna add your STT in the list? Open [an issue](../../issues) with the link of your STT or send a pull request to update the list directly. + + diff --git a/Docs/trigger.md b/Docs/trigger.md index effdc051..8edef4ae 100644 --- a/Docs/trigger.md +++ b/Docs/trigger.md @@ -22,12 +22,14 @@ E.g ``` kalliope-FR kalliope-EN +kalliope-RU ``` Then, open an issue or create a pull request to add the model to the list bellow. ## List of available Snowboy Kalliope model -| Name | language | -|-----------------------------------------------------|----------| -| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French | -| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English | +| Name | language | Pronouced | +|-----------------------------------------------------|----------|---------------| +| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French | Ka-lio-pé | +| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English | kə-LIE-ə-pee | +| [kalliope-RU](https://snowboy.kitt.ai/hotword/2964) | Russian | каллиопа | diff --git a/Docs/tts.md b/Docs/tts.md index 7fcabf9d..8473ee42 100644 --- a/Docs/tts.md +++ b/Docs/tts.md @@ -50,6 +50,7 @@ cache_path: "/tmp/kalliope_tts_cache" generated audio files that will not be played more than once. ## Current Available TTS +Core TTSs are already packaged with the installation of Kalliope an can be used out of the box. - [acapela](../kalliope/tts/acapela/README.md) - [googletts](../kalliope/tts/googletts/README.md) @@ -57,6 +58,17 @@ generated audio files that will not be played more than once. - [voicerss](../kalliope/tts/voicerss/README.md) - [voxygen](../kalliope/tts/voxygen/README.md) +## TTS Community Installation + +Community TTSs need to be installed manually. + +Use the CLI +``` +kalliope install --git-url +``` + +You may be prompted to type your `sudo` password during the process. You can see the list of [available TTS here](tts_list.md) + ## Full Example ``` @@ -81,4 +93,4 @@ text_to_speech: - voicerss: language: "fr-fr" cache: True -``` \ No newline at end of file +``` diff --git a/Docs/tts_list.md b/Docs/tts_list.md new file mode 100644 index 00000000..7e329ef8 --- /dev/null +++ b/Docs/tts_list.md @@ -0,0 +1,28 @@ +# List of available TTS + +A TTS is a module that Kalliope will use to speak out. You can define them in your [settings.yml file](settings.md). +See the [complete TTS documentation](stt.md) for more information. + +## Core TTS +Core TTSs are already packaged with the installation of Kalliope an can be used out of the box. + +| Name | Description | +|-----------|--------------------------------------------------| +| Acapela | [Acapela](../kalliope/tts/acapela/README.md) | +| GoogleTTS | [GoogleTTS](../kalliope/tts/googletts/README.md) | +| VoiceRSS | [VoiceRSS](../kalliope/tts/voicerss/README.md) | +| Pico2wave | [Pico2wave](../kalliope/tts/pico2wave/README.md) | +| Voxygen | [Voxygen](../kalliope/tts/voxygen/README.md) | + +## Community TTS +Community TTSs need to be installed manually. + +To know how to install a community TTS, read the "Installation" section of the [TTS documentation](tts.md). + +| Name | Description | +|--------|------------------------------------------------------| +| Espeak | [Espeak](https://github.com/Ultchad/kalliope-espeak) | + +Wanna add your TTS in the list? Open [an issue](../../issues) with the link of your TTS or send a pull request to update the list directly. + + diff --git a/README.md b/README.md index 63837e45..d1cd8373 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ Kalliope is easy-peasy to use, see the hello world - [Kalliope installation documentation](Docs/installation.md) +## Quick start + +We made starter kits that only needs to be cloned and launched. Starter kits can help you to learn basics of Kalliope +- [French starter kit](https://github.com/kalliope-project/kalliope_starter_fr) +- [English starter kit](https://github.com/kalliope-project/kalliope_starter_en) ## Usage @@ -55,7 +60,7 @@ Kalliope is easy-peasy to use, see the hello world If you'd like to contribute to Kalliope, please read our [Contributing Guide](Docs/contributing.md), which contains the philosophies to preserve, tests to run, and more. Reading through this guide before writing any code is recommended. -- Contribute +- Read the [contributing guide](Docs/contributing.md) - Add [issues and feature requests](../../issues) ## Credits @@ -64,6 +69,7 @@ Reading through this guide before writing any code is recommended. In Greek mythology she was a goddess of epic poetry and eloquence, one of the nine Muses. PRONOUNCED: kə-LIE-ə-pee (English) PRONOUNCED: Ka-li-o-pé (French) +PRONOUNCED: каллиопа (Russian) ## Links diff --git a/Tests/modules/dna.yml b/Tests/modules/dna.yml new file mode 100644 index 00000000..3ce3eea3 --- /dev/null +++ b/Tests/modules/dna.yml @@ -0,0 +1,2 @@ +# empty, just for testing file exist + diff --git a/Tests/modules/install.yml b/Tests/modules/install.yml new file mode 100644 index 00000000..75864506 --- /dev/null +++ b/Tests/modules/install.yml @@ -0,0 +1 @@ +# empty, just for testing file exist diff --git a/Tests/modules/test_invalid_dna.yml b/Tests/modules/test_invalid_dna.yml new file mode 100644 index 00000000..fff470ee --- /dev/null +++ b/Tests/modules/test_invalid_dna.yml @@ -0,0 +1,10 @@ +--- + name: "neuron_test" + type: "non existing" + author: "Kalliope project team" + + kalliope_supported_version: + - 0.4.0 + + tags: + - "test" diff --git a/Tests/modules/test_valid_dna.yml b/Tests/modules/test_valid_dna.yml new file mode 100644 index 00000000..81c13df6 --- /dev/null +++ b/Tests/modules/test_valid_dna.yml @@ -0,0 +1,10 @@ +--- + name: "neuron_test" + type: "neuron" + author: "Kalliope project team" + + kalliope_supported_version: + - 0.4.0 + + tags: + - "test" diff --git a/Tests/settings/settings_test.yml b/Tests/settings/settings_test.yml index 7c1cde10..d6578a41 100644 --- a/Tests/settings/settings_test.yml +++ b/Tests/settings/settings_test.yml @@ -78,3 +78,12 @@ rest_api: # --------------------------- # Specify an optional default synapse response in case your order is not found. default_synapse: "Default-synapse" + +# --------------------------- +# resource directory path +# --------------------------- +resource_directory: + neuron: "/tmp/kalliope/tests/kalliope_resources_dir/neurons" + stt: "/tmp/kalliope/tests/kalliope_resources_dir/stt" + tts: "/tmp/kalliope/tests/kalliope_resources_dir/tts" + trigger: "/tmp/kalliope/tests/kalliope_resources_dir/trigger" diff --git a/Tests/test_dna_loader.py b/Tests/test_dna_loader.py new file mode 100644 index 00000000..384d4df4 --- /dev/null +++ b/Tests/test_dna_loader.py @@ -0,0 +1,107 @@ +import os +import unittest + +from kalliope.core.ConfigurationManager.DnaLoader import DnaLoader +from kalliope.core.Models.Dna import Dna + + +class TestDnaLoader(unittest.TestCase): + + def setUp(self): + if "/Tests" in os.getcwd(): + self.dna_test_file = "modules/test_valid_dna.yml" + else: + self.dna_test_file = "Tests/modules/test_valid_dna.yml" + + def tearDown(self): + pass + + def test_get_yaml_config(self): + + expected_result = {'kalliope_supported_version': ['0.4.0'], + 'author': 'Kalliope project team', + 'type': 'neuron', + 'name': 'neuron_test', + 'tags': ['test']} + + dna_file_content = DnaLoader(self.dna_test_file).get_yaml_config() + + self.assertEqual(dna_file_content, expected_result) + + def test_get_dna(self): + + expected_result = Dna() + expected_result.name = "neuron_test" + expected_result.module_type = "neuron" + expected_result.tags = ['test'] + expected_result.author = 'Kalliope project team' + expected_result.kalliope_supported_version = ['0.4.0'] + + dna_to_test = DnaLoader(self.dna_test_file).get_dna() + + self.assertTrue(dna_to_test.__eq__(expected_result)) + + def test_load_dna(self): + # test with a valid DNA file + dna_to_test = DnaLoader(self.dna_test_file)._load_dna() + + self.assertTrue(isinstance(dna_to_test, Dna)) + + # test with a non valid DNA file + if "/Tests" in os.getcwd(): + dna_invalid_test_file = "modules/test_invalid_dna.yml" + else: + dna_invalid_test_file = "Tests/modules/test_invalid_dna.yml" + + self.assertIsNone(DnaLoader(dna_invalid_test_file)._load_dna()) + + def test_check_dna(self): + # check with valid DNA file + test_dna = {'kalliope_supported_version': ['0.4.0'], + 'author': 'Kalliope project team', + 'type': 'neuron', + 'name': 'neuron_test', + 'tags': ['test']} + + self.assertTrue(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) + + # invalid DNA file, no name + test_dna = {'kalliope_supported_version': ['0.4.0'], + 'author': 'Kalliope project team', + 'type': 'neuron', + 'tags': ['test']} + + self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) + + # invalid DNA file, no type + test_dna = {'kalliope_supported_version': ['0.4.0'], + 'author': 'Kalliope project team', + 'name': 'neuron_test', + 'tags': ['test']} + + self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) + + # invalid DNA, wrong type + test_dna = {'kalliope_supported_version': ['0.4.0'], + 'author': 'Kalliope project team', + 'type': 'doesnotexist', + 'name': 'neuron_test', + 'tags': ['test']} + + self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) + + # invalid DNA, no kalliope_supported_version + test_dna = {'author': 'Kalliope project team', + 'type': 'neuron', + 'name': 'neuron_test', + 'tags': ['test']} + self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) + + # invalid DNA, kalliope_supported_version empty + test_dna = {'kalliope_supported_version': [], + 'author': 'Kalliope project team', + 'type': 'neuron', + 'name': 'neuron_test', + 'tags': ['test']} + + self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna)) diff --git a/Tests/test_launchers.py b/Tests/test_launchers.py index d59f696a..a682bbd7 100644 --- a/Tests/test_launchers.py +++ b/Tests/test_launchers.py @@ -1,10 +1,11 @@ import unittest import mock -import os +from kalliope.core.Models.Resources import Resources from kalliope.core.NeuronLauncher import NeuronLauncher from kalliope.core.SynapseLauncher import SynapseLauncher, SynapseNameNotFound from kalliope.core.TriggerLauncher import TriggerLauncher +from kalliope.core.ConfigurationManager import SettingLoader from kalliope.core.Models.Trigger import Trigger from kalliope.core.Models.Neuron import Neuron @@ -32,9 +33,9 @@ def test_get_trigger(self): TriggerLauncher.get_trigger(trigger=trigger, callback=None) - mock_get_class_instantiation.assert_called_once_with("trigger", - trigger.name.capitalize(), - trigger.parameters) + mock_get_class_instantiation.assert_called_once_with(package_name="trigger", + module_name=trigger.name, + parameters=trigger.parameters) mock_get_class_instantiation.reset_mock() #### @@ -63,12 +64,21 @@ def test_start_synapse(self): br = Brain(synapses=all_synapse_list) + sl = SettingLoader() + r = Resources(neuron_folder="/var/tmp/test/resources") + sl.settings.resources = r with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation: # Success SynapseLauncher.start_synapse("Synapse1", brain=br) - calls = [mock.call("neurons", neuron1.name.capitalize(), neuron1.parameters), - mock.call("neurons", neuron2.name.capitalize(), neuron2.parameters)] + calls = [mock.call(package_name="neurons", + module_name=neuron1.name, + parameters=neuron1.parameters, + resources_dir='/var/tmp/test/resources'), + mock.call(package_name="neurons", + module_name=neuron2.name, + parameters=neuron2.parameters, + resources_dir='/var/tmp/test/resources')] mock_get_class_instantiation.assert_has_calls(calls=calls) mock_get_class_instantiation.reset_mock() @@ -85,11 +95,20 @@ def test_run_synapse(self): signal1 = Order(sentence="this is the sentence") synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) synapse_empty = Synapse(name="Synapse_empty", neurons=[], signals=[signal1]) + sl = SettingLoader() + resources = Resources(neuron_folder='/var/tmp/test/resources') + sl.settings.resources = resources with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation: SynapseLauncher._run_synapse(synapse=synapse1) - calls = [mock.call("neurons",neuron1.name.capitalize(),neuron1.parameters), - mock.call("neurons",neuron2.name.capitalize(),neuron2.parameters)] + calls = [mock.call(package_name="neurons", + module_name=neuron1.name, + parameters=neuron1.parameters, + resources_dir="/var/tmp/test/resources"), + mock.call(package_name="neurons", + module_name=neuron2.name, + parameters=neuron2.parameters, + resources_dir="/var/tmp/test/resources")] mock_get_class_instantiation.assert_has_calls(calls=calls) mock_get_class_instantiation.reset_mock() @@ -105,11 +124,15 @@ def test_start_neuron(self): Test the Neuron Launcher trying to start a Neuron """ neuron = Neuron(name='neurone1', parameters={'var1': 'val1'}) + sl = SettingLoader() + resources = Resources(neuron_folder='/var/tmp/test/resources') + sl.settings.resources = resources with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation: NeuronLauncher.start_neuron(neuron=neuron) - mock_get_class_instantiation.assert_called_once_with("neurons", - neuron.name.capitalize(), - neuron.parameters) + mock_get_class_instantiation.assert_called_once_with(package_name="neurons", + module_name=neuron.name, + parameters=neuron.parameters, + resources_dir=sl.settings.resources.neuron_folder) mock_get_class_instantiation.reset_mock() diff --git a/Tests/test_neuron_module.py b/Tests/test_neuron_module.py index a486db4f..3b4e9736 100644 --- a/Tests/test_neuron_module.py +++ b/Tests/test_neuron_module.py @@ -3,6 +3,10 @@ import mock from kalliope.core.NeuronModule import NeuronModule, TemplateFileNotFoundException +from kalliope.core.Models.Neuron import Neuron +from kalliope.core.Models.Synapse import Synapse +from kalliope.core.Models.Brain import Brain +from kalliope.core.Models.Order import Order class TestNeuronModule(unittest.TestCase): @@ -29,11 +33,12 @@ def test_get_audio_from_stt(self): """ with mock.patch("kalliope.core.OrderListener.start") as mock_orderListener_start: - def callback(): - pass - NeuronModule.get_audio_from_stt(callback=callback()) - mock_orderListener_start.assert_called_once_with() - mock_orderListener_start.reset_mock() + with mock.patch("kalliope.core.OrderListener.join") as mock_orderListener_join: + def callback(): + pass + NeuronModule.get_audio_from_stt(callback=callback()) + mock_orderListener_start.assert_called_once_with() + mock_orderListener_start.reset_mock() def test_update_cache_var(self): """ @@ -107,3 +112,63 @@ def test_get_file_template(self): def test_get_content_of_file(self): expected_result = "hello, this is a {{ test }}" self.assertEqual(NeuronModule._get_content_of_file(self.file_template), expected_result) + + def test_run_synapse_by_name_with_order(self): + """ + Test to start a synapse with a specific given order + Scenarii : + - Neuron has been found and launched + - Neuron has not been found + """ + + # Init + neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) + neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) + neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) + neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) + + signal1 = Order(sentence="the sentence") + signal2 = Order(sentence="the second sentence") + signal3 = Order(sentence="part of the third sentence") + + synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) + synapse3 = Synapse(name="Synapse3", neurons=[neuron2, neuron4], signals=[signal3]) + + all_synapse_list = [synapse1, + synapse2, + synapse3] + + br = Brain(synapses=all_synapse_list) + + order = "This is the order" + synapse_name = "Synapse2" + answer = "This is the {{ answer }}" + + with mock.patch("kalliope.core.OrderAnalyser.start") as mock_orderAnalyser_start: + neuron_mod = NeuronModule() + neuron_mod.brain = br + + # Success + self.assertTrue(neuron_mod.run_synapse_by_name_with_order(order=order, + synapse_name=synapse_name, + order_template=answer), + "fail to find the proper synapse") + + # mock_orderAnalyser_start.assert_called_once() + mock_orderAnalyser_start.assert_called_once_with(synapses_to_run=[synapse2], + external_order=answer) + mock_orderAnalyser_start.reset_mock() + + # Fail + synapse_name = "Synapse5" + self.assertFalse(neuron_mod.run_synapse_by_name_with_order(order=order, + synapse_name=synapse_name, + order_template=answer), + "fail to NOT find the synapse") + + mock_orderAnalyser_start.assert_not_called() + mock_orderAnalyser_start.reset_mock() + + + diff --git a/Tests/test_order_analyser.py b/Tests/test_order_analyser.py index c64dce95..13ab7d55 100644 --- a/Tests/test_order_analyser.py +++ b/Tests/test_order_analyser.py @@ -5,6 +5,7 @@ from kalliope.core.Models.Neuron import Neuron from kalliope.core.Models.Synapse import Synapse from kalliope.core.Models.Brain import Brain +from kalliope.core.Models.Settings import Settings from kalliope.core.Models.Order import Order @@ -21,7 +22,9 @@ def test_start(self): Scenarii : - Order matchs a synapse and the synapse has been launched. - Order does not match but have a default synapse. - - Order does not match and does not ahve default synapse. + - Order does not match and does not have default synapse. + - Provide synapse without any external orders + - Provide synapse with any external orders """ # Init neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) @@ -43,9 +46,6 @@ def test_start(self): br = Brain(synapses=all_synapse_list) - def _start_neuron_mock(cls, neuron, params): - pass - with mock.patch("kalliope.core.OrderAnalyser._start_neuron") as mock_start_neuron_method: # assert synapses have been launched order_to_match = "this is the sentence" @@ -81,6 +81,35 @@ def _start_neuron_mock(cls, neuron, params): expected_result, "Fail to no synapse because no synapse matchs and no default defined") + # Provide synapse to run + order_to_match = "this is the sentence" + oa = OrderAnalyser(order=order_to_match, + brain=br) + expected_result = [synapse1] + synapses_to_run = [synapse1] + + self.assertEquals(oa.start(synapses_to_run=synapses_to_run), + expected_result, + "Fail to run the provided synapse to run") + calls = [mock.call(neuron1, {}), mock.call(neuron2, {})] + mock_start_neuron_method.assert_has_calls(calls=calls) + mock_start_neuron_method.reset_mock() + + # Provide synapse and external orders + order_to_match = "this is an external sentence" + oa = OrderAnalyser(order=order_to_match, + brain=br) + external_orders = "this is an external {{ order }}" + synapses_to_run = [synapse2] + expected_result = [synapse2] + + self.assertEquals(oa.start(synapses_to_run=synapses_to_run, external_order=external_orders), + expected_result, + "Fail to run a provided synapse with external order") + calls = [mock.call(neuron3, {"order":u"sentence"}), mock.call(neuron4, {"order":u"sentence"})] + mock_start_neuron_method.assert_has_calls(calls=calls) + mock_start_neuron_method.reset_mock() + def test_start_neuron(self): """ Testing params association and starting a Neuron @@ -181,12 +210,12 @@ def test_spelt_order_match_brain_order_via_table(self): sentence_to_test = "this is the order" # Success - self.assertTrue(OrderAnalyser._spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), + self.assertTrue(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), "Fail matching order with the expected sentence") # Failure sentence_to_test = "unexpected sentence" - self.assertFalse(OrderAnalyser._spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), + self.assertFalse(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), "Fail to ensure the expected sentence is not matching the order") def test_get_split_order_without_bracket(self): @@ -335,11 +364,13 @@ def test_get_matching_synapse_list(self): synapse2, synapse3] - expected_result = [synapse1] + # Success - self.assertEquals(OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match), + expected_result = synapse1 + oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, + order_to_match=order_to_match) + self.assertEquals(oa_tuple_list[0].synapse, expected_result, "Fail matching 'the expected synapse' from the complete synapse list and the order") @@ -355,10 +386,14 @@ def test_get_matching_synapse_list(self): expected_result = [synapse1, synapse2] - self.assertEquals(OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match), - expected_result, - "Fail 'Multiple Matching synapses' from the complete synapse list and the order") + oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, + order_to_match=order_to_match) + self.assertEquals(oa_tuple_list[0].synapse, + expected_result[0], + "Fail 'Multiple Matching synapses' from the complete synapse list and the order (first element)") + self.assertEquals(oa_tuple_list[1].synapse, + expected_result[1], + "Fail 'Multiple Matching synapses' from the complete synapse list and the order (second element)") # matching no synapses order_to_match = "this is not the correct word" @@ -384,88 +419,77 @@ def test_get_matching_synapse_list(self): expected_result = [synapse1, synapse2] - self.assertEquals(OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match), - expected_result, - "Fail matching 'synapse with all key worlds' from the complete synapse list and the order") + oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, + order_to_match=order_to_match) - def test_get_synapse_params(self): - # Init - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) - - signal1 = Order(sentence="this is the {{ sentence }}") + self.assertEquals(oa_tuple_list[0].synapse, + expected_result[0], + "Fail matching 'synapse with all key worlds' from the complete synapse list and the order (first element)") + self.assertEquals(oa_tuple_list[1].synapse, + expected_result[1], + "Fail matching 'synapse with all key worlds' from the complete synapse list and the order (second element)") - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + def test_get_params_from_order(self): + string_order = "this is the {{ sentence }}" order_to_check = "this is the value" expected_result = {'sentence': 'value'} - self.assertEquals(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEquals(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve 'the params' of the synapse from the order") + "Fail to retrieve 'the params' of the string_order from the order") # Multiple match - signal1 = Order(sentence="this is the {{ sentence }}") - - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + string_order = "this is the {{ sentence }}" order_to_check = "this is the value with multiple words" expected_result = {'sentence': 'value with multiple words'} - self.assertEqual(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve the 'multiple words params' of the synapse from the order") + "Fail to retrieve the 'multiple words params' of the string_order from the order") # Multiple params - signal1 = Order(sentence="this is the {{ sentence }} with multiple {{ params }}") - - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + string_order = "this is the {{ sentence }} with multiple {{ params }}" order_to_check = "this is the value with multiple words" expected_result = {'sentence': 'value', 'params':'words'} - self.assertEqual(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve the 'multiple params' of the synapse from the order") + "Fail to retrieve the 'multiple params' of the string_order from the order") # Multiple params with multiple words - signal1 = Order(sentence="this is the {{ sentence }} with multiple {{ params }}") - - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + string_order = "this is the {{ sentence }} with multiple {{ params }}" order_to_check = "this is the multiple values with multiple values as words" expected_result = {'sentence': 'multiple values', 'params': 'values as words'} - self.assertEqual(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve the 'multiple params with multiple words' of the synapse from the order") + "Fail to retrieve the 'multiple params with multiple words' of the string_order from the order") # params at the begining of the sentence - signal1 = Order(sentence="{{ sentence }} this is the sentence") - - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + string_order = "{{ sentence }} this is the sentence" order_to_check = "hello world this is the multiple values with multiple values as words" expected_result = {'sentence': 'hello world'} - self.assertEqual(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve the 'params at the begining of the sentence' of the synapse from the order") + "Fail to retrieve the 'params at the begining of the sentence' of the string_order from the order") # all of the sentence is a variable - signal1 = Order(sentence="{{ sentence }}") - - synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + string_order = "{{ sentence }}" order_to_check = "this is the all sentence is a variable" expected_result = {'sentence': 'this is the all sentence is a variable'} - self.assertEqual(OrderAnalyser._get_synapse_params(synapse=synapse1, order_to_check=order_to_check), + self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), expected_result, - "Fail to retrieve the 'all of the sentence is a variable' of the synapse from the order") + "Fail to retrieve the 'all of the sentence is a variable' of the string_order from the order") def test_get_default_synapse_from_sysnapses_list(self): # Init @@ -494,6 +518,62 @@ def test_get_default_synapse_from_sysnapses_list(self): expected_result, "Fail to match the expected default Synapse") + def test_find_synapse_to_run(self): + """ + Test to find the good synapse to run + Scenarii: + - Find the synapse + - No synpase found, no default synapse + - No synapse found, run the default synapse + """ + # Init + neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) + neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) + neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) + neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) + + signal1 = Order(sentence="this is the sentence") + signal2 = Order(sentence="this is the second sentence") + signal3 = Order(sentence="that is part of the third sentence") + + synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) + synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) + synapse3 = Synapse(name="Synapse3", neurons=[neuron2, neuron4], signals=[signal3]) + + all_synapse_list = [synapse1, + synapse2, + synapse3] + + br = Brain(synapses=all_synapse_list) + st = Settings() + # Find synapse + order = "this is the sentence" + expected_result = synapse1 + oa_tuple_list = OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order) + self.assertEquals(oa_tuple_list[0].synapse, + expected_result, + "Fail to run the proper synapse matching the order") + + expected_result = signal1.sentence + self.assertEquals(oa_tuple_list[0].order, + expected_result, + "Fail to run the proper synapse matching the order") + # No Default synapse + order = "No default synapse" + expected_result = [] + self.assertEquals(OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order), + expected_result, + "Fail to run no synapse, when no default is defined") + + # Default synapse + st = Settings(default_synapse="Synapse2") + order = "default synapse" + expected_result = synapse2 + oa_tuple_list = OrderAnalyser._find_synapse_to_run(brain=br, settings=st, order=order) + self.assertEquals(oa_tuple_list[0].synapse, + expected_result, + "Fail to run the default synapse") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_resources_manager.py b/Tests/test_resources_manager.py new file mode 100644 index 00000000..23878420 --- /dev/null +++ b/Tests/test_resources_manager.py @@ -0,0 +1,152 @@ +import os +import unittest + +from mock import mock + +from kalliope import ResourcesManager +from kalliope.core.Models import Resources +from kalliope.core.Models.Dna import Dna + + +class TestResourcesmanager(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_settings_ok(self): + # ----------------- + # valid resource + # ----------------- + # valid neuron + valid_resource = Resources() + valid_resource.neuron_folder = "/path" + dna = Dna() + dna.module_type = "neuron" + self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid stt + valid_resource = Resources() + valid_resource.stt_folder = "/path" + dna = Dna() + dna.module_type = "stt" + self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid tts + valid_resource = Resources() + valid_resource.tts_folder = "/path" + dna = Dna() + dna.module_type = "tss" + self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid trigger + valid_resource = Resources() + valid_resource.trigger_folder = "/path" + dna = Dna() + dna.module_type = "trigger" + self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # ----------------- + # invalid resource + # ----------------- + # valid neuron + valid_resource = Resources() + valid_resource.neuron_folder = None + dna = Dna() + dna.module_type = "neuron" + self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid stt + valid_resource = Resources() + valid_resource.stt_folder = None + dna = Dna() + dna.module_type = "stt" + self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid tts + valid_resource = Resources() + valid_resource.tts_folder = None + dna = Dna() + dna.module_type = "tts" + self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + + # valid trigger + valid_resource = Resources() + valid_resource.trigger_folder = None + dna = Dna() + dna.module_type = "trigger" + self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + + def test_is_repo_ok(self): + # valid repo + if "/Tests" in os.getcwd(): + dna_file_path = "modules/dna.yml" + install_file_path = "modules/install.yml" + else: + dna_file_path = "Tests/modules/dna.yml" + install_file_path = "Tests/modules/install.yml" + self.assertTrue(ResourcesManager.is_repo_ok(dna_file_path=dna_file_path, install_file_path=install_file_path)) + + # missing dna + if "/Tests" in os.getcwd(): + dna_file_path = "" + install_file_path = "modules/install.yml" + else: + dna_file_path = "T" + install_file_path = "Tests/modules/install.yml" + self.assertFalse(ResourcesManager.is_repo_ok(dna_file_path=dna_file_path, install_file_path=install_file_path)) + + # missing install + if "/Tests" in os.getcwd(): + dna_file_path = "modules/dna.yml" + install_file_path = "" + else: + dna_file_path = "Tests/modules/dna.yml" + install_file_path = "" + self.assertFalse(ResourcesManager.is_repo_ok(dna_file_path=dna_file_path, install_file_path=install_file_path)) + + def test_get_target_folder(self): + # test get neuron folder + resources = Resources() + resources.neuron_folder = '/var/tmp/test/resources' + self.assertEqual(ResourcesManager._get_target_folder(resources, "neuron"), "/var/tmp/test/resources") + + # test get stt folder + resources = Resources() + resources.stt_folder = '/var/tmp/test/resources' + self.assertEqual(ResourcesManager._get_target_folder(resources, "stt"), "/var/tmp/test/resources") + + # test get tts folder + resources = Resources() + resources.tts_folder = '/var/tmp/test/resources' + self.assertEqual(ResourcesManager._get_target_folder(resources, "tts"), "/var/tmp/test/resources") + + # test get trigger folder + resources = Resources() + resources.trigger_folder = '/var/tmp/test/resources' + self.assertEqual(ResourcesManager._get_target_folder(resources, "trigger"), "/var/tmp/test/resources") + + # test get non existing resource + resources = Resources() + self.assertIsNone(ResourcesManager._get_target_folder(resources, "not_existing")) + + def test_check_supported_version(self): + # version ok + current_version = '0.4.0' + supported_version = ['0.4.0', '0.3.0', '0.2.0'] + + self.assertTrue(ResourcesManager._check_supported_version(current_version=current_version, + supported_versions=supported_version)) + + # version non ok, useer does not confir + current_version = '0.4.0' + supported_version = ['0.3.0', '0.2.0'] + + with mock.patch('kalliope.Utils.query_yes_no', return_value=True): + self.assertTrue(ResourcesManager._check_supported_version(current_version=current_version, + supported_versions=supported_version)) + + with mock.patch('kalliope.Utils.query_yes_no', return_value=False): + self.assertFalse(ResourcesManager._check_supported_version(current_version=current_version, + supported_versions=supported_version)) diff --git a/Tests/test_settings_loader.py b/Tests/test_settings_loader.py index 21b8081f..c9f17e8f 100644 --- a/Tests/test_settings_loader.py +++ b/Tests/test_settings_loader.py @@ -1,9 +1,12 @@ import os +import inspect import platform +import shutil import unittest from kalliope.core.ConfigurationManager import SettingLoader from kalliope.core.Models import Singleton +from kalliope.core.Models import Resources from kalliope.core.Models.RestAPI import RestAPI from kalliope.core.Models.Settings import Settings from kalliope.core.Models.Stt import Stt @@ -14,8 +17,12 @@ class TestSettingLoader(unittest.TestCase): def setUp(self): + # get current script directory path. We are in /an/unknown/path/kalliope/core/Tests + cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + # get parent dir. Now we are in /an/unknown/path/kalliope + root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir) - self.settings_file_to_test = os.getcwd() + os.sep + "/Tests/settings/settings_test.yml" + self.settings_file_to_test = root_dir + os.sep + "Tests/settings/settings_test.yml" self.settings_dict = { 'rest_api': @@ -36,10 +43,25 @@ def setUp(self): {'pico2wave': {'cache': True, 'language': 'fr-FR'}}, {'voxygen': {'voice': 'Agnes', 'cache': True}} ], - 'default_synapse': 'Default-synapse' + 'default_synapse': 'Default-synapse', + 'resource_directory':{ + 'neuron': "/tmp/kalliope/tests/kalliope_resources_dir/neurons", + 'stt': "/tmp/kalliope/tests/kalliope_resources_dir/stt", + 'tts': "/tmp/kalliope/tests/kalliope_resources_dir/tts", + 'trigger': "/tmp/kalliope/tests/kalliope_resources_dir/trigger" + } } + # Init the folders, otherwise it raises an exceptions + os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/neurons") + os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/stt") + os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/tts") + os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/trigger") + def tearDown(self): + # Cleanup + shutil.rmtree('/tmp/kalliope/tests/kalliope_resources_dir') + Singleton._instances = {} def test_singleton(self): @@ -72,6 +94,12 @@ def test_get_settings(self): login="admin", password="secret", port=5000) settings_object.cache_path = '/tmp/kalliope_tts_cache' settings_object.default_synapse = 'Default-synapse' + resources = Resources(neuron_folder="/tmp/kalliope/tests/kalliope_resources_dir/neurons", + stt_folder="/tmp/kalliope/tests/kalliope_resources_dir/stt", + tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts", + trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger") + + settings_object.resources=resources settings_object.machine = platform.machine() sl = SettingLoader(file_path=self.settings_file_to_test) @@ -133,5 +161,16 @@ def test_get_default_synapse(self): sl = SettingLoader(file_path=self.settings_file_to_test) self.assertEqual(expected_default_synapse, sl._get_default_synapse(self.settings_dict)) + def test_get_resources(self): + + resources = Resources(neuron_folder="/tmp/kalliope/tests/kalliope_resources_dir/neurons", + stt_folder="/tmp/kalliope/tests/kalliope_resources_dir/stt", + tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts", + trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger") + expected_resource = resources + sl = SettingLoader(file_path=self.settings_file_to_test) + self.assertEquals(expected_resource, sl._get_resources(self.settings_dict)) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_utils.py b/Tests/test_utils.py index 35ae790e..bfc5fda2 100644 --- a/Tests/test_utils.py +++ b/Tests/test_utils.py @@ -1,10 +1,16 @@ import unittest import os +import mock from kalliope.core.Models.Neuron import Neuron +from kalliope.core.Models.Order import Order +from kalliope.core.Models.Synapse import Synapse from kalliope.neurons.say.say import Say from kalliope.core.Utils.Utils import Utils +from kalliope.core.ConfigurationManager import SettingLoader +from kalliope.core.ConfigurationManager import BrainLoader + class TestUtils(unittest.TestCase): """ @@ -121,11 +127,14 @@ def test_get_dynamic_class_instantiation(self): """ Test that an instance as been instantiate properly. """ + sl = SettingLoader() + sl.settings.resource_dir = '/var/tmp/test/resources' neuron = Neuron(name='Say', parameters={'message': 'test dynamic class instantiate'}) - self.assertTrue(isinstance(Utils.get_dynamic_class_instantiation("neurons", - neuron.name.capitalize(), - neuron.parameters), + self.assertTrue(isinstance(Utils.get_dynamic_class_instantiation(package_name="neurons", + module_name=neuron.name.capitalize(), + parameters=neuron.parameters, + resources_dir='/var/tmp/test/resources'), Say), "Fail instantiate a class") diff --git a/brain_examples/neurotransmitter.yml b/brain_examples/neurotransmitter.yml index e995b612..3c013916 100644 --- a/brain_examples/neurotransmitter.yml +++ b/brain_examples/neurotransmitter.yml @@ -18,7 +18,6 @@ - name: "synapse1" signals: - order: "pose moi une question" - - order: "pose-moi une question" neurons: - say: message: "aimez vous les frites?" @@ -51,6 +50,42 @@ - name: "synapse4" signals: - order: "synapse4" + neurons: + - say: + message: "Je n'ai pas compris votre réponse" + + + - name: "synapse5" + signals: + - order: "demande-moi la météo" + neurons: + - say: + message: "De quelle ville voulez-vous connaitre la météo?" + - neurotransmitter: + from_answer_link: + - synapse: "synapse6" + answers: + - "la météo à {{ location }}" + default: "synapse7" + + - name: "synapse6" + signals: + - order: "quel temps fait-il {{ location }}" + neurons: + - openweathermap: + api_key: "your-api" + lang: "fr" + temp_unit: "celsius" + country: "FR" + args: + - location + say_template: + - "Aujourd'hui a {{ location }} le temps est {{ weather_today }} avec une température de {{ temp_today_temp }} degrés et demain le temps sera {{ weather_tomorrow }} avec une température de {{ temp_tomorrow_temp }} degrés" + + + - name: "synapse7" + signals: + - order: "synapse7" neurons: - say: message: "Je n'ai pas compris votre réponse" \ No newline at end of file diff --git a/install/files/deb-packages_requirements.txt b/install/files/deb-packages_requirements.txt index b77d8e74..441719f6 100644 --- a/install/files/deb-packages_requirements.txt +++ b/install/files/deb-packages_requirements.txt @@ -10,7 +10,6 @@ python-pycparser python-paramiko python-markupsafe apt-transport-https -python-pip python-dev libsmpeg0 libttspico-utils diff --git a/install/files/python_requirements.txt b/install/files/python_requirements.txt index 1b9913c2..55d66ae3 100644 --- a/install/files/python_requirements.txt +++ b/install/files/python_requirements.txt @@ -6,18 +6,13 @@ ansible==2.2.0.0 python2-pythondialog==3.4.0 jinja2==2.8 cffi==1.9.1 -pygmail==0.0.5.4 -pushetta==1.0.15 -wakeonlan==0.2.2 ipaddress==1.0.17 -pyowm==2.5.0 -python-twitter==3.1 flask==0.11.1 Flask-Restful==0.3.5 -wikipedia==1.4.0 -requests==2.12.1 +requests==2.12.4 httpretty==0.8.14 mock==2.0.0 -feedparser==5.2.1 Flask-Testing==0.6.1 apscheduler==3.3.0 +GitPython==2.1.1 +packaging>=16.8 diff --git a/kalliope/__init__.py b/kalliope/__init__.py index 2d6cfad4..a79a41a7 100644 --- a/kalliope/__init__.py +++ b/kalliope/__init__.py @@ -8,9 +8,12 @@ from kalliope.core.ConfigurationManager.BrainLoader import BrainLoader from kalliope.core.EventManager import EventManager from kalliope.core.MainController import MainController + +from _version import version_str import signal import sys +from kalliope.core.ResourcesManager import ResourcesManager from kalliope.core.SynapseLauncher import SynapseLauncher logging.basicConfig() @@ -28,7 +31,7 @@ def signal_handler(signal, frame): sys.exit(0) # actions available -ACTION_LIST = ["start", "gui"] +ACTION_LIST = ["start", "gui", "install"] def main(): @@ -37,10 +40,12 @@ def main(): """ # create arguments parser = argparse.ArgumentParser(description='Kalliope') - parser.add_argument("action", help="[start|gui]") + parser.add_argument("action", help="[start|gui|install]") parser.add_argument("--run-synapse", help="Name of a synapse to load surrounded by quote") parser.add_argument("--brain-file", help="Full path of a brain file") parser.add_argument("--debug", action='store_true', help="Show debug output") + parser.add_argument("--git-url", help="Git URL of the neuron to install") + parser.add_argument('-v', '--version', action='version', version='Kalliope ' + version_str) # parse arguments from script parameters args = parser.parse_args() @@ -89,6 +94,16 @@ def main(): if args.action == "gui": ShellGui(brain=brain) + if args.action == "install": + if not args.git_url: + Utils.print_danger("You must specify the git url") + else: + parameters = { + "git_url": args.git_url + } + res_manager = ResourcesManager(**parameters) + res_manager.install() + def configure_logging(debug=None): """ diff --git a/kalliope/_version.py b/kalliope/_version.py index 95d4e320..f91e472d 100644 --- a/kalliope/_version.py +++ b/kalliope/_version.py @@ -1,2 +1,2 @@ # https://www.python.org/dev/peps/pep-0440/ -version_str = "0.3.0" +version_str = "0.4.0" diff --git a/kalliope/core/ConfigurationManager/ConfigurationChecker.py b/kalliope/core/ConfigurationManager/ConfigurationChecker.py index f60cb765..8b944829 100644 --- a/kalliope/core/ConfigurationManager/ConfigurationChecker.py +++ b/kalliope/core/ConfigurationManager/ConfigurationChecker.py @@ -1,7 +1,9 @@ import re +import os +import imp from kalliope.core.Utils.Utils import ModuleNotFoundError - +from kalliope.core.ConfigurationManager.SettingLoader import SettingLoader class InvalidSynapeName(Exception): """ @@ -139,13 +141,26 @@ def check_neuron_exist(neuron_module_name): :type neuron_module_name: str :return: """ - package_name = "kalliope.neurons" - mod = __import__(package_name, fromlist=[neuron_module_name]) + sl = SettingLoader() + settings = sl.settings + package_name = "kalliope.neurons" + "." + neuron_module_name.lower() + "." + neuron_module_name.lower() + if settings.resources is not None: + neuron_resource_path = settings.resources.neuron_folder + \ + os.sep + neuron_module_name.lower() + os.sep + \ + neuron_module_name.lower()+".py" + if os.path.exists(neuron_resource_path): + imp.load_source(neuron_module_name.capitalize(), neuron_resource_path) + package_name = neuron_module_name.capitalize() + try: - getattr(mod, neuron_module_name) + mod = __import__(package_name, fromlist=[neuron_module_name.capitalize()]) + getattr(mod, neuron_module_name.capitalize()) except AttributeError: - raise ModuleNotFoundError("The module %s does not exist in package %s" % (neuron_module_name, - package_name)) + raise ModuleNotFoundError("[AttributeError] The module %s does not exist in the package %s " % (neuron_module_name.capitalize(), + package_name)) + except ImportError: + raise ModuleNotFoundError("[ImportError] The module %s does not exist in the package %s " % (neuron_module_name.capitalize(), + package_name)) return True if isinstance(neuron_dict, dict): diff --git a/kalliope/core/ConfigurationManager/DnaLoader.py b/kalliope/core/ConfigurationManager/DnaLoader.py new file mode 100644 index 00000000..09899209 --- /dev/null +++ b/kalliope/core/ConfigurationManager/DnaLoader.py @@ -0,0 +1,93 @@ +from kalliope.core import Utils +from kalliope.core.ConfigurationManager import YAMLLoader +from kalliope.core.Models.Dna import Dna + + +class InvalidDNAException(Exception): + pass + +VALID_DNA_MODULE_TYPE = ["neuron", "stt", "tts", "trigger"] + + +class DnaLoader(object): + + def __init__(self, file_path): + """ + Load a DNA file and check the content of this one + :param file_path: path the the DNA file to load + """ + self.file_path = file_path + if self.file_path is None: + raise InvalidDNAException("[DnaLoader] You must set a file file") + + self.yaml_config = YAMLLoader.get_config(self.file_path) + self.dna = self._load_dna() + + def get_yaml_config(self): + """ + Class Methods which loads default or the provided YAML file and return it as a String + :return: The loaded DNA YAML file + :rtype: String + """ + return self.yaml_config + + def get_dna(self): + """ + Return the loaded DNA object if this one is valid + :return: + """ + return self.dna + + def _load_dna(self): + """ + retur a DNA object from a loaded yaml file + :return: + """ + new_dna = None + if self._check_dna_file(self.yaml_config): + new_dna = Dna() + new_dna.name = self.yaml_config["name"] + new_dna.module_type = self.yaml_config["type"] + new_dna.author = self.yaml_config["author"] + new_dna.kalliope_supported_version = self.yaml_config["kalliope_supported_version"] + new_dna.tags = self.yaml_config["tags"] + + return new_dna + + @staticmethod + def _check_dna_file(dna_file): + """ + Check the content of a DNA file + :param dna_file: the dna to check + :return: True if ok, False otherwise + """ + success_loading = True + if "name" not in dna_file: + Utils.print_danger("The DNA of does not contains a \"name\" tag") + success_loading = False + + if "type" not in dna_file: + Utils.print_danger("The DNA of does not contains a \"type\" tag") + success_loading = False + + else: + # we have a type, check that is a valid one + if dna_file["type"] not in VALID_DNA_MODULE_TYPE: + Utils.print_danger("The DNA type %s is not valid" % dna_file["type"]) + Utils.print_danger("The DNA type must be one of the following: %s" % VALID_DNA_MODULE_TYPE) + success_loading = False + + if "kalliope_supported_version" not in dna_file: + Utils.print_danger("The DNA of does not contains a \"kalliope_supported_version\" tag") + success_loading = False + else: + # kalliope_supported_version must be a non empty list + if not isinstance(dna_file["kalliope_supported_version"], list): + Utils.print_danger("kalliope_supported_version is not a list") + success_loading = False + else: + if not dna_file["kalliope_supported_version"]: + Utils.print_danger("kalliope_supported_version cannot be empty") + success_loading = False + + return success_loading diff --git a/kalliope/core/ConfigurationManager/SettingLoader.py b/kalliope/core/ConfigurationManager/SettingLoader.py index 65be39ae..a4488aff 100644 --- a/kalliope/core/ConfigurationManager/SettingLoader.py +++ b/kalliope/core/ConfigurationManager/SettingLoader.py @@ -1,7 +1,9 @@ import logging +import os from YAMLLoader import YAMLLoader -from kalliope.core.Utils import Utils +from kalliope.core.Models.Resources import Resources +from kalliope.core.Utils.Utils import Utils from kalliope.core.Models import Singleton from kalliope.core.Models.RestAPI import RestAPI from kalliope.core.Models.Settings import Settings @@ -106,6 +108,7 @@ def _get_settings(self): rest_api = self._get_rest_api(settings) cache_path = self._get_cache_path(settings) default_synapse = self._get_default_synapse(settings) + resources = self._get_resources(settings) # Load the setting singleton with the parameters setting_object.default_tts_name = default_tts_name @@ -119,6 +122,7 @@ def _get_settings(self): setting_object.rest_api = rest_api setting_object.cache_path = cache_path setting_object.default_synapse = default_synapse + setting_object.resources = resources return setting_object @@ -507,4 +511,67 @@ def _get_default_synapse(settings): return default_synapse + @staticmethod + def _get_resources(settings): + """ + Return a resources object that contains path of third party modules + + :param settings: The YAML settings file + :type settings: dict + :return: the resource object + :rtype: Resources + + :Example: + + resource_directory = cls._get_resource_dir(settings) + + .. seealso:: + .. raises:: SettingNotFound, NullSettingException, SettingInvalidException + .. warnings:: Class Method and Private + """ + try: + resource_dir = settings["resource_directory"] + logger.debug("Resource directory synapse: %s" % resource_dir) + + neuron_folder = None + stt_folder = None + tts_folder = None + trigger_folder = None + if "neuron" in resource_dir: + neuron_folder = resource_dir["neuron"] + if not os.path.exists(neuron_folder): + raise SettingInvalidException("The path %s does not exist on the system" % neuron_folder) + + if "stt" in resource_dir: + stt_folder = resource_dir["stt"] + if not os.path.exists(stt_folder): + raise SettingInvalidException("The path %s does not exist on the system" % stt_folder) + + if "tts" in resource_dir: + tts_folder = resource_dir["tts"] + if not os.path.exists(tts_folder): + raise SettingInvalidException("The path %s does not exist on the system" % tts_folder) + + if "trigger" in resource_dir: + trigger_folder = resource_dir["trigger"] + if not os.path.exists(trigger_folder): + raise SettingInvalidException("The path %s does not exist on the system" % trigger_folder) + + if neuron_folder is None \ + and stt_folder is None \ + and tts_folder is None \ + and trigger_folder is None: + raise SettingInvalidException("No required folder has been provided in the setting resource_directory. " + "Define : \'neuron\' or/and \'stt\' or/and \'tts\' or/and \'trigger\'") + + resource_object = Resources(neuron_folder=neuron_folder, + stt_folder=stt_folder, + tts_folder=tts_folder, + trigger_folder=trigger_folder) + except KeyError: + logger.debug("Resource directory not found in settings") + resource_object = None + + return resource_object + diff --git a/kalliope/core/Models/Brain.py b/kalliope/core/Models/Brain.py index f1d0bde8..6714b6c4 100644 --- a/kalliope/core/Models/Brain.py +++ b/kalliope/core/Models/Brain.py @@ -10,6 +10,22 @@ def __init__(self, synapses=None, brain_file=None, brain_yaml=None): self.brain_file = brain_file self.brain_yaml = brain_yaml + def get_synapse_by_name(self, synapse_name): + """ + Get the synapse, using its synapse name, from the synapse list + :param synapse_name: the name of the synapse to get + :type synapse_name: str + :return: The Synapse corresponding to the name + :rtype: Synapse + """ + synapse_launched = None + for synapse in self.synapses: + if synapse.name == synapse_name: + synapse_launched = synapse + # we found the synapse, we don't need to check the rest of the list + break + return synapse_launched + def __eq__(self, other): """ This is used to compare 2 objects diff --git a/kalliope/core/Models/Dna.py b/kalliope/core/Models/Dna.py new file mode 100644 index 00000000..2d8daf48 --- /dev/null +++ b/kalliope/core/Models/Dna.py @@ -0,0 +1,40 @@ + + +class Dna(object): + + def __init__(self, name=None, module_type=None, author=None, kalliope_supported_version=None, tags=None): + self.name = name + self.module_type = module_type # type is a reserved python + self.author = author + self.kalliope_supported_version = kalliope_supported_version + self.tags = tags + + def serialize(self): + """ + This method allows to serialize in a proper way this object + + :return: A dict of name and parameters + :rtype: Dict + """ + return { + 'name': self.name, + 'type': self.module_type, + 'author': self.author, + 'kalliope_supported_version': self.kalliope_supported_version, + 'tags': self.tags + } + + def __str__(self): + return "Dna: name: %s, " \ + "type: %s, " \ + "author: %s, " \ + "kalliope_supported_version: %s, " \ + "tags: %s" % (self.name, self.module_type, self.author, self.kalliope_supported_version, self.tags) + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/Neuron.py b/kalliope/core/Models/Neuron.py index 6dc07038..2e282ef0 100644 --- a/kalliope/core/Models/Neuron.py +++ b/kalliope/core/Models/Neuron.py @@ -18,7 +18,7 @@ def serialize(self): """ return { 'name': self.name, - 'parameters': str(self.parameters) + 'parameters': self.parameters } def __str__(self): diff --git a/kalliope/core/Models/Resources.py b/kalliope/core/Models/Resources.py new file mode 100644 index 00000000..b2b30911 --- /dev/null +++ b/kalliope/core/Models/Resources.py @@ -0,0 +1,41 @@ + + +class Resources(object): + """ + + """ + def __init__(self, neuron_folder=None, stt_folder=None, tts_folder=None, trigger_folder=None): + self.neuron_folder = neuron_folder + self.stt_folder = stt_folder + self.tts_folder = tts_folder + self.trigger_folder = trigger_folder + + def __str__(self): + return "%s: neuron_folder: %s, stt_folder: %s, tts_folder: %s, trigger_folder: %s" % (self.__class__.__name__, + self.neuron_folder, + self.stt_folder, + self.tts_folder, + self.trigger_folder) + + def serialize(self): + """ + This method allows to serialize in a proper way this object + + :return: A dict of order + :rtype: Dict + """ + + return { + 'neuron_folder': self.neuron_folder, + 'stt_folder': self.stt_folder, + 'tts_folder': self.tts_folder, + 'trigger_folder': self.trigger_folder + } + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/Settings.py b/kalliope/core/Models/Settings.py index a4057e41..41d39565 100644 --- a/kalliope/core/Models/Settings.py +++ b/kalliope/core/Models/Settings.py @@ -1,4 +1,5 @@ import platform +from kalliope._version import version_str as current_kalliope_version class Settings(object): @@ -19,7 +20,9 @@ def __init__(self, rest_api=None, cache_path=None, default_synapse=None, - machine=None): + resources=None, + machine=None, + kalliope_version=None): self.default_tts_name = default_tts_name self.default_stt_name = default_stt_name @@ -32,7 +35,9 @@ def __init__(self, self.rest_api = rest_api self.cache_path = cache_path self.default_synapse = default_synapse + self.resources = resources self.machine = platform.machine() # can be x86_64 or armv7l + self.kalliope_version = current_kalliope_version def __eq__(self, other): """ diff --git a/kalliope/core/Models/__init__.py b/kalliope/core/Models/__init__.py index 0b4f9bd6..60ab5dab 100644 --- a/kalliope/core/Models/__init__.py +++ b/kalliope/core/Models/__init__.py @@ -1,5 +1,6 @@ from Singleton import Singleton from Event import Event +from Resources import Resources from Brain import Brain from Order import Order from Synapse import Synapse diff --git a/kalliope/core/NeuronLauncher.py b/kalliope/core/NeuronLauncher.py index 6edad1b9..6c72725a 100644 --- a/kalliope/core/NeuronLauncher.py +++ b/kalliope/core/NeuronLauncher.py @@ -1,6 +1,7 @@ import logging from kalliope.core.Utils.Utils import Utils +from kalliope.core.ConfigurationManager.SettingLoader import SettingLoader logging.basicConfig() logger = logging.getLogger("kalliope") @@ -20,6 +21,12 @@ def start_neuron(cls, neuron): :return: """ logger.debug("Run plugin \"%s\" with parameters %s" % (neuron.name, neuron.parameters)) - return Utils.get_dynamic_class_instantiation("neurons", - neuron.name.capitalize(), - neuron.parameters) + sl = SettingLoader() + settings = sl.settings + neuron_folder = None + if settings.resources: + neuron_folder = settings.resources.neuron_folder + return Utils.get_dynamic_class_instantiation(package_name="neurons", + module_name=neuron.name, + parameters=neuron.parameters, + resources_dir=neuron_folder) diff --git a/kalliope/core/NeuronModule.py b/kalliope/core/NeuronModule.py index 59c6acac..7a985597 100644 --- a/kalliope/core/NeuronModule.py +++ b/kalliope/core/NeuronModule.py @@ -1,12 +1,12 @@ # coding: utf8 import logging -import os import random import sys from jinja2 import Template from kalliope.core import OrderListener +from kalliope.core import OrderAnalyser from kalliope.core.SynapseLauncher import SynapseLauncher from kalliope.core.Utils.Utils import Utils from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader @@ -129,8 +129,9 @@ def say(self, message): logger.debug("NeuroneModule: TTS args: %s" % tts_object) # get the instance of the TTS module - tts_module_instance = Utils.get_dynamic_class_instantiation("tts", tts_object.name.capitalize(), - tts_object.parameters) + tts_module_instance = Utils.get_dynamic_class_instantiation(package_name="tts", + module_name=tts_object.name, + parameters=tts_object.parameters) # generate the audio file and play it tts_module_instance.say(tts_message) @@ -182,6 +183,32 @@ def _get_file_template(cls, file_template, message_dict): def run_synapse_by_name(self, name): SynapseLauncher.start_synapse(name=name, brain=self.brain) + def is_order_matching(self, order_said, order_match): + oa = OrderAnalyser(order=order_said, brain=self.brain) + return oa.spelt_order_match_brain_order_via_table(order_to_analyse=order_match, user_said=order_said) + + def run_synapse_by_name_with_order(self, order, synapse_name, order_template): + """ + Run a synapse using its name, and giving an order so it can retrieve its params. + Useful for neurotransmitters. + :param order: the order to match + :param synapse_name: the name of the synapse + :param order_template: order_template coming from the neurotransmitter + :return: True if a synapse as been found and started using its params + """ + synapse_to_run = self.brain.get_synapse_by_name(synapse_name=synapse_name) + if synapse_to_run: + # Make a list with the synapse + logger.debug("[run_synapse_by_name_with_order]-> a synapse has been found %s" % synapse_to_run.name) + list_to_run = list() + list_to_run.append(synapse_to_run) + + oa = OrderAnalyser(order=order, brain=self.brain) + oa.start(synapses_to_run=list_to_run, external_order=order_template) + else: + logger.debug("[NeuronModule]-> run_synapse_by_name_with_order, the synapse has not been found : %s" % synapse_name) + return synapse_to_run is not None + @staticmethod def _get_content_of_file(real_file_template_path): """ @@ -212,8 +239,9 @@ def get_audio_from_stt(callback): :param callback: A callback function """ # call the order listener - oa = OrderListener(callback=callback) - oa.start() + ol = OrderListener(callback=callback) + ol.start() + ol.join() def get_neuron_name(self): """ diff --git a/kalliope/core/OrderAnalyser.py b/kalliope/core/OrderAnalyser.py index 54b6109e..b6c57964 100644 --- a/kalliope/core/OrderAnalyser.py +++ b/kalliope/core/OrderAnalyser.py @@ -1,5 +1,6 @@ # coding: utf8 import re +import collections from collections import Counter from kalliope.core.Utils.Utils import Utils @@ -17,6 +18,7 @@ class OrderAnalyser: """ This Class is used to compare the incoming message to the Signal/Order sentences. """ + def __init__(self, order, brain=None): """ Class used to load brain and run neuron attached to the received order @@ -31,73 +33,125 @@ def __init__(self, order, brain=None): self.brain = brain logger.debug("OrderAnalyser, Received order: %s" % self.order) - def start(self): + def start(self, synapses_to_run=None, external_order=None): + """ + This method matches the incoming messages to the signals/order sentences provided in the Brain. + + Note: we use named tuples: + tuple_synapse_order = collections.namedtuple('tuple_synapse_matchingOrder',['synapse', 'order']) """ - This method matches the incoming messages to the signals/order sentences provided in the Brain + + synapse_order_tuple = collections.namedtuple('tuple_synapse_matchingOrder', ['synapse', 'order']) + synapses_order_tuple_list = list() + + if synapses_to_run is not None and external_order is not None: + for synapse in synapses_to_run: + synapses_order_tuple_list.append(synapse_order_tuple(synapse=synapse, + order=external_order)) + + # if list of synapse is not provided, let's find one + else: # synapses_to_run is None or external_order is None: + # create a dict of synapses that have been launched + logger.debug("[orderAnalyser.start]-> No Synapse provided, let's find one") + synapses_order_tuple_list = self._find_synapse_to_run(brain=self.brain, + settings=self.settings, + order=self.order) + + # retrieve params + synapses_launched = list() + for tuple in synapses_order_tuple_list: + logger.debug("[orderAnalyser.start]-> Grab the params") + params = self._get_params_from_order(tuple.order, self.order) + + # Start a neuron list with params + self._start_list_neurons(list_neurons=tuple.synapse.neurons, + params=params) + synapses_launched.append(tuple.synapse) + + # return the list of launched synapse + return synapses_launched + + @classmethod + def _find_synapse_to_run(cls, brain, settings, order): """ + Find the list of the synapse matching the order. - # create a dict of synapses that have been launched - launched_synapses = self._get_matching_synapse_list(self.brain.synapses, self.order) + Note: we use named tuples: + tuple_synapse_order = collections.namedtuple('tuple_synapse_matchingOrder',['synapse', 'order']) - if not launched_synapses: - Utils.print_info("No synapse match the captured order: %s" % self.order) + :param brain: the brain + :param settings: the settings + :param order: the provided order to match + :return: the list of synapses launched (named tuples) + """ + + synapse_to_run = cls._get_matching_synapse_list(brain.synapses, order) + if not synapse_to_run: + Utils.print_info("No synapse match the captured order: %s" % order) - if self.settings.default_synapse is not None: - default_synapse = self._get_default_synapse_from_sysnapses_list(self.brain.synapses, - self.settings.default_synapse) + if settings.default_synapse is not None: + default_synapse = cls._get_default_synapse_from_sysnapses_list(brain.synapses, + settings.default_synapse) if default_synapse is not None: logger.debug("Default synapse found %s" % default_synapse) Utils.print_info("Default synapse found: %s, running it" % default_synapse.name) - launched_synapses.append(default_synapse) + tuple_synapse_order = collections.namedtuple('tuple_synapse_matchingOrder', + ['synapse', 'order']) + synapse_to_run.append(tuple_synapse_order(synapse=default_synapse, + order="")) - for synapse in launched_synapses: - params = self._get_synapse_params(synapse, self.order) - for neuron in synapse.neurons: - self._start_neuron(neuron, params) - - # return the list of launched synapse - return launched_synapses + return synapse_to_run @classmethod def _get_matching_synapse_list(cls, all_synapses_list, order_to_match): """ Class method to return all the matching synapses with the order from the complete of synapses. + Note: we use named tuples: + tuple_synapse_matchingOrder = collections.namedtuple('tuple_synapse_matchingOrder',['synapse', 'order']) + :param all_synapses_list: the complete list of all synapses :param order_to_match: the order to match :type order_to_match: str - :return: the list of matching synapses + :return: the list of matching synapses (named tuples) """ + tuple_synapse_order = collections.namedtuple('tuple_synapse_matchingOrder', ['synapse', 'order']) matching_synapses_list = list() for synapse in all_synapses_list: for signal in synapse.signals: if type(signal) == Order: - if cls._spelt_order_match_brain_order_via_table(signal.sentence, order_to_match): - matching_synapses_list.append(synapse) + if cls.spelt_order_match_brain_order_via_table(signal.sentence, order_to_match): + matching_synapses_list.append(tuple_synapse_order(synapse=synapse, + order=signal.sentence)) logger.debug("Order found! Run neurons: %s" % synapse.neurons) Utils.print_success("Order matched in the brain. Running synapse \"%s\"" % synapse.name) return matching_synapses_list @classmethod - def _get_synapse_params(cls, synapse, order_to_check): + def _get_params_from_order(cls, string_order, order_to_check): """ - Class method to get all params coming from a synapse. Returns a dict of key/value. + Class method to get all params coming from a string order. Returns a dict of key/value. - :param synapse: the synapse to check + :param string_order: the string_order to check :param order_to_check: the order to match :type order_to_check: str :return: the dict key/value """ params = dict() - for signal in synapse.signals: - if cls._is_containing_bracket(signal.sentence): - params = cls._associate_order_params_to_values(order_to_check, signal.sentence) - logger.debug("Parameters for order: %s" % params) + if cls._is_containing_bracket(string_order): + params = cls._associate_order_params_to_values(order_to_check, string_order) + logger.debug("Parameters for order: %s" % params) return params @classmethod - def _start_neuron(cls, neuron, params): + def _start_list_neurons(cls, list_neurons, params): + # start neurons + for neuron in list_neurons: + cls._start_neuron(neuron, params) + + @staticmethod + def _start_neuron(neuron, params): """ Associate params and Starts a neuron. @@ -146,6 +200,9 @@ def _associate_order_params_to_values(cls, order, order_to_check): :type order: str :return: the dict corresponding to the key / value of the params """ + logger.debug("[OrderAnalyser._associate_order_params_to_values] user order: %s, " + "order to check: %s" % (order, order_to_check)) + pattern = '\s+(?=[^\{\{\}\}]*\}\})' # Remove white spaces (if any) between the variable and the double brace then split list_word_in_order = re.sub(pattern, '', order_to_check).split() @@ -201,7 +258,7 @@ def _get_next_value_list(list_to_check): return next(ite, None) @classmethod - def _spelt_order_match_brain_order_via_table(cls, order_to_analyse, user_said): + def spelt_order_match_brain_order_via_table(cls, order_to_analyse, user_said): """ return true if all string that are in the sentence are present in the order to test :param order_to_analyse: String order to test diff --git a/kalliope/core/OrderListener.py b/kalliope/core/OrderListener.py index a0d8dc15..2f348c07 100644 --- a/kalliope/core/OrderListener.py +++ b/kalliope/core/OrderListener.py @@ -21,7 +21,7 @@ class OrderListener(Thread): def __init__(self, callback=None, stt=None): """ - This class is called after we catch the hotword that have woke up Kalliope. + This class is called after we catch the hotword that has woken up Kalliope. We now wait for an order spoken out loud by the user, translate the order into a text and run the action attached to this order from settings :param callback: callback function to call @@ -55,8 +55,8 @@ def load_stt_plugin(self): for stt_object in self.settings.stts: if stt_object.name == self.stt_module_name: stt_object.parameters["callback"] = self.callback - Utils.get_dynamic_class_instantiation('stt', - stt_object.name.capitalize(), + Utils.get_dynamic_class_instantiation(package_name='stt', + module_name=stt_object.name.capitalize(), parameters=stt_object.parameters) @staticmethod diff --git a/kalliope/core/ResourcesManager.py b/kalliope/core/ResourcesManager.py new file mode 100644 index 00000000..f59ff5a1 --- /dev/null +++ b/kalliope/core/ResourcesManager.py @@ -0,0 +1,275 @@ +import getpass +import logging +import os +import shutil + +from git import Repo +from packaging import version + +from kalliope.core.ConfigurationManager import SettingLoader +from kalliope.core.ConfigurationManager.DnaLoader import DnaLoader +from kalliope.core.Models import Neuron +from kalliope.core.NeuronLauncher import NeuronLauncher +from kalliope.core.Utils import Utils + +logging.basicConfig() +logger = logging.getLogger("kalliope") + +# Global values for processing: +LOCAL_TMP_FOLDER = "/tmp/kalliope/resources/" +TMP_GIT_FOLDER = "kalliope_new_module_temp_name" +DNA_FILE_NAME = "dna.yml" +INSTALL_FILE_NAME = "install.yml" + +# Global values for required parameters in DNA: +DNA_NAME = "name" +DNA_TYPE = "type" + +# Global_Names for 'types' to match: +TYPE_NEURON = "neuron" +TYPE_TTS = "tts" +TYPE_STT = "stt" +TYPE_TRIGGER = "trigger" + + +class ResourcesManagerException(Exception): + pass + + +class ResourcesManager(object): + def __init__(self, **kwargs): + """ + This class is used to manage community resources. + :param kwargs: + git-url: the url of the module to clone and install + """ + super(ResourcesManager, self).__init__() + # get settings + sl = SettingLoader() + self.settings = sl.settings + + # in case of update or install, url where + self.git_url = kwargs.get('git_url', None) + + # temp path where we install the new module + self.tmp_path = LOCAL_TMP_FOLDER + TMP_GIT_FOLDER + self.dna_file_path = self.tmp_path + os.sep + DNA_FILE_NAME + self.install_file_path = self.tmp_path + os.sep + INSTALL_FILE_NAME + self.dna = None + + def install(self): + """ + Module installation method. + """ + # first, we clone the repo + self._clone_repo(path=self.tmp_path, + git_url=self.git_url) + + # check the content of the cloned repo + if self.is_repo_ok(dna_file_path=self.dna_file_path, + install_file_path=self.install_file_path): + + # Load the dna.yml file + self.dna = DnaLoader(self.dna_file_path).get_dna() + if self.dna is not None: + logger.debug("[ResourcesManager] DNA file content: " + str(self.dna)) + if self.is_settings_ok(resources=self.settings.resources, dna=self.dna): + # the dna file is ok, check the supported version + if self._check_supported_version(current_version=self.settings.kalliope_version, + supported_versions=self.dna.kalliope_supported_version): + + # Let's find the target folder depending the type + module_type = self.dna.module_type.lower() + target_folder = self._get_target_folder(resources=self.settings.resources, + module_type=module_type) + if target_folder is not None: + # let's move the tmp folder in the right folder and get a new path for the module + module_name = self.dna.name.lower() + target_path = self._rename_temp_folder(name=self.dna.name.lower(), + target_folder=target_folder, + tmp_path=self.tmp_path) + + # if the target_path exists, then run the install file within the new repository + if target_path is not None: + self.install_file_path = target_path + os.sep + INSTALL_FILE_NAME + self.run_ansible_playbook_module(install_file_path=self.install_file_path) + Utils.print_success("Module: %s installed" % module_name) + else: + logger.debug("[ResourcesManager] installation cancelled, deleting temp repo %s" + % str(self.tmp_path)) + shutil.rmtree(self.tmp_path) + + @staticmethod + def is_settings_ok(resources, dna): + """ + Test if required settings files in config of Kalliope are ok. + The resource object must not be empty + Check id the use have set the an installation path in his settings for the target module type + :param resources: the Resources model + :param dna: DNA info about the module to install + :return: + """ + settings_ok = True + if resources is None: + message = "Resources folder not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False + else: + if dna.module_type == "neuron" and resources.neuron_folder is None: + message = "Resources folder for neuron installation not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False + if dna.module_type == "stt" and resources.stt_folder is None: + message = "Resources folder for stt installation not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False + if dna.module_type == "tts" and resources.tts_folder is None: + message = "Resources folder for tts installation not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False + if dna.module_type == "trigger" and resources.trigger_folder is None: + message = "Resources folder for trigger installation not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False + + return settings_ok + + @staticmethod + def is_repo_ok(dna_file_path, install_file_path): + """ + Check if the git cloned repo is fine to be installed + :return: True if repo is ok to be installed, False otherwise + """ + Utils.print_info("Checking repository...") + repo_ok = True + # check that a install.yml file is present + if not os.path.exists(install_file_path): + Utils.print_danger("Missing %s file" % INSTALL_FILE_NAME) + repo_ok = False + + if not os.path.exists(dna_file_path): + Utils.print_danger("Missing %s file" % DNA_FILE_NAME) + repo_ok = False + + return repo_ok + + @staticmethod + def _get_target_folder(resources, module_type): + """ + Return the folder from the resources and given a module type + :param resources: Resource object + :type resources: Resources + :param module_type: type of the module + :return: path of the folder + """ + # dict to get the path behind a type of resource + module_type_converter = { + TYPE_NEURON: resources.neuron_folder, + TYPE_STT: resources.stt_folder, + TYPE_TTS: resources.tts_folder, + TYPE_TRIGGER: resources.trigger_folder + } + # Let's find the right path depending of the type + try: + folder_path = module_type_converter[module_type] + except KeyError: + folder_path = None + # No folder_path has been found + message = "No %s folder set in settings, cannot install." % module_type + if folder_path is None: + logger.debug(message) + Utils.print_danger(message) + + return folder_path + + @staticmethod + def _clone_repo(path, git_url): + """ + Use git to clone locally the neuron in a temp folder + :return: + """ + # clone the repo + logger.debug("[ResourcesManager] GIT clone into folder: %s" % path) + Utils.print_info("Cloning repository...") + # if the folder already exist we remove it + if os.path.exists(path): + shutil.rmtree(path) + else: + os.makedirs(path) + Repo.clone_from(git_url, path) + + @staticmethod + def _rename_temp_folder(name, target_folder, tmp_path): + """ + Rename the temp folder of the cloned repo + Return the name of the path to install + :return: path to install, None if already exists + """ + logger.debug("[ResourcesManager] Rename temp folder") + new_absolute_neuron_path = target_folder + os.sep + name + try: + os.rename(tmp_path, new_absolute_neuron_path) + return new_absolute_neuron_path + except OSError: + # the folder already exist + Utils.print_warning("The module %s already exist in the path %s" % (name, target_folder)) + # remove the cloned repo + logger.debug("[ResourcesManager] Deleting temp folder %s" % str(tmp_path)) + shutil.rmtree(tmp_path) + + @staticmethod + def run_ansible_playbook_module(install_file_path): + """ + Run the install.yml file through an Ansible playbook using the dedicated neuron ! + + :param install_file_path: the path of the Ansible playbook to run. + :return: + """ + logger.debug("[ResourcesManager] Run ansible playbook") + Utils.print_info("Starting neuron installation") + # ask the sudo password + pswd = getpass.getpass('Sudo password:') + ansible_neuron_parameters = { + "task_file": install_file_path, + "sudo": True, + "sudo_user": "root", + "sudo_password": pswd + } + neuron = Neuron(name="ansible_playbook", parameters=ansible_neuron_parameters) + NeuronLauncher.start_neuron(neuron) + + @staticmethod + def _check_supported_version(current_version, supported_versions): + """ + The dna file contains supported Kalliope version for the module to install. + Check if supported versions are match the current installed version. If not, ask the user to confirm the + installation anyway + :param current_version: current version installed of Kalliope. E.g 0.4.0 + :param supported_versions: list of supported version + :return: True if the version is supported or user has confirmed the installation + """ + logger.debug("[ResourcesManager] Current installed version of Kalliope: %s" % str(current_version)) + logger.debug("[ResourcesManager] Module supported version: %s" % str(supported_versions)) + + supported_version_found = False + for supported_version in supported_versions: + if version.parse(current_version) == version.parse(supported_version): + # we found the exact version + supported_version_found = True + break + + if not supported_version_found: + # we ask the user if we want to install the module even if the version doesn't match + Utils.print_info("Current installed version of Kalliope: %s" % current_version) + Utils.print_info("Module supported versions: %s" % str(supported_versions)) + Utils.print_warning("The neuron seems to be not supported by your current version of Kalliope") + supported_version_found = Utils.query_yes_no("install it anyway?") + logger.debug("[ResourcesManager] install it anyway user answer: %s" % supported_version_found) + + logger.debug("[ResourcesManager] check_supported_version: %s" % str(supported_version_found)) + return supported_version_found diff --git a/kalliope/core/RestAPI/FlaskAPI.py b/kalliope/core/RestAPI/FlaskAPI.py index 01c38566..5ce92fdc 100644 --- a/kalliope/core/RestAPI/FlaskAPI.py +++ b/kalliope/core/RestAPI/FlaskAPI.py @@ -23,12 +23,18 @@ def __init__(self, app, port=5000, brain=None): self.port = port self.brain = brain - self.app.add_url_rule('/synapses/', view_func=self.get_synapses, methods=['GET']) + # Flask configuration remove default Flask behaviour to encode to ASCII + self.app.url_map.strict_slashes = False + self.app.config['JSON_AS_ASCII'] = False + + # Add routing rules + self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET']) self.app.add_url_rule('/synapses/', view_func=self.get_synapse, methods=['GET']) self.app.add_url_rule('/synapses/', view_func=self.run_synapse, methods=['POST']) self.app.add_url_rule('/order/', view_func=self.run_order, methods=['POST']) self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST']) + def run(self): self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False) @@ -38,11 +44,11 @@ def _get_synapse_by_name(self, synapse_name): :param synapse_name: :return: """ - all_synapse = self.brain.brain_yaml - for el in all_synapse: + all_synapse = self.brain.synapses + for synapse in all_synapse: try: - if el["name"] == synapse_name: - return el + if synapse.name == synapse_name: + return synapse except KeyError: pass return None @@ -50,9 +56,9 @@ def _get_synapse_by_name(self, synapse_name): @requires_auth def get_synapses(self): """ - get all synapse + get all synapses. """ - data = jsonify(synapses=self.brain.brain_yaml) + data = jsonify(synapses=[e.serialize() for e in self.brain.synapses]) return data, 200 @requires_auth @@ -62,7 +68,7 @@ def get_synapse(self, synapse_name): """ synapse_target = self._get_synapse_by_name(synapse_name) if synapse_target is not None: - data = jsonify(synapses=synapse_target) + data = jsonify(synapses=synapse_target.serialize()) return data, 200 data = { diff --git a/kalliope/core/SynapseLauncher.py b/kalliope/core/SynapseLauncher.py index f3737c25..785ab622 100644 --- a/kalliope/core/SynapseLauncher.py +++ b/kalliope/core/SynapseLauncher.py @@ -22,21 +22,14 @@ def start_synapse(cls, name, brain=None): :param name: Name (Unique ID) of the synapse to launch :param brain: Brain instance """ - synapse_name_launch = name - # get the brain - cls.brain = brain # check if we have found and launched the synapse - synapse_launched = False - for synapse in cls.brain.synapses: - if synapse.name == synapse_name_launch: - cls._run_synapse(synapse) - synapse_launched = True - # we found the synapse, we don't need to check the rest of the list - break - - if not synapse_launched: + synapse = brain.get_synapse_by_name(synapse_name=name) + + if not synapse: raise SynapseNameNotFound("The synapse name \"%s\" does not exist in the brain file" % name) + else: + cls._run_synapse(synapse=synapse) @classmethod def _run_synapse(cls, synapse): diff --git a/kalliope/core/TTS/TTSLauncher.py b/kalliope/core/TTS/TTSLauncher.py index 2cc88c65..28a972e1 100644 --- a/kalliope/core/TTS/TTSLauncher.py +++ b/kalliope/core/TTS/TTSLauncher.py @@ -22,4 +22,6 @@ def get_tts(cls, tts): .. warnings:: Class Method and Public """ logger.debug("get TTS module \"%s\" with parameters %s" % (tts.name, tts.parameters)) - return Utils.get_dynamic_class_instantiation("tts", tts.name.capitalize(), tts.parameters) + return Utils.get_dynamic_class_instantiation(package_name="tts", + module_name=tts.name, + parameters=tts.parameters) diff --git a/kalliope/core/TriggerLauncher.py b/kalliope/core/TriggerLauncher.py index c3de2a81..64fedfeb 100644 --- a/kalliope/core/TriggerLauncher.py +++ b/kalliope/core/TriggerLauncher.py @@ -23,6 +23,6 @@ def get_trigger(cls, trigger, callback): # add the callback method to parameters trigger.parameters["callback"] = callback logger.debug("TriggerLauncher: Start trigger %s with parameters: %s" % (trigger.name, trigger.parameters)) - return Utils.get_dynamic_class_instantiation("trigger", - trigger.name.capitalize(), - trigger.parameters) + return Utils.get_dynamic_class_instantiation(package_name="trigger", + module_name=trigger.name, + parameters=trigger.parameters) diff --git a/kalliope/core/Utils/Utils.py b/kalliope/core/Utils/Utils.py index 09397d4a..18a5dec8 100644 --- a/kalliope/core/Utils/Utils.py +++ b/kalliope/core/Utils/Utils.py @@ -1,6 +1,9 @@ import logging import os import inspect +import imp + +import sys logging.basicConfig() logger = logging.getLogger("kalliope") @@ -84,8 +87,8 @@ def print_yaml_nicely(to_print): # Dynamic loading # ######### - @staticmethod - def get_dynamic_class_instantiation(package_name, module_name, parameters=None): + @classmethod + def get_dynamic_class_instantiation(cls, package_name, module_name, parameters=None, resources_dir=None): """ Load a python class dynamically @@ -96,16 +99,27 @@ def get_dynamic_class_instantiation(package_name, module_name, parameters=None): :param package_name: name of the package where we will find the module to load (neurons, tts, stt, trigger) :param module_name: name of the module from the package_name to load. This one is capitalized. Eg: Snowboy :param parameters: dict parameters to send as argument to the module + :param resources_dir: the resource directory to check for external resources :return: """ logger.debug("Run plugin %s with parameter %s" % (module_name, parameters)) - module_name_with_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower() - mod = __import__(module_name_with_path, fromlist=[module_name]) + package_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower() + if resources_dir is not None: + neuron_resource_path = resources_dir + os.sep + module_name.lower() \ + + os.sep + module_name.lower() + ".py" + if os.path.exists(neuron_resource_path): + imp.load_source(module_name.capitalize(), neuron_resource_path) + package_path = module_name.capitalize() + logger.debug("[Utils]-> get_dynamic_class_instantiation : loading path : %s, as package %s" % ( + neuron_resource_path, package_path)) + + mod = __import__(package_path, fromlist=[module_name.capitalize()]) + try: - klass = getattr(mod, module_name) + klass = getattr(mod, module_name.capitalize()) except AttributeError: - logger.debug("Error: No module named %s " % module_name) - raise ModuleNotFoundError("The module %s does not exist in package %s" % (module_name, package_name)) + logger.debug("Error: No module named %s " % module_name.capitalize()) + raise ModuleNotFoundError("The module %s does not exist in package %s" % (module_name.capitalize(), package_name)) if klass is not None: # run the plugin @@ -171,3 +185,35 @@ def get_real_file_path(cls, file_path_to_test): return file_path_to_test else: return None + + @staticmethod + def query_yes_no(question, default="yes"): + """Ask a yes/no question via raw_input() and return their answer. + + "question" is a string that is presented to the user. + "default" is the presumed answer if the user just hits . + It must be "yes" (the default), "no" or None (meaning + an answer is required of the user). + + The "answer" return value is True for "yes" or False for "no". + """ + valid = {"yes": True, "y": True, "ye": True, + "no": False, "n": False} + if default is None: + prompt = " [y/n] " + elif default == "yes": + prompt = " [Y/n] " + elif default == "no": + prompt = " [y/N] " + else: + raise ValueError("invalid default answer: '%s'" % default) + + while True: + Utils.print_warning(question + prompt) + choice = raw_input().lower() + if default is not None and choice == '': + return valid[default] + elif choice in valid: + return valid[choice] + else: + Utils.print_warning("Please respond with 'yes' or 'no' or 'y' or 'n').\n") diff --git a/kalliope/core/__init__.py b/kalliope/core/__init__.py index de1d269f..0a083c94 100755 --- a/kalliope/core/__init__.py +++ b/kalliope/core/__init__.py @@ -3,5 +3,6 @@ from kalliope.core.ShellGui import ShellGui from kalliope.core.Utils.Utils import Utils from kalliope.core.Utils import FileManager +from kalliope.core.ResourcesManager import ResourcesManager diff --git a/kalliope/neurons/ansible_playbook/README.md b/kalliope/neurons/ansible_playbook/README.md index fe4af1ad..66ff4ee0 100644 --- a/kalliope/neurons/ansible_playbook/README.md +++ b/kalliope/neurons/ansible_playbook/README.md @@ -8,17 +8,25 @@ Playbooks are Ansible’s configuration, deployment, and orchestration language. This neuron can be used to perform complex operation with all [modules available from Ansible](http://docs.ansible.com/ansible/modules.html). +## Installation + +CORE NEURON : No installation needed. ## Options -| parameter | required | default | choices | comment | -|-----------|----------|---------|---------|----------------------------------------------| -| task_file | YES | | | path to the Playbook file that contain tasks | +| parameter | required | default | choices | comment | +|---------------|----------|---------|--------------|----------------------------------------------------------------------------------------------------------------------------------| +| task_file | YES | | | path to the Playbook file that contains tasks | +| sudo | NO | FALSE | True | False | If the playbook will require root privileges (become=true) , this must be set to True and sudo_user and password set accordingly | +| sudo_user | NO | | | The target user with admin privileges. In most of case "root" | +| sudo_password | NO | | | The password of the sudo_user | ## Synapses example +### Playbook without admin privileges + Call the playbook named playbook.yml ``` - name: "Ansible-test" @@ -28,7 +36,7 @@ Call the playbook named playbook.yml - ansible_playbook: task_file: "playbook.yml" - say: - message: "Tache terminée" + message: "The task is done" ``` Content of the playbook. This playbook will use the [URI module](http://docs.ansible.com/ansible/uri_module.html) to interact with a webservice on a remote server. @@ -54,23 +62,42 @@ Content of the playbook. This playbook will use the [URI module](http://docs.ans {"app_name": "music", "state": "start"} ``` +### Playbook with admin privileges + +In some cases, a playbook requires sudo right to perform admin operations like installing a package. +In this case, you must give to the neuron the login and password of the user which has admin privileges. +``` + - name: "Ansible-root" + signals: + - order: "playbook" + neurons: + - ansible_playbook: + task_file: "playbook-root.yml" + sudo: true + sudo_user: "root" + sudo_password: "secret" +``` + +And the playbook would be. Notice that we use `become: True` +``` +- hosts: localhost + gather_facts: no + connection: local + become: True + + tasks: + - name: "Install a useful train package" + apt: + name: sl + state: present +``` ## Note -Ansible contain a lot of modules that can be useful for Kalliope +Ansible contains a lot of modules that can be useful for Kalliope - [Notification](http://docs.ansible.com/ansible/list_of_notification_modules.html): can be used to send a message to Pushbullet, IRC channel, Rocket Chat and a lot of other notification services - [Files](http://docs.ansible.com/ansible/list_of_files_modules.html): can be used to perform a backup or synchronize two file path - [Windows](http://docs.ansible.com/ansible/list_of_windows_modules.html): Can be used to control a Windows Desktop Shell neuron or script neuron can perform same actions. Ansible is just a way to simplify some execution or enjoy some [already made plugin](http://docs.ansible.com/ansible/modules_by_category.html). - -Here is the example of synapse you would use to perform a call to a web service without Ansible: -``` -- name: "start-music" - signals: - - order: "start music rock" - neurons: - - shell: - cmd: "curl -i --user admin:secret -H \"Content-Type: application/json\" -X POST -d '{\"app_name\":\"music\",\"state\":\"start\"}' http://192.168.0.17:8000/app" -``` diff --git a/kalliope/neurons/ansible_playbook/ansible_playbook.py b/kalliope/neurons/ansible_playbook/ansible_playbook.py index 4b5ebf49..6c69c0fa 100644 --- a/kalliope/neurons/ansible_playbook/ansible_playbook.py +++ b/kalliope/neurons/ansible_playbook/ansible_playbook.py @@ -1,4 +1,6 @@ from collections import namedtuple + +import logging from ansible.parsing.dataloader import DataLoader from ansible.vars import VariableManager from ansible.inventory import Inventory @@ -6,27 +8,28 @@ from kalliope.core.NeuronModule import NeuronModule, MissingParameterException +logging.basicConfig() +logger = logging.getLogger("kalliope") + class Ansible_playbook(NeuronModule): def __init__(self, **kwargs): super(Ansible_playbook, self).__init__(**kwargs) self.task_file = kwargs.get('task_file', None) + self.sudo = kwargs.get('sudo', False) + self.sudo_user = kwargs.get('sudo_user', False) + self.sudo_password = kwargs.get('sudo_password', False) # check if parameters have been provided if self._is_parameters_ok(): - Options = namedtuple('Options', - ['connection', 'forks', 'become', 'become_method', 'become_user', 'check', 'listhosts', - 'listtasks', 'listtags', 'syntax', 'module_path']) - variable_manager = VariableManager() loader = DataLoader() - options = Options(connection='local', forks=100, become=None, become_method=None, become_user=None, check=False, - listhosts=False, listtasks=False, listtags=False, syntax=False, module_path="") - passwords = dict(vault_pass='secret') + options = self._get_options() + passwords = {'become_pass': self.sudo_password} - inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') + inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list="localhost") variable_manager.set_inventory(inventory) playbooks = [self.task_file] @@ -43,4 +46,33 @@ def __init__(self, **kwargs): def _is_parameters_ok(self): if self.task_file is None: raise MissingParameterException("task_file parameter required") + + # check if the user want to use sudo for root privileges + if self.sudo: + # the user must set a login and password + if not self.sudo_user: + raise MissingParameterException("sudo_user parameter required with sudo True") + if not self.sudo_password: + raise MissingParameterException("sudo_password parameter required with sudo True") + return True + + def _get_options(self): + """ + Return a valid dict of option usable by Ansible depending on the sudo value if set + :return: dict of option + """ + Options = namedtuple('Options', + ['connection', 'forks', 'become', 'become_method', 'become_user', 'check', 'listhosts', + 'listtasks', 'listtags', 'syntax', 'module_path']) + if self.sudo: + options = Options(connection='local', forks=100, become=True, become_method="sudo", + become_user=self.sudo_user, check=False, listhosts=False, listtasks=False, listtags=False, + syntax=False, module_path="") + else: + options = Options(connection='local', forks=100, become=None, become_method=None, become_user=None, + check=False, listhosts=False, listtasks=False, listtags=False, syntax=False, + module_path="") + + logger.debug("Ansible options: %s" % str(options)) + return options diff --git a/kalliope/neurons/gmail_checker/README.md b/kalliope/neurons/gmail_checker/README.md deleted file mode 100644 index 540519ec..00000000 --- a/kalliope/neurons/gmail_checker/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# gmail_checker - -## Synopsis - -This neuron access to Gmail and gives the number of unread mails and their titles. - -## Options - -| parameter | required | default | choices | comment | -|-----------|----------|---------|---------|------------| -| username | YES | | | User info. | -| password | YES | | | User info. | - -## Return Values - -| Name | Description | Type | sample | -|----------|----------------------------------------------|------|--------------------------------------------------------------| -| unread | Number of unread messages | int | 5 | -| subjects | A List with all the unread messages subjects | list | ['Kalliope commit', 'Beer tonight?', 'cats have superpower'] | - -## Synapses example - -Simple example : - -``` - - name: "check-email" - signals: - - order: "Do I have emails" - neurons: - - gmail_checker: - username: "me@gmail.com" - password: "my_password" - say_template: - - "You have {{ unread }} new emails" -``` - -A complex example that read subject emails. This is based on a file_template -``` - - name: "check-email" - signals: - - order: "Do I have emails" - neurons: - - gmail_checker: - username: "me@gmail.com" - password: "my_password" - file_template: /templates/my_email_template.j2 -``` - -Here the content of the `my_email_template.j2` -``` -You have {{ unread }} email - -{% set count = 1 %} -{% if unread > 0 %} - {% for subject in subjects %} - email number {{ count }}. {{ subject }} - {% set count = count + 1 %} - {% endfor %} -{% endif %} -``` -## Notes - -Gmail now prevent some mailbox to be accessed from tier application. If you receive a mail like the following: -``` -Sign-in attempt prevented ... Someone just tried to sign in to your Google Account mail@gmail.com from an app that doesn't meet modern security standards. -``` - -You can allow this neuron to get un access to your email in your [Gmail account settings](https://www.google.com/settings/security/lesssecureapps). \ No newline at end of file diff --git a/kalliope/neurons/gmail_checker/__init__.py b/kalliope/neurons/gmail_checker/__init__.py deleted file mode 100644 index 03d1e361..00000000 --- a/kalliope/neurons/gmail_checker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from gmail_checker import Gmail_checker diff --git a/kalliope/neurons/gmail_checker/gmail_checker.py b/kalliope/neurons/gmail_checker/gmail_checker.py deleted file mode 100644 index a1332005..00000000 --- a/kalliope/neurons/gmail_checker/gmail_checker.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -from gmail import Gmail -from email.header import decode_header -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - -logging.basicConfig() -logger = logging.getLogger("kalliope") - - -class Gmail_checker(NeuronModule): - def __init__(self, **kwargs): - super(Gmail_checker, self).__init__(**kwargs) - - self.username = kwargs.get('username', None) - self.password = kwargs.get('password', None) - - # check if parameters have been provided - if self._is_parameters_ok(): - - # prepare a returned dict - returned_dict = dict() - - g = Gmail() - g.login(self.username, self.password) - - # check if login succeed - logging.debug("Gmail loggin ok: %s" % g.logged_in) # Should be True, AuthenticationError if login fails - - # get unread mail - unread = g.inbox().mail(unread=True) - - returned_dict["unread"] = len(unread) - - if len(unread) > 0: - # add a list of subject - subject_list = list() - for email in unread: - email.fetch() - encoded_subject = email.subject - subject = self._parse_subject(encoded_subject) - subject_list.append(subject) - - returned_dict["subjects"] = subject_list - - logger.debug("gmail neuron returned dict: %s" % str(returned_dict)) - - # logout of gmail - g.logout() - self.say(returned_dict) - - def _parse_subject(self, encoded_subject): - dh = decode_header(encoded_subject) - - return ''.join([self.try_parse(t[0], t[1]) for t in dh]) - - @staticmethod - def try_parse(header, encoding): - """ - Verifying the Encoding and return unicode - - :param header: the header to decode - :param encoding: the targeted encoding - :return: either 'ASCII' or 'ISO-8859-1' or 'UTF-8' - - .. raises:: UnicodeDecodeError - """ - if encoding is None: - encoding = 'ASCII' - try: - return unicode(header, encoding) - except UnicodeDecodeError: - try: - return unicode(header, 'ISO-8859-1') - except UnicodeDecodeError: - return unicode(header, 'UTF-8') - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: MissingParameterException - """ - if self.username is None: - raise MissingParameterException("Username parameter required") - - if self.password is None: - raise MissingParameterException("Password parameter required") - - return True \ No newline at end of file diff --git a/kalliope/neurons/gmail_checker/tests/__init__.py b/kalliope/neurons/gmail_checker/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/gmail_checker/tests/test_gmail_checker.py b/kalliope/neurons/gmail_checker/tests/test_gmail_checker.py deleted file mode 100644 index 886bea31..00000000 --- a/kalliope/neurons/gmail_checker/tests/test_gmail_checker.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.gmail_checker.gmail_checker import Gmail_checker - - -class TestGmail_Checker(unittest.TestCase): - - def setUp(self): - self.username="username" - self.password="password" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Gmail_checker(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - - # missing password - parameters = { - "username": self.username - } - run_test(parameters) - - # missing username - parameters = { - "password": self.password - } - run_test(parameters) - - -if __name__ == '__main__': - unittest.main() - diff --git a/kalliope/neurons/kill_switch/README.md b/kalliope/neurons/kill_switch/README.md index 7e93791e..3b042ee0 100644 --- a/kalliope/neurons/kill_switch/README.md +++ b/kalliope/neurons/kill_switch/README.md @@ -4,6 +4,10 @@ This neuron exits the Kalliope process. +## Installation + +CORE NEURON : No installation needed. + ## Options No parameters diff --git a/kalliope/neurons/neurotransmitter/README.md b/kalliope/neurons/neurotransmitter/README.md index ea8d132e..db70d17a 100644 --- a/kalliope/neurons/neurotransmitter/README.md +++ b/kalliope/neurons/neurotransmitter/README.md @@ -4,6 +4,10 @@ Link synapses together. Call a synapse directly or depending on the captured speech from the user. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | @@ -84,6 +88,39 @@ If the user say something that is not present in `answers`, he will be redirecte message: "I haven't understood your answer" ``` + +Neurotransmitter also uses parameters in answers. You can provide parameters to your answers so they can be used by the synapse you are about to launch. +/!\ The params defined in answers must match with the expected "args" params in the target synapse, otherwise an error is raised. + +``` + + - name: "synapse5" + signals: + - order: "give me the weather" + neurons: + - say: + message: "which town ?" + - neurotransmitter: + from_answer_link: + - synapse: "synapse6" + answers: + - "the weather in {{ location }}" + + - name: "synapse6" + signals: + - order: "What is the weather in {{ location }}" + neurons: + - openweathermap: + api_key: "your-api" + lang: "fr" + temp_unit: "celsius" + country: "FR" + args: + - location + say_template: + - "Today in {{ location }} the weather is {{ weather_today }} with {{ temp_today_temp }} celsius" +``` + ## Notes > When using the neuron neurotransmitter, you must set a `direct_link` or a `from_answer_link`, no both at the same time. diff --git a/kalliope/neurons/neurotransmitter/neurotransmitter.py b/kalliope/neurons/neurotransmitter/neurotransmitter.py index 5213a444..6093970d 100644 --- a/kalliope/neurons/neurotransmitter/neurotransmitter.py +++ b/kalliope/neurons/neurotransmitter/neurotransmitter.py @@ -1,6 +1,6 @@ import logging -from kalliope.core.NeuronModule import NeuronModule, InvalidParameterException +from kalliope.core.NeuronModule import NeuronModule, MissingParameterException, InvalidParameterException logging.basicConfig() logger = logging.getLogger("kalliope") @@ -33,23 +33,25 @@ def callback(self, audio): logger.debug("Neurotransmitter, receiver audio from STT: %s" % audio) # print self.links # set a bool to know if we have found a valid answer - found = False - for el in self.from_answer_link: - if audio in el["answers"]: - found = True - self.run_synapse_by_name(el["synapse"]) - # we don't need to check to rest of answer - break - if not found: - # the answer do not correspond to any answer. We run the default synapse + if audio is None: self.run_synapse_by_name(self.default) + else: + found = False + for el in self.from_answer_link: + for answer in el["answers"]: + if self.is_order_matching(audio, answer): + found = self.run_synapse_by_name_with_order(order=audio, + synapse_name=el["synapse"], + order_template=answer) + if not found: # the answer do not correspond to any answer. We run the default synapse + self.run_synapse_by_name(self.default) def _is_parameters_ok(self): """ Check if received links are ok to perform operations :return: true if the neuron is well configured, raise an exception otherwise - .. raises:: MissingParameterException + .. raises:: MissingParameterException, InvalidParameterException """ # with the neuron the user has the choice of a direct link that call another synapse, # or a link with an answer caught from the STT engine @@ -59,15 +61,15 @@ def _is_parameters_ok(self): raise InvalidParameterException("neurotransmitter cannot be used with both direct_link and from_answer_link") if self.direct_link is None and self.from_answer_link is None: - raise InvalidParameterException("neurotransmitter must be used with direct_link or from_answer_link") + raise MissingParameterException("neurotransmitter must be used with direct_link or from_answer_link") if self.from_answer_link is not None: if self.default is None: raise InvalidParameterException("default parameter is required and must contain a valid synapse name") for el in self.from_answer_link: if "synapse" not in el: - raise InvalidParameterException("Links must contain a synapse name: %s" % el) + raise MissingParameterException("Links must contain a synapse name: %s" % el) if "answers" not in el: - raise InvalidParameterException("Links must contain answers: %s" % el) + raise MissingParameterException("Links must contain answers: %s" % el) return True diff --git a/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py b/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py new file mode 100644 index 00000000..0d4ca4c1 --- /dev/null +++ b/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py @@ -0,0 +1,154 @@ +import unittest + +from mock import mock + +from kalliope.core.NeuronModule import NeuronModule, MissingParameterException, InvalidParameterException +from kalliope.neurons.neurotransmitter import Neurotransmitter + + +class TestNeurotransmitter(unittest.TestCase): + + def setUp(self): + self.from_answer_link = [ + { + "synapse": "synapse2", + "answers": [ + "answer one" + ] + }, + { + "synapse": "synapse3", + "answers": [ + "answer two", + "answer three" + ] + }, + ] + self.direct_link = "direct_link" + self.default = "default" + + def testParameters(self): + """ + Testing the Parameters checking + """ + def run_test_InvalidParameterException(parameters_to_test): + with self.assertRaises(InvalidParameterException): + Neurotransmitter(**parameters_to_test) + + def run_test_MissingParameterException(parameters_to_test): + with self.assertRaises(MissingParameterException): + Neurotransmitter(**parameters_to_test) + + # empty + parameters = dict() + run_test_MissingParameterException(parameters) + + # missing direct_link and from_answer_link + parameters = { + "default": self.default + } + run_test_MissingParameterException(parameters) + + # missing direct_link and from_answer_link + parameters = { + "default": self.default, + "from_answer_link": self.from_answer_link, + "direct_link": self.direct_link + } + run_test_InvalidParameterException(parameters) + + # missing default + parameters = { + "from_answer_link": self.from_answer_link, + "direct_link": self.direct_link + } + run_test_InvalidParameterException(parameters) + + # Missing answer in from_answer_link + self.from_answer_link = [ + { + "synapse": "synapse2", + } + ] + + parameters = { + "default": self.default, + "from_answer_link": self.from_answer_link + } + run_test_MissingParameterException(parameters) + + # Missing synapse in from_answer_link + self.from_answer_link = [ + { + "answer": "blablablbla", + } + ] + + parameters = { + "default": self.default, + "from_answer_link": self.from_answer_link + } + run_test_MissingParameterException(parameters) + + def testCallback(self): + """ + Testing the callback provided when audio has been provided by the User as an answer. + """ + parameters = { + "default": self.default, + "from_answer_link": self.from_answer_link + } + with mock.patch.object(NeuronModule, 'get_audio_from_stt', create=True) as mock_get_audio_from_stt: + with mock.patch.object(NeuronModule, 'run_synapse_by_name', create=True) as mock_run_synapse_by_name: + # testing running the default when no order matching + nt = Neurotransmitter(**parameters) + mock_get_audio_from_stt.assert_called_once() + mock_get_audio_from_stt.reset_mock() + # testing running the default when audio None + audio_text = None + nt.callback(audio=audio_text) + mock_run_synapse_by_name.assert_called_once_with(self.default) + mock_run_synapse_by_name.reset_mock() + # testing running the default when no order matching + audio_text = "try test audio " + nt.callback(audio=audio_text) + mock_run_synapse_by_name.assert_called_once_with(self.default) + mock_run_synapse_by_name.reset_mock() + + with mock.patch.object(NeuronModule, + 'run_synapse_by_name_with_order', + create=True) as mock_run_synapse_by_name_with_order: + + audio_text="answer one" + nt.callback(audio=audio_text) + mock_run_synapse_by_name_with_order.assert_called_once_with(order=audio_text, + synapse_name="synapse2", + order_template="answer one") + + def testInit(self): + """ + Testing the init method of the neurontransmitter. + """ + + with mock.patch.object(NeuronModule, 'run_synapse_by_name', create=True) as mock_run_synapse_by_name: + # Test direct link + parameters = { + "default": self.default, + "direct_link": self.direct_link + } + nt = Neurotransmitter(**parameters) + mock_run_synapse_by_name.assert_called_once_with(self.direct_link) + + with mock.patch.object(NeuronModule, 'get_audio_from_stt', create=True) as mock_get_audio_from_stt: + # Test get_audio_from_stt + parameters = { + "default": self.default, + "from_answer_link": self.from_answer_link, + } + nt = Neurotransmitter(**parameters) + mock_get_audio_from_stt.assert_called_once() + + + + + diff --git a/kalliope/neurons/openweathermap/README.md b/kalliope/neurons/openweathermap/README.md deleted file mode 100644 index d104736d..00000000 --- a/kalliope/neurons/openweathermap/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# OpenWeatherMap API - -## Synopsis - -Give the today and tomorrow weather with the related data (humidity, temperature, etc ...) for a given location. - -## Options - -| parameter | required | default | choices | comment | -|-----------|----------|---------|-----------------------------|---------------------------------------------------------------------------------------------------| -| api_key | YES | None | | User API key of the OWM API | -| location | YES | None | | The location | -| lang | No | en | multiple | First 2 letters cf : section Multilingual support in : [lang](https://openweathermap.org/current) | -| temp_unit | No | Kelvin | Celsius, Kelvin, Fahrenheit | | -| country | No | US | multiple | Frist 2 letters of the country cf API doc | - -## Return Values - -| Name | Description | Type | sample | -|-----------------------------|--------------------------------------------|--------|------------------------| -| location | The current location | String | Grenoble | -| weather_today | Today : The weather sentence | String | cloudy | -| sunset_today_time | Today : The sunset time (iso) | String | 2016-10-15 20:07:57+00 | -| sunrise_today_time | Today : The sunrise time (iso) | String | 2016-10-15 07:07:57+00 | -| temp_today_temp | Today : Average temperature | float | 25 | -| temp_today_temp_max | Today : Max temperature | float | 45 | -| temp_today_temp_min | Today : Min temperatue | float | 5 | -| pressure_today_press | Today : Pressure | float | 1009 | -| pressure_today_sea_level | Today : Pressure at the Sea level | float | 1038.381 | -| humidity_today | Today : % of humidity | float | 60 | -| wind_today_deg | Today : Direction of the wind in degree | float | 45 | -| wind_today_speed | Today : Wind speed | float | 2.66 | -| snow_today | Today : Volume of snow | float | 0 | -| rain_today | Today : Rain volume | float | 0 | -| clouds_coverage_today | Today : % Cloud coverage | float | 65 | -| weather_tomorrow | Tomorrow : The weather sentence | String | sunny | -| sunset_time_tomorrow | Tomorrow : The sunset time (iso) | String | 2016-10-16 20:07:57+00 | -| sunrise_time_tomorrow | Tomorrow : The sunrise time (iso) | String | 2016-10-16 07:07:57+00 | -| temp_tomorrow_temp | Tomorrow : Average temperature | float | 25 | -| temp_tomorrow_temp_max | Tomorrow : Max temperature | float | 45 | -| temp_tomorrow_temp_min | Tomorrow : Min temperatue | float | 5 | -| pressure_tomorrow_press | Tomorrow : Pressure | float | 1009 | -| pressure_tomorrow_sea_level | Tomorrow : Pressure at the Sea level | float | 1038.381 | -| humidity_tomorrow | Tomorrow : % of humidity | float | 60 | -| wind_tomorrow_deg | Tomorrow : Direction of the wind in degree | float | 45 | -| wind_tomorrow_speed | Tomorrow : Wind speed | float | 2.66 | -| snow_tomorrow | Tomorrow : Volume of snow | float | 0 | -| rain_tomorrow | Tomorrow : Rain volume | float | 0 | -| clouds_coverage_tomorrow | Tomorrow : % Cloud coverage | float | 65 | - -## Synapses example - -``` - - name: "getthe-weather" - signals: - - order: "what is the weather in {{ location }}" - neurons: - - openweathermap: - api_key: "fdfba4097c318aed7836b2a85a6a05ef" - lang: "en" - temp_unit: "celsius" - say_template: - - "Today in {{ location }} the weather is {{ weather_today }} with a temperature of {{ temp_today_temp }} degree and tomorrow the weather will be {{ weather_tomorrow }} with a temperature of {{ temp_tomorrow_temp }} degree" - args: - - location -``` - -You also can define the "location" args directly in neuron argument list. -``` - - name: "get-the-weather" - signals: - - order: "quel temps fait-il" - neurons: - - openweathermap: - api_key: "fdfba4097c318aed7836b2a85a6a05ef" - lang: "fr" - temp_unit: "celsius" - location : "grenoble" - country: "FR" - say_template: - - "Aujourd'hui a {{ location }} le temps est {{ weather_today }} avec une température de {{ temp_today_temp }} degrés et demain le temps sera {{ weather_tomorrow }} avec une température de {{ temp_tomorrow_temp }} degrés" -``` - -## Templates example - -``` -Today in {{ location }} the weather is {{ weather_today }} with a temperature of {{ temp_today_temp }} degree -``` - - -## Notes - -> **Note:** You need to create a free account on [openweathermap.org](http://openweathermap.org/) to get your API key. \ No newline at end of file diff --git a/kalliope/neurons/openweathermap/__init__.py b/kalliope/neurons/openweathermap/__init__.py deleted file mode 100644 index 7cff1534..00000000 --- a/kalliope/neurons/openweathermap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from openweathermap import Openweathermap diff --git a/kalliope/neurons/openweathermap/openweathermap.py b/kalliope/neurons/openweathermap/openweathermap.py deleted file mode 100644 index 9756bf67..00000000 --- a/kalliope/neurons/openweathermap/openweathermap.py +++ /dev/null @@ -1,127 +0,0 @@ -import pyowm - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - - -class Openweathermap(NeuronModule): - def __init__(self, **kwargs): - # get message to spell out loud - super(Openweathermap, self).__init__(**kwargs) - - self.api_key = kwargs.get('api_key', None) - self.location = kwargs.get('location', None) - self.lang = kwargs.get('lang', 'en') - self.temp_unit = kwargs.get('temp_unit', 'celsius') - self.country = kwargs.get('country', None) - - # check if parameters have been provided - if self._is_parameters_ok(): - extended_location = self.location - if self.country is not None: - extended_location = self.location + "," + self.country - - - owm = pyowm.OWM(API_key=self.api_key, language=self.lang) - - # Tomorrow - forecast = owm.daily_forecast(extended_location) - tomorrow = pyowm.timeutils.tomorrow() - weather_tomorrow = forecast.get_weather_at(tomorrow) - weather_tomorrow_status = weather_tomorrow.get_detailed_status() - sunset_time_tomorrow = weather_tomorrow.get_sunset_time('iso') - sunrise_time_tomorrow = weather_tomorrow.get_sunrise_time('iso') - - temp_tomorrow = weather_tomorrow.get_temperature(unit=self.temp_unit) - temp_tomorrow_temp = temp_tomorrow['day'] - temp_tomorrow_temp_max = temp_tomorrow['max'] - temp_tomorrow_temp_min = temp_tomorrow['min'] - - pressure_tomorrow = weather_tomorrow.get_pressure() - pressure_tomorrow_press = pressure_tomorrow['press'] - pressure_tomorrow_sea_level = pressure_tomorrow['sea_level'] - - humidity_tomorrow = weather_tomorrow.get_humidity() - - wind_tomorrow = weather_tomorrow.get_wind() - # wind_tomorrow_deg = wind_tomorrow['deg'] - wind_tomorrow_speed = wind_tomorrow['speed'] - - snow_tomorrow = weather_tomorrow.get_snow() - rain_tomorrow = weather_tomorrow.get_rain() - clouds_coverage_tomorrow = weather_tomorrow.get_clouds() - - # Today - observation = owm.weather_at_place(extended_location) - weather_today = observation.get_weather() - weather_today_status = weather_today.get_detailed_status() - sunset_time_today = weather_today.get_sunset_time('iso') - sunrise_time_today = weather_today.get_sunrise_time('iso') - - temp_today = weather_today.get_temperature(unit=self.temp_unit) - temp_today_temp = temp_today['temp'] - temp_today_temp_max = temp_today['temp_max'] - temp_today_temp_min = temp_today['temp_min'] - - pressure_today = weather_today.get_pressure() - pressure_today_press = pressure_today['press'] - pressure_today_sea_level = pressure_today['sea_level'] - - humidity_today = weather_today.get_humidity() - - wind_today= weather_today.get_wind() - wind_today_deg = wind_today['deg'] - wind_today_speed = wind_today['speed'] - - snow_today = weather_today.get_snow() - rain_today = weather_today.get_rain() - clouds_coverage_today = weather_today.get_clouds() - - message = { - "location": self.location, - - "weather_today": weather_today_status, - "sunset_today_time": sunset_time_today, - "sunrise_today_time": sunrise_time_today, - "temp_today_temp": temp_today_temp, - "temp_today_temp_max": temp_today_temp_max, - "temp_today_temp_min": temp_today_temp_min, - "pressure_today_press": pressure_today_press, - "pressure_today_sea_level": pressure_today_sea_level, - "humidity_today": humidity_today, - "wind_today_deg": wind_today_deg, - "wind_today_speed": wind_today_speed, - "snow_today": snow_today, - "rain_today": rain_today, - "clouds_coverage_today": clouds_coverage_today, - - "weather_tomorrow": weather_tomorrow_status, - "sunset_time_tomorrow": sunset_time_tomorrow, - "sunrise_time_tomorrow": sunrise_time_tomorrow, - "temp_tomorrow_temp": temp_tomorrow_temp, - "temp_tomorrow_temp_max": temp_tomorrow_temp_max, - "temp_tomorrow_temp_min": temp_tomorrow_temp_min, - "pressure_tomorrow_press": pressure_tomorrow_press, - "pressure_tomorrow_sea_level": pressure_tomorrow_sea_level, - "humidity_tomorrow": humidity_tomorrow, - # "wind_tomorrow_deg": wind_tomorrow_deg, - "wind_tomorrow_speed": wind_tomorrow_speed, - "snow_tomorrow": snow_tomorrow, - "rain_tomorrow": rain_tomorrow, - "clouds_coverage_tomorrow": clouds_coverage_tomorrow - } - - self.say(message) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: NotImplementedError - """ - if self.api_key is None: - raise MissingParameterException("OpenWeatherMap neuron needs an api_key") - if self.location is None: - raise MissingParameterException("OpenWeatherMap neuron needs a location") - - return True diff --git a/kalliope/neurons/openweathermap/tests/__init__.py b/kalliope/neurons/openweathermap/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/openweathermap/tests/test_openweathermap.py b/kalliope/neurons/openweathermap/tests/test_openweathermap.py deleted file mode 100644 index aaf0a5f5..00000000 --- a/kalliope/neurons/openweathermap/tests/test_openweathermap.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.openweathermap.openweathermap import Openweathermap - - -class TestOpenWeatherMap(unittest.TestCase): - - def setUp(self): - self.location="location" - self.api_key="api_key" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Openweathermap(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - - # missing api_key - parameters = { - "location": self.location - } - run_test(parameters) - - # missing location - parameters = { - "api_key": self.api_key - } - run_test(parameters) - - -if __name__ == '__main__': - unittest.main() diff --git a/kalliope/neurons/push_message/Readme.md b/kalliope/neurons/push_message/Readme.md deleted file mode 100644 index e42ca1d2..00000000 --- a/kalliope/neurons/push_message/Readme.md +++ /dev/null @@ -1,49 +0,0 @@ -# Push notification - -## Synopsis - -Send broadcast communications to groups of subscribers. - -Available client are: -- Android phone -- iOS phone/ -- Windows Phone -- Chrome Browser - -This neuron is based on [Pushetta API](http://www.pushetta.com/). -You need to [create a free account](http://www.pushetta.com/accounts/signup/) and a chanel before using it. -You need to install a [client App](http://www.pushetta.com/pushetta-downloads/) on the target device. - -## Options - -| parameter | requiered | default | choices | comment | -|--------------|-----------|---------|---------|-------------------------------------------------------------------------------------------------------| -| message | yes | | | Message that will be send to the android phone | -| api_key | yes | | | Token API key availlable from [Pushetta dashboard](http://www.pushetta.com/my/dashboard/) | -| channel_name | yes | | | Name of the subscribed [channel](http://www.pushetta.com/pushetta-docs/#create) | - - -## Return Values - -No returned value - - -## Synapses example - -The following synapse will send a push message to device that have subscribed to the channel name "my_chanel_name" when you say "push message". -``` - - name: "send-push-message" - signals: - - order: "push message" - neurons: - - android_pushetta: - message: "Message to send" - api_key: "TOEKENEXAMPLE1234" - channel_name: "my_chanel_name" -``` - -## Notes - -> **Note:** You must install a [client App](http://www.pushetta.com/pushetta-downloads/) on the target device. - -> **Note:** You must create a channel an get a token key on [Pushetta website](http://www.pushetta.com/) before using the neuron. diff --git a/kalliope/neurons/push_message/__init__.py b/kalliope/neurons/push_message/__init__.py deleted file mode 100644 index a24b3eec..00000000 --- a/kalliope/neurons/push_message/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from push_message import Push_message - diff --git a/kalliope/neurons/push_message/push_message.py b/kalliope/neurons/push_message/push_message.py deleted file mode 100644 index 4b67b1b1..00000000 --- a/kalliope/neurons/push_message/push_message.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import absolute_import -from pushetta import Pushetta - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - - -class Push_message(NeuronModule): - """ - Neuron based on pushetta api. http://www.pushetta.com/ - """ - def __init__(self, **kwargs): - """ - Send a push message to an android phone via Pushetta API - :param message: Message to send - :param api_key: The Pushetta service secret token - :param channel_name: Pushetta channel name - """ - super(Push_message, self).__init__(**kwargs) - - self.message = kwargs.get('message', None) - self.api_key = kwargs.get('api_key', None) - self.channel_name = kwargs.get('channel_name', None) - - # check if parameters have been provided - if self._is_parameters_ok(): - p = Pushetta(self.api_key) - p.pushMessage(self.channel_name, self.message) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: NotImplementedError - """ - if self.message is None: - raise MissingParameterException("Pushetta neuron needs message to send") - if self.api_key is None: - raise MissingParameterException("Pushetta neuron needs api_key") - if self.channel_name is None: - raise MissingParameterException("Pushetta neuron needs channel_name") - - return True diff --git a/kalliope/neurons/push_message/tests/__init__.py b/kalliope/neurons/push_message/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/push_message/tests/test_push_message.py b/kalliope/neurons/push_message/tests/test_push_message.py deleted file mode 100644 index 1b7cdb40..00000000 --- a/kalliope/neurons/push_message/tests/test_push_message.py +++ /dev/null @@ -1,46 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.push_message.push_message import Push_message - - -class TestPush_Message(unittest.TestCase): - - def setUp(self): - self.message="message" - self.api_key="api_key" - self.channel_name = "channel_name" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Push_message(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - - # missing api_key - parameters = { - "message": self.message, - "channel_name": self.channel_name - } - run_test(parameters) - - # missing channel_name - parameters = { - "api_key": self.api_key, - "message":self.message - } - run_test(parameters) - - # missing message - parameters = { - "api_key": self.api_key, - "channel_name": self.channel_name - } - run_test(parameters) - - -if __name__ == '__main__': - unittest.main() diff --git a/kalliope/neurons/rss_reader/README.md b/kalliope/neurons/rss_reader/README.md deleted file mode 100644 index 4191dffd..00000000 --- a/kalliope/neurons/rss_reader/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# rss_reader - -## Synopsis - -This neuron access to a RSS feed and gives their items. - -## Options - -| parameter | required | default | choices | comment | -|-----------|----------|---------|---------|-----------------------| -| feed_url | YES | | | Url of the feed. | -| max_items | NO | 30 | | Max items to returns. | - -## Return Values - -| Name | Description | Type | sample | -|----------|----------------------------------------------------------------------------------------|---------|---------------------------------| -| feed | Title of the feed | string | The Verge | -| items | A List with feed items (see [RSS spec](https://validator.w3.org/feed/docs/rss2.html)) | list | | - -## Synapses example - -Simple example. This is based on a file_template - -``` - - name: "news-theVerge" - signals: - - order: "What are the news from the verge ?" - neurons: - - rss_reader: - feed_url: "http://www.theverge.com/rss/index.xml" - file_template: templates/en_rss.j2 - -``` - -A example with max items set to 10. This is based on a file_template -``` - - name: "news-sport" - signals: - - order: "What are the sport news ?" - neurons: - - rss_reader: - feed_url: "https://sports.yahoo.com/top/rss.xml" - max_items: 10 - file_template: templates/en_rss.j2 -``` - -Here the content of the `en_rss.j2` -``` -Here's the news from {{ feed }} - -{% set count = 1 %} -{% for item in items %} -News {{ count }}. {{ item.title }}. -{% set count = count + 1 %} -{% endfor %} -``` -## Notes - diff --git a/kalliope/neurons/rss_reader/__init__.py b/kalliope/neurons/rss_reader/__init__.py deleted file mode 100644 index 6db50510..00000000 --- a/kalliope/neurons/rss_reader/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from rss_reader import Rss_reader diff --git a/kalliope/neurons/rss_reader/rss_reader.py b/kalliope/neurons/rss_reader/rss_reader.py deleted file mode 100644 index 7a98c9e5..00000000 --- a/kalliope/neurons/rss_reader/rss_reader.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -import feedparser - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - -logging.basicConfig() -logger = logging.getLogger("kalliope") - - -class Rss_reader(NeuronModule): - def __init__(self, **kwargs): - super(Rss_reader, self).__init__(**kwargs) - - self.feedUrl = kwargs.get('feed_url', None) - self.limit = kwargs.get('max_items', 30) - - # check if parameters have been provided - if self._is_parameters_ok(): - - # prepare a returned dict - returned_dict = dict() - - logging.debug("Reading feed from: %s" % self.feedUrl) - - feed = feedparser.parse( self.feedUrl ) - - logging.debug("Read title from feed: %s" % feed["channel"]["title"]) - - returned_dict["feed"] = feed["channel"]["title"] - returned_dict["items"] = feed["items"][:self.limit] - - self.say(returned_dict) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: MissingParameterException - """ - if self.feedUrl is None: - raise MissingParameterException("feed url parameter required") - - return True diff --git a/kalliope/neurons/rss_reader/tests/__init__.py b/kalliope/neurons/rss_reader/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/rss_reader/tests/test_rss_reader.py b/kalliope/neurons/rss_reader/tests/test_rss_reader.py deleted file mode 100644 index 732b81aa..00000000 --- a/kalliope/neurons/rss_reader/tests/test_rss_reader.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.rss_reader.rss_reader import Rss_reader - - -class TestRss_Reader(unittest.TestCase): - - def setUp(self): - self.feedUrl="http://www.lemonde.fr/rss/une.xml" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Rss_reader(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - -if __name__ == '__main__': - unittest.main() - diff --git a/kalliope/neurons/say/README.md b/kalliope/neurons/say/README.md index c07cd814..f5824336 100644 --- a/kalliope/neurons/say/README.md +++ b/kalliope/neurons/say/README.md @@ -4,6 +4,10 @@ This neuron is the mouth of Kalliope and uses the [TTS](../../Docs/tts.md) to say the given message. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | diff --git a/kalliope/neurons/script/README.md b/kalliope/neurons/script/README.md index 6560845e..b04e6e8c 100644 --- a/kalliope/neurons/script/README.md +++ b/kalliope/neurons/script/README.md @@ -4,6 +4,10 @@ This neuron runs a script located on the Kalliope system. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | diff --git a/kalliope/neurons/shell/README.md b/kalliope/neurons/shell/README.md index 99f1dc09..45a610eb 100644 --- a/kalliope/neurons/shell/README.md +++ b/kalliope/neurons/shell/README.md @@ -4,6 +4,9 @@ Run a shell command on the local system where Kalliope is installed. +## Installation + +CORE NEURON : No installation needed. ## Options diff --git a/kalliope/neurons/sleep/README.md b/kalliope/neurons/sleep/README.md index 192b1be4..75b8048e 100644 --- a/kalliope/neurons/sleep/README.md +++ b/kalliope/neurons/sleep/README.md @@ -4,6 +4,10 @@ This neuron sleeps the system for a given time in seconds. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | diff --git a/kalliope/neurons/systemdate/README.md b/kalliope/neurons/systemdate/README.md index be456b74..6dcac6d6 100644 --- a/kalliope/neurons/systemdate/README.md +++ b/kalliope/neurons/systemdate/README.md @@ -4,6 +4,10 @@ Give the current time from the system where Kalliope is installed. Return a dict of parameters that can be used in a template. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | diff --git a/kalliope/neurons/tasker_autoremote/Readme.md b/kalliope/neurons/tasker_autoremote/Readme.md deleted file mode 100644 index 4d0fdf87..00000000 --- a/kalliope/neurons/tasker_autoremote/Readme.md +++ /dev/null @@ -1,83 +0,0 @@ -# Tasker autoremote - -## Synopsis - -[Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) is an application for Android which performs -tasks (sets of actions) based on contexts (application, time, date, location, event, gesture) in user-defined profiles or in -clickable or timer home screen widgets. - -[Tasker autoremote](https://play.google.com/store/apps/details?id=com.joaomgcd.autoremote&hl=fr) is a plugin for Tasker that allow -the program to receive push message from the cloud as profile. - -This is how it works: -- Send an AutoRemote message from Kalliope -- Setup an AutoRemote profile in Tasker to react to the message -- Do whatever you like with that message! - -The example usage is a "find my phone" task. -You could send a "Where are you?" message to your phone, and have Tasker respond with a repetitive "I'm here! I'm here!" -or play a music. - - -## Options - -| parameter | required | default | choices | comment | -|-----------|----------|---------|---------|---------------------------------------------------------------| -| key | yes | | | API key. Can be found in your personal URL given by the app. | -| message | yes | | | Message to send to your phone | - -## Return Values - -None - -## Synapses example - -Description of what the synapse will do -``` -- name: "find-my-phone" - signals: - - order: "where is my phone" - neurons: - - say: - message: "I'll make your phone ringing, sir" - - tasker_autoremote: - key: "MY_VERY_LONG_KEY" - message: "lost" -``` - - -## Notes - -### How to create a find my phone task -This walk through will show you how to send a message to your phone so that even if it is set to silent, -will play any music file at full volume so you can find your phone if you have lost it in the couch. - -First, create a task, that you could call "start_ringing" that will perform: -- Disable the silent mode -- Set media volume to the maximum value -- Play a local music - -![task play music](images/task_play_music.png) - -Then, create a new task with just one action: -- Stop the music - -![task stop music](images/task_stop_music.png) - -Create the input profile. -- create a context of type Event > Plugin > Autoremote -- Set the word you want -- Attach the event to the task "start_ringing" - -![task stop music](images/profile_auto_remote.png) - -Finally, create a event, to stop the music when we unlock the phone -- create a context of type Event > Display > Display Unlocked -- Attach the event to the stop that stop the music - -![task stop music](images/profile_display_unlocked.png) - -Exit Tasker with the exit menu to be sure all events and task have been saved. - - - diff --git a/kalliope/neurons/tasker_autoremote/__init__.py b/kalliope/neurons/tasker_autoremote/__init__.py deleted file mode 100644 index 0c600da1..00000000 --- a/kalliope/neurons/tasker_autoremote/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from tasker_autoremote import Tasker_autoremote diff --git a/kalliope/neurons/tasker_autoremote/images/profile_auto_remote.png b/kalliope/neurons/tasker_autoremote/images/profile_auto_remote.png deleted file mode 100755 index 48e6ba45..00000000 Binary files a/kalliope/neurons/tasker_autoremote/images/profile_auto_remote.png and /dev/null differ diff --git a/kalliope/neurons/tasker_autoremote/images/profile_display_unlocked.png b/kalliope/neurons/tasker_autoremote/images/profile_display_unlocked.png deleted file mode 100755 index a2417691..00000000 Binary files a/kalliope/neurons/tasker_autoremote/images/profile_display_unlocked.png and /dev/null differ diff --git a/kalliope/neurons/tasker_autoremote/images/task_play_music.png b/kalliope/neurons/tasker_autoremote/images/task_play_music.png deleted file mode 100755 index 260ae6ed..00000000 Binary files a/kalliope/neurons/tasker_autoremote/images/task_play_music.png and /dev/null differ diff --git a/kalliope/neurons/tasker_autoremote/images/task_stop_music.png b/kalliope/neurons/tasker_autoremote/images/task_stop_music.png deleted file mode 100755 index c2154fd9..00000000 Binary files a/kalliope/neurons/tasker_autoremote/images/task_stop_music.png and /dev/null differ diff --git a/kalliope/neurons/tasker_autoremote/tasker_autoremote.py b/kalliope/neurons/tasker_autoremote/tasker_autoremote.py deleted file mode 100644 index dbde1860..00000000 --- a/kalliope/neurons/tasker_autoremote/tasker_autoremote.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging - -import requests - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - -logging.basicConfig() -logger = logging.getLogger("kalliope") - - -class Tasker_autoremote(NeuronModule): - def __init__(self, **kwargs): - super(Tasker_autoremote, self).__init__(**kwargs) - - # check if parameters have been provided - self.key = kwargs.get('key', None) - self.message = kwargs.get('message', None) - - # check parameters - if self._is_parameters_ok(): - # create the payload - data = {'key': self.key, - 'message': self.message} - url = "https://autoremotejoaomgcd.appspot.com/sendmessage" - # post - r = requests.post(url, data=data) - logging.debug("Post to tasker automore response: %s" % r.status_code) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: MissingParameterException - """ - if self.key is None: - raise MissingParameterException("key parameter required") - if self.message is None: - raise MissingParameterException("message parameter required") - - return True \ No newline at end of file diff --git a/kalliope/neurons/tasker_autoremote/tests/__init__.py b/kalliope/neurons/tasker_autoremote/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/tasker_autoremote/tests/test_tasker_autoremote.py b/kalliope/neurons/tasker_autoremote/tests/test_tasker_autoremote.py deleted file mode 100644 index f987ad91..00000000 --- a/kalliope/neurons/tasker_autoremote/tests/test_tasker_autoremote.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.sleep.sleep import Sleep - - -class TestSleep(unittest.TestCase): - - def setUp(self): - self.key="key" - self.message="message" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Sleep(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - - # missing key - parameters = { - "message": self.message - } - run_test(parameters) - - # missing message - parameters = { - "key": self.key - } - run_test(parameters) - - -if __name__ == '__main__': - unittest.main() diff --git a/kalliope/neurons/twitter/README.md b/kalliope/neurons/twitter/README.md deleted file mode 100644 index 3e237a7b..00000000 --- a/kalliope/neurons/twitter/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Twitter - -## Synopsis - -This neuron allows you to send a tweet on your timeline. - -## Options - -| parameter | required | default | choices | comment | -|---------------------|----------|---------|---------|-----------------------------| -| consumer_key | yes | None | | User info | -| consumer_secret | yes | None | | User info | -| access_token_key | yes | None | | User info | -| access_token_secret | yes | None | | User info | -| tweet | yes | None | | The sentence to be tweeted | - -## Return Values - -| Name | Description | Type | sample | -|-------|---------------------------------|--------|-----------------| -| tweet | The tweet which has been posted | string | coucou kalliopé | - -## Synapses example - -``` -- name: "post-tweet" - neurons: - - twitter: - consumer_key: "" - consumer_secret: "" - access_token_key: "" - access_token_secret: "" - args: - - tweet - signals: - - order: "post on Twitter {{ tweet }}" -``` - -## Notes - -In order to be able to post on Twitter, you need to grant access of your application on Twitter by creating your own app associate to your profile. - -### How to create my Twitter app - -1. Sign in your [Twitter account](https://www.twitter.com) -2. Let's create your app [apps.twitter.com](https://apps.twitter.com) -3. click on the button "Create New App" -4. Fill in your application details -5. Create your access token (to post a tweet, you need at least "Read and Write" access) -6. Get your consumer_key, consumer_secret, access_token_key and access_token_secret from the tab "Key and access token" (Keep them secret !) -7. Post your first message with this neuron ! \ No newline at end of file diff --git a/kalliope/neurons/twitter/__init__.py b/kalliope/neurons/twitter/__init__.py deleted file mode 100644 index 3c41da4e..00000000 --- a/kalliope/neurons/twitter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from twitter import Twitter \ No newline at end of file diff --git a/kalliope/neurons/twitter/tests/__init__.py b/kalliope/neurons/twitter/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/twitter/tests/test_twitter_neuron.py b/kalliope/neurons/twitter/tests/test_twitter_neuron.py deleted file mode 100644 index dd042a16..00000000 --- a/kalliope/neurons/twitter/tests/test_twitter_neuron.py +++ /dev/null @@ -1,68 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import MissingParameterException -from kalliope.neurons.twitter.twitter import Twitter - - -class TestTwitter(unittest.TestCase): - - def setUp(self): - self.consumer_key="kalliokey" - self.consumer_secret = "kalliosecret" - self.access_token_key = "kalliotokenkey" - self.access_token_secret = "kalliotokensecret" - self.tweet = "kalliotweet" - - def testParameters(self): - def run_test(parameters_to_test): - with self.assertRaises(MissingParameterException): - Twitter(**parameters_to_test) - - # empty - parameters = dict() - run_test(parameters) - - # missing tweet - parameters = { - "consumer_key": self.consumer_key, - "consumer_secret": self.consumer_secret, - "access_token_key": self.access_token_key, - "access_token_secret": self.access_token_secret - } - run_test(parameters) - - # missing consumer_key - parameters = { - "consumer_secret": self.consumer_secret, - "access_token_key": self.access_token_key, - "access_token_secret": self.access_token_secret, - "tweet": self.tweet - } - run_test(parameters) - - # missing consumer_secret - parameters = { - "consumer_key": self.consumer_key, - "access_token_key": self.access_token_key, - "access_token_secret": self.access_token_secret, - "tweet": self.tweet - } - run_test(parameters) - - # missing access_token_key - parameters = { - "consumer_key": self.consumer_key, - "consumer_secret": self.consumer_secret, - "access_token_secret": self.access_token_secret, - "tweet": self.tweet - } - run_test(parameters) - - # missing access_token_secret - parameters = { - "consumer_key": self.consumer_key, - "consumer_secret": self.consumer_secret, - "access_token_key": self.access_token_key, - "tweet": self.tweet - } - run_test(parameters) diff --git a/kalliope/neurons/twitter/twitter.py b/kalliope/neurons/twitter/twitter.py deleted file mode 100644 index f3b597ad..00000000 --- a/kalliope/neurons/twitter/twitter.py +++ /dev/null @@ -1,53 +0,0 @@ -import twitter - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException - - -class Twitter(NeuronModule): - def __init__(self, **kwargs): - - super(Twitter, self).__init__(**kwargs) - - self.consumer_key = kwargs.get('consumer_key', None) - self.consumer_secret = kwargs.get('consumer_secret', None) - self.access_token_key = kwargs.get('access_token_key', None) - self.access_token_secret = kwargs.get('access_token_secret', None) - self.tweet = kwargs.get('tweet', None) - - # check parameters - if self._is_parameters_ok(): - api = twitter.Api(consumer_key=self.consumer_key, - consumer_secret=self.consumer_secret, - access_token_key=self.access_token_key, - access_token_secret=self.access_token_secret) - - status = api.PostUpdate(self.tweet) - message = { - "tweet" : status.text - } - - self.say(message) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: MissingParameterException - """ - if self.consumer_key is None: - raise MissingParameterException("Twitter needs a consumer_key") - if self.consumer_secret is None: - raise MissingParameterException("Twitter needs a consumer_secret") - if self.access_token_key is None: - raise MissingParameterException("Twitter needs an access_token_key") - if self.access_token_secret is None: - raise MissingParameterException("Twitter needs and access_token_secret") - if self.tweet is None: - raise MissingParameterException("You need to provide something to tweet !") - - return True - - - - diff --git a/kalliope/neurons/uri/README.md b/kalliope/neurons/uri/README.md index b14993bc..c284f8ff 100644 --- a/kalliope/neurons/uri/README.md +++ b/kalliope/neurons/uri/README.md @@ -4,6 +4,10 @@ Interacts with HTTP and HTTPS web services. +## Installation + +CORE NEURON : No installation needed. + ## Options | parameter | required | default | choices | comment | diff --git a/kalliope/neurons/wake_on_lan/README.md b/kalliope/neurons/wake_on_lan/README.md deleted file mode 100644 index 52a586b6..00000000 --- a/kalliope/neurons/wake_on_lan/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# wake_on_lan - -## Synopsis - -Allows a computer to be turned on or awakened from the [WOL](https://en.wikipedia.org/wiki/Wake-on-LAN) protocol by Kalliope. - -## Options - -| parameter | required | default | choices | comment | -|-------------------|----------|-----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------| -| mac_address | yes | | | Mac address of the target PC to wake up. Accepted format: 'ff.ff.ff.ff.ff.ff', '00-00-00-00-00-00', 'FFFFFFFFFFFF' | -| broadcast_address | no | 255.255.255.255 | | Broadcast address where the magic packet will bee sent. By default on most LAN is 255.255.255.255 | -| port | no | 9 | | The magic packet is typically sent as a UDP datagram to port 0,6 7 or 9. This parameter must be an integer. Do not add 'quotes' in your configuration | - - -## Return Values - -None - - -## Synapses example - -Kalliope will send a magic packet to the mac address `00-00-00-00-00-00` -``` -- name: "wake-my-PC" - signals: - - order: "wake my PC" - neurons: - - wake_on_lan: - mac_address: "00-00-00-00-00-00" -``` - -If your broadcast address is not 255.255.255.255, or if your ethernet card does not listen on the standard 9 port, you can override default parameters. -In the following example, we suppose that kalliope is on a local areal network 172.16.0.0/16. The broadcast address would be 172.16.255.255. -``` -- name: "wake-my-PC" - signals: - - order: "wake my PC" - neurons: - - wake_on_lan: - mac_address: "00-00-00-00-00-00" - broadcast_address: "172.16.255.255" - port: 7 -``` - -## Notes - -> **Note:** The target computer must be on the same local area network as Kalliope. - -> **Note:** The target computer must has wake on lan activated in BIOS settings and my be in [OS settings](http://www.groovypost.com/howto/enable-wake-on-lan-windows-10/) too. diff --git a/kalliope/neurons/wake_on_lan/__init__.py b/kalliope/neurons/wake_on_lan/__init__.py deleted file mode 100644 index 8253bdcc..00000000 --- a/kalliope/neurons/wake_on_lan/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from wake_on_lan import Wake_on_lan \ No newline at end of file diff --git a/kalliope/neurons/wake_on_lan/tests/__init__.py b/kalliope/neurons/wake_on_lan/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/wake_on_lan/tests/test_wake_on_lan.py b/kalliope/neurons/wake_on_lan/tests/test_wake_on_lan.py deleted file mode 100644 index b57e88ae..00000000 --- a/kalliope/neurons/wake_on_lan/tests/test_wake_on_lan.py +++ /dev/null @@ -1,67 +0,0 @@ -import unittest -import ipaddress - -from kalliope.core.NeuronModule import InvalidParameterException, MissingParameterException -from kalliope.neurons.wake_on_lan.wake_on_lan import Wake_on_lan - - -class TestWakeOnLan(unittest.TestCase): - - def setUp(self): - self.mac_address="00:0a:95:9d:68:16" - self.broadcast_address = "255.255.255.255" - self.port = 42 - - def testParameters(self): - def run_test_invalidParam(parameters_to_test): - with self.assertRaises(InvalidParameterException): - Wake_on_lan(**parameters_to_test) - - def run_test_missingParam(parameters_to_test): - with self.assertRaises(MissingParameterException): - Wake_on_lan(**parameters_to_test) - - def run_test_valueError(parameters_to_test): - with self.assertRaises(ValueError): - Wake_on_lan(**parameters_to_test) - - # empty - parameters = dict() - run_test_missingParam(parameters) - - # missing mac_address - parameters = { - "broadcast_address": self.broadcast_address, - "port": self.port - } - run_test_missingParam(parameters) - - # port is not an int - self.port = "port" - parameters = { - "broadcast_address": self.broadcast_address, - "mac_address": self.mac_address, - "port": self.port - } - run_test_invalidParam(parameters) - self.port = 42 - - # is broadcast not a valid format - self.broadcast_address = "broadcast" - parameters = { - "broadcast_address": self.broadcast_address, - "mac_address": self.mac_address, - "port": self.port - } - run_test_valueError(parameters) - self.broadcast_address = "255.255.255.255" - - # is mac_address not a valid IPv4 or IPv6 format - self.mac_address = "mac_address" - parameters = { - "broadcast_address": self.broadcast_address, - "mac_address": self.mac_address, - "port": self.port - } - run_test_valueError(parameters) - self.mac_address = "00:0a:95:9d:68:16" diff --git a/kalliope/neurons/wake_on_lan/wake_on_lan.py b/kalliope/neurons/wake_on_lan/wake_on_lan.py deleted file mode 100644 index b35309e6..00000000 --- a/kalliope/neurons/wake_on_lan/wake_on_lan.py +++ /dev/null @@ -1,47 +0,0 @@ -import ipaddress -import logging - -from kalliope.core.NeuronModule import NeuronModule, MissingParameterException, InvalidParameterException -from wakeonlan import wol - -logging.basicConfig() -logger = logging.getLogger("kalliope") - - -class Wake_on_lan(NeuronModule): - def __init__(self, **kwargs): - super(Wake_on_lan, self).__init__(**kwargs) - - self.mac_address = kwargs.get('mac_address', None) - self.broadcast_address = kwargs.get('broadcast_address', '255.255.255.255') - self.port = kwargs.get('port', 9) - - # check parameters - if self._is_parameters_ok(): - # convert to unicode for testing - broadcast_address_unicode = self.broadcast_address.decode('utf-8') - # check the ip address is a valid one - ipaddress.ip_address(broadcast_address_unicode) - - logger.debug("Call Wake_on_lan_neuron with parameters: mac_address: %s, broadcast_address: %s, port: %s" - % (self.mac_address, self.broadcast_address, self.port)) - - # send the magic packet, the mac address format will be check by the lib - wol.send_magic_packet(self.mac_address, ip_address=self.broadcast_address, port=self.port) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: InvalidParameterException, MissingParameterException - """ - # check we provide a mac address - if self.mac_address is None: - raise MissingParameterException("mac_address parameter required") - # check the port - if type(self.port) is not int: - raise InvalidParameterException( - "port argument must be an integer. Remove quotes in your configuration.") - - return True diff --git a/kalliope/neurons/wikipedia_searcher/README.md b/kalliope/neurons/wikipedia_searcher/README.md deleted file mode 100644 index 05d2f431..00000000 --- a/kalliope/neurons/wikipedia_searcher/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# wikipedia_searcher - -## Synopsis - -Get the summary of a Wikipedia page. - -## Options - -| parameter | required | default | choices | comment | -|-----------|----------|---------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| language | yes | | E.g: "fr", "en", "it", "es" | See the list of available language in the "Note" section | -| query | yes | | | The wikipedia page you are looking for. This parameter can be passed as an argument in the neuron from the order with {{ query}} | -| sentences | no | 10 | Integer in range 1-10 | if set, return the first number of sentences(can be no greater than 10) specified in this parameter. | - - -## Return Values - -| Name | Description | Type | sample | -|------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------| -| summary | Plain text summary of the searched page | string | Wikipedia is a collaboratively edited, multilingual, free Internet encyclopedia supported by the non-profit Wikimedia Foundation.. | -| returncode | Error code. See bellow | string | SummaryFound | -| may_refer | List of pages that can refer the query | list | ['Marc Le Bot', 'Bot', 'Jean-Marc Bot', 'bot', 'pied bot', 'robot', 'Sam Bot', 'Famille Both', 'Yves Bot', 'Ben Bot', 'Botswana'] | - - -| returncode | Description | -|---------------------|-----------------------------------------| -| SummaryFound | A summary hs been found from the querry | -| DisambiguationError | The query match more than ony one page. | -| PageError | No Wikipedia matched a query | - -## Synapses example - -This synapse will look for the {{ query }} spelt by the user on Wikipedia -``` -- name: "wikipedia-search" - signals: - - order: "look on wikipedia {{ query }}" - neurons: - - wikipedia_searcher: - language: "en" - args: - - query - file_template: "wikipedia_returned_value.j2" - -``` - -## Templates example - -This template will simply make Kalliope speak out loud the summary section of the Wikipédia page of the query. -If the query match more than one page, Kaliope will give the user all matched pages. -If the query doesn't match any page on Wikipedia, kalliope will notify the user. -``` -{% if returncode == "DisambiguationError" %} - The query match following pages - {% if may_refer is not none %} - {% for page in may_refer %} - {{ page }} - {% endfor %} - {% endif %} -{% elif returncode == "PageError" %} - I haven't found anything on this -{% else %} - {{ summary }} -{% endif %} -``` - -## Notes - -Available languages in [the detailed list of the offical Wikipedia page](https://en.wikipedia.org/wiki/List_of_Wikipedias#Detailed_list). The column is called "Wiki". E.g: "en" diff --git a/kalliope/neurons/wikipedia_searcher/__init__.py b/kalliope/neurons/wikipedia_searcher/__init__.py deleted file mode 100644 index c6c2c1bf..00000000 --- a/kalliope/neurons/wikipedia_searcher/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from wikipedia_searcher import Wikipedia_searcher diff --git a/kalliope/neurons/wikipedia_searcher/tests/__init__.py b/kalliope/neurons/wikipedia_searcher/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kalliope/neurons/wikipedia_searcher/tests/test_wikipedia_searcher.py b/kalliope/neurons/wikipedia_searcher/tests/test_wikipedia_searcher.py deleted file mode 100644 index 91b81140..00000000 --- a/kalliope/neurons/wikipedia_searcher/tests/test_wikipedia_searcher.py +++ /dev/null @@ -1,67 +0,0 @@ -import unittest - -from kalliope.core.NeuronModule import InvalidParameterException -from kalliope.neurons.wikipedia_searcher import Wikipedia_searcher - - -class TestWikipediaSearcher(unittest.TestCase): - def setUp(self): - pass - - # def test_parameters(self): - # def run_test(parameters_to_test): - # with self.assertRaises(InvalidParameterException): - # Wikipedia_searcher(**parameters_to_test) - # - # parameters = dict() - # run_test(parameters) - # - # # sentences must be an integer - # parameters = { - # "language": "en", - # "query": "this is the query", - # "sentences": "invalid" - # - # } - # run_test(parameters) - # - # # test non existing language - # parameters = { - # "language": "foo", - # "query": "this is the query", - # "sentences": 1 - # - # } - # run_test(parameters) - - # def test_get_DisambiguationError(self): - # - # parameters = { - # "language": "fr", - # "query": "bot", - # "sentences": 1 - # } - # - # wiki = Wikipedia_searcher(**parameters) - # self.assertEqual(wiki.returncode, "DisambiguationError") - # - # def test_page_error(self): - # parameters = { - # "language": "fr", - # "query": "fudu foo bar non exist", - # "sentences": 1 - # } - # - # wiki = Wikipedia_searcher(**parameters) - # self.assertEqual(wiki.returncode, "PageError") - # - # def test_summary_found(self): - # parameters = { - # "language": "fr", - # "query": "kalliope" - # } - # wiki = Wikipedia_searcher(**parameters) - # self.assertEqual(wiki.returncode, "SummaryFound") - -if __name__ == '__main__': - unittest.main() diff --git a/kalliope/neurons/wikipedia_searcher/wikipedia_searcher.py b/kalliope/neurons/wikipedia_searcher/wikipedia_searcher.py deleted file mode 100644 index ed2972bc..00000000 --- a/kalliope/neurons/wikipedia_searcher/wikipedia_searcher.py +++ /dev/null @@ -1,80 +0,0 @@ -import logging - -from kalliope.core.NeuronModule import NeuronModule, InvalidParameterException -import wikipedia - -logging.basicConfig() -logger = logging.getLogger("kalliope") - - -class Wikipedia_searcher(NeuronModule): - def __init__(self, **kwargs): - # we don't need the TTS cache for this neuron - cache = kwargs.get('cache', None) - if cache is None: - cache = False - kwargs["cache"] = cache - super(Wikipedia_searcher, self).__init__(**kwargs) - - # get parameters form the neuron - self.query = kwargs.get('query', None) - self.language = kwargs.get('language', None) - self.sentences = kwargs.get('sentences', None) - - self.may_refer = None - self.returncode = None - self.message = None - - # check parameters - if self._is_parameters_ok(): - # set the language - wikipedia.set_lang(self.language) - # do the summary search - try: - summary = wikipedia.summary(self.query, auto_suggest=True, sentences=self.sentences) - # if we are here, no exception raised, we got a summary - self.returncode = "SummaryFound" - except wikipedia.exceptions.DisambiguationError, e: - # Exception raised when a page resolves to a Disambiguation page. - # The options property contains a list of titles of Wikipedia pages that the query may refer to. - self.may_refer = e.options - # Removing duplicates in lists. - self.may_refer = list(set(self.may_refer)) - self.returncode = "DisambiguationError" - summary = "" - except wikipedia.exceptions.PageError: - # Exception raised when no Wikipedia matched a query. - self.returncode = "PageError" - summary = "" - - self.message = { - "summary": summary, - "may_refer": self.may_refer, - "returncode": self.returncode - } - logger.debug("Wikipedia returned message: %s" % str(self.message)) - - self.say(self.message) - - def _is_parameters_ok(self): - """ - Check if received parameters are ok to perform operations in the neuron - :return: true if parameters are ok, raise an exception otherwise - - .. raises:: InvalidParameterException - """ - - if self.query is None: - raise InvalidParameterException("Wikipedia needs a query") - if self.language is None: - raise InvalidParameterException("Wikipedia needs a language") - - valid_language = wikipedia.languages().keys() - if self.language not in valid_language: - raise InvalidParameterException("Wikipedia needs a valid language: %s" % valid_language) - - if self.sentences is not None: - if not isinstance(self.sentences, int): - raise InvalidParameterException("Number of sentences must be an integer") - - return True diff --git a/kalliope/settings.yml b/kalliope/settings.yml index 7eda9449..f6ee8d8f 100644 --- a/kalliope/settings.yml +++ b/kalliope/settings.yml @@ -113,3 +113,18 @@ rest_api: # --------------------------- # Specify an optional default synapse response in case your order is not found. default_synapse: "Default-synapse" + +# --------------------------- +# Resource directory path +# +# Accepted types : +# - neuron +# - stt +# - tts +# - trigger /!\ we do not manage trigger properly yet... +# --------------------------- +#resource_directory: +# neuron: "/var/tmp/resources/neurons" +# stt: "resources/stt" +# tts: "resources/tts" +# trigger: "resources/trigger" diff --git a/kalliope/stt/google/google.py b/kalliope/stt/google/google.py index f403c85c..c1ed9b13 100644 --- a/kalliope/stt/google/google.py +++ b/kalliope/stt/google/google.py @@ -36,24 +36,24 @@ def __init__(self, callback=None, **kwargs): captured_audio = r.recognize_google(audio, key=key, language=language, show_all=show_all) Utils.print_success("Google Speech Recognition thinks you said %s" % captured_audio) - self._analyse_audio(captured_audio) + self._analyse_audio(audio_to_text=captured_audio) except sr.UnknownValueError: Utils.print_warning("Google Speech Recognition could not understand audio") # callback anyway, we need to listen again for a new order - self._analyse_audio(audio=None) + self._analyse_audio(audio_to_text=None) except sr.RequestError as e: Utils.print_danger("Could not request results from Google Speech Recognition service; {0}".format(e)) # callback anyway, we need to listen again for a new order - self._analyse_audio(audio=None) + self._analyse_audio(audio_to_text=None) - def _analyse_audio(self, audio): + def _analyse_audio(self, audio_to_text): """ - Confirm the audio exists annd run it in a Callback + Confirm the audio exists and run it in a Callback :param audio: the captured audio """ # if self.main_controller is not None: # self.main_controller.analyse_order(audio) if self.callback is not None: - self.callback(audio) + self.callback(audio_to_text) diff --git a/kalliope/trigger/snowboy/resources/GlaDOS.pmdl b/kalliope/trigger/snowboy/resources/GlaDOS.pmdl deleted file mode 100755 index b4245ff3..00000000 Binary files a/kalliope/trigger/snowboy/resources/GlaDOS.pmdl and /dev/null differ diff --git a/kalliope/trigger/snowboy/resources/jarviss.pmdl b/kalliope/trigger/snowboy/resources/jarviss.pmdl deleted file mode 100755 index cfaf111b..00000000 Binary files a/kalliope/trigger/snowboy/resources/jarviss.pmdl and /dev/null differ diff --git a/setup.py b/setup.py index e0ab11b7..0ba5ee03 100644 --- a/setup.py +++ b/setup.py @@ -65,21 +65,16 @@ def read_version_py(file_name): 'python2-pythondialog==3.4.0', 'jinja2==2.8', 'cffi==1.9.1', - 'pygmail==0.0.5.4', - 'pushetta==1.0.15', - 'wakeonlan==0.2.2', 'ipaddress==1.0.17', - 'pyowm==2.5.0', - 'python-twitter==3.1', 'flask==0.11.1', 'Flask-Restful==0.3.5', - 'wikipedia==1.4.0', - 'requests==2.12.1', + 'requests==2.12.4', 'httpretty==0.8.14', 'mock==2.0.0', - 'feedparser==5.2.1', 'Flask-Testing==0.6.1', - 'apscheduler==3.3.0' + 'apscheduler==3.3.0', + 'GitPython==2.1.1', + 'packaging>=16.8' ],