Browse Source

Merge pull request #156 from kalliope-project/dev

V0.4.0
Nicolas Marcq 8 years ago
parent
commit
a68377d2c8
100 changed files with 2351 additions and 1227 deletions
  1. 14 1
      CHANGELOG.md
  2. 17 111
      Docs/contributing.md
  3. 104 0
      Docs/contributing/contribute_neuron.md
  4. 77 0
      Docs/contributing/contribute_stt.md
  5. 90 0
      Docs/contributing/contribute_tts.md
  6. 30 0
      Docs/contributing/dna.md
  7. 53 0
      Docs/contributing/installation_file.md
  8. 9 0
      Docs/contributing/neuron_template.md
  9. 28 0
      Docs/contributing/stt_template.md
  10. 2 1
      Docs/installation.md
  11. 10 0
      Docs/installation/ubuntu_14.04.md
  12. 1 1
      Docs/installation/ubuntu_16.04.md
  13. 31 1
      Docs/kalliope_cli.md
  14. 29 18
      Docs/neuron_list.md
  15. 18 0
      Docs/neurons.md
  16. 19 0
      Docs/settings.md
  17. 3 0
      Docs/signals.md
  18. 13 2
      Docs/stt.md
  19. 28 0
      Docs/stt_list.md
  20. 6 4
      Docs/trigger.md
  21. 13 1
      Docs/tts.md
  22. 28 0
      Docs/tts_list.md
  23. 7 1
      README.md
  24. 2 0
      Tests/modules/dna.yml
  25. 1 0
      Tests/modules/install.yml
  26. 10 0
      Tests/modules/test_invalid_dna.yml
  27. 10 0
      Tests/modules/test_valid_dna.yml
  28. 9 0
      Tests/settings/settings_test.yml
  29. 107 0
      Tests/test_dna_loader.py
  30. 34 11
      Tests/test_launchers.py
  31. 70 5
      Tests/test_neuron_module.py
  32. 131 51
      Tests/test_order_analyser.py
  33. 152 0
      Tests/test_resources_manager.py
  34. 41 2
      Tests/test_settings_loader.py
  35. 12 3
      Tests/test_utils.py
  36. 36 1
      brain_examples/neurotransmitter.yml
  37. 0 1
      install/files/deb-packages_requirements.txt
  38. 3 8
      install/files/python_requirements.txt
  39. 17 2
      kalliope/__init__.py
  40. 1 1
      kalliope/_version.py
  41. 21 6
      kalliope/core/ConfigurationManager/ConfigurationChecker.py
  42. 93 0
      kalliope/core/ConfigurationManager/DnaLoader.py
  43. 68 1
      kalliope/core/ConfigurationManager/SettingLoader.py
  44. 16 0
      kalliope/core/Models/Brain.py
  45. 40 0
      kalliope/core/Models/Dna.py
  46. 1 1
      kalliope/core/Models/Neuron.py
  47. 41 0
      kalliope/core/Models/Resources.py
  48. 6 1
      kalliope/core/Models/Settings.py
  49. 1 0
      kalliope/core/Models/__init__.py
  50. 10 3
      kalliope/core/NeuronLauncher.py
  51. 33 5
      kalliope/core/NeuronModule.py
  52. 86 29
      kalliope/core/OrderAnalyser.py
  53. 3 3
      kalliope/core/OrderListener.py
  54. 275 0
      kalliope/core/ResourcesManager.py
  55. 14 8
      kalliope/core/RestAPI/FlaskAPI.py
  56. 5 12
      kalliope/core/SynapseLauncher.py
  57. 3 1
      kalliope/core/TTS/TTSLauncher.py
  58. 3 3
      kalliope/core/TriggerLauncher.py
  59. 53 7
      kalliope/core/Utils/Utils.py
  60. 1 0
      kalliope/core/__init__.py
  61. 42 15
      kalliope/neurons/ansible_playbook/README.md
  62. 40 8
      kalliope/neurons/ansible_playbook/ansible_playbook.py
  63. 0 68
      kalliope/neurons/gmail_checker/README.md
  64. 0 1
      kalliope/neurons/gmail_checker/__init__.py
  65. 0 92
      kalliope/neurons/gmail_checker/gmail_checker.py
  66. 0 0
      kalliope/neurons/gmail_checker/tests/__init__.py
  67. 0 37
      kalliope/neurons/gmail_checker/tests/test_gmail_checker.py
  68. 4 0
      kalliope/neurons/kill_switch/README.md
  69. 37 0
      kalliope/neurons/neurotransmitter/README.md
  70. 16 14
      kalliope/neurons/neurotransmitter/neurotransmitter.py
  71. 154 0
      kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py
  72. 0 93
      kalliope/neurons/openweathermap/README.md
  73. 0 1
      kalliope/neurons/openweathermap/__init__.py
  74. 0 127
      kalliope/neurons/openweathermap/openweathermap.py
  75. 0 0
      kalliope/neurons/openweathermap/tests/__init__.py
  76. 0 36
      kalliope/neurons/openweathermap/tests/test_openweathermap.py
  77. 0 49
      kalliope/neurons/push_message/Readme.md
  78. 0 2
      kalliope/neurons/push_message/__init__.py
  79. 0 43
      kalliope/neurons/push_message/push_message.py
  80. 0 0
      kalliope/neurons/push_message/tests/__init__.py
  81. 0 46
      kalliope/neurons/push_message/tests/test_push_message.py
  82. 0 59
      kalliope/neurons/rss_reader/README.md
  83. 0 1
      kalliope/neurons/rss_reader/__init__.py
  84. 0 45
      kalliope/neurons/rss_reader/rss_reader.py
  85. 0 0
      kalliope/neurons/rss_reader/tests/__init__.py
  86. 0 23
      kalliope/neurons/rss_reader/tests/test_rss_reader.py
  87. 4 0
      kalliope/neurons/say/README.md
  88. 4 0
      kalliope/neurons/script/README.md
  89. 3 0
      kalliope/neurons/shell/README.md
  90. 4 0
      kalliope/neurons/sleep/README.md
  91. 4 0
      kalliope/neurons/systemdate/README.md
  92. 0 83
      kalliope/neurons/tasker_autoremote/Readme.md
  93. 0 1
      kalliope/neurons/tasker_autoremote/__init__.py
  94. BIN
      kalliope/neurons/tasker_autoremote/images/profile_auto_remote.png
  95. BIN
      kalliope/neurons/tasker_autoremote/images/profile_display_unlocked.png
  96. BIN
      kalliope/neurons/tasker_autoremote/images/task_play_music.png
  97. BIN
      kalliope/neurons/tasker_autoremote/images/task_stop_music.png
  98. 0 41
      kalliope/neurons/tasker_autoremote/tasker_autoremote.py
  99. 0 0
      kalliope/neurons/tasker_autoremote/tests/__init__.py
  100. 0 36
      kalliope/neurons/tasker_autoremote/tests/test_tasker_autoremote.py

+ 14 - 1
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
+- Initial Release

+ 17 - 111
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.

+ 104 - 0
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
+```

+ 77 - 0
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)
+        # -------------------
+        
+```

+ 90 - 0
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 !
+    # -------------------
+    
+```

+ 30 - 0
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"
+```

+ 53 - 0
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
+```

+ 9 - 0
Docs/neuron_template.md → 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.

+ 28 - 0
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.

+ 2 - 1
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

+ 10 - 0
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
+```

+ 1 - 1
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
 

+ 31 - 1
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 <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
-```
+```
+
+### --git-url
+
+Used by the `install` argument to specify the URL of a git repository of the module to install.
+

+ 29 - 18
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).

+ 18 - 0
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 <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)
 

+ 19 - 0
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.

+ 3 - 0
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

+ 13 - 2
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 <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 :

+ 28 - 0
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.
+
+

+ 6 - 4
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  | каллиопа      |

+ 13 - 1
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 <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
-```
+```

+ 28 - 0
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.
+
+

+ 7 - 1
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
 

+ 2 - 0
Tests/modules/dna.yml

@@ -0,0 +1,2 @@
+# empty, just for testing file exist
+

+ 1 - 0
Tests/modules/install.yml

@@ -0,0 +1 @@
+# empty, just for testing file exist

+ 10 - 0
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"

+ 10 - 0
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"

+ 9 - 0
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"

+ 107 - 0
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))

+ 34 - 11
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 @@ class TestLaunchers(unittest.TestCase):
             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 @@ class TestLaunchers(unittest.TestCase):
 
         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 @@ class TestLaunchers(unittest.TestCase):
         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 @@ class TestLaunchers(unittest.TestCase):
         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()
 

+ 70 - 5
Tests/test_neuron_module.py

@@ -3,6 +3,10 @@ import unittest
 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 @@ class TestNeuronModule(unittest.TestCase):
         """
 
         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 @@ class TestNeuronModule(unittest.TestCase):
     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()
+
+
+

+ 131 - 51
Tests/test_order_analyser.py

@@ -5,6 +5,7 @@ from kalliope.core.OrderAnalyser import OrderAnalyser
 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 @@ class TestOrderAnalyser(unittest.TestCase):
         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 @@ class TestOrderAnalyser(unittest.TestCase):
 
         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 @@ class TestOrderAnalyser(unittest.TestCase):
                               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 @@ class TestOrderAnalyser(unittest.TestCase):
         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 @@ class TestOrderAnalyser(unittest.TestCase):
                             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 @@ class TestOrderAnalyser(unittest.TestCase):
 
         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 @@ class TestOrderAnalyser(unittest.TestCase):
         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 @@ class TestOrderAnalyser(unittest.TestCase):
                           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()

+ 152 - 0
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))

+ 41 - 2
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 @@ from kalliope.core.Models.Tts import Tts
 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 @@ class TestSettingLoader(unittest.TestCase):
                 {'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 @@ class TestSettingLoader(unittest.TestCase):
                                            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 @@ class TestSettingLoader(unittest.TestCase):
         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()

+ 12 - 3
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 @@ class TestUtils(unittest.TestCase):
         """
         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")
 

+ 36 - 1
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"

+ 0 - 1
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

+ 3 - 8
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

+ 17 - 2
kalliope/__init__.py

@@ -8,9 +8,12 @@ from kalliope.core import Utils
 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):
     """

+ 1 - 1
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"

+ 21 - 6
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 @@ class ConfigurationChecker:
             :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):

+ 93 - 0
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

+ 68 - 1
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 @@ class SettingLoader(object):
         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 @@ class SettingLoader(object):
         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 @@ class SettingLoader(object):
 
         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
+
 

+ 16 - 0
kalliope/core/Models/Brain.py

@@ -10,6 +10,22 @@ class Brain:
         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

+ 40 - 0
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__

+ 1 - 1
kalliope/core/Models/Neuron.py

@@ -18,7 +18,7 @@ class Neuron(object):
         """
         return {
             'name': self.name,
-            'parameters': str(self.parameters)
+            'parameters': self.parameters
         }
 
     def __str__(self):

+ 41 - 0
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__

+ 6 - 1
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 @@ class Settings(object):
                  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 @@ class Settings(object):
         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):
         """

+ 1 - 0
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

+ 10 - 3
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 @@ class NeuronLauncher:
         :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)

+ 33 - 5
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 @@ class NeuronModule(object):
             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 @@ class NeuronModule(object):
     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 @@ class NeuronModule(object):
         :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):
         """

+ 86 - 29
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 @@ class OrderAnalyser:
         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 @@ class OrderAnalyser:
         :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 @@ class OrderAnalyser:
         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

+ 3 - 3
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 @@ class OrderListener(Thread):
         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

+ 275 - 0
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

+ 14 - 8
kalliope/core/RestAPI/FlaskAPI.py

@@ -23,12 +23,18 @@ class FlaskAPI(threading.Thread):
         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/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
         self.app.add_url_rule('/synapses/<synapse_name>', 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 @@ class FlaskAPI(threading.Thread):
         :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 @@ class FlaskAPI(threading.Thread):
     @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 @@ class FlaskAPI(threading.Thread):
         """
         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 = {

+ 5 - 12
kalliope/core/SynapseLauncher.py

@@ -22,21 +22,14 @@ class SynapseLauncher(object):
         :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):

+ 3 - 1
kalliope/core/TTS/TTSLauncher.py

@@ -22,4 +22,6 @@ class TTSLauncher(object):
         .. 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)

+ 3 - 3
kalliope/core/TriggerLauncher.py

@@ -23,6 +23,6 @@ class TriggerLauncher(object):
         # 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)

+ 53 - 7
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 @@ class Utils(object):
     # 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 @@ class Utils(object):
         :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 @@ class Utils(object):
                 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 <Enter>.
+            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")

+ 1 - 0
kalliope/core/__init__.py

@@ -3,5 +3,6 @@ from kalliope.core.OrderListener import OrderListener
 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
 
 

+ 42 - 15
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"      
-```

+ 40 - 8
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 ansible.executor.playbook_executor import PlaybookExecutor
 
 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 @@ class Ansible_playbook(NeuronModule):
     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

+ 0 - 68
kalliope/neurons/gmail_checker/README.md

@@ -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).

+ 0 - 1
kalliope/neurons/gmail_checker/__init__.py

@@ -1 +0,0 @@
-from gmail_checker import Gmail_checker

+ 0 - 92
kalliope/neurons/gmail_checker/gmail_checker.py

@@ -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

+ 0 - 0
kalliope/neurons/gmail_checker/tests/__init__.py


+ 0 - 37
kalliope/neurons/gmail_checker/tests/test_gmail_checker.py

@@ -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()
-

+ 4 - 0
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

+ 37 - 0
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.
 

+ 16 - 14
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 @@ class Neurotransmitter(NeuronModule):
         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 @@ class Neurotransmitter(NeuronModule):
             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

+ 154 - 0
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()
+
+
+
+
+

+ 0 - 93
kalliope/neurons/openweathermap/README.md

@@ -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.

+ 0 - 1
kalliope/neurons/openweathermap/__init__.py

@@ -1 +0,0 @@
-from openweathermap import Openweathermap

+ 0 - 127
kalliope/neurons/openweathermap/openweathermap.py

@@ -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

+ 0 - 0
kalliope/neurons/openweathermap/tests/__init__.py


+ 0 - 36
kalliope/neurons/openweathermap/tests/test_openweathermap.py

@@ -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()

+ 0 - 49
kalliope/neurons/push_message/Readme.md

@@ -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.

+ 0 - 2
kalliope/neurons/push_message/__init__.py

@@ -1,2 +0,0 @@
-from push_message import Push_message
-

+ 0 - 43
kalliope/neurons/push_message/push_message.py

@@ -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

+ 0 - 0
kalliope/neurons/push_message/tests/__init__.py


+ 0 - 46
kalliope/neurons/push_message/tests/test_push_message.py

@@ -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()

+ 0 - 59
kalliope/neurons/rss_reader/README.md

@@ -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
-

+ 0 - 1
kalliope/neurons/rss_reader/__init__.py

@@ -1 +0,0 @@
-from rss_reader import Rss_reader

+ 0 - 45
kalliope/neurons/rss_reader/rss_reader.py

@@ -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

+ 0 - 0
kalliope/neurons/rss_reader/tests/__init__.py


+ 0 - 23
kalliope/neurons/rss_reader/tests/test_rss_reader.py

@@ -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()
-

+ 4 - 0
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                                |

+ 4 - 0
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                                                                    |

+ 3 - 0
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
 

+ 4 - 0
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                         |

+ 4 - 0
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                                                               |

+ 0 - 83
kalliope/neurons/tasker_autoremote/Readme.md

@@ -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.
-
-
- 

+ 0 - 1
kalliope/neurons/tasker_autoremote/__init__.py

@@ -1 +0,0 @@
-from tasker_autoremote import Tasker_autoremote

BIN
kalliope/neurons/tasker_autoremote/images/profile_auto_remote.png


BIN
kalliope/neurons/tasker_autoremote/images/profile_display_unlocked.png


BIN
kalliope/neurons/tasker_autoremote/images/task_play_music.png


BIN
kalliope/neurons/tasker_autoremote/images/task_stop_music.png


+ 0 - 41
kalliope/neurons/tasker_autoremote/tasker_autoremote.py

@@ -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

+ 0 - 0
kalliope/neurons/tasker_autoremote/tests/__init__.py


+ 0 - 36
kalliope/neurons/tasker_autoremote/tests/test_tasker_autoremote.py

@@ -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()

Some files were not shown because too many files changed in this diff