Kaynağa Gözat

Merge pull request #134 from kalliope-project/dev

v0.3.0 GA
Monf 8 yıl önce
ebeveyn
işleme
665d4002c8
100 değiştirilmiş dosya ile 2620 ekleme ve 1207 silme
  1. 14 0
      .travis.yml
  2. 12 0
      CHANGELOG.md
  3. 23 10
      Docs/brain.md
  4. 45 5
      Docs/contributing.md
  5. 121 0
      Docs/installation.md
  6. 12 38
      Docs/installation/debian_jessie.md
  7. 8 30
      Docs/installation/raspbian_jessie.md
  8. 3 41
      Docs/installation/ubuntu_16.04.md
  9. 8 9
      Docs/kalliope_cli.md
  10. 18 17
      Docs/neuron_list.md
  11. 4 3
      Docs/neurons.md
  12. 1 1
      Docs/rest_api.md
  13. 33 5
      Docs/settings.md
  14. 100 22
      Docs/signals.md
  15. 5 5
      Docs/stt.md
  16. 6 4
      Docs/trigger.md
  17. 5 5
      Docs/tts.md
  18. 1 0
      MANIFEST.in
  19. 6 3
      README.md
  20. 11 0
      Tests/__init__.py
  21. 0 0
      Tests/brains/brain_test.yml
  22. 0 0
      Tests/brains/included_brain_test.yml
  23. 7 0
      Tests/settings/settings_test.yml
  24. 1 0
      Tests/templates/template_test.j2
  25. 107 0
      Tests/test_brain_loader.py
  26. 12 4
      Tests/test_configuration_checker.py
  27. 11 11
      Tests/test_dynamic_loading.py
  28. 171 0
      Tests/test_file_manager.py
  29. 115 0
      Tests/test_launchers.py
  30. 109 0
      Tests/test_neuron_module.py
  31. 499 0
      Tests/test_order_analyser.py
  32. 230 0
      Tests/test_rest_api.py
  33. 59 61
      Tests/test_settings_loader.py
  34. 34 0
      Tests/test_singleton.py
  35. 127 0
      Tests/test_tts_module.py
  36. 131 0
      Tests/test_utils.py
  37. 3 2
      Tests/test_yaml_loader.py
  38. 0 16
      brain.yml
  39. 2 1
      brain_examples/ansible_playbook.yml
  40. 8 0
      brain_examples/default.yml
  41. 0 0
      brain_examples/gmail_checker.yml
  42. 0 0
      brain_examples/kill_switch.yml
  43. 0 0
      brain_examples/neurotransmitter.yml
  44. 0 0
      brain_examples/openweathermap.yml
  45. 0 0
      brain_examples/push_message.yml
  46. 19 0
      brain_examples/rss_reader.yml
  47. 0 0
      brain_examples/say.yml
  48. 0 0
      brain_examples/script.yml
  49. 0 0
      brain_examples/shell.yml
  50. 0 0
      brain_examples/systemdate.yml
  51. 0 0
      brain_examples/tasker_autoremote.yml
  52. 0 0
      brain_examples/twitter.yml
  53. 0 0
      brain_examples/wake_on_lan.yml
  54. 0 0
      brain_examples/wikipedia.yml
  55. 0 118
      core/CrontabManager.py
  56. 0 33
      core/Models/Event.py
  57. 0 40
      core/Models/Singleton.py
  58. 0 1
      core/Tests/__init__.py
  59. 0 108
      core/Tests/test_brain_loader.py
  60. 0 250
      core/Tests/test_order_analyser.py
  61. 0 102
      core/Utils.py
  62. 0 6
      core/__init__.py
  63. 24 0
      install/files/deb-packages_requirements.txt
  64. 10 6
      install/files/python_requirements.txt
  65. 3 0
      install/files/travis_repo_trusty_requirements.txt
  66. 0 2
      install/install.yml
  67. 2 113
      kalliope.py
  68. 113 0
      kalliope/__init__.py
  69. 2 0
      kalliope/_version.py
  70. 18 0
      kalliope/brain.yml
  71. 45 12
      kalliope/core/ConfigurationManager/BrainLoader.py
  72. 28 3
      kalliope/core/ConfigurationManager/ConfigurationChecker.py
  73. 46 11
      kalliope/core/ConfigurationManager/SettingLoader.py
  74. 1 6
      kalliope/core/ConfigurationManager/YAMLLoader.py
  75. 0 0
      kalliope/core/ConfigurationManager/__init__.py
  76. 49 0
      kalliope/core/EventManager.py
  77. 11 16
      kalliope/core/MainController.py
  78. 0 0
      kalliope/core/Models/Brain.py
  79. 51 0
      kalliope/core/Models/Event.py
  80. 0 0
      kalliope/core/Models/Neuron.py
  81. 0 0
      kalliope/core/Models/Order.py
  82. 0 0
      kalliope/core/Models/RestAPI.py
  83. 2 0
      kalliope/core/Models/Settings.py
  84. 7 0
      kalliope/core/Models/Singleton.py
  85. 0 0
      kalliope/core/Models/Stt.py
  86. 0 0
      kalliope/core/Models/Synapse.py
  87. 0 0
      kalliope/core/Models/Trigger.py
  88. 0 0
      kalliope/core/Models/Tts.py
  89. 0 0
      kalliope/core/Models/__init__.py
  90. 6 4
      kalliope/core/NeuronLauncher.py
  91. 48 51
      kalliope/core/NeuronModule.py
  92. 42 12
      kalliope/core/OrderAnalyser.py
  93. 3 3
      kalliope/core/OrderListener.py
  94. 0 0
      kalliope/core/Players/Mplayer.py
  95. 0 0
      kalliope/core/Players/__init__.py
  96. 17 6
      kalliope/core/RestAPI/FlaskAPI.py
  97. 0 0
      kalliope/core/RestAPI/__init__.py
  98. 3 3
      kalliope/core/RestAPI/utils.py
  99. 6 6
      kalliope/core/ShellGui.py
  100. 2 2
      kalliope/core/SynapseLauncher.py

+ 14 - 0
.travis.yml

@@ -0,0 +1,14 @@
+language: python
+python:
+  - "2.7"
+# command to install dependencies
+before_install:
+- sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu trusty main restricted universe multiverse"
+- sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu trusty-updates main restricted universe multiverse"
+- sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse"
+- sudo apt-get update
+- sudo apt-get install $(cat install/files/deb-packages_requirements.txt)
+- sudo apt-get install libstdc++6
+install: "pip install -r install/files/python_requirements.txt"
+# command to run tests
+script: pytest

+ 12 - 0
CHANGELOG.md

@@ -1,3 +1,15 @@
+v0.3.0 / 2016-12-7
+=================
+- add unit tests for core & neurons
+- add CI (Travis)
+- refactor Event manager
+- support installation with setup.py
+- support pip installation
+- fix ansible_playbook neuron
+- add rss_reader neuron
+- review settings and brain file loading
+- add default neuron used if Kalliope does not match the spelt order
+
 v0.2 / 2016-11-21
 =================
 

+ 23 - 10
Docs/brain.md

@@ -1,6 +1,6 @@
 # Brain
 
-The brain is a main component of Kalliope. It's a module that gives a confuguration of your own personal assistant and, so, determines it's behavior and fonctionnalities.
+The brain is a main component of Kalliope. It's a module that gives a configuration of your own personal assistant and, so, determines it's behavior and fonctionnalities.
 
 Brain is composed by synapses: a synapse is the link between input and output actions.
 
@@ -9,10 +9,14 @@ An input action, called a "[signal](signals.md)" can be:
 - **an event:** A date or a frequency (E.G: repeat each morning at 8:30)
 
 An output action is
-- **a list of neurons:** A [neuron](neurons.md) is a module or plugin that will perform some actions like simply talking, run a script, run a command or a complex Ansible playbook.
+- **a list of neurons:** A [neuron](neurons.md) is a module or plugin that will perform some actions like simply talking, run a script, run a command or call a web service.
 
 Brain is expressed in YAML format (see YAML Syntax) and has a minimum of syntax, which intentionally tries to not be a programming language or script, 
 but rather a model of a configuration or a process.
+Kalliope will look for the brain in the order bellow:
+- From you current folder, E.g `/home/pi/my_kalliope/brain.yml`
+- From `/etc/kalliope/brain.yml`
+- From the default `brain.yml`. You can take a look into the default [`brain.yml`](../kalliope/brain.yml) file which is located in the root of the project tree.
 
 Let's take a look on a basic synapse in our brain:
 
@@ -28,11 +32,7 @@ Let's take a look on a basic synapse in our brain:
 
 Let's break this down in sections so we can understand how the file is built and what each part means.
 
-The file starts with:
-```
----
-```
-This is a requirement for YAML to interpret the file as a proper document.
+The file starts with: `---`. This is a requirement for YAML to interpret the file as a proper document.
 
 Items that begin with a ```-``` are considered as list items. Items have the format of ```key: value``` where value can be a simple string or a sequence of other items.
 
@@ -95,7 +95,7 @@ neurons:
 Note here that parameters are indented with one tabulation bellow the neuron's name (YAML syntax requirement).
 
 In this example, the neuron called "say" will make Kalliope speak out loud the sentence in parameter **message**.
-See the complete list of [available neurons](neurons.md) here.
+See the complete list of [available neurons](neuron_list.md) here.
 
 ## Manage synapses
 
@@ -108,7 +108,6 @@ If you want a better visibly, or simply sort your actions in different files, yo
 
 To do that, use the import statement in the entry brain.yml file with the following syntax:
 ```
----
   - includes:
       - path/to/sub_brain.yml
       - path/to/another_sub_brain.yml
@@ -116,7 +115,6 @@ To do that, use the import statement in the entry brain.yml file with the follow
 
 E.g:
 ```
----
   - includes:
       - brains/rolling_shutter_commands.yml
       - brains/find_my_phone.yml
@@ -125,3 +123,18 @@ E.g:
 >**Note:** You can only use the `include` statement in the main brain file. 
 
 >**Note:** the includes statement must start with a `-`
+
+
+## The default Synapse
+
+You can provide a default synapse in case none of them are matching when an order is given.
+>**Note:** This default synapse is optional.
+>**Note:** You need to define it in the settings.yml cf :[Setting](settings.md).
+
+## Next: Start Kalliope
+Now you take a look into the [CLI documentation](kalliope_cli.md) to learn how to start kalliope.
+
+## Notes
+- What is a [neuron](neurons.md)
+- What is a [signal](signals.md)
+

+ 45 - 5
Docs/contributing.md

@@ -58,10 +58,10 @@ The constructor has a __**kwargs argument__ which is corresponding to the Dict o
     ```
 
 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
-```
+    ```
+    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:
@@ -71,9 +71,49 @@ python -m unittest discover
 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 languages__ inside its own logic.
+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.
 
 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

+ 121 - 0
Docs/installation.md

@@ -0,0 +1,121 @@
+# Kalliope installation
+
+## Prerequisites
+
+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)
+- [Debian Jessie](installation/debian_jessie.md)
+
+## Installation
+
+### Method 1 - User install using the PIP package
+
+You can install kalliope on your system by using Pypi:
+```
+sudo pip install kalliope
+```
+
+### Method 2 - Manual setup using sources
+
+Clone the project:
+```
+git clone https://github.com/kalliope-project/kalliope.git
+cd kalliope
+```
+
+Install the project:
+```
+sudo python setup.py install
+```
+
+### Method 3 - Developer install using Virtualenv
+
+Install the `python-virtualenv` package:
+```
+sudo apt-get install python-virtualenv
+```
+
+Clone the project:
+```
+git clone https://github.com/kalliope-project/kalliope.git
+cd kalliope
+```
+
+Generate a local python environment:
+```
+virtualenv venv
+```
+
+Install the project using the local environment:
+```
+venv/bin/pip install --editable .
+```
+
+### Method 4 - Developer, dependencies install only
+
+Clone the project:
+```
+git clone https://github.com/kalliope-project/kalliope.git
+cd kalliope
+```
+
+Install the python dependencies directly:
+```
+sudo pip install -r install/python_requirements.txt
+```
+
+## Test your env
+
+To ensure that you can record your voice, run the following command to capture audio input from your microphone:
+```
+rec test.wav
+```
+
+Press CTRL-C after capturing a sample of your voice.
+
+Then play the recorded audio file
+```
+mplayer test.wav
+```
+
+You can then test that your Kalliope is working by using the "bonjour" order integrated in the [default brain](../kalliope/brain.yml).
+Start kalliope:
+```
+kalliope start
+```
+
+Kalliope will load default settings and brain, the output should looks the following
+```
+Starting event manager
+Events loaded
+Starting Kalliope
+Press Ctrl+C for stopping
+Starting REST API Listening port: 5000
+```
+
+Then speak the hotwork out loud to wake up Kalliope. By default, the hotwork is "Kalliopé" with the french pronunciation.
+If the trigger is successfully raised, you'll see "say something" into the console. 
+```
+2016-12-05 20:54:21,950 :: INFO :: Keyword 1 detected at time: 2016-12-05 20:54:21
+Say something!
+```
+
+Then you can say "bonjour" and listen the Kalliope response.
+```
+Say something!
+Google Speech Recognition thinks you said Bonjour
+Order matched in the brain. Running synapse "say-hello-fr"
+Waiting for trigger detection
+```
+
+## Get a starter configuration
+We create some starter configuration that only need to be downloaded and then started. 
+Those repositories provide you a basic structure to start playing with kalliope. We recommend you to clone one of them and then go to the next section.
+
+- [French starter config](https://github.com/kalliope-project/kalliope_starter_fr)
+- [English starter config](https://github.com/kalliope-project/kalliope_starter_en)
+
+
+## Next: Create you own bot
+If everything is ok, you can start playing with Kalliope. First, take a look to the [default settings](settings.md).

+ 12 - 38
Docs/installation/debian_jessie.md

@@ -1,54 +1,28 @@
-# Kalliope installation on Debian Jessie
+# Kalliope requirements for Debian Jessie
 
-## Automated install
+## Debian packages requirements
 
-Clone the project
+Edit `/etc/apt/sources.list` and check that you have `contrib` and `non-free` are enabled:
 ```
-cd
-git clone https://github.com/kalliope-project/kalliope.git
+deb http://httpredir.debian.org/debian jessie main contrib non-free
+deb-src http://httpredir.debian.org/debian jessie main contrib non-free
 ```
 
-Edit `/etc/apt/sources.list` and check that your mirror accept "non-free" package
-```
-deb http://ftp.fr.debian.org/debian/ jessie main contrib non-free
-deb-src http://ftp.fr.debian.org/debian/ jessie main contrib non-free
-```
+Install some required system libraries and softwares:
 
-Run the install script.
-```
-./kalliope/install/install_kalliope.sh
-```
-
-## Manual install
-
-To make Kalliope work, you will have to install a certain number of libraries:
 ```
 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 portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
-```
-
-Clone the project
-```
-git clone https://github.com/kalliope-project/kalliope.git
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
 ```
 
-Install libs
+Let's install the last release of python-pip
 ```
-sudo pip install -r install/files/python_requirements.txt
+wget https://bootstrap.pypa.io/get-pip.py
+sudo python get-pip.py
 ```
 
-## Test your env
-
-To ensure that you can record your voice, run the following command to capture audio input from your microphone
-```
-rec test.wav
-```
-
-Press CTRL-C after capturing a sample of your voice.
-
-Then play the recorded audio file
+Then, with pip, the last release of setuptools
 ```
-mplayer test.wav
+sudo pip install -U pip setuptools
 ```
 
-If everything is ok, you can start playing with Kalliope. First, take a look to the [default settings](settings.md).

+ 8 - 30
Docs/installation/raspbian_jessie.md

@@ -1,41 +1,19 @@
-# Kalliope installation on Raspbian
+# Kalliope requirements for Raspbian
 
-## Automated install
+## Debian packages requirements
 
-Clone the project
-```
-cd
-git clone https://github.com/kalliope-project/kalliope.git
-```
-
-Run the install script.
-```
-./kalliope/install/install_kalliope.sh
-```
-
-## Manual install
+Install some required system libraries and software:
 
-To make Kalliope work, you will have to install a certain number of libraries:
 ```
 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 portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
 ```
 
-Clone the project
-```
-git clone https://github.com/kalliope-project/kalliope.git
-```
-
-Install libs
-```
-sudo pip install -r install/files/python_requirements.txt
-```
-
-# Raspberry Pi configuration
+## Raspberry Pi configuration
 
-This documentation deals with the special configuration needed for get kalliope working on a RPi.
+This part deals with the special configuration needed to get kalliope working on a RPi.
 
-## Packages
+### Packages
 
 On a Raspberry Pi, pulseaudio is not installed by default
 ```
@@ -47,7 +25,7 @@ Start the pulseaudio server
 pulseaudio -D
 ```
 
-## Microphone configuration
+### Microphone configuration
 
 Get your output card
 ```
@@ -141,7 +119,7 @@ mplayer test.wav
 ```
 
 
-## HDMI / Analog audio
+### HDMI / Analog audio
 
 By default the audio stream will get out by HDMI if something is plugged to this port.
 Check the [official documentation](https://www.raspberrypi.org/documentation/configuration/audio-config.md) to switch from HDMI to analog.

+ 3 - 41
Docs/installation/ubuntu_16.04.md

@@ -1,48 +1,10 @@
-# Kalliope installation on Ubuntu 16.04
+# Kalliope requirements for Ubuntu 14.04/16.04
 
-## Automated install
+## Debian packages requirements
 
-Clone the project
-```
-cd
-git clone https://github.com/kalliope-project/kalliope.git
-```
-
-Run the install script.
-```
-./kalliope/install/install_kalliope.sh
-```
-
-## Manual install
+Install some required system libraries and softwares:
 
-To make Kalliope work, you will have to install a certain number of libraries:
 ```
 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 portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
 ```
-
-Clone the project
-```
-git clone https://github.com/kalliope-project/kalliope.git
-```
-
-Install libs
-```
-sudo pip install -r install/files/python_requirements.txt
-```
-
-## Test your env
-
-To ensure that you can record your voice, run the following command to capture audio input from your microphone
-```
-rec test.wav
-```
-
-Press CTRL-C after capturing a sample of your voice.
-
-Then play the recorded audio file
-```
-mplayer test.wav
-```
-
-If everything is ok, you can start playing with Kalliope. First, take a look to the [default settings](settings.md).

+ 8 - 9
Docs/kalliope_cli.md

@@ -3,13 +3,12 @@
 ## SYNOPSIS
 This is the syntax used to run Kalliope from command line
 ```
-cd /path/to/kalliope
-python kalliope.py command --option <argument>
+kalliope command --option <argument>
 ```
 
 For example, to start Kalliope we simply use
 ```
-python kalliope.py start
+kalliope start
 ```
 
 ## ARGUMENTS
@@ -19,7 +18,7 @@ Start Kalliope main program
 
 Example of use
 ```
-python kalliope.py start
+kalliope start
 ```
 
 To kill Kalliope, you can press "Ctrl-C" on your keyboard.
@@ -30,7 +29,7 @@ The GUI allows you to test your [STT](stt.md) and [TTS](tts.md) that you have co
 
 Example of use
 ```
-python kalliope.py gui
+kalliope gui
 ```
 
 ## OPTIONS
@@ -43,7 +42,7 @@ Run a specific synapse from the brain file.
 
 Example of use
 ```
-python kalliope.py start --run-synapse "say hello"
+kalliope start --run-synapse "say-hello"
 ```
 
 ### --brain-file BRAIN_FILE
@@ -53,12 +52,12 @@ Replace the default brain file from the root of the project folder by a custom o
 
 Example of use
 ```
-python kalliope.py start --brain-file /home/me/my_other_brain.yml
+kalliope start --brain-file /home/me/my_other_brain.yml
 ```
 
 You can combine the options together like, for example:
 ```
-python kalliope.py start --run-synapse "say hello" --brain-file /home/me/my_other_brain.yml
+kalliope start --run-synapse "say-hello" --brain-file /home/me/my_other_brain.yml
 ```
 
 ### --debug
@@ -67,5 +66,5 @@ Show debug output in the console
 
 Example of use
 ```
-python kalliope.py start --debug
+kalliope start --debug
 ```

+ 18 - 17
Docs/neuron_list.md

@@ -2,21 +2,22 @@
 
 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](../neurons/ansible_playbook/)   | Run an ansible playbook                                                                 |
-| [gmail_checker](../neurons/gmail_checker/)         | Get the number of unread email and their subjects from a gmail account                  |
-| [kill_switch](../neurons/kill_switch/)             | Stop Kalliope process                                                                   |
-| [neurotransmitter](../neurons/neurotransmitter/)   | Link synapse together                                                                   |
-| [push_message](../neurons/push_message/)           | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser |
-| [say](../neurons/say/)                             | Make Kalliope talk by using TTS                                                         |
-| [script](../neurons/script/)                       | Run an executable script                                                                |
-| [shell](../neurons/shell/)                         | Run a shell command                                                                     |
-| [sleep](../neurons/sleep/)                         | Make Kalliope sleep for a while before continuing                                       |
-| [systemdate](../neurons/systemdate/)               | Give the local system date and time                                                     |
-| [tasker_autoremote](../neurons/tasker_autoremote/) | Send a message to Android tasker app                                                    |
-| [twitter](../neurons/twitter/)                     | Send a Twit from kalliope                                                               |
-| [uri](../neurons/uri/)                             | Interacts with HTTP and HTTPS web services.                                             |
-| [wake_on_lan](../neurons/wake_on_lan/)             | Wake on lan a computer                                                                  |
-| [wikipedia_searcher](../neurons/wikipedia/)        | Search for a page on Wikipedia                                                          |
+| 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                                                          |
 

+ 4 - 3
Docs/neurons.md

@@ -55,6 +55,7 @@ From the captured order:
 Here, the spoken value captured by the TTS engine will be passed as an argument to the neuron in the variable named `parameter3`.
 
 Example, with the synapse declaration above, if you say "this is an order with the parameter Amy Winehouse". The neuron will receive a parameter named `parameter3` with "Amy Winehouse" as a value of this parameter.
+We recommend the reading of the [signals documentation](signals.md) for a complete understanding of how arguments in a neuron work.
 
 
 ## Output values
@@ -66,7 +67,7 @@ the [template engine](https://en.wikipedia.org/wiki/Jinja_(template_engine)), an
 
 The template engine used in Kalliope is [Jinja2](http://jinja.pocoo.org/docs/dev/).
 
-For example, if we look at the [documentation of the neuron systemedate](../neurons/systemdate), we can see that the neuron will return a dictionary of value like `minute`, `hours` and all other values about the current time on the system where Kalliope is installed.
+For example, if we look at the [documentation of the neuron systemedate](../kalliope/neurons/systemdate/README.md), we can see that the neuron will return a dictionary of value like `minute`, `hours` and all other values about the current time on the system where Kalliope is installed.
 
 A simple, that only use **variables**, template would be
 ```
@@ -110,12 +111,12 @@ As this is multi-lines, we can put the content in a file and use a `file_templat
 ## Overridable parameters
 
 For each neuron, you can override some parameters to use a specific configuration of TTS instead of the default one 
-set in [settings.yml](settings.yml) file.
+set in [settings.yml](settings.md) file.
 
 ### Cache
 
 You can override the default cache configuration. By default Kalliope uses a cache to save a generated audio from a TTS engine.
-This cache is usefull to manage sentences that are not suppose to be changed very often. For exemple, the following sentence will not change in time, so it's more optimized to generate it once and to keep it in cash:
+This cache is useful to manage sentences that are not suppose to be changed very often. For example, the following sentence will not change in time, so it's more optimized to generate it once and to keep it in cash:
 ```
 - say:
     message:

+ 1 - 1
Docs/rest_api.md

@@ -134,7 +134,7 @@ Output example:
 ```
 
 
-Run a synapse from an order
+### Run a synapse from an order
 
 Normal response codes: 201
 Error response codes: unauthorized(401), itemNotFound(404)

+ 33 - 5
Docs/settings.md

@@ -1,13 +1,24 @@
 # Kalliope settings
 
 This part of the documentation explains the main configuration of Kalliope.
-You will have to modify the configuration file called `settings.yml` which is located in the root of the project tree.
 
-The file is written on YAML syntax.
+Kalliope needs two files to works, a `settings.yml` and a `brain.yml`. Files are written on YAML syntax.
 
-## Tunable settings
+When you start kalliope using the CLI, the program will try to load your settings.yml and brain.yml in the following order:
+- From you current folder, E.g `/home/pi/my_kalliope/settings.yml`
+- From `/etc/kalliope/settings.yml`
+- From the default `settings.yml`. You can take a look into the default [`settings.yml`](../kalliope/settings.yml) file which is located in the root of the project tree.
 
-The following settings can be modified:
+This a common tree of a Kalliope configuration folder:
+```
+kalliope_config/
+├── brains
+│   └── included_brain.yml
+├── brain.yml
+├── files
+│   └── kalliope-FR-13samples.pmdl
+└── settings.yml
+```
 
 ## Triggers configuration
 
@@ -111,7 +122,7 @@ E.g
 text_to_speech:
   - pico2wave:
       language: "fr-FR"
-  - voxygen:      
+  - voxygen:
       voice: "michel"
 ```
 
@@ -199,3 +210,20 @@ Login used by the basic HTTP authentication. Must be provided if `password_prote
 
 #### Password
 Password used by the basic HTTP authentication. Must be provided if `password_protected` is `True`
+
+
+## Default synapse
+
+Run a default [synapse](brain.md) when Kalliope can't find the order in any synapse. 
+
+```
+default_synapse: "synapse-name"
+```
+
+E.g
+```
+default_synapse: "Default-response"
+```
+
+## Next: configure the brain of Kalliope
+Now your settings are ok, you can start creating the [brain](brain.md) of your assistant.

+ 100 - 22
Docs/signals.md

@@ -10,6 +10,7 @@ signals:
 
 ## Order
 
+### Simple order
 An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine.
 
 Syntax:
@@ -32,39 +33,97 @@ For example, if you say "Kalliope please do this", the SST engine can return "ca
 > 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
 will be started by Kalliope. So keep in mind that the best practice is to use really different sentences with more than one word for your order.
 
+### Order with arguments
+You can add one or more arguments to an order by adding bracket to the sentence.
+
+Syntax:
+```
+signals:
+    - order: "<sentence> {{ arg_name }}"
+    - order: "<sentence> {{ arg_name }} <sentence>"
+    - order: "<sentence> {{ arg_name }} <sentence> {{ arg_name }}"
+```
+
+Example:
+```
+signals:
+    - order: "I want to listen {{ artist_name }}"
+    - order: "start the {{ episode_number }} episode"
+    - order: "give me the weather at {{ location }} for {{ date }}"
+```
+
+Here, an example order would be speaking out loud the order: "I want to listen Amy Winehouse"
+In this example, both word "Amy" and "Winehouse" will be passed as an unique argument called `artist_name` to the neuron.
+
+If you want to send more than one argument, you must split your argument with a word that Kalliope will use to recognise the start and the end of each arguments.
+For example:  "give me the weather at {{ location }} for {{ date }}"
+And the order would be: "give me the weather at Paris for tomorrow"
+And so, it will work too with: "give me the weather at St-Pierre de Chartreuse for tomorrow"
+
+See the **input values** section of the [neuron documentation](neurons) to know how to send arguments to a neuron.
+
+>**Important note:** The following syntax cannot be used: "<sentence> {{ arg_name }} {{ arg_name2 }}" as Kalliope cannot know when a block starts and when it finishes.
+
 ## Event
 
 An event is a way to schedule the launching of a synapse periodically at fixed times, dates, or intervals.
 
-The event system is based on [Linux crontab](https://en.wikipedia.org/wiki/Cron). A crontab is file that specifies shell commands to run periodically
- on a given schedule.
-When you declare an event in the signal, Kalliope will load the crontab file to schedule the launching of the target synapse.
+The event system is based on [APScheduler](http://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html) which it is itself based on [Linux crontab](https://en.wikipedia.org/wiki/Cron). 
+When you declare an event in the signal, Kalliope will schedule the launching of the target synapse.
 
 The syntax of an event declaration in a synapse is the following
 ```
 signals:
-    - event: "<contab period>"
+  - event:
+      parameter1: "value1"
+      parameter2: "value2"
 ```
 
-Where a crontab period follows the syntax bellow:
+For example, if we want Kalliope to run the synapse every day a 8:30, the event will be declared like this:
 ```
- ┌───────────── min (0 - 59)
- │ ┌────────────── hour (0 - 23)
- │ │ ┌─────────────── day of month (1 - 31)
- │ │ │ ┌──────────────── month (1 - 12)
- │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to
- │ │ │ │ │                  Saturday, or use names; 7 is also Sunday)
- │ │ │ │ │
- │ │ │ │ │
- * * * * *  
+- event:
+    hour: "8"
+    minute: "30"
 ```
 
-For example, if we want Kalliope to run the synapse every day a 8 PM, the event will be declared like this:
-```
-- event: "0 20 * * *"
-```
+### Parameters
+Parameters are keyword you can use to build your event
+
+List of available parameter:
+
+| parameter   | required | default | choices                                                         | comment   |
+|-------------|----------|---------|-----------------------------------------------------------------|-----------|
+| year        | no       | *       | 4 digit                                                         | E.g: 2016 |
+| month       | no       | *       | month (1-12)                                                    |           |
+| day         | no       | *       | day of the (1-31)                                               |           |
+| week        | no       | *       | ISO week (1-53)                                                 |           |
+| day_of_week | no       | *       | number or name of weekday  (0-6 or mon,tue,wed,thu,fri,sat,sun) | 6=Sunday  |
+| hour        | no       | *       | hour (0-23)                                                     |           |
+| minute      | no       | *       | minute (0-59)                                                   |           |
+| second      | no       | *       | second (0-59)                                                   |           |
+
+> **Note:** You must set at least one parameter from the list of parameter
+
+### Expression 
+Expressions can be used in value of each parameter. Multiple expression can be given in a single field, separated by commas.
 
-Let's make a complete example. We want Kalliope to wake us up each morning of working day (Monday to friday) at 7 AM and:
+| Expression | Field | Description                                                                             |
+|------------|-------|-----------------------------------------------------------------------------------------|
+| *          | any   | Fire on every value                                                                     |
+| */a        | any   | Fire every `a` values, starting from the minimum                                        |
+| a-b        | any   | Fire on any value within the `a-b` range (a must be smaller than b)                     |
+| a-b/c      | any   | Fire every c values within the `a-b` range                                              |
+| xrd y      | day   | Fire on the `x` -rd occurrence of weekday `y` within the month                          |
+| last x     | day   | Fire on the last occurrence of weekday `x` within the month                             |
+| last x     | day   | Fire on the last day within the month                                                   |
+| x,y,z      | day   | Fire on any matching expression; can combine any number of any of the above expressions |
+
+
+### Examples
+
+#### Web clock radio
+
+Let's make a complete example. We want Kalliope to wake us up each morning of working day (Monday to friday) at 7:30 AM and:
 - Wish us good morning
 - Give us the time
 - Play our favourite web radio
@@ -73,7 +132,10 @@ The synapse in the brain would be
 ```
   - name: "wake-up"
     signals:
-      - event: "0 7 * * 1,2,3,4,5"
+      - event:
+          hour: "7"
+          minute: "30"
+          day_of_week: "1,2,3,4,5"
     neurons:
       - say:
           message:
@@ -83,6 +145,7 @@ The synapse in the brain would be
             - "It is {{ hours }} hours and {{ minutes }} minutes"
       - shell: 
           cmd: "mplayer http://192.99.17.12:6410/"
+          async: True
 ```
 
 After setting up an event, you must restart Kalliope
@@ -92,8 +155,23 @@ python kalliope.py start
 
 If the syntax is ok, Kalliope will show you each synapse that it has loaded in the crontab
 ```
-Synapse "wake up" added to the crontab
-Event loaded in crontab
+Add synapse name "wake-up" to the scheduler: cron[day_of_week='1,2,3,4,5', hour='7', minute='30']
+Event loaded
 ```
 
 That's it, the synapse is now scheduled and will be started automatically.
+
+
+####  Make Kalliope say something on the third Friday of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00
+```
+- name: "wake-up"
+  signals:
+    - event:
+        day: "3rd fri"        
+        month: "6-8,11-12"
+        hour: "0-3"        
+  neurons:
+    - say:
+        message:
+          - "This is a schedulled sentence"
+```

+ 5 - 5
Docs/stt.md

@@ -28,11 +28,11 @@ Sometime, an API key will be necessary to use an engine. Click on a TTS engine l
 
 ## Current Available STT
 
-- [apiai](../stt/apiai/README.md)
-- [bing](../stt/bing/README.md)
-- [google](../stt/google/README.md)
-- [houndify](../stt/houndify/README.md)
-- [witai](../stt/wit/README.md)
+- [apiai](../kalliope/stt/apiai/README.md)
+- [bing](../kalliope/stt/bing/README.md)
+- [google](../kalliope/stt/google/README.md)
+- [houndify](../kalliope/stt/houndify/README.md)
+- [witai](../kalliope/stt/wit/README.md)
 
 ## Full Example
 

+ 6 - 4
Docs/trigger.md

@@ -8,8 +8,8 @@ With Kalliope project, you can set whatever Hotword you want to wake it up.
 You can create your magic word by connecting to [Snowboy](https://snowboy.kitt.ai/) and then download the trained model file.
 
 Once downloaded:
-- place the file in **trigger/snowboy/resources**.
-- update the path in [your settings](settings.md).
+- place the file in your personal config folder.
+- update the path of **pmdl_file** in [your settings](settings.md).
 
 If you want to keep "Kalliope" as the name of your bot, we recommend you to enhance the existing Snowboy model for your language.
 
@@ -20,7 +20,8 @@ kalliope-<language_code>
 
 E.g
 ```
-kalliope-en
+kalliope-FR
+kalliope-EN
 ```
 Then, open an issue or create a pull request to add the model to the list bellow.
 
@@ -28,4 +29,5 @@ Then, open an issue or create a pull request to add the model to the list bellow
 
 | Name                                                | language |
 |-----------------------------------------------------|----------|
-| [kalliope-fr](https://snowboy.kitt.ai/hotword/1363) | French   |
+| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French   |
+| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English  |

+ 5 - 5
Docs/tts.md

@@ -51,11 +51,11 @@ generated audio files that will not be played more than once.
 
 ## Current Available TTS
 
-- [acapela](../tts/acapela/README.md)
-- [googletts](../tts/googletts/README.md)
-- [pico2wave](../tts/pico2wave/README.md)
-- [voicerss](../tts/voicerss/README.md)
-- [voxygen](../tts/voxygen/README.md)
+- [acapela](../kalliope/tts/acapela/README.md)
+- [googletts](../kalliope/tts/googletts/README.md)
+- [pico2wave](../kalliope/tts/pico2wave/README.md)
+- [voicerss](../kalliope/tts/voicerss/README.md)
+- [voxygen](../kalliope/tts/voxygen/README.md)
 
 ## Full Example
 

+ 1 - 0
MANIFEST.in

@@ -0,0 +1 @@
+include *.md

+ 6 - 3
README.md

@@ -1,5 +1,9 @@
 # Kalliope
 
+[![Build Status](https://travis-ci.org/kalliope-project/kalliope.svg)](https://travis-ci.org/kalliope-project/kalliope)
+[![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/kalliope-project/Lobby)
+
+
 ![logo](images/Kalliope_logo_large.png)
 
 Kalliope is a modular always-on voice controlled personal assistant designed for home automation.
@@ -22,9 +26,8 @@ Kalliope is easy-peasy to use, see the hello world
 
 ## Installation
 
-- [Raspbian Jessie(Raspberry pi)](Docs/installation/raspbian_jessie.md)
-- [Ubuntu 16.04](Docs/installation/ubuntu_16.04.md)
-- [Debian Jessie](Docs/installation/debian_jessie.md)
+- [Kalliope installation documentation](Docs/installation.md)
+
 
 ## Usage
 

+ 11 - 0
Tests/__init__.py

@@ -0,0 +1,11 @@
+from test_brain_loader import TestBrainLoader
+from test_configuration_checker import TestConfigurationChecker
+from test_dynamic_loading import TestDynamicLoading
+from test_file_manager import TestFileManager
+from test_order_analyser import TestOrderAnalyser
+from test_rest_api import TestRestAPI
+from test_settings_loader import TestSettingLoader
+from test_singleton import TestSingleton
+from test_tts_module import TestTTSModule
+from test_yaml_loader import TestYAMLLoader
+from test_neuron_module import TestNeuronModule

+ 0 - 0
core/Tests/brains/brain_test.yml → Tests/brains/brain_test.yml


+ 0 - 0
core/Tests/brains/included_brain_test.yml → Tests/brains/included_brain_test.yml


+ 7 - 0
core/Tests/settings/settings_test.yml → Tests/settings/settings_test.yml

@@ -71,3 +71,10 @@ rest_api:
   password_protected: True
   login: admin
   password: secret
+
+
+# ---------------------------
+# Default Synapse
+# ---------------------------
+# Specify an optional default synapse response in case your order is not found.
+default_synapse: "Default-synapse"

+ 1 - 0
Tests/templates/template_test.j2

@@ -0,0 +1 @@
+hello, this is a {{ test }}

+ 107 - 0
Tests/test_brain_loader.py

@@ -0,0 +1,107 @@
+import os
+import unittest
+
+from kalliope.core.Models import Singleton
+
+from kalliope.core.ConfigurationManager import BrainLoader
+from kalliope.core.Models import Event
+from kalliope.core.Models import Neuron
+from kalliope.core.Models import Synapse
+from kalliope.core.Models import Order
+from kalliope.core.Models.Brain import Brain
+
+
+class TestBrainLoader(unittest.TestCase):
+
+    def setUp(self):
+        self.brain_to_test = os.getcwd() + os.sep + "Tests/brains/brain_test.yml"
+        self.expected_result = [
+            {'signals': [{'order': 'test_order'}],
+             'neurons': [{'say': {'message': ['test message']}}],
+             'name': 'test'},
+            {'signals': [{'order': 'test_order_2'}],
+             'neurons': [{'say': {'message': ['test message']}}],
+             'name': 'test2'},
+            {'includes': ['included_brain_test.yml']},
+            {'signals': [{'order': 'test_order_3'}],
+             'neurons': [{'say': {'message': ['test message']}}],
+             'name': 'test3'}
+        ]
+
+    def tearDown(self):
+        Singleton._instances = {}
+
+    def test_get_yaml_config(self):
+        """
+        Test we can get a yaml config from the path
+        """
+        brain_loader = BrainLoader(file_path=self.brain_to_test)
+        self.assertEqual(brain_loader.yaml_config, self.expected_result)
+
+    def test_get_brain(self):
+        """
+        Test the class return a valid brain object
+        """
+
+        neuron = Neuron(name='say', parameters={'message': ['test message']})
+
+        signal1 = Order(sentence="test_order")
+        signal2 = Order(sentence="test_order_2")
+        signal3 = Order(sentence="test_order_3")
+
+        synapse1 = Synapse(name="test", neurons=[neuron], signals=[signal1])
+        synapse2 = Synapse(name="test2", neurons=[neuron], signals=[signal2])
+        synapse3 = Synapse(name="test3", neurons=[neuron], signals=[signal3])
+        synapses = [synapse1, synapse2, synapse3]
+
+        brain = Brain()
+        brain.synapses = synapses
+        brain.brain_file = self.brain_to_test
+        brain.brain_yaml = self.expected_result
+
+        brain_loader = BrainLoader(file_path=self.brain_to_test)
+        self.assertEqual(brain, brain_loader.brain)
+
+    def test_get_neurons(self):
+        neuron_list = [{'say': {'message': ['test message']}}]
+
+        neuron = Neuron(name='say', parameters={'message': ['test message']})
+
+        bl = BrainLoader(file_path=self.brain_to_test)
+        neurons_from_brain_loader = bl._get_neurons(neuron_list)
+
+        self.assertEqual([neuron], neurons_from_brain_loader)
+
+    def test_get_signals(self):
+        signals = [{'order': 'test_order'}]
+
+        signal = Order(sentence='test_order')
+
+        bl = BrainLoader(file_path=self.brain_to_test)
+        signals_from_brain_loader = bl._get_signals(signals)
+
+        self.assertEqual([signal], signals_from_brain_loader)
+
+    def test_get_event_or_order_from_dict(self):
+
+        order_object = Order(sentence="test_order")
+        event_object = Event(hour="7")
+
+        dict_order = {'order': 'test_order'}
+        dict_event = {'event': {'hour': '7'}}
+
+        bl = BrainLoader(file_path=self.brain_to_test)
+        order_from_bl = bl._get_event_or_order_from_dict(dict_order)
+        event_from_bl = bl._get_event_or_order_from_dict(dict_event)
+
+        self.assertEqual(order_from_bl, order_object)
+        self.assertEqual(event_from_bl, event_object)
+
+    def test_singleton(self):
+        bl1 = BrainLoader(file_path=self.brain_to_test)
+        bl2 = BrainLoader(file_path=self.brain_to_test)
+
+        self.assertTrue(bl1.brain is bl2.brain)
+
+if __name__ == '__main__':
+    unittest.main()

+ 12 - 4
core/Tests/test_configuration_checker.py → Tests/test_configuration_checker.py

@@ -1,9 +1,9 @@
 import unittest
 
-from core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker, NoSynapeName, NoSynapeNeurons, \
+from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker, NoSynapeName, NoSynapeNeurons, \
     NoSynapeSignals, NoValidSignal, NoEventPeriod, NoValidOrder, MultipleSameSynapseName
-from core.Models import Synapse
-from core.Utils import ModuleNotFoundError
+from kalliope.core.Models import Synapse
+from kalliope.core.Utils.Utils import ModuleNotFoundError
 
 
 class TestConfigurationChecker(unittest.TestCase):
@@ -68,9 +68,15 @@ class TestConfigurationChecker(unittest.TestCase):
             ConfigurationChecker.check_signal_dict(invalid_signal)
 
     def test_check_event_dict(self):
-        valid_event = '0 * * * *'
+        valid_event = {
+            "hour": "18",
+            "minute": "16"
+          }
         invalid_event = None
         invalid_event2 = ""
+        invalid_event3 = {
+            "notexisting": "12"
+        }
 
         self.assertTrue(ConfigurationChecker.check_event_dict(valid_event))
 
@@ -78,6 +84,8 @@ class TestConfigurationChecker(unittest.TestCase):
             ConfigurationChecker.check_event_dict(invalid_event)
         with self.assertRaises(NoEventPeriod):
             ConfigurationChecker.check_event_dict(invalid_event2)
+        with self.assertRaises(NoEventPeriod):
+            ConfigurationChecker.check_event_dict(invalid_event3)
 
     def test_check_order_dict(self):
         valid_order = 'test_order'

+ 11 - 11
core/Tests/test_dynamic_loading.py → Tests/test_dynamic_loading.py

@@ -18,21 +18,21 @@ class TestDynamicLoading(unittest.TestCase):
         # 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 + os.sep + os.pardir)
+        root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
 
         # get the neuron dir
-        self.neurons_dir = os.path.normpath(root_dir + os.sep + "neurons")
+        self.neurons_dir = os.path.normpath(root_dir + os.sep + "kalliope/neurons")
 
         # get stt dir
-        self.stt_dir = os.path.normpath(root_dir + os.sep + "stt")
+        self.stt_dir = os.path.normpath(root_dir + os.sep + "kalliope/stt")
 
         # get tts dir
-        self.tts_dir = os.path.normpath(root_dir + os.sep + "tts")
+        self.tts_dir = os.path.normpath(root_dir + os.sep + "kalliope/tts")
 
         # get trigger dir
-        self.trigger_dir = os.path.normpath(root_dir + os.sep + "trigger")
+        self.trigger_dir = os.path.normpath(root_dir + os.sep + "kalliope/trigger")
 
-    def test_01_packages_present(self):
+    def test_packages_present(self):
         """
         Check that the neurons folder exist in the root of the project
         """
@@ -41,7 +41,7 @@ class TestDynamicLoading(unittest.TestCase):
         self.assertTrue(os.path.isdir(self.tts_dir))
         self.assertTrue(os.path.isdir(self.trigger_dir))
 
-    def test_02_can_import_neurons(self):
+    def test_can_import_neurons(self):
         """
         Try to import each neurons that are present in the neurons package
         :return:
@@ -52,7 +52,7 @@ class TestDynamicLoading(unittest.TestCase):
             module_name = neuron_name.capitalize()
             self.dynamic_import(package_name, module_name)
 
-    def test_03_can_import_stt(self):
+    def test_can_import_stt(self):
         """
         Try to import each stt that are present in the stt package
         :return:
@@ -63,7 +63,7 @@ class TestDynamicLoading(unittest.TestCase):
             module_name = stt_name.capitalize()
             self.dynamic_import(package_name, module_name)
 
-    def test_04_can_import_tts(self):
+    def test_can_import_tts(self):
         """
         Try to import each tts that are present in the tts package
         :return:
@@ -74,7 +74,7 @@ class TestDynamicLoading(unittest.TestCase):
             module_name = tts_name.capitalize()
             self.dynamic_import(package_name, module_name)
 
-    def test_05_can_import_trigger(self):
+    def test_can_import_trigger(self):
         """
         Try to import each trigger that are present in the trigger package
         :return:
@@ -114,7 +114,7 @@ class TestDynamicLoading(unittest.TestCase):
         :param module_name: module name to load
         :return:
         """
-        module_name_with_path = package_name + "." + module_name.lower() + "." + module_name.lower()
+        module_name_with_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower()
         mod = __import__(module_name_with_path, fromlist=[module_name])
         try:
             getattr(mod, module_name)

+ 171 - 0
Tests/test_file_manager.py

@@ -0,0 +1,171 @@
+import unittest
+import os
+
+from kalliope.core.Utils.FileManager import FileManager
+
+
+class TestFileManager(unittest.TestCase):
+    """
+    Class to test FileManager
+    """
+
+    def setUp(self):
+        pass
+
+    def test_create_directory(self):
+        """
+        Test to create a new directory.
+        """
+        # set up
+        cache_path = "/tmp/kalliope/tests/testDirectory"
+        if os.path.exists(cache_path):
+            os.removedirs(cache_path)
+
+        # Test FileManager.create_directory
+        FileManager.create_directory(cache_path)
+        self.assertTrue(os.path.exists(cache_path),
+                        "Fail creating a directory to the path ")
+
+        # Remove the directory
+        os.removedirs(cache_path)
+
+    def test_write_in_file(self):
+        """
+        Test to write in file.
+        """
+
+        # set up the context
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_FileManager_writeInFile"
+        file_path = os.path.join(dir_path,file_name)
+        in_file_text = "[Kalliope] Testing the write_in_file method from Utils.FileManager"
+        if os.path.exists(file_path):
+            os.remove(file_path)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # Test FileManager.write_in_file
+        FileManager.write_in_file(file_path=file_path, content=in_file_text)
+        with open(file_path, 'r') as content_file:
+            content = content_file.read()
+            self.assertEqual(content, in_file_text,
+                             "Fail writing in the file ")
+
+        # Clean up
+        if os.path.exists(file_path):
+            os.remove(file_path)
+
+    def test_file_is_empty(self):
+        """
+        Test that the file is empty
+        """
+
+        # set up the context
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_FileManager_fileIsEmpty"
+        file_path = os.path.join(dir_path, file_name)
+        if os.path.exists(file_path):
+            os.remove(file_path)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # Test FileManager.file_is_empty
+        with open(file_path, "wb") as file_open:
+            file_open.write("")
+            file_open.close()
+        self.assertTrue(FileManager.file_is_empty(file_path=file_path),
+                        "Fail matching to verify that file is empty ")
+
+        # Clean up
+        if os.path.exists(file_path):
+            os.remove(file_path)
+
+    def test_remove_file(self):
+        """
+        Test to remove a file
+        """
+
+        # set up the context
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_FileManager_fileRemove"
+        file_path = os.path.join(dir_path, file_name)
+        if os.path.exists(file_path):
+            os.remove(file_path)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # Test to remove the file
+        # FileManager.remove_file
+        with open(file_path, "wb") as file_open:
+            file_open.write("")
+            file_open.close()
+        FileManager.remove_file(file_path=file_path)
+        self.assertFalse(os.path.exists(file_path),
+                         "Fail removing the file")
+
+    def test_is_path_creatable(self):
+        """
+        Test if the path is creatable for the user
+        Does the user has the permission to use this path ?
+        """
+
+        # set up the context
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_FileManager_filePathCreatable"
+        file_path = os.path.join(dir_path, file_name)
+        if os.path.exists(file_path):
+            os.remove(file_path)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # test not allowed : return False
+        not_allowed_root_path = "/root/"
+        not_allowed_path = os.path.join(not_allowed_root_path, file_name)
+        self.assertFalse(FileManager.is_path_creatable(not_allowed_path),
+                         "Fail to assert not accessing this path ")
+        # test allowed : return True
+        self.assertTrue(FileManager.is_path_creatable(file_path))
+
+    def test_is_path_exists_or_creatable(self):
+        """
+        Test the _is_path_exists_or_creatable
+        4 scenarii :
+            - the file exists and is creatable : return True
+            - the file does not exist but is creatable : return True
+            - the file exists but is not allowed : return True --> need a review !
+            - the file does not exist and is not allowed : return False
+        """
+
+        # set up the context
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_FileManager_fileIsPathExistsOrCreatable"
+        file_path = os.path.join(dir_path, file_name)
+        if os.path.exists(file_path):
+            os.remove(file_path)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # Test the file exist and creatable : return True
+        with open(file_path, "wb") as file_open:
+            file_open.write("[Kalliope] Test Running the test_is_path_exists_or_creatable method")
+            file_open.close()
+        self.assertTrue(FileManager.is_path_exists_or_creatable(file_path),
+                        "Fail to assert the file exist ")
+
+        # test the file not exist but creatable : return True
+        os.remove(file_path)
+        self.assertTrue(FileManager.is_path_exists_or_creatable(file_path),
+                         "Fail asserting the file does not exist ")
+
+        # test the file exist but not creatable : return True
+        # file_exist_not_allowed = "/root/.ssh/known_hosts"
+        # self.assertTrue(FileManager.is_path_creatable(file_exist_not_allowed))
+
+        # test the file not exist and not allowed : return False
+        not_allowed_root_path = "/root/"
+        not_allowed_path = os.path.join(not_allowed_root_path, file_name)
+        self.assertFalse(FileManager.is_path_creatable(not_allowed_path),
+                         "Fail to assert not accessing this path ")
+
+if __name__ == '__main__':
+    unittest.main()

+ 115 - 0
Tests/test_launchers.py

@@ -0,0 +1,115 @@
+import unittest
+import mock
+import os
+
+from kalliope.core.NeuronLauncher import NeuronLauncher
+from kalliope.core.SynapseLauncher import SynapseLauncher, SynapseNameNotFound
+from kalliope.core.TriggerLauncher import TriggerLauncher
+
+from kalliope.core.Models.Trigger import Trigger
+from kalliope.core.Models.Neuron import Neuron
+from kalliope.core.Models.Order import Order
+from kalliope.core.Models.Brain import Brain
+from kalliope.core.Models.Synapse import Synapse
+
+
+class TestLaunchers(unittest.TestCase):
+    """
+    Class to test Launchers Classes (TriggerLauncher, SynapseLauncher, NeuronLauncher) and methods
+    """
+
+    def setUp(self):
+        pass
+
+    ####
+    # Trigger Launcher
+    def test_get_trigger(self):
+        """
+        Test the Trigger Launcher trying to run the trigger
+        """
+        trigger = Trigger("Trigger", {})
+        with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation:
+            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.reset_mock()
+
+    ####
+    # Synapse Launcher
+    def test_start_synapse(self):
+        """
+        Test the Synapse launcher trying to start 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)
+
+        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)]
+            mock_get_class_instantiation.assert_has_calls(calls=calls)
+            mock_get_class_instantiation.reset_mock()
+
+            # Fail
+            with self.assertRaises(SynapseNameNotFound):
+                SynapseLauncher.start_synapse("Synapse4", brain=br)
+
+    def test_run_synapse(self):
+        """
+        Test to run a Synapse
+        """
+        neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
+        neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'})
+        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])
+        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)]
+            mock_get_class_instantiation.assert_has_calls(calls=calls)
+            mock_get_class_instantiation.reset_mock()
+
+            # Do not any Neurons
+            SynapseLauncher._run_synapse(synapse=synapse_empty)
+            mock_get_class_instantiation.assert_not_called()
+            mock_get_class_instantiation.reset_mock()
+
+    ####
+    # Neurons Launcher
+    def test_start_neuron(self):
+        """
+        Test the Neuron Launcher trying to start a Neuron
+        """
+        neuron = Neuron(name='neurone1', parameters={'var1': 'val1'})
+        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.reset_mock()
+

+ 109 - 0
Tests/test_neuron_module.py

@@ -0,0 +1,109 @@
+import os
+import unittest
+import mock
+
+from kalliope.core.NeuronModule import NeuronModule, TemplateFileNotFoundException
+
+
+class TestNeuronModule(unittest.TestCase):
+
+    def setUp(self):
+        self.expected_result = "hello, this is a replaced word"
+        # this allow us to run the test from an IDE and from the root with python -m unittest Tests.TestNeuronModule
+        if "/Tests" in os.getcwd():
+            self.file_template = "templates/template_test.j2"
+        else:
+            self.file_template = "Tests/templates/template_test.j2"
+        self.say_template = "hello, this is a {{ test }}"
+        self.message = {
+            "test": "replaced word"
+        }
+        self.neuron_module_test = NeuronModule()
+
+    def tearDown(self):
+        del self.neuron_module_test
+
+    def test_get_audio_from_stt(self):
+        """
+        Test the OrderListener thread is started
+        """
+
+        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()
+
+    def test_update_cache_var(self):
+        """
+        Test Update the value of the cache in the provided arg list
+        """
+
+        # True -> False
+        args_dict = {
+            "cache": True
+        }
+        expected_dict = {
+            "cache": False
+        }
+        self.assertEquals(NeuronModule._update_cache_var(False, args_dict=args_dict),
+                          expected_dict,
+                          "Fail to update the cache value from True to False")
+        self.assertFalse(args_dict["cache"])
+
+        # False -> True
+        args_dict = {
+            "cache": False
+        }
+        expected_dict = {
+            "cache": True
+        }
+        self.assertEquals(NeuronModule._update_cache_var(True, args_dict=args_dict),
+                          expected_dict,
+                          "Fail to update the cache value from False to True")
+
+        self.assertTrue(args_dict["cache"])
+
+    def test_get_message_from_dict(self):
+
+        self.neuron_module_test.say_template = self.say_template
+
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), self.expected_result)
+        del self.neuron_module_test
+        self.neuron_module_test = NeuronModule()
+
+        # test with file_template
+        self.neuron_module_test.file_template = self.file_template
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), self.expected_result)
+        del self.neuron_module_test
+
+        # test with no say_template and no file_template
+        self.neuron_module_test = NeuronModule()
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), None)
+
+    def test_get_say_template(self):
+        # test with a string
+        self.assertEqual(NeuronModule._get_say_template(self.say_template, self.message), self.expected_result)
+
+        # test with a list
+        say_template = list()
+        say_template.append("hello, this is a {{ test }} one")
+        say_template.append("hello, this is a {{ test }} two")
+        expected_result = list()
+        expected_result.append("hello, this is a replaced word one")
+        expected_result.append("hello, this is a replaced word two")
+        self.assertTrue(NeuronModule._get_say_template(say_template, self.message) in expected_result)
+
+    def test_get_file_template(self):
+        # test with a valid template
+        self.assertEqual(NeuronModule._get_file_template(self.file_template, self.message), self.expected_result)
+
+        # test raise with a non existing template
+        file_template = "does_not_exist.j2"
+        with self.assertRaises(TemplateFileNotFoundException):
+            NeuronModule._get_file_template(file_template, self.message)
+
+    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)

+ 499 - 0
Tests/test_order_analyser.py

@@ -0,0 +1,499 @@
+import unittest
+import mock
+
+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.Order import Order
+
+
+class TestOrderAnalyser(unittest.TestCase):
+
+    """Test case for the OrderAnalyser Class"""
+
+    def setUp(self):
+        pass
+
+    def test_start(self):
+        """
+        Testing if the matches from the incoming messages and the signals/order sentences.
+        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.
+        """
+        # 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)
+
+        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"
+            oa = OrderAnalyser(order=order_to_match,
+                               brain=br)
+            expected_result = [synapse1]
+
+            self.assertEquals(oa.start(),
+                              expected_result,
+                              "Fail to run the expected Synapse matching the order")
+
+            calls = [mock.call(neuron1, {}), mock.call(neuron2, {})]
+            mock_start_neuron_method.assert_has_calls(calls=calls)
+            mock_start_neuron_method.reset_mock()
+
+            # No order matching Default Synapse to run
+            order_to_match = "random sentence"
+            oa = OrderAnalyser(order=order_to_match,
+                               brain=br)
+            oa.settings = mock.MagicMock(default_synapse="Synapse3")
+            expected_result = [synapse3]
+            self.assertEquals(oa.start(),
+                              expected_result,
+                              "Fail to run the default Synapse because no other synapses match the order")
+
+            # No order matching no Default Synapse
+            order_to_match = "random sentence"
+            oa = OrderAnalyser(order=order_to_match,
+                               brain=br)
+            oa.settings = mock.MagicMock()
+            expected_result = []
+            self.assertEquals(oa.start(),
+                              expected_result,
+                              "Fail to no synapse because no synapse matchs and no default defined")
+
+    def test_start_neuron(self):
+        """
+        Testing params association and starting a Neuron
+        """
+
+        neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'})
+
+        with mock.patch("kalliope.core.NeuronLauncher.NeuronLauncher.start_neuron") as mock_start_neuron_method:
+            # Assert to the neuron is launched
+            neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
+            params = {
+                'param1':'parval1'
+            }
+            OrderAnalyser._start_neuron(neuron=neuron1,params=params)
+            mock_start_neuron_method.assert_called_with(neuron1)
+            mock_start_neuron_method.reset_mock()
+
+            # Assert the params are well passed to the neuron
+            neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2', 'args': ['arg1', 'arg2']})
+            params = {
+                'arg1':'argval1',
+                'arg2':'argval2'
+            }
+            OrderAnalyser._start_neuron(neuron=neuron2, params=params)
+            neuron2_params = Neuron(name='neurone2',
+                                    parameters={'var2': 'val2',
+                                                'args': ['arg1', 'arg2'],
+                                                'arg1':'argval1',
+                                                'arg2':'argval2'}
+                                    )
+            mock_start_neuron_method.assert_called_with(neuron2_params)
+            mock_start_neuron_method.reset_mock()
+
+            # Assert the Neuron is not started when missing args
+            neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3', 'args': ['arg3', 'arg4']})
+            params = {
+                'arg1': 'argval1',
+                'arg2': 'argval2'
+            }
+            OrderAnalyser._start_neuron(neuron=neuron3, params=params)
+            mock_start_neuron_method.assert_not_called()
+            mock_start_neuron_method.reset_mock()
+
+            # Assert no neuron is launched when waiting for args and none are given
+            neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4', 'args': ['arg5', 'arg6']})
+            params = {}
+            OrderAnalyser._start_neuron(neuron=neuron4, params=params)
+            mock_start_neuron_method.assert_not_called()
+            mock_start_neuron_method.reset_mock()
+
+
+    def test_is_containing_bracket(self):
+        #  Success
+        order_to_test = "This test contains {{ bracket }}"
+        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains spaced brackets")
+
+        order_to_test = "This test contains {{bracket }}"
+        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains right spaced bracket")
+
+        order_to_test = "This test contains {{ bracket}}"
+        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains left spaced bracket")
+
+        order_to_test = "This test contains {{bracket}}"
+        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains no spaced bracket")
+
+        #  Failure
+        order_to_test = "This test does not contain bracket"
+        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
+                         "Fail returning False when order has no brackets")
+
+        #  Behaviour
+        order_to_test = ""
+        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
+                         "Fail returning False when no order")
+
+    def test_get_next_value_list(self):
+        # Success
+        list_to_test = {1, 2, 3}
+        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), 2,
+                         "Fail to match the expected next value from the list")
+
+        # Failure
+        list_to_test = {1}
+        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
+                         "Fail to ensure there is no next value from the list")
+
+        # Behaviour
+        list_to_test = {}
+        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
+                         "Fail to ensure the empty list return None value")
+
+    def test_spelt_order_match_brain_order_via_table(self):
+        order_to_test = "this is the order"
+        sentence_to_test = "this is the order"
+
+        # Success
+        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),
+                         "Fail to ensure the expected sentence is not matching the order")
+
+    def test_get_split_order_without_bracket(self):
+
+        # Success
+        order_to_test = "this is the order"
+        expected_result = ["this", "is", "the", "order"]
+        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
+                         "No brackets Fails to return the expected list")
+
+        order_to_test = "this is the {{ order }}"
+        expected_result = ["this", "is", "the"]
+        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
+                         "With spaced brackets Fails to return the expected list")
+
+        order_to_test = "this is the {{order }}"    # left bracket without space
+        expected_result = ["this", "is", "the"]
+        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
+                         "Left brackets Fails to return the expected list")
+
+        order_to_test = "this is the {{ order}}"    # right bracket without space
+        expected_result = ["this", "is", "the"]
+        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
+                         "Right brackets Fails to return the expected list")
+
+        order_to_test = "this is the {{order}}"  # bracket without space
+        expected_result = ["this", "is", "the"]
+        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
+                         "No space brackets Fails to return the expected list")
+
+    def test_associate_order_params_to_values(self):
+        ##
+        # Testing the brackets position behaviour
+        ##
+
+        # Success
+        order_brain = "This is the {{ variable }}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{ variable }} to the 'value'")
+
+        # Success
+        order_brain = "This is the {{variable }}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{variable }} to the 'value'")
+
+        # Success
+        order_brain = "This is the {{ variable}}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{ variable}} to the 'value'")
+
+        # Success
+        order_brain = "This is the {{variable}}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{variable}} to the 'value'")
+
+        # Fail
+        order_brain = "This is the {variable}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                             "Should not match the order_brain {variable} to the 'value'")
+
+        # Fail
+        order_brain = "This is the { variable}}"
+        order_user = "This is the value"
+        expected_result = {'variable': 'value'}
+        self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                             "Should not match the order_brain { variable}} to the 'value'")
+
+        ##
+        # Testing the brackets position in the sentence
+        ##
+
+        # Success
+        order_brain = "{{ variable }} This is the"
+        order_user = "value This is the"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{ variable }} in first position "
+                         "ins the sentence to the 'value'")
+
+        # Success
+        order_brain = "This is {{ variable }} the"
+        order_user = " This is value the"
+        expected_result = {'variable': 'value'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{ variable }} in middle position ins "
+                         "the sentence to the 'value'")
+
+        ##
+        # Testing multi variables
+        ##
+
+        # Success
+        order_brain = "This is {{ variable }} the {{ variable2 }}"
+        order_user = "This is value the value2"
+        expected_result = {'variable': 'value',
+                           'variable2': 'value2'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain multi variable to the multi values")
+
+        ##
+        # Testing multi words in variable
+        ##
+
+        # Success
+        order_brain = "This is the {{ variable }}"
+        order_user = "This is the value with multiple words"
+        expected_result = {'variable': 'value with multiple words'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain {{ variable }} to the 'value with multiple words'")
+
+        # Success
+        order_brain = "This is the {{ variable }} and  {{ variable2 }}"
+        order_user = "This is the value with multiple words and second value multiple"
+        expected_result = {'variable': 'value with multiple words',
+                           'variable2': 'second value multiple'}
+        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
+                         "Fail to match the order_brain multiple variables with multiple words as values'")
+
+    def test_get_matching_synapse_list(self):
+        # 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])
+
+        order_to_match = "this is the sentence"
+        all_synapse_list = [synapse1,
+                            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,
+                          "Fail matching 'the expected synapse' from the complete synapse list and the order")
+
+        # Multiple Matching synapses
+        signal2 = Order(sentence="this is the sentence")
+
+        synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2])
+        order_to_match = "this is the sentence"
+
+        all_synapse_list = [synapse1,
+                            synapse2,
+                            synapse3]
+
+        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")
+
+        # matching no synapses
+        order_to_match = "this is not the correct word"
+
+        all_synapse_list = [synapse1,
+                            synapse2,
+                            synapse3]
+
+        expected_result = []
+
+        self.assertEquals(OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list,
+                                                                   order_to_match=order_to_match),
+                          expected_result,
+                          "Fail matching 'no synapses' from the complete synapse list and the order")
+
+        # matching synapse with all key worlds
+        # /!\ Some words in the order are matching all words in synapses signals !
+        order_to_match = "this is not the correct sentence"
+        all_synapse_list = [synapse1,
+                            synapse2,
+                            synapse3]
+
+        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")
+
+    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 }}")
+
+        synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
+
+        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),
+                          expected_result,
+                          "Fail to retrieve 'the params' of the synapse from the order")
+
+        # Multiple match
+        signal1 = Order(sentence="this is the {{ sentence }}")
+
+        synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
+
+        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),
+                         expected_result,
+                         "Fail to retrieve the 'multiple words params' of the synapse from the order")
+
+        # Multiple params
+        signal1 = Order(sentence="this is the {{ sentence }} with multiple {{ params }}")
+
+        synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
+
+        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),
+                         expected_result,
+                         "Fail to retrieve the 'multiple params' of the synapse 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])
+
+        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),
+                         expected_result,
+                         "Fail to retrieve the 'multiple params with multiple words' of the synapse 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])
+
+        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),
+                         expected_result,
+                         "Fail to retrieve the 'params at the begining of the sentence' of the synapse from the order")
+
+        # all of the sentence is a variable
+        signal1 = Order(sentence="{{ sentence }}")
+
+        synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
+
+        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),
+                         expected_result,
+                         "Fail to retrieve the 'all of the sentence is a variable' of the synapse from the order")
+
+    def test_get_default_synapse_from_sysnapses_list(self):
+        # 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])
+
+        default_synapse_name = "Synapse2"
+        all_synapse_list = [synapse1,
+                            synapse2,
+                            synapse3]
+        expected_result = synapse2
+
+        # Assert equals
+        self.assertEquals(OrderAnalyser._get_default_synapse_from_sysnapses_list(all_synapses_list=all_synapse_list,
+                                                                                 default_synapse_name=default_synapse_name),
+                          expected_result,
+                          "Fail to match the expected default Synapse")
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 230 - 0
Tests/test_rest_api.py

@@ -0,0 +1,230 @@
+import os
+import unittest
+
+from flask import Flask
+from flask_testing import LiveServerTestCase
+
+from kalliope.core.Models import Singleton
+
+from kalliope.core.ConfigurationManager import BrainLoader
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
+
+
+class TestRestAPI(LiveServerTestCase):
+
+    def create_app(self):
+        """
+        executed once at the beginning of the test
+        """
+        # be sure that the singleton haven't been loaded before
+        Singleton._instances = {}
+        current_path = os.getcwd()
+        full_path_brain_to_test = current_path + os.sep + "Tests/brains/brain_test.yml"
+        print full_path_brain_to_test
+
+        # rest api config
+        sl = SettingLoader()
+        sl.settings.rest_api.password_protected = False
+        sl.settings.active = True
+        sl.settings.port = 5000
+
+        # prepare a test brain
+        brain_to_test = full_path_brain_to_test
+        brain_loader = BrainLoader(file_path=brain_to_test)
+        brain = brain_loader.brain
+
+        self.app = Flask(__name__)
+        self.flask_api = FlaskAPI(self.app, port=5000, brain=brain)
+        self.flask_api.app.config['TESTING'] = True
+        return self.flask_api.app
+
+    # TODO all following test passes with 'python -m unittest Tests.TestRestAPI' but not with discover
+    # def test_get_all_synapses(self):
+    #     url = "http://127.0.0.1:5000/synapses"
+    #
+    #     result = requests.get(url=url)
+    #     expected_content = {
+    #         "synapses": [
+    #             {
+    #                 "name": "test",
+    #                 "neurons": [
+    #                     {
+    #                         "say": {
+    #                             "message": [
+    #                                 "test message"
+    #                             ]
+    #                         }
+    #                     }
+    #                 ],
+    #                 "signals": [
+    #                     {
+    #                         "order": "test_order"
+    #                     }
+    #                 ]
+    #             },
+    #             {
+    #                 "name": "test2",
+    #                 "neurons": [
+    #                     {
+    #                         "say": {
+    #                             "message": [
+    #                                 "test message"
+    #                             ]
+    #                         }
+    #                     }
+    #                 ],
+    #                 "signals": [
+    #                     {
+    #                         "order": "test_order_2"
+    #                     }
+    #                 ]
+    #             },
+    #             {
+    #                 "includes": [
+    #                     "included_brain_test.yml"
+    #                 ]
+    #             },
+    #             {
+    #                 "name": "test3",
+    #                 "neurons": [
+    #                     {
+    #                         "say": {
+    #                             "message": [
+    #                                 "test message"
+    #                             ]
+    #                         }
+    #                     }
+    #                 ],
+    #                 "signals": [
+    #                     {
+    #                         "order": "test_order_3"
+    #                     }
+    #                 ]
+    #             }
+    #         ]
+    #     }
+    #
+    #     self.assertEqual(result.status_code, 200)
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #
+    # def test_get_one_synapse(self):
+    #     url = "http://127.0.0.1:5000/synapses/test"
+    #     result = requests.get(url=url)
+    #
+    #     expected_content = {
+    #         "synapses": {
+    #             "name": "test",
+    #             "neurons": [
+    #                 {
+    #                     "say": {
+    #                         "message": [
+    #                             "test message"
+    #                         ]
+    #                     }
+    #                 }
+    #             ],
+    #             "signals": [
+    #                 {
+    #                     "order": "test_order"
+    #                 }
+    #             ]
+    #         }
+    #     }
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #
+    # def test_get_synapse_not_found(self):
+    #     url = "http://127.0.0.1:5000/synapses/test-none"
+    #     result = requests.get(url=url)
+    #
+    #     expected_content = {
+    #         "error": {
+    #             "synapse name not found": "test-none"
+    #         }
+    #     }
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #     self.assertEqual(result.status_code, 404)
+    #
+    # def test_run_synapse_by_name(self):
+    #     url = "http://127.0.0.1:5000/synapses/test"
+    #     result = requests.post(url=url)
+    #
+    #     expected_content = {
+    #         "synapses": {
+    #             "name": "test",
+    #             "neurons": [
+    #                 {
+    #                     "say": {
+    #                         "message": [
+    #                             "test message"
+    #                         ]
+    #                     }
+    #                 }
+    #             ],
+    #             "signals": [
+    #                 {
+    #                     "order": "test_order"
+    #                 }
+    #             ]
+    #         }
+    #     }
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #     self.assertEqual(result.status_code, 201)
+    #
+    # def test_post_synapse_not_found(self):
+    #     url = "http://127.0.0.1:5000/synapses/test-none"
+    #     result = requests.post(url=url)
+    #
+    #     expected_content = {
+    #         "error": {
+    #             "synapse name not found": "test-none"
+    #         }
+    #     }
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #     self.assertEqual(result.status_code, 404)
+    #
+    # def test_run_synapse_with_order(self):
+    #     url = "http://127.0.0.1:5000/order/"
+    #     headers = {"Content-Type": "application/json"}
+    #     data = {"order": "test_order"}
+    #     result = requests.post(url=url, headers=headers, json=data)
+    #
+    #     expected_content = {
+    #         "synapses": [
+    #             {
+    #                 "name": "test",
+    #                 "neurons": [
+    #                     {
+    #                         "name": "say",
+    #                         "parameters": "{'message': ['test message']}"
+    #                     }
+    #                 ],
+    #                 "signals": [
+    #                     {
+    #                         "order": "test_order"
+    #                     }
+    #                 ]
+    #             }
+    #         ]
+    #     }
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #     self.assertEqual(result.status_code, 201)
+    #
+    # def test_post_synapse_by_order_not_found(self):
+    #     url = "http://127.0.0.1:5000/order/"
+    #     data = {"order": "non existing order"}
+    #     headers = {"Content-Type": "application/json"}
+    #     result = requests.post(url=url, headers=headers, json=data)
+    #
+    #     expected_content = {'error': {'error': "The given order doesn't match any synapses"}}
+    #
+    #     self.assertEqual(expected_content, json.loads(result.content))
+    #     self.assertEqual(result.status_code, 400)
+
+if __name__ == '__main__':
+    unittest.main()

+ 59 - 61
core/Tests/test_settings_loader.py → Tests/test_settings_loader.py

@@ -1,19 +1,21 @@
+import os
 import platform
 import unittest
 
-from core.ConfigurationManager import SettingLoader
-from core.Models.RestAPI import RestAPI
-from core.Models.Settings import Settings
-from core.Models.Stt import Stt
-from core.Models.Trigger import Trigger
-from core.Models.Tts import Tts
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.Models import Singleton
+from kalliope.core.Models.RestAPI import RestAPI
+from kalliope.core.Models.Settings import Settings
+from kalliope.core.Models.Stt import Stt
+from kalliope.core.Models.Trigger import Trigger
+from kalliope.core.Models.Tts import Tts
 
 
 class TestSettingLoader(unittest.TestCase):
 
     def setUp(self):
 
-        self.settings_file_to_test = "core/Tests/settings/settings_test.yml"
+        self.settings_file_to_test = os.getcwd() + os.sep + "/Tests/settings/settings_test.yml"
 
         self.settings_dict = {
             'rest_api':
@@ -33,107 +35,103 @@ class TestSettingLoader(unittest.TestCase):
             'text_to_speech': [
                 {'pico2wave': {'cache': True, 'language': 'fr-FR'}},
                 {'voxygen': {'voice': 'Agnes', 'cache': True}}
-            ]
+            ],
+            'default_synapse': 'Default-synapse'
         }
 
+    def tearDown(self):
+        Singleton._instances = {}
+
     def test_singleton(self):
-        s1 = SettingLoader.Instance(file_path=self.settings_file_to_test)
-        s2 = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        s1 = SettingLoader(file_path=self.settings_file_to_test)
+        s2 = SettingLoader(file_path=self.settings_file_to_test)
 
         self.assertTrue(s1.settings is s2.settings)
 
-        del s1
-        del s2
-
-    # def test_get_yaml_config(self):
-    #
-    #     sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
-    #     self.assertEqual(sl.yaml_config, self.settings_dict)
-    #     del sl
-    #
-    # def test_get_settings(self):
-    #     settings_object = Settings()
-    #     settings_object.default_tts_name = "pico2wave"
-    #     settings_object.default_stt_name = "google"
-    #     settings_object.default_trigger_name = "snowboy"
-    #     tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
-    #     tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
-    #     settings_object.ttss = [tts1, tts2]
-    #     stt = Stt(name="google", parameters={'language': 'fr-FR'})
-    #     settings_object.stts = [stt]
-    #     settings_object.random_wake_up_answers = ['Oui monsieur?']
-    #     settings_object.random_wake_up_sounds = ['ding.wav', 'dong.wav']
-    #     trigger1 = Trigger(name="snowboy",
-    #                        parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
-    #     settings_object.triggers = [trigger1]
-    #     settings_object.rest_api = RestAPI(password_protected=True, active=True,
-    #                                        login="admin", password="secret", port=5000)
-    #     settings_object.cache_path = '/tmp/kalliope_tts_cache'
-    #     settings_object.machine = platform.machine()
-    #
-    #     sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
-    #
-    #     self.assertEqual(settings_object, sl.settings)
-    #     del sl
+    def test_get_yaml_config(self):
+
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+        self.assertEqual(sl.yaml_config, self.settings_dict)
+
+    def test_get_settings(self):
+        settings_object = Settings()
+        settings_object.default_tts_name = "pico2wave"
+        settings_object.default_stt_name = "google"
+        settings_object.default_trigger_name = "snowboy"
+        tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
+        tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
+        settings_object.ttss = [tts1, tts2]
+        stt = Stt(name="google", parameters={'language': 'fr-FR'})
+        settings_object.stts = [stt]
+        settings_object.random_wake_up_answers = ['Oui monsieur?']
+        settings_object.random_wake_up_sounds = ['ding.wav', 'dong.wav']
+        trigger1 = Trigger(name="snowboy",
+                           parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
+        settings_object.triggers = [trigger1]
+        settings_object.rest_api = RestAPI(password_protected=True, active=True,
+                                           login="admin", password="secret", port=5000)
+        settings_object.cache_path = '/tmp/kalliope_tts_cache'
+        settings_object.default_synapse = 'Default-synapse'
+        settings_object.machine = platform.machine()
+
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+
+        self.assertEqual(settings_object, sl.settings)
 
     def test_get_default_speech_to_text(self):
         expected_default_speech_to_text = "google"
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
 
         self.assertEqual(expected_default_speech_to_text, sl._get_default_speech_to_text(self.settings_dict))
-        del sl
 
     def test_get_default_text_to_speech(self):
         expected_default_text_to_speech = "pico2wave"
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_default_text_to_speech, sl._get_default_text_to_speech(self.settings_dict))
-        del sl
 
     def test_get_default_trigger(self):
         expected_default_trigger = "snowboy"
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_default_trigger, sl._get_default_trigger(self.settings_dict))
-        del sl
 
     def test_get_stts(self):
         stt = Stt(name="google", parameters={'language': 'fr-FR'})
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual([stt], sl._get_stts(self.settings_dict))
-        del sl
 
     def test_get_ttss(self):
         tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
         tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual([tts1, tts2], sl._get_ttss(self.settings_dict))
-        del sl
 
     def test_get_triggers(self):
         trigger1 = Trigger(name="snowboy",
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual([trigger1], sl._get_triggers(self.settings_dict))
-        del sl
 
     def test_get_random_wake_up_answers(self):
         expected_random_wake_up_answers = ['Oui monsieur?']
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_random_wake_up_answers, sl._get_random_wake_up_answers(self.settings_dict))
-        del sl
 
     def test_get_rest_api(self):
         expected_rest_api = RestAPI(password_protected=True, active=True,
                                     login="admin", password="secret", port=5000)
 
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_rest_api, sl._get_rest_api(self.settings_dict))
-        del sl
 
     def test_get_cache_path(self):
         expected_cache_path = '/tmp/kalliope_tts_cache'
-        sl = SettingLoader.Instance(file_path=self.settings_file_to_test)
+        sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_cache_path, sl._get_cache_path(self.settings_dict))
-        del sl
+
+    def test_get_default_synapse(self):
+        expected_default_synapse = 'Default-synapse'
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+        self.assertEqual(expected_default_synapse, sl._get_default_synapse(self.settings_dict))
 
 if __name__ == '__main__':
     unittest.main()

+ 34 - 0
Tests/test_singleton.py

@@ -0,0 +1,34 @@
+import unittest
+
+from kalliope.core.Models import Singleton
+
+
+class MyClass(object):
+    __metaclass__ = Singleton
+
+    def __init__(self):
+        self.value = "test"
+
+
+class TestSingleton(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def test_singleton(self):
+
+        obj1 = MyClass()
+        obj2 = MyClass()
+
+        self.assertEqual(id(obj1), id(obj2))
+
+    def test_drop_singleton(self):
+
+        obj1 = MyClass()
+        obj2 = MyClass()
+        # drop the singleton instance
+        Singleton._instances = {}
+        obj3 = MyClass()
+
+        self.assertEqual(id(obj1), id(obj2))
+        self.assertNotEqual(id(obj1), id(obj3))

+ 127 - 0
Tests/test_tts_module.py

@@ -0,0 +1,127 @@
+import os
+import unittest
+
+import mock
+
+from kalliope.core.Models.Settings import Settings
+from kalliope.core.TTS.TTSModule import TTSModule, TtsGenerateAudioFunctionNotFound
+from kalliope.core.Utils.FileManager import FileManager
+
+
+class TestTTSModule(unittest.TestCase):
+    """
+    Class to test TTSModule
+    """
+
+    def setUp(self):
+        self.TTSMod = TTSModule(language='tests')
+        pass
+
+    def test_generate_md5_from_words(self):
+        """
+        Test generate md5 method
+        """
+
+        word = "kalliope"
+        expected_result = "5c186d1e123be2667fb5fd54640e4fd0"
+
+        self.assertEquals(TTSModule.generate_md5_from_words(words=word),
+                          expected_result,
+                          "Fail md5")
+
+    def test_get_path_to_store_audio(self):
+        """
+        Test the path to store audio
+        """
+
+        self.TTSMod.words = "kalliope"
+        settings = Settings(cache_path="/tmp/kalliope/tests")
+        self.TTSMod.settings = settings
+
+        expected_result = "/tmp/kalliope/tests/TTSModule/tests/default/5c186d1e123be2667fb5fd54640e4fd0.tts"
+
+        self.assertEquals(self.TTSMod._get_path_to_store_audio(),
+                          expected_result,
+                          "fail test_get_path_to_store_audio, expected path not corresponding to result")
+
+    def test_generate_and_play(self):
+        """
+        Test to generate and play sound
+        """
+        def new_play_audio(TTSModule):
+            pass
+
+        words = "kalliope"
+
+        with mock.patch.object(TTSModule, 'play_audio', new=new_play_audio):
+            settings = Settings(cache_path="/tmp/kalliope/tests")
+            self.TTSMod.settings = settings
+
+            # test missing callback
+            with self.assertRaises(TtsGenerateAudioFunctionNotFound):
+                self.TTSMod.generate_and_play(words=words)
+
+            # Assert Callback is called
+            # no Cache
+            self.TTSMod.cache = False
+            generate_audio_function_from_child = mock.Mock()
+            self.TTSMod.generate_and_play(words=words,
+                                          generate_audio_function_from_child=generate_audio_function_from_child)
+            generate_audio_function_from_child.assert_called()
+
+            # with cache True but not existing on system
+            self.TTSMod.cache = True
+            generate_audio_function_from_child = mock.Mock()
+            self.TTSMod.generate_and_play(words=words,
+                                          generate_audio_function_from_child=generate_audio_function_from_child)
+            generate_audio_function_from_child.assert_called()
+
+            # with cache True and existing on system
+            # create tmp file
+            tmp_base_path = "/tmp/kalliope/tests/TTSModule/tests/default/"
+            file_path = os.path.join(tmp_base_path, "5c186d1e123be2667fb5fd54640e4fd0.tts")
+            if os.path.isfile(file_path):
+                # Remove the file
+                FileManager.remove_file(file_path)
+            if not os.path.exists(tmp_base_path):
+                os.makedirs(tmp_base_path)
+            FileManager.write_in_file(file_path, "[kalliope-test] test_generate_and_play")
+            self.TTSMod.cache = True
+            generate_audio_function_from_child = mock.Mock()
+            self.TTSMod.generate_and_play(words=words,
+                                          generate_audio_function_from_child=generate_audio_function_from_child)
+            generate_audio_function_from_child.assert_not_called()
+            # Remove the tmp file
+            FileManager.remove_file(file_path)
+
+    def test_is_file_already_in_cache(self):
+        """
+        Test if file is already stored in cache
+        """
+
+        base_cache_path = "/tmp/kalliope/tests/TTSModule/tests/default/"
+        md5_word = "5c186d1e123be2667fb5fd54640e4fd0"
+        file_path = os.path.join(base_cache_path, "5c186d1e123be2667fb5fd54640e4fd0.tts")
+
+        if os.path.isfile(file_path):
+            # Remove the file
+            FileManager.remove_file(file_path)
+        # Create a tmp file
+        if not os.path.exists(base_cache_path):
+            os.makedirs(base_cache_path)
+        tmp_path = os.path.join(base_cache_path, md5_word+".tts")
+        FileManager.write_in_file(tmp_path, "[kalliope-test] test_is_file_already_in_cache")
+
+        # Test true
+        self.assertTrue(TTSModule._is_file_already_in_cache(base_cache_path=base_cache_path, file_path=file_path),
+                        "Fail retrieving the cached file. The file does not exist but it should !")
+
+        # Remove the tmp file
+        FileManager.remove_file(tmp_path)
+
+        # Test False
+        self.assertFalse(TTSModule._is_file_already_in_cache(base_cache_path=base_cache_path, file_path=file_path),
+                         "Fail asserting that the file does not exist.")
+
+if __name__ == '__main__':
+    unittest.main()

+ 131 - 0
Tests/test_utils.py

@@ -0,0 +1,131 @@
+import unittest
+import os
+
+from kalliope.core.Models.Neuron import Neuron
+from kalliope.neurons.say.say import Say
+from kalliope.core.Utils.Utils import Utils
+
+
+class TestUtils(unittest.TestCase):
+    """
+    Class to test Utils methods
+    """
+
+    def setUp(self):
+        pass
+
+    def test_get_current_file_parent_path(self):
+        """
+        Expect to get back the parent path file
+        """
+        path_to_test = "../kalliope/core/Utils"
+        expected_result = os.path.normpath("../kalliope/core")
+
+        self.assertEquals(Utils.get_current_file_parent_path(path_to_test),
+                          expected_result,
+                          "fail getting the parent parent path from the given path")
+
+    def test_get_current_file_parent_parent_path(self):
+        """
+        Expect to get back the parent parent path file
+        """
+        path_to_test = "../kalliope/core/Utils"
+        expected_result = os.path.normpath("../kalliope")
+
+        self.assertEquals(Utils.get_current_file_parent_parent_path(path_to_test),
+                          expected_result,
+                          "fail getting the parent parent path from the given path")
+
+    def test_get_real_file_path(self):
+        """
+        Expect to load the proper file following the order :
+            - Provided absolute path
+            - Current user path + file_name
+            - /etc/kalliope + file_name
+            - /path/to/kalliope/ +file_name
+        """
+        ###
+        # Test the absolute path
+        dir_path = "/tmp/kalliope/tests/"
+        file_name = "test_real_file_path"
+        absolute_path_to_test = os.path.join(dir_path,file_name)
+        expected_result = absolute_path_to_test
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # touch the file
+        open(absolute_path_to_test, 'a').close()
+
+        self.assertEquals(Utils.get_real_file_path(absolute_path_to_test),
+                          expected_result,
+                          "Fail to match the given absolute path ")
+        # Clean up
+        if os.path.exists(absolute_path_to_test):
+            os.remove(absolute_path_to_test)
+
+        ###
+        # test the Current path
+        file_name = "test_real_file_path"
+        expected_result = os.getcwd() + os.sep + file_name
+
+        # touch the file
+        open(file_name, 'a').close()
+
+        self.assertEquals(Utils.get_real_file_path(file_name),
+                          expected_result,
+                          "Fail to match the Current path ")
+        # Clean up
+        if os.path.exists(file_name):
+            os.remove(file_name)
+
+        ###
+        # test /etc/kalliope
+        # /!\ need permissions
+        # dir_path = "/etc/kalliope/"
+        # file_name = "test_real_file_path"
+        # path_to_test = os.path.join(dir_path,file_name)
+        # expected_result = "/etc/kalliope" + os.sep + file_name
+        # if not os.path.exists(dir_path):
+        #     os.makedirs(dir_path)
+        #
+        # # touch the file
+        # open(path_to_test, 'a').close()
+        #
+        # self.assertEquals(Utils.get_real_file_path(file_name),
+        #                   expected_result,
+        #                   "Fail to match the /etc/kalliope path")
+        # # Clean up
+        # if os.path.exists(file_name):
+        #     os.remove(file_name)
+
+        ###
+        # /an/unknown/path/kalliope/
+        dir_path = "../kalliope/"
+        file_name = "test_real_file_path"
+        path_to_test = os.path.join(dir_path, file_name)
+        expected_result = os.path.normpath(os.getcwd() + os.sep + os.pardir + os.sep +"kalliope" + os.sep + file_name)
+        if not os.path.exists(dir_path):
+            os.makedirs(dir_path)
+
+        # touch the file
+        open(path_to_test, 'a').close()
+
+        self.assertEquals(Utils.get_real_file_path(file_name),
+                          expected_result,
+                          "Fail to match the /an/unknown/path/kalliope path")
+        # Clean up
+        if os.path.exists(file_name):
+            os.remove(file_name)
+
+    def test_get_dynamic_class_instantiation(self):
+        """
+        Test that an instance as been instantiate properly.
+        """
+
+        neuron = Neuron(name='Say', parameters={'message': 'test dynamic class instantiate'})
+        self.assertTrue(isinstance(Utils.get_dynamic_class_instantiation("neurons",
+                                                                         neuron.name.capitalize(),
+                                                                         neuron.parameters),
+                                   Say),
+                        "Fail instantiate a class")
+

+ 3 - 2
core/Tests/test_yaml_loader.py → Tests/test_yaml_loader.py

@@ -1,6 +1,7 @@
+import os
 import unittest
 
-from core.ConfigurationManager.YAMLLoader import YAMLFileNotFound, YAMLLoader
+from kalliope.core.ConfigurationManager.YAMLLoader import YAMLFileNotFound, YAMLLoader
 
 
 class TestYAMLLoader(unittest.TestCase):
@@ -13,7 +14,7 @@ class TestYAMLLoader(unittest.TestCase):
 
     def test_get_config(self):
 
-        valid_file_path_to_test = "core/Tests/brains/brain_test.yml"
+        valid_file_path_to_test = os.getcwd() + os.sep + "Tests/brains/brain_test.yml"
         invalid_file_path = "brains/non_existing_brain.yml"
         expected_result = [
             {'signals': [{'order': 'test_order'}],

+ 0 - 16
brain.yml

@@ -1,16 +0,0 @@
----
-  - includes:
-    - brains/ansible_playbook.yml
-    - brains/gmail_checker.yml
-    - brains/kill_switch.yml
-    - brains/openweathermap.yml
-    - brains/push_message.yml
-    - brains/say.yml
-    - brains/script.yml
-    - brains/shell.yml
-    - brains/systemdate.yml
-    - brains/tasker_autoremote.yml
-    - brains/wake_on_lan.yml
-    - brains/twitter.yml
-    - brains/neurotransmitter.yml
-    - brains/wikipedia.yml

+ 2 - 1
brains/ansible_playbook.yml → brain_examples/ansible_playbook.yml

@@ -3,6 +3,7 @@
     signals:
       - order: "playbook"
     neurons:
-      - ansible_playbook: "tasks.yml"
+      - ansible_playbook:
+          task_file: "tasks.yml"
       - say:
           message: "Tache terminée"

+ 8 - 0
brain_examples/default.yml

@@ -0,0 +1,8 @@
+---
+  - name: "Default-synapse"
+    signals:
+      - order: "Default-synapse"
+    neurons:
+      - say:
+          message:
+            - "I didn't understand, Sir"

+ 0 - 0
brains/gmail_checker.yml → brain_examples/gmail_checker.yml


+ 0 - 0
brains/kill_switch.yml → brain_examples/kill_switch.yml


+ 0 - 0
brains/neurotransmitter.yml → brain_examples/neurotransmitter.yml


+ 0 - 0
brains/openweathermap.yml → brain_examples/openweathermap.yml


+ 0 - 0
brains/push_message.yml → brain_examples/push_message.yml


+ 19 - 0
brain_examples/rss_reader.yml

@@ -0,0 +1,19 @@
+---
+
+  - 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
+          
+  - 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
+

+ 0 - 0
brains/say.yml → brain_examples/say.yml


+ 0 - 0
brains/script.yml → brain_examples/script.yml


+ 0 - 0
brains/shell.yml → brain_examples/shell.yml


+ 0 - 0
brains/systemdate.yml → brain_examples/systemdate.yml


+ 0 - 0
brains/tasker_autoremote.yml → brain_examples/tasker_autoremote.yml


+ 0 - 0
brains/twitter.yml → brain_examples/twitter.yml


+ 0 - 0
brains/wake_on_lan.yml → brain_examples/wake_on_lan.yml


+ 0 - 0
brains/wikipedia.yml → brain_examples/wikipedia.yml


+ 0 - 118
core/CrontabManager.py

@@ -1,118 +0,0 @@
-import logging
-
-from crontab import CronSlices
-from crontab import CronTab
-
-from core import Utils
-from core.Models import Event
-
-logging.basicConfig()
-logger = logging.getLogger("kalliope")
-
-
-class InvalidCrontabPeriod(Exception):
-    """
-    Event are based on the Crontab. The Period must be corresponding to the Crontab format
-    .. seealso:: Event
-    """
-    pass
-
-CRONTAB_COMMENT = "KALLIOPE"
-KALLIOPE_ENTRY_POINT_SCRIPT = "kalliope.py"
-
-
-class CrontabManager:
-
-    def __init__(self, brain=None):
-        self.my_user_cron = CronTab(user=True)
-        self.brain = brain
-        self.base_command = self._get_base_command()
-
-    def load_events_in_crontab(self):
-        """
-        Remove all line in crontab with the CRONTAB_COMMENT
-        Then add back line from event in the brain.yml
-        """
-        # clean the current crontab from all Kalliope event
-        self._remove_all_job()
-        # load the brain file
-        for synapse in self.brain.synapses:
-            for signal in synapse.signals:
-                # print signal
-                # if the signal is an event we add it to the crontab
-                if type(signal) == Event:
-                    # for all synapse with an event, we add the task id to the crontab
-                    self._add_event(period_string=signal.period, event_id=synapse.name)
-
-    def _add_event(self, period_string, event_id):
-        """
-        Add a single event in the crontab.
-        Will add a line like:
-        <period_string> python /path/to/kalliope.py start --brain-file /path/to/brain.yml --run-synapse "<event_id>"
-
-        E.g:
-        30 7 * * * python /home/me/kalliope/kalliope.py start --brain-file /home/me/brain.yml --run-synapse  "Say-hello"
-        :param period_string: crontab period
-        :type period_string: str
-        :param event_id:
-        :type event_id: str
-        :return:
-        """
-        my_user_cron = CronTab(user=True)
-        job = my_user_cron.new(command=self.base_command+" "+str("\"" + event_id + "\""), comment=CRONTAB_COMMENT)
-        if CronSlices.is_valid(period_string):
-            job.setall(period_string)
-            job.enable()
-        else:
-            raise InvalidCrontabPeriod("The crontab period %s is not valid" % period_string)
-        # write the file
-        my_user_cron.write()
-        Utils.print_info("Synapse \"%s\" added to the crontab" % event_id)
-
-    def get_jobs(self):
-        """
-        Return all current jobs in the crontab
-        :return:
-        """
-        return self.my_user_cron.find_comment(CRONTAB_COMMENT)
-
-    def _remove_all_job(self):
-        """
-        Remove all line in crontab that are attached to Kalliope
-        """
-        iter_item = self.my_user_cron.find_comment(CRONTAB_COMMENT)
-        for job in iter_item:
-            logger.debug("remove job %s from crontab" % job)
-            self.my_user_cron.remove(job)
-        # write the file
-        self.my_user_cron.write()
-
-        # this is a fix for the CronTab lib
-        # see https://github.com/peak6/python-crontab/issues/1
-        new_iter = self.my_user_cron.find_comment(CRONTAB_COMMENT)
-        sum_job = sum(1 for _ in new_iter)
-        while sum_job > 0:
-            self._remove_all_job()
-
-    def _get_base_command(self):
-        """
-        Return the path of the entry point of Kalliope
-        Example: /home/user/kalliope/kalliope.py
-        :return: The path of the entry point script kalliope.py
-        :rtype: str
-        """
-        import inspect
-        import os
-        # get current script directory path. We are in /an/unknown/path/kalliope/core
-        cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
-        # get parent dir. Now we are in /an/unknown/path/kalliope
-        parent_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
-        # we add the kalliope.py file name
-        real_entry_point_path = parent_dir + os.sep + KALLIOPE_ENTRY_POINT_SCRIPT
-        # We test that the file exist before return it
-        logger.debug("Real Kalliope.py path: %s" % real_entry_point_path)
-        if os.path.isfile(real_entry_point_path):
-            crontab_cmd = "python %s start --brain-file %s --run-synapse " % (real_entry_point_path,
-                                                                              self.brain.brain_file)
-            return crontab_cmd
-        raise IOError("kalliope.py file not found")

+ 0 - 33
core/Models/Event.py

@@ -1,33 +0,0 @@
-class Event(object):
-    """
-    This Class is representing an Event which is raised by when the System at some defined time.
-
-    .. note:: Events are based on the system crontab
-    """
-
-    def __init__(self, period):
-        self.period = period
-
-    def __str__(self):
-        return "%s: period: %s" % (self.__class__.__name__,
-                                   self.period)
-
-    def serialize(self):
-        """
-        This method allows to serialize in a proper way this object
-
-        :return: A dict of name / period
-        :rtype: Dict
-        """
-
-        return {
-            'event': self.period
-        }
-
-    def __eq__(self, other):
-        """
-        This is used to compare 2 objects
-        :param other:
-        :return:
-        """
-        return self.__dict__ == other.__dict__

+ 0 - 40
core/Models/Singleton.py

@@ -1,40 +0,0 @@
-class Singleton:
-    """
-    (From Stackoverflow : http://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons-in-python)
-
-    A non-thread-safe helper class to ease implementing singletons.
-    This should be used as a decorator -- not a metaclass -- to the
-    class that should be a singleton.
-
-    The decorated class can define one `__init__` function that
-    takes only the `self` argument. Other than that, there are
-    no restrictions that apply to the decorated class.
-
-    To get the singleton instance, use the `Instance` method. Trying
-    to use `__call__` will result in a `TypeError` being raised.
-
-    Limitations: The decorated class cannot be inherited from.
-
-    """
-
-    def __init__(self, decorated):
-        self._decorated = decorated
-
-    def Instance(self, **kwargs):
-        """
-        Returns the singleton instance. Upon its first call, it creates a
-        new instance of the decorated class and calls its `__init__` method.
-        On all subsequent calls, the already created instance is returned.
-
-        """
-        try:
-            return self._instance
-        except AttributeError:
-            self._instance = self._decorated(**kwargs)
-            return self._instance
-
-    def __call__(self):
-        raise TypeError('Singletons must be accessed through `Instance()`.')
-
-    def __instancecheck__(self, inst):
-        return isinstance(inst, self._decorated)

+ 0 - 1
core/Tests/__init__.py

@@ -1 +0,0 @@
-from test_order_analyser import TestOrderAnalyser

+ 0 - 108
core/Tests/test_brain_loader.py

@@ -1,108 +0,0 @@
-import unittest
-
-from core.ConfigurationManager import BrainLoader
-from core.Models import Brain
-from core.Models import Event
-from core.Models import Neuron
-from core.Models import Order
-from core.Models import Synapse
-
-
-class TestBrainLoader(unittest.TestCase):
-
-    def setUp(self):
-        self.brain_to_test = "core/Tests/brains/brain_test.yml"
-        self.expected_result = [
-            {'signals': [{'order': 'test_order'}],
-             'neurons': [{'say': {'message': ['test message']}}],
-             'name': 'test'},
-            {'signals': [{'order': 'test_order_2'}],
-             'neurons': [{'say': {'message': ['test message']}}],
-             'name': 'test2'},
-            {'includes': ['included_brain_test.yml']},
-            {'signals': [{'order': 'test_order_3'}],
-             'neurons': [{'say': {'message': ['test message']}}],
-             'name': 'test3'}
-        ]
-
-    # def test_get_yaml_config(self):
-    #     """
-    #     Test we can get a yaml config from the path
-    #     """
-    #     brain_loader = BrainLoader.Instance(file_path=self.brain_to_test)
-    #     self.assertEqual(brain_loader.yaml_config, self.expected_result)
-    #     del brain_loader
-    #
-    # def test_get_brain(self):
-    #     """
-    #     Test the class return a valid brain object
-    #     """
-    #
-    #     neuron = Neuron(name='say', parameters={'message': ['test message']})
-    #
-    #     signal1 = Order(sentence="test_order")
-    #     signal2 = Order(sentence="test_order_2")
-    #     signal3 = Order(sentence="test_order_3")
-    #
-    #     synapse1 = Synapse(name="test", neurons=[neuron], signals=[signal1])
-    #     synapse2 = Synapse(name="test2", neurons=[neuron], signals=[signal2])
-    #     synapse3 = Synapse(name="test3", neurons=[neuron], signals=[signal3])
-    #     synapses = [synapse1, synapse2, synapse3]
-    #
-    #     brain = Brain()
-    #     brain.synapses = synapses
-    #     brain.brain_file = self.brain_to_test
-    #     brain.brain_yaml = self.expected_result
-    #
-    #     brain_loader = BrainLoader.Instance(file_path=self.brain_to_test)
-    #     self.assertEqual(brain, brain_loader.brain)
-    #     del brain_loader
-
-    def test_get_neurons(self):
-        neuron_list = [{'say': {'message': ['test message']}}]
-
-        neuron = Neuron(name='say', parameters={'message': ['test message']})
-
-        bl = BrainLoader.Instance(file_path=self.brain_to_test)
-        neurons_from_brain_loader = bl._get_neurons(neuron_list)
-
-        self.assertEqual([neuron], neurons_from_brain_loader)
-        del bl
-
-    def test_get_signals(self):
-        signals = [{'order': 'test_order'}]
-
-        signal = Order(sentence='test_order')
-
-        bl = BrainLoader.Instance(file_path=self.brain_to_test)
-        signals_from_brain_loader = bl._get_signals(signals)
-
-        self.assertEqual([signal], signals_from_brain_loader)
-        del bl
-
-    def test_get_event_or_order_from_dict(self):
-
-        order_object = Order(sentence="test_order")
-        event_object = Event(period="0 7 * * *")
-
-        dict_order = {'order': 'test_order'}
-        dict_event = {'event': '0 7 * * *'}
-
-        bl = BrainLoader.Instance(file_path=self.brain_to_test)
-        order_from_bl = bl._get_event_or_order_from_dict(dict_order)
-        event_from_bl = bl._get_event_or_order_from_dict(dict_event)
-
-        self.assertEqual(order_from_bl, order_object)
-        self.assertEqual(event_from_bl, event_object)
-        del bl
-
-    def test_singleton(self):
-        bl1 = BrainLoader.Instance(file_path=self.brain_to_test)
-        bl2 = BrainLoader.Instance(file_path=self.brain_to_test)
-
-        self.assertTrue(bl1.brain is bl2.brain)
-        del bl1
-        del bl2
-
-if __name__ == '__main__':
-    unittest.main()

+ 0 - 250
core/Tests/test_order_analyser.py

@@ -1,250 +0,0 @@
-import unittest
-
-
-from core.OrderAnalyser import OrderAnalyser
-from core.Models.Neuron import Neuron
-from core.Models.Synapse import Synapse
-from core.Models.Order import Order
-
-
-class TestOrderAnalyser(unittest.TestCase):
-
-    """Test case for the OrderAnalyser Class"""
-
-    def setUp(self):
-        pass
-
-    def test_is_containing_bracket(self):
-        #  Success
-        order_to_test = "This test contains {{ bracket }}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains spaced brackets")
-
-        order_to_test = "This test contains {{bracket }}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains right spaced bracket")
-
-        order_to_test = "This test contains {{ bracket}}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains left spaced bracket")
-
-        order_to_test = "This test contains {{bracket}}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains no spaced bracket")
-
-        #  Failure
-        order_to_test = "This test does not contain bracket"
-        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
-                         "Fail returning False when order has no brackets")
-
-        #  Behaviour
-        order_to_test = ""
-        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
-                         "Fail returning False when no order")
-
-    def test_get_next_value_list(self):
-        # Success
-        list_to_test = {1, 2, 3}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), 2,
-                         "Fail to match the expected next value from the list")
-
-        # Failure
-        list_to_test = {1}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
-                         "Fail to ensure there is no next value from the list")
-
-        # Behaviour
-        list_to_test = {}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
-                         "Fail to ensure the empty list return None value")
-
-    def test_spelt_order_match_brain_order_via_table(self):
-        order_to_test = "this is the order"
-        sentence_to_test = "this is the order"
-
-        # Success
-        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),
-                         "Fail to ensure the expected sentence is not matching the order")
-
-    def test_get_split_order_without_bracket(self):
-
-        # Success
-        order_to_test = "this is the order"
-        expected_result = ["this", "is", "the", "order"]
-        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
-                         "No brackets Fails to return the expected list")
-
-        order_to_test = "this is the {{ order }}"
-        expected_result = ["this", "is", "the"]
-        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
-                         "With spaced brackets Fails to return the expected list")
-
-        order_to_test = "this is the {{order }}"    # left bracket without space
-        expected_result = ["this", "is", "the"]
-        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
-                         "Left brackets Fails to return the expected list")
-
-        order_to_test = "this is the {{ order}}"    # right bracket without space
-        expected_result = ["this", "is", "the"]
-        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
-                         "Right brackets Fails to return the expected list")
-
-        order_to_test = "this is the {{order}}"  # bracket without space
-        expected_result = ["this", "is", "the"]
-        self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
-                         "No space brackets Fails to return the expected list")
-
-    def test_associate_order_params_to_values(self):
-        ##
-        # Testing the brackets position behaviour
-        ##
-
-        # Success
-        order_brain = "This is the {{ variable }}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{ variable }} to the 'value'")
-
-        # Success
-        order_brain = "This is the {{variable }}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{variable }} to the 'value'")
-
-        # Success
-        order_brain = "This is the {{ variable}}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{ variable}} to the 'value'")
-
-        # Success
-        order_brain = "This is the {{variable}}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{variable}} to the 'value'")
-
-        # Fail
-        order_brain = "This is the {variable}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                             "Should not match the order_brain {variable} to the 'value'")
-
-        # Fail
-        order_brain = "This is the { variable}}"
-        order_user = "This is the value"
-        expected_result = {'variable': 'value'}
-        self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                             "Should not match the order_brain { variable}} to the 'value'")
-
-        ##
-        # Testing the brackets position in the sentence
-        ##
-
-        # Success
-        order_brain = "{{ variable }} This is the"
-        order_user = "value This is the"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{ variable }} in first position "
-                         "ins the sentence to the 'value'")
-
-        # Success
-        order_brain = "This is {{ variable }} the"
-        order_user = " This is value the"
-        expected_result = {'variable': 'value'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{ variable }} in middle position ins "
-                         "the sentence to the 'value'")
-
-        ##
-        # Testing multi variables
-        ##
-
-        # Success
-        order_brain = "This is {{ variable }} the {{ variable2 }}"
-        order_user = "This is value the value2"
-        expected_result = {'variable': 'value',
-                           'variable2': 'value2'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain multi variable to the multi values")
-
-        ##
-        # Testing multi words in variable
-        ##
-
-        # Success
-        order_brain = "This is the {{ variable }}"
-        order_user = "This is the value with multiple words"
-        expected_result = {'variable': 'value with multiple words'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain {{ variable }} to the 'value with multiple words'")
-
-        # Success
-        order_brain = "This is the {{ variable }} and  {{ variable2 }}"
-        order_user = "This is the value with multiple words and second value multiple"
-        expected_result = {'variable': 'value with multiple words',
-                           'variable2': 'second value multiple'}
-        self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result,
-                         "Fail to match the order_brain multiple variables with multiple words as values'")
-
-    def test_get_matching_synapse_list(self):
-        # 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="this is 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])
-
-        order_to_match = "this is the sentence"
-        all_synapse_list = [synapse1,
-                            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,
-                          "Fail matching the expected synapse from the complete synapse list and the order")
-
-        # TODO : to be continued
-
-    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 }}")
-
-        synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
-
-        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),
-                          expected_result,
-                          "Fail to retrieve the params of the synapse from the order")
-
-        # TODO : to be continued
-
-
-if __name__ == '__main__':
-    unittest.main()

+ 0 - 102
core/Utils.py

@@ -1,102 +0,0 @@
-import logging
-
-logging.basicConfig()
-logger = logging.getLogger("kalliope")
-
-
-class ModuleNotFoundError(Exception):
-    """
-    The module can not been found
-
-    .. notes: Check the case: must be in lower case.
-    """
-    pass
-
-
-class Utils(object):
-
-    color_list = dict(
-        PURPLE='\033[95m',
-        BLUE='\033[94m',
-        GREEN='\033[92m',
-        YELLOW='\033[93m',
-        RED='\033[91m',
-        ENDLINE='\033[0m',
-        BOLD='\033[1m',
-        UNDERLINE='\033[4m'
-    )
-
-    @classmethod
-    def print_info(cls, text_to_print):
-        print cls.color_list["BLUE"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_success(cls, text_to_print):
-        print cls.color_list["GREEN"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_warning(cls, text_to_print):
-        print cls.color_list["YELLOW"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_danger(cls, text_to_print):
-        print cls.color_list["RED"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_header(cls, text_to_print):
-        print cls.color_list["HEADER"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_header(cls, text_to_print):
-        print cls.color_list["PURPLE"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_bold(cls, text_to_print):
-        print cls.color_list["BOLD"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def print_underline(cls, text_to_print):
-        print cls.color_list["UNDERLINE"] + text_to_print + cls.color_list["ENDLINE"]
-
-    @classmethod
-    def get_dynamic_class_instantiation(cls, package_name, module_name, parameters=None):
-        """
-        Load a python class dynamically
-
-        from my_package.my_module import my_class
-        mod = __import__('my_package.my_module', fromlist=['my_class'])
-        klass = getattr(mod, 'my_class')
-
-        :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
-        :return:
-        """
-        logger.debug("Run plugin %s with parameter %s" % (module_name, parameters))
-        module_name_with_path = package_name + "." + module_name.lower() + "." + module_name.lower()
-        mod = __import__(module_name_with_path, fromlist=[module_name])
-        try:
-            klass = getattr(mod, module_name)
-        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))
-
-        if klass is not None:
-            # run the plugin
-            if not parameters:
-                return klass()
-            elif isinstance(parameters, dict):
-                return klass(**parameters)
-            else:
-                return klass(parameters)
-        return None
-
-    @classmethod
-    def print_yaml_nicely(cls, to_print):
-        """
-        Used for debug
-        :param to_print: Dict to print nicely
-        :return:
-        """
-        import json
-        print json.dumps(to_print, indent=2)

+ 0 - 6
core/__init__.py

@@ -1,6 +0,0 @@
-from core.OrderAnalyser import OrderAnalyser
-from core.OrderListener import OrderListener
-from core.ShellGui import ShellGui
-from core.FileManager import FileManager
-from core.Utils import Utils
-

+ 24 - 0
install/files/deb-packages_requirements.txt

@@ -0,0 +1,24 @@
+python-pip
+libssl-dev
+libportaudio0
+libportaudio2
+libportaudiocpp0
+portaudio19-dev
+libffi-dev
+python-yaml
+python-pycparser
+python-paramiko
+python-markupsafe
+apt-transport-https
+python-pip
+python-dev
+libsmpeg0
+libttspico-utils
+libsmpeg0
+flac
+dialog
+portaudio19-dev
+build-essential
+sox
+libatlas3-base
+mplayer

+ 10 - 6
install/files/python_requirements.txt

@@ -1,15 +1,15 @@
-SpeechRecognition==3.4.6
+six==1.10.0
+SpeechRecognition>=3.5.0
 markupsafe==0.23
 pyaudio==0.2.9
-ansible==2.1.1.0
+ansible==2.2.0.0
 python2-pythondialog==3.4.0
-jinja==1.2
-python-crontab==2.1.1
-cffi==1.8.3
+jinja2==2.8
+cffi==1.9.1
 pygmail==0.0.5.4
 pushetta==1.0.15
 wakeonlan==0.2.2
-ipaddress==1.0.16
+ipaddress==1.0.17
 pyowm==2.5.0
 python-twitter==3.1
 flask==0.11.1
@@ -17,3 +17,7 @@ Flask-Restful==0.3.5
 wikipedia==1.4.0
 requests==2.12.1
 httpretty==0.8.14
+mock==2.0.0
+feedparser==5.2.1
+Flask-Testing==0.6.1
+apscheduler==3.3.0

+ 3 - 0
install/files/travis_repo_trusty_requirements.txt

@@ -0,0 +1,3 @@
+"deb http://archive.ubuntu.com/ubuntu trusty main restricted universe multiverse"
+"deb http://archive.ubuntu.com/ubuntu trusty-updates main restricted universe multiverse"
+"deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse"

+ 0 - 2
install/install.yml

@@ -37,8 +37,6 @@
         - libssl-dev
         - portaudio19-dev
         - build-essential
-        - libssl-dev
-        - libffi-dev
         - sox
         - libatlas3-base
         - mplayer

+ 2 - 113
kalliope.py

@@ -1,117 +1,6 @@
 #!/usr/bin/env python
 # coding: utf8
-import argparse
-import logging
-
-from core import ShellGui
-from core import Utils
-from core.ConfigurationManager.BrainLoader import BrainLoader
-from core.CrontabManager import CrontabManager
-from core.MainController import MainController
-import signal
-import sys
-
-from core.SynapseLauncher import SynapseLauncher
-
-logging.basicConfig()
-logger = logging.getLogger("kalliope")
-
-
-def signal_handler(signal, frame):
-    """
-    Used to catch a keyboard signal like Ctrl+C in order to kill the kalliope program
-    :param signal: signal handler
-    :param frame: execution frame
-    """
-    print "\n"
-    Utils.print_info("Ctrl+C pressed. Killing Kalliope")
-    sys.exit(0)
-
-# actions available
-ACTION_LIST = ["start", "gui"]
-
-
-def main():
-    """
-    Entry point of Kalliope program
-    """
-    # create arguments
-    parser = argparse.ArgumentParser(description='Kalliope')
-    parser.add_argument("action", help="[start|gui]")
-    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")
-
-    # parse arguments from script parameters
-    args = parser.parse_args()
-
-    # require at least one parameter, the action
-    if len(sys.argv[1:]) == 0:
-        parser.print_usage()
-        sys.exit(1)
-
-    # check if we want debug
-    configure_logging(debug=args.debug)
-
-    logger.debug("kalliope args: %s" % args)
-
-    # by default, no brain file is set. Use the default one: brain.yml in the root path
-    brain_file = None
-    # check if user set a brain.yml file
-    if args.brain_file:
-        brain_file = args.brain_file
-    # load the brain once
-    brain_loader = BrainLoader.Instance(file_path=brain_file)
-    brain = brain_loader.brain
-
-    # check the user provide a valid action
-    if args.action not in ACTION_LIST:
-        Utils.print_warning("%s is not a recognised action\n" % args.action)
-        parser.print_help()
-
-    if args.action == "start":
-        # user set a synapse to start
-        if args.run_synapse is not None:
-            SynapseLauncher.start_synapse(args.run_synapse, brain=brain)
-
-        if args.run_synapse is None:
-            # first, load events in crontab
-            crontab_manager = CrontabManager(brain=brain)
-            crontab_manager.load_events_in_crontab()
-            Utils.print_success("Events loaded in crontab")
-            # then start kalliope
-            Utils.print_success("Starting Kalliope")
-            Utils.print_info("Press Ctrl+C for stopping")
-            # catch signal for killing on Ctrl+C pressed
-            signal.signal(signal.SIGINT, signal_handler)
-            # start the main controller
-            MainController(brain=brain)
-
-    if args.action == "gui":
-        ShellGui(brain=brain)
-
-
-def configure_logging(debug=None):
-    """
-    Prepare log folder in current home directory
-    :param debug: If true, set the lof level to debug
-    """
-    logger = logging.getLogger("kalliope")
-    logger.propagate = False
-    ch = logging.StreamHandler()
-    ch.setLevel(logging.DEBUG)
-    formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s')
-    ch.setFormatter(formatter)
-
-    # add the handlers to logger
-    logger.addHandler(ch)
-
-    if debug:
-        logger.setLevel(logging.DEBUG)
-    else:
-        logger.setLevel(logging.INFO)
-
-    logger.debug("Logger ready")
+import kalliope
 
 if __name__ == '__main__':
-    main()
+    kalliope.main()

+ 113 - 0
kalliope/__init__.py

@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# coding: utf8
+import argparse
+import logging
+
+from kalliope.core import ShellGui
+from kalliope.core import Utils
+from kalliope.core.ConfigurationManager.BrainLoader import BrainLoader
+from kalliope.core.EventManager import EventManager
+from kalliope.core.MainController import MainController
+import signal
+import sys
+
+from kalliope.core.SynapseLauncher import SynapseLauncher
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+def signal_handler(signal, frame):
+    """
+    Used to catch a keyboard signal like Ctrl+C in order to kill the kalliope program
+    :param signal: signal handler
+    :param frame: execution frame
+    """
+    print "\n"
+    Utils.print_info("Ctrl+C pressed. Killing Kalliope")
+    sys.exit(0)
+
+# actions available
+ACTION_LIST = ["start", "gui"]
+
+
+def main():
+    """
+    Entry point of Kalliope program
+    """
+    # create arguments
+    parser = argparse.ArgumentParser(description='Kalliope')
+    parser.add_argument("action", help="[start|gui]")
+    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")
+
+    # parse arguments from script parameters
+    args = parser.parse_args()
+
+    # require at least one parameter, the action
+    if len(sys.argv[1:]) == 0:
+        parser.print_usage()
+        sys.exit(1)
+
+    # check if we want debug
+    configure_logging(debug=args.debug)
+
+    logger.debug("kalliope args: %s" % args)
+
+    # by default, no brain file is set. Use the default one: brain.yml in the root path
+    brain_file = None
+    # check if user set a brain.yml file
+    if args.brain_file:
+        brain_file = args.brain_file
+    # load the brain once
+    brain_loader = BrainLoader(file_path=brain_file)
+    brain = brain_loader.brain
+
+    # check the user provide a valid action
+    if args.action not in ACTION_LIST:
+        Utils.print_warning("%s is not a recognised action\n" % args.action)
+        parser.print_help()
+
+    if args.action == "start":
+        # user set a synapse to start
+        if args.run_synapse is not None:
+            SynapseLauncher.start_synapse(args.run_synapse, brain=brain)
+
+        if args.run_synapse is None:
+            # first, load events in event manager
+            EventManager(brain.synapses)
+            Utils.print_success("Events loaded")
+            # then start kalliope
+            Utils.print_success("Starting Kalliope")
+            Utils.print_info("Press Ctrl+C for stopping")
+            # catch signal for killing on Ctrl+C pressed
+            signal.signal(signal.SIGINT, signal_handler)
+            # start the main controller
+            MainController(brain=brain)
+
+    if args.action == "gui":
+        ShellGui(brain=brain)
+
+
+def configure_logging(debug=None):
+    """
+    Prepare log folder in current home directory
+    :param debug: If true, set the lof level to debug
+    """
+    logger = logging.getLogger("kalliope")
+    logger.propagate = False
+    ch = logging.StreamHandler()
+    ch.setLevel(logging.DEBUG)
+    formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s')
+    ch.setFormatter(formatter)
+
+    # add the handlers to logger
+    logger.addHandler(ch)
+
+    if debug:
+        logger.setLevel(logging.DEBUG)
+    else:
+        logger.setLevel(logging.INFO)
+
+    logger.debug("Logger ready")

+ 2 - 0
kalliope/_version.py

@@ -0,0 +1,2 @@
+# https://www.python.org/dev/peps/pep-0440/
+version_str = "0.3.0"

+ 18 - 0
kalliope/brain.yml

@@ -0,0 +1,18 @@
+---
+  - name: "say-hello-fr"
+    signals:
+      - order: "bonjour"
+      - order: "Bonjour"
+    neurons:
+      - say:
+          message:
+            - "Bonjour monsieur"
+
+  - name: "say-hello-en"
+    signals:
+      - order: "hello"
+      - order: "Hello"
+    neurons:
+      - say:
+          message:
+            - "Hello sir"

+ 45 - 12
core/ConfigurationManager/BrainLoader.py → kalliope/core/ConfigurationManager/BrainLoader.py

@@ -3,27 +3,40 @@ import logging
 import os
 
 from YAMLLoader import YAMLLoader
-from core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker
-from core.Models import Singleton
-from core.Models.Brain import Brain
-from core.Models.Event import Event
-from core.Models.Neuron import Neuron
-from core.Models.Order import Order
-from core.Models.Synapse import Synapse
+from kalliope.core.Utils import Utils
+from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker
+from kalliope.core.Models import Singleton
+from kalliope.core.Models.Brain import Brain
+from kalliope.core.Models.Event import Event
+from kalliope.core.Models.Neuron import Neuron
+from kalliope.core.Models.Order import Order
+from kalliope.core.Models.Synapse import Synapse
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
+FILE_NAME = "brain.yml"
+
+
+class BrainNotFound(Exception):
+    pass
+
 
-@Singleton
 class BrainLoader(object):
     """
     This Class is used to get the brain YAML and the Brain as an object
     """
+    __metaclass__ = Singleton
 
     def __init__(self, file_path=None):
-        logger.debug("Loading brain with file path: %s" % file_path)
         self.file_path = file_path
+        if self.file_path is None:  # we don't provide a file path, so search for the default one
+            self.file_path = Utils.get_real_file_path(FILE_NAME)
+        else:
+            self.file_path = Utils.get_real_file_path(file_path)
+        # if the returned file path is none, the file doesn't exist
+        if self.file_path is None:
+            raise BrainNotFound("brain file not found")
         self.yaml_config = self.get_yaml_config()
         self.brain = self.get_brain()
 
@@ -152,8 +165,8 @@ class BrainLoader(object):
 
         return signals
 
-    @staticmethod
-    def _get_event_or_order_from_dict(signal_or_event_dict):
+    @classmethod
+    def _get_event_or_order_from_dict(cls, signal_or_event_dict):
         """
         The signal is either an Event or an Order
 
@@ -173,7 +186,7 @@ class BrainLoader(object):
             # print "is event"
             event = signal_or_event_dict["event"]
             if ConfigurationChecker.check_event_dict(event):
-                return Event(period=event)
+                return cls._get_event_object(event)
 
         if 'order' in signal_or_event_dict:
             order = signal_or_event_dict["order"]
@@ -202,3 +215,23 @@ class BrainLoader(object):
         if os.path.isfile(brain_path):
             return brain_path
         raise IOError("Default brain.yml file not found")
+
+    @classmethod
+    def _get_event_object(cls, event_dict):
+        def get_key(key_name):
+            try:
+                return event_dict[key_name]
+            except KeyError:
+                return None
+
+        year = get_key("year")
+        month = get_key("month")
+        day = get_key("day")
+        week = get_key("week")
+        day_of_week = get_key("day_of_week")
+        hour = get_key("hour")
+        minute = get_key("minute")
+        second = get_key("second")
+
+        return Event(year=year, month=month, day=day, week=week,
+                     day_of_week=day_of_week, hour=hour, minute=minute, second=second)

+ 28 - 3
core/ConfigurationManager/ConfigurationChecker.py → kalliope/core/ConfigurationManager/ConfigurationChecker.py

@@ -1,6 +1,6 @@
 import re
 
-from core.Utils import ModuleNotFoundError
+from kalliope.core.Utils.Utils import ModuleNotFoundError
 
 
 class InvalidSynapeName(Exception):
@@ -139,7 +139,7 @@ class ConfigurationChecker:
             :type neuron_module_name: str
             :return:
             """
-            package_name = "neurons"
+            package_name = "kalliope.neurons"
             mod = __import__(package_name, fromlist=[neuron_module_name])
             try:
                 getattr(mod, neuron_module_name)
@@ -196,8 +196,33 @@ class ConfigurationChecker:
         .. raises:: NoEventPeriod
         .. warnings:: Static and Public
         """
+        def get_key(key_name):
+            try:
+                return event_dict[key_name]
+            except KeyError:
+                return None
+
         if event_dict is None or event_dict == "":
-            raise NoEventPeriod("Event must contain a period: %s" % event_dict)
+            raise NoEventPeriod("Event must contain at least one of those elements: "
+                                "year, month, day, week, day_of_week, hour, minute, second")
+
+        # check content as at least on key
+        year = get_key("year")
+        month = get_key("month")
+        day = get_key("day")
+        week = get_key("week")
+        day_of_week = get_key("day_of_week")
+        hour = get_key("hour")
+        minute = get_key("minute")
+        second = get_key("second")
+
+        list_to_check = [year, month, day, week, day_of_week, hour, minute, second]
+        number_of_none_object = list_to_check.count(None)
+        list_size = len(list_to_check)
+        if number_of_none_object >= list_size:
+            raise NoEventPeriod("Event must contain at least one of those elements: "
+                                "year, month, day, week, day_of_week, hour, minute, second")
+
         return True
 
     @staticmethod

+ 46 - 11
core/ConfigurationManager/SettingLoader.py → kalliope/core/ConfigurationManager/SettingLoader.py

@@ -1,13 +1,14 @@
 import logging
 
 from YAMLLoader import YAMLLoader
-from core.FileManager import FileManager
-from core.Models import Singleton
-from core.Models.RestAPI import RestAPI
-from core.Models.Settings import Settings
-from core.Models.Stt import Stt
-from core.Models.Trigger import Trigger
-from core.Models.Tts import Tts
+from kalliope.core.Utils import Utils
+from kalliope.core.Models import Singleton
+from kalliope.core.Models.RestAPI import RestAPI
+from kalliope.core.Models.Settings import Settings
+from kalliope.core.Models.Stt import Stt
+from kalliope.core.Models.Trigger import Trigger
+from kalliope.core.Models.Tts import Tts
+from kalliope.core.Utils.FileManager import FileManager
 
 FILE_NAME = "settings.yml"
 
@@ -42,18 +43,21 @@ class SettingNotFound(Exception):
     pass
 
 
-@Singleton
 class SettingLoader(object):
     """
     This Class is used to get the Settings YAML and the Settings as an object
     """
+    __metaclass__ = Singleton
 
     def __init__(self, file_path=None):
-        logger.debug("Loading settings with file path: %s" % file_path)
         self.file_path = file_path
         if self.file_path is None:
-            # use default file if not provided
-            self.file_path = FILE_NAME
+            self.file_path = Utils.get_real_file_path(FILE_NAME)
+        else:
+            self.file_path = Utils.get_real_file_path(file_path)
+        # if the returned file path is none, the file doesn't exist
+        if self.file_path is None:
+            raise SettingNotFound("Settings.yml file not found")
         self.yaml_config = self._get_yaml_config()
         self.settings = self._get_settings()
 
@@ -101,6 +105,7 @@ class SettingLoader(object):
         random_wake_up_sounds = self._get_random_wake_up_sounds(settings)
         rest_api = self._get_rest_api(settings)
         cache_path = self._get_cache_path(settings)
+        default_synapse = self._get_default_synapse(settings)
 
         # Load the setting singleton with the parameters
         setting_object.default_tts_name = default_tts_name
@@ -113,6 +118,7 @@ class SettingLoader(object):
         setting_object.random_wake_up_sounds = random_wake_up_sounds
         setting_object.rest_api = rest_api
         setting_object.cache_path = cache_path
+        setting_object.default_synapse = default_synapse
 
         return setting_object
 
@@ -473,3 +479,32 @@ class SettingLoader(object):
             return cache_path
         else:
             raise SettingInvalidException("The cache_path seems to be invalid: %s" % cache_path)
+
+    @staticmethod
+    def _get_default_synapse(settings):
+        """
+        Return the name of the default synapse
+
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return: the default synapse name
+        :rtype: String
+
+        :Example:
+
+            default_synapse = cls._get_default_synapse(settings)
+
+        .. seealso::
+        .. raises:: SettingNotFound, NullSettingException, SettingInvalidException
+        .. warnings:: Class Method and Private
+        """
+
+        try:
+            default_synapse = settings["default_synapse"]
+            logger.debug("Default synapse: %s" % default_synapse)
+        except KeyError:
+            default_synapse = None
+
+        return default_synapse
+
+

+ 1 - 6
core/ConfigurationManager/YAMLLoader.py → kalliope/core/ConfigurationManager/YAMLLoader.py

@@ -41,12 +41,7 @@ class YAMLLoader:
         .. warnings:: Class Method and Public
         """
 
-        current_dir = os.path.dirname(os.path.abspath(__file__))
-        logger.debug("Current dir: %s " % current_dir)
-        root_dir = os.path.join(current_dir, "../../")
-        root_dir = os.path.normpath(root_dir)
-        logger.debug("Root dir: %s " % root_dir)
-        cls.file_path_to_load = os.path.join(root_dir, yaml_file)
+        cls.file_path_to_load = yaml_file
         logger.debug("File path to load: %s " % cls.file_path_to_load)
         if os.path.isfile(cls.file_path_to_load):
             inc_import = IncludeImport(cls.file_path_to_load)

+ 0 - 0
core/ConfigurationManager/__init__.py → kalliope/core/ConfigurationManager/__init__.py


+ 49 - 0
kalliope/core/EventManager.py

@@ -0,0 +1,49 @@
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+
+from kalliope.core.ConfigurationManager import BrainLoader
+from kalliope.core.SynapseLauncher import SynapseLauncher
+from kalliope.core import Utils
+from kalliope.core.Models import Event
+
+
+class EventManager(object):
+
+    def __init__(self, synapses):
+        Utils.print_info('Starting event manager')
+        self.scheduler = BackgroundScheduler()
+        self.synapses = synapses
+        self.load_events()
+        self.scheduler.start()
+
+    def load_events(self):
+        """
+        For each received synapse that have an event as signal, we add a new job scheduled
+        to launch the synapse
+        :return:
+        """
+        for synapse in self.synapses:
+            for signal in synapse.signals:
+                # if the signal is an event we add it to the task list
+                if type(signal) == Event:
+                    my_cron = CronTrigger(year=signal.year,
+                                          month=signal.month,
+                                          day=signal.day,
+                                          week=signal.week,
+                                          day_of_week=signal.day_of_week,
+                                          hour=signal.hour,
+                                          minute=signal.minute,
+                                          second=signal.second)
+                    Utils.print_info("Add synapse name \"%s\" to the scheduler: %s" % (synapse.name, my_cron))
+                    self.scheduler.add_job(self.run_synapse_by_name, my_cron, args=[synapse.name])
+
+    @staticmethod
+    def run_synapse_by_name(synapse_name):
+        """
+        This method will run the synapse
+        """
+        Utils.print_info("Event triggered, running synapse: %s" % synapse_name)
+        # get a brain
+        brain_loader = BrainLoader()
+        brain = brain_loader.brain
+        SynapseLauncher.start_synapse(synapse_name, brain=brain)

+ 11 - 16
core/MainController.py → kalliope/core/MainController.py

@@ -4,14 +4,14 @@ import random
 
 from flask import Flask
 
-from core import Utils
-from core.ConfigurationManager import SettingLoader
-from core.OrderAnalyser import OrderAnalyser
-from core.OrderListener import OrderListener
-from core.Players import Mplayer
-from core.RestAPI.FlaskAPI import FlaskAPI
-from core.TriggerLauncher import TriggerLauncher
-from neurons.say.say import Say
+from kalliope.core import Utils
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.OrderAnalyser import OrderAnalyser
+from kalliope.core.OrderListener import OrderListener
+from kalliope.core.Players import Mplayer
+from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
+from kalliope.core.TriggerLauncher import TriggerLauncher
+from kalliope.neurons.say.say import Say
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -24,7 +24,7 @@ class MainController:
     def __init__(self, brain=None):
         self.brain = brain
         # get global configuration
-        sl = SettingLoader.Instance()
+        sl = SettingLoader()
         self.settings = sl.settings
 
         # run the api if the user want it
@@ -65,7 +65,7 @@ class MainController:
         :type order: str
         """
         if order is not None:   # maybe we have received a null audio from STT engine
-            order_analyser = OrderAnalyser(order, main_controller=self, brain=self.brain)
+            order_analyser = OrderAnalyser(order, brain=self.brain)
             order_analyser.start()
 
         # restart the trigger when the order analyser has finish his job
@@ -95,9 +95,4 @@ class MainController:
         # take first randomly a path
         random_path = random.choice(random_wake_up_sounds)
         logger.debug("Selected sound: %s" % random_path)
-        if os.path.isabs(random_path):
-            logger.debug("Path of file %s is absolute" % random_path)
-            return random_path
-        else:
-            logger.debug("Path of file %s is relative" % random_path)
-            return "sounds" + os.sep + random_path
+        return Utils.get_real_file_path(random_path)

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


+ 51 - 0
kalliope/core/Models/Event.py

@@ -0,0 +1,51 @@
+class Event(object):
+    """
+    This Class is representing an Event which is raised by when the System at some defined time.
+
+    .. note:: Events are based on the system crontab
+    """
+
+    def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None,
+                 hour=None, minute=None, second=None):
+        self.year = year
+        self.month = month
+        self.day = day
+        self.week = week
+        self.day_of_week = day_of_week
+        self.hour = hour
+        self.minute = minute
+        self.second = second
+
+    def __str__(self):
+        return "%s:  year: %s, month: %s, day: %s, week: %s, day_of_week: %s, hour: %s, minute: %s, second: %s" \
+               % (self.__class__.__name__, self.year, self.month, self.day, self.week,
+                  self.day_of_week, self.hour, self.minute, self.second)
+
+    def serialize(self):
+        """
+        This method allows to serialize in a proper way this object
+
+        :return: A dict of name / period
+        :rtype: Dict
+        """
+
+        return {
+            'event': {
+                "year": self.year,
+                "month": self.month,
+                "day": self.day,
+                "week": self.week,
+                "day_of_week": self.day_of_week,
+                "hour": self.hour,
+                "minute": self.minute,
+                "second": self.second,
+            }
+        }
+
+    def __eq__(self, other):
+        """
+        This is used to compare 2 objects
+        :param other:
+        :return:
+        """
+        return self.__dict__ == other.__dict__

+ 0 - 0
core/Models/Neuron.py → kalliope/core/Models/Neuron.py


+ 0 - 0
core/Models/Order.py → kalliope/core/Models/Order.py


+ 0 - 0
core/Models/RestAPI.py → kalliope/core/Models/RestAPI.py


+ 2 - 0
core/Models/Settings.py → kalliope/core/Models/Settings.py

@@ -18,6 +18,7 @@ class Settings(object):
                  triggers=None,
                  rest_api=None,
                  cache_path=None,
+                 default_synapse=None,
                  machine=None):
 
         self.default_tts_name = default_tts_name
@@ -30,6 +31,7 @@ class Settings(object):
         self.triggers = triggers
         self.rest_api = rest_api
         self.cache_path = cache_path
+        self.default_synapse = default_synapse
         self.machine = platform.machine()   # can be x86_64 or armv7l
 
     def __eq__(self, other):

+ 7 - 0
kalliope/core/Models/Singleton.py

@@ -0,0 +1,7 @@
+class Singleton(type):
+    _instances = {}
+
+    def __call__(cls, *args, **kwargs):
+        if cls not in cls._instances:
+            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+        return cls._instances[cls]

+ 0 - 0
core/Models/Stt.py → kalliope/core/Models/Stt.py


+ 0 - 0
core/Models/Synapse.py → kalliope/core/Models/Synapse.py


+ 0 - 0
core/Models/Trigger.py → kalliope/core/Models/Trigger.py


+ 0 - 0
core/Models/Tts.py → kalliope/core/Models/Tts.py


+ 0 - 0
core/Models/__init__.py → kalliope/core/Models/__init__.py


+ 6 - 4
core/NeuroneLauncher.py → kalliope/core/NeuronLauncher.py

@@ -1,18 +1,18 @@
 import logging
 
-from core.Utils import Utils
+from kalliope.core.Utils.Utils import Utils
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
 
-class NeuroneLauncher:
+class NeuronLauncher:
 
     def __init__(self):
         pass
 
     @classmethod
-    def start_neurone(cls, neuron):
+    def start_neuron(cls, neuron):
         """
         Start a neuron plugin
         :param neuron: neuron object
@@ -20,4 +20,6 @@ class NeuroneLauncher:
         :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)
+        return Utils.get_dynamic_class_instantiation("neurons",
+                                                     neuron.name.capitalize(),
+                                                     neuron.parameters)

+ 48 - 51
core/NeuronModule.py → kalliope/core/NeuronModule.py

@@ -6,10 +6,10 @@ import random
 import sys
 from jinja2 import Template
 
-from core import OrderListener
-from core.SynapseLauncher import SynapseLauncher
-from core.Utils import Utils
-from core.ConfigurationManager import SettingLoader, BrainLoader
+from kalliope.core import OrderListener
+from kalliope.core.SynapseLauncher import SynapseLauncher
+from kalliope.core.Utils.Utils import Utils
+from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -66,9 +66,9 @@ class NeuronModule(object):
         self.neuron_name = child_name
         logger.debug("NeuronModule called from class %s with parameters: %s" % (child_name, str(kwargs)))
 
-        sl = SettingLoader.Instance()
+        sl = SettingLoader()
         self.settings = sl.settings
-        brain_loader = BrainLoader.Instance()
+        brain_loader = BrainLoader()
         self.brain = brain_loader.brain
 
         # check if the user has overrider the TTS
@@ -118,7 +118,7 @@ class NeuronModule(object):
         if tts_message is not None:
             logger.debug("tts_message to say: %s" % tts_message)
 
-            # create a tts object from the tts the user want to user
+            # create a tts object from the tts the user want to use
             tts_object = next((x for x in self.settings.ttss if x.name == self.tts), None)
             if tts_object is None:
                 raise TTSModuleNotFound("The tts module name %s does not exist in settings file" % self.tts)
@@ -144,43 +144,40 @@ class NeuronModule(object):
         """
         returned_message = None
 
-        if (self.say_template is not None and self.file_template is None) or \
-                (self.say_template is None and self.file_template is not None):
-
-            # the user choose a say_template option
-            if self.say_template is not None:
-                if isinstance(self.say_template, list):
-                    # then we pick randomly one template
-                    self.say_template = random.choice(self.say_template)
-                t = Template(self.say_template)
-                returned_message = t.render(**message_dict)
-
-            # trick to remobe unicode problem when loading jinja template with non ascii char
-            reload(sys)
-            sys.setdefaultencoding('utf-8')
-            # the user choose a file_template option
-            if self.file_template is not None:  # the user choose a file_template option
-                if not os.path.isabs(self.file_template):  # os.path.isabs returns True if the path is absolute
-                    # here we are
-                    dir_we_are = os.path.dirname(os.path.realpath(__file__))
-                    # root directory
-                    root_dir = os.path.normpath(dir_we_are + os.sep + os.pardir)
-                    # real path of the template
-                    real_file_template_path = os.path.join(root_dir, self.file_template)
-                else:
-                    real_file_template_path = self.file_template
-                if os.path.isfile(real_file_template_path):
-                    # load the content of the file as template
-                    t = Template(self._get_content_of_file(real_file_template_path))
-                    returned_message = t.render(**message_dict)
-                else:
-                    raise TemplateFileNotFoundException("Template file %s not found in templates folder"
-                                                        % real_file_template_path)
-            return returned_message
-
-        # we don't force the usage of a template. The user can choose to do nothing with returned value
-        # else:
-        #     raise NoTemplateException("You must specify a say_template or a file_template")
+        # the user chooses a say_template option
+        if self.say_template is not None:
+            returned_message = self._get_say_template(self.say_template, message_dict)
+
+        # trick to remove unicode problem when loading jinja template with non ascii char
+        reload(sys)
+        sys.setdefaultencoding('utf-8')
+
+        # the user chooses a file_template option
+        if self.file_template is not None:  # the user choose a file_template option
+            returned_message = self._get_file_template(self.file_template, message_dict)
+
+        return returned_message
+
+    @staticmethod
+    def _get_say_template(list_say_template, message_dict):
+        if isinstance(list_say_template, list):
+            # then we pick randomly one template
+            list_say_template = random.choice(list_say_template)
+        t = Template(list_say_template)
+        return t.render(**message_dict)
+
+    @classmethod
+    def _get_file_template(cls, file_template, message_dict):
+        real_file_template_path = Utils.get_real_file_path(file_template)
+        if real_file_template_path is None:
+            raise TemplateFileNotFoundException("Template file %s not found in templates folder"
+                                                % real_file_template_path)
+
+        # load the content of the file as template
+        t = Template(cls._get_content_of_file(real_file_template_path))
+        returned_message = t.render(**message_dict)
+
+        return returned_message
 
     def run_synapse_by_name(self, name):
         SynapseLauncher.start_synapse(name=name, brain=self.brain)
@@ -196,17 +193,17 @@ class NeuronModule(object):
             return content_file.read()
 
     @staticmethod
-    def _update_cache_var(new_override_cache, args_list):
+    def _update_cache_var(new_override_cache, args_dict):
         """
         update the value for the key "cache" in the dict args_list
-        :param new_override_cache: cache bolean to set in place of the current one in args_list
-        :param args_list: arg list that contain "cache" to update
+        :param new_override_cache: cache boolean to set in place of the current one in args_list
+        :param args_dict: arg list that contain "cache" to update
         :return:
         """
-        logger.debug("args for TTS plugin before update: %s" % str(args_list))
-        args_list["cache"] = new_override_cache
-        logger.debug("args for TTS plugin after update: %s" % str(args_list))
-        return args_list
+        logger.debug("args for TTS plugin before update: %s" % str(args_dict))
+        args_dict["cache"] = new_override_cache
+        logger.debug("args for TTS plugin after update: %s" % str(args_dict))
+        return args_dict
 
     @staticmethod
     def get_audio_from_stt(callback):

+ 42 - 12
core/OrderAnalyser.py → kalliope/core/OrderAnalyser.py

@@ -2,9 +2,10 @@
 import re
 from collections import Counter
 
-from core.Utils import Utils
-from core.Models import Order
-from core.NeuroneLauncher import NeuroneLauncher
+from kalliope.core.Utils.Utils import Utils
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.Models import Order
+from kalliope.core.NeuronLauncher import NeuronLauncher
 
 import logging
 
@@ -16,14 +17,14 @@ class OrderAnalyser:
     """
     This Class is used to compare the incoming message to the Signal/Order sentences.
     """
-    def __init__(self, order, main_controller=None, brain=None):
+    def __init__(self, order, brain=None):
         """
         Class used to load brain and run neuron attached to the received order
         :param order: spelt order
-        :param main_controller
         :param brain: loaded brain
         """
-        self.main_controller = main_controller
+        sl = SettingLoader()
+        self.settings = sl.settings
         self.order = order
         if isinstance(self.order, str):
             self.order = order.decode('utf-8')
@@ -40,11 +41,20 @@ class OrderAnalyser:
 
         if not launched_synapses:
             Utils.print_info("No synapse match the captured order: %s" % self.order)
-        else:
-            for synapse in launched_synapses:
-                params = self._get_synapse_params(synapse, self.order)
-                for neuron in synapse.neurons:
-                    self._start_neuron(neuron, params)
+
+            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 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)
+
+        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
@@ -122,7 +132,7 @@ class OrderAnalyser:
 
         # if no error detected, we run the neuron
         if not problem_in_neuron_found:
-            NeuroneLauncher.start_neurone(neuron)
+            NeuronLauncher.start_neuron(neuron)
         else:
             Utils.print_danger("A problem has been found in the Synapse.")
 
@@ -234,3 +244,23 @@ class OrderAnalyser:
             if n > c2[k]:
                 return False
         return True
+
+    @staticmethod
+    def _get_default_synapse_from_sysnapses_list(all_synapses_list, default_synapse_name):
+        """
+        Static method to get the default synapse if it exists.
+
+        :param all_synapses_list: the complete list of all synapses
+        :param default_synapse_name: the synapse to find
+        :return: the Synapse
+        """
+        default_synapse = None
+        for synapse in all_synapses_list:
+            if synapse.name == default_synapse_name:
+                logger.debug("Default synapse found: %s" % synapse.name)
+                default_synapse = synapse
+                break
+        if default_synapse is None:
+            logger.debug("Default synapse not found")
+            Utils.print_warning("Default synapse not found")
+        return default_synapse

+ 3 - 3
core/OrderListener.py → kalliope/core/OrderListener.py

@@ -4,8 +4,8 @@ from threading import Thread
 
 from cffi import FFI as _FFI
 
-from core.Utils import Utils
-from core.ConfigurationManager import SettingLoader
+from kalliope.core.Utils.Utils import Utils
+from kalliope.core.ConfigurationManager import SettingLoader
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -39,7 +39,7 @@ class OrderListener(Thread):
         self._ignore_stderr()
         self.stt_module_name = stt
         self.callback = callback
-        sl = SettingLoader.Instance()
+        sl = SettingLoader()
         self.settings = sl.settings
 
     def run(self):

+ 0 - 0
core/Players/Mplayer.py → kalliope/core/Players/Mplayer.py


+ 0 - 0
core/Players/__init__.py → kalliope/core/Players/__init__.py


+ 17 - 6
core/RestAPI/FlaskAPI.py → kalliope/core/RestAPI/FlaskAPI.py

@@ -4,9 +4,9 @@ from flask import jsonify
 from flask import request
 from flask_restful import abort
 
-from core import OrderAnalyser
-from core.RestAPI.utils import requires_auth
-from core.SynapseLauncher import SynapseLauncher
+from kalliope.core import OrderAnalyser
+from kalliope.core.RestAPI.utils import requires_auth
+from kalliope.core.SynapseLauncher import SynapseLauncher
 
 
 class FlaskAPI(threading.Thread):
@@ -27,6 +27,7 @@ class FlaskAPI(threading.Thread):
         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)
@@ -39,9 +40,11 @@ class FlaskAPI(threading.Thread):
         """
         all_synapse = self.brain.brain_yaml
         for el in all_synapse:
-            print el
-            if el[0]["name"] in synapse_name:
-                return el[0]
+            try:
+                if el["name"] == synapse_name:
+                    return el
+            except KeyError:
+                pass
         return None
 
     @requires_auth
@@ -126,3 +129,11 @@ class FlaskAPI(threading.Thread):
                 "error": "order cannot be null"
             }
             return jsonify(error=data), 400
+
+    @requires_auth
+    def shutdown_server(self):
+        func = request.environ.get('werkzeug.server.shutdown')
+        if func is None:
+            raise RuntimeError('Not running with the Werkzeug Server')
+        func()
+        return "Shutting down..."

+ 0 - 0
core/RestAPI/__init__.py → kalliope/core/RestAPI/__init__.py


+ 3 - 3
core/RestAPI/utils.py → kalliope/core/RestAPI/utils.py

@@ -1,14 +1,14 @@
 from functools import wraps
 from flask import request, Response
 
-from core.ConfigurationManager import SettingLoader
+from kalliope.core.ConfigurationManager import SettingLoader
 
 
 def check_auth(username, password):
     """This function is called to check if a username /
     password combination is valid.
     """
-    sl = SettingLoader.Instance()
+    sl = SettingLoader()
     settings = sl.settings
     return username == settings.rest_api.login and password == settings.rest_api.password
 
@@ -24,7 +24,7 @@ def authenticate():
 def requires_auth(f):
     @wraps(f)
     def decorated(*args, **kwargs):
-        sl = SettingLoader.Instance()
+        sl = SettingLoader()
         settings = sl.settings
         if settings.rest_api.password_protected:
             auth = request.authorization

+ 6 - 6
core/ShellGui.py → kalliope/core/ShellGui.py

@@ -7,11 +7,11 @@ import sys
 
 from dialog import Dialog
 
-from core import OrderListener
-from core.ConfigurationManager import SettingLoader
-from core.SynapseLauncher import SynapseLauncher
-from core.Utils import Utils
-from neurons.say.say import Say
+from kalliope.core import OrderListener
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.SynapseLauncher import SynapseLauncher
+from kalliope.core.Utils.Utils import Utils
+from kalliope.neurons.say.say import Say
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -43,7 +43,7 @@ class ShellGui:
         self.brain = brain
 
         # get settings
-        sl = SettingLoader.Instance()
+        sl = SettingLoader()
         self.settings = sl.settings
         locale.setlocale(locale.LC_ALL, '')
 

+ 2 - 2
core/SynapseLauncher.py → kalliope/core/SynapseLauncher.py

@@ -1,4 +1,4 @@
-from core.NeuroneLauncher import NeuroneLauncher
+from kalliope.core.NeuronLauncher import NeuronLauncher
 
 
 class SynapseNameNotFound(Exception):
@@ -46,5 +46,5 @@ class SynapseLauncher(object):
         :return:
         """
         for neuron in synapse.neurons:
-            NeuroneLauncher.start_neurone(neuron)
+            NeuronLauncher.start_neuron(neuron)
         return True

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor