Browse Source

Merge pull request #86 from kalliope-project/dev

Dev to master
Nicolas Marcq 8 years ago
parent
commit
3aa67fb85a

+ 12 - 0
CHANGELOG.md

@@ -0,0 +1,12 @@
+v0.2 / 2016-11-21
+=================
+
+- add neuron URI to call web services
+- fix wikipedia neuron. renamed wikipedia_searcher
+- update neuron neurotransmitter: can now call another synapse directly without asking a question
+
+
+v0.1 / 2016-11-02
+=================
+
+- Initial Release

+ 17 - 16
Docs/neuron_list.md

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

+ 0 - 12
brain_examples/fr/fr_clock_radio.yml

@@ -1,12 +0,0 @@
----
-  - name: "wake-up"
-    signals:
-        - event: "30 7 * * 1,2,3,4,5" # each morning of working day at 7:30
-    neurons:
-      - say:
-          message:
-            - "Bonjour monsieur"
-      - systemdate:
-          say_template:
-            - "Il est {{ hours }} heures et {{ minutes }} minutes"
-      - command: "mplayer http://192.99.17.12:6410/"  # Launch Asterisk radio

+ 0 - 16
brain_examples/fr/fr_systemdate.yml

@@ -1,16 +0,0 @@
-- name: "say-local-date"
-  signals:
-    - order: "test"
-  neurons:
-    - systemdate:
-        say_template:
-          - "Il est {{ hours }} heures et {{ minutes }} minutes"
-        tts: "voxygen"
-
-- name: "say-local-date2"
-  signals:
-    - order: "tempate"
-  neurons:
-    - systemdate:
-        file_template: fr_systemdate_template_example.j2
-        tts: "voxygen"

+ 0 - 9
brain_examples/fr/hello_world.yml

@@ -1,9 +0,0 @@
----
-  - name: "say-hello"
-    signals:
-      - order: "dit bonjour"
-    neurons:
-      - say:
-          message:
-            - "Bonjour monsieur"
-            - "Bonjour maitre"

+ 0 - 8
brain_examples/fr/say_examples.yml

@@ -1,8 +0,0 @@
----
-  - name: "meaning-of-life"
-    signals:
-      - order: "quel est le sens de la vie"
-    neurons:
-      - say:
-          message:
-            - "42"

+ 19 - 2
brains/neurotransmitter.yml

@@ -1,12 +1,29 @@
 ---
+  - name: "direct-link"
+    signals:
+      - order: "lien direct"
+    neurons:
+      - say:
+          message: "je lance directement le synapse 1"
+      - neurotransmitter:
+          direct_link: "synapse-direct-link-1"
+
+  - name: "synapse-direct-link-1"
+    signals:
+      - order: "synapse-direct-link-1"
+    neurons:
+      - say:
+          message: "Synapse 1 lancé"
+
   - name: "synapse1"
     signals:
       - order: "pose moi une question"
+      - order: "pose-moi une question"
     neurons:
       - say:
           message: "aimez vous les frites?"
       - neurotransmitter:
-          links:
+          from_answer_link:
             - synapse: "synapse2"
               answers:
                 - "absolument"
@@ -36,4 +53,4 @@
       - order: "synapse4"
     neurons:
       - say:
-          message: "Je n'ai pas compris votre réponse"
+          message: "Je n'ai pas compris votre réponse"

+ 3 - 2
brains/wikipedia.yml

@@ -4,8 +4,9 @@
     signals:
       - order: "cherche sur Wikipédia {{ query }}"
     neurons:
-      - wikipedia:
+      - wikipedia_searcher:
           language: "fr"
+          sentences: 1
           args:
             - query
-          file_template: "wikipedia_returned_value.j2"
+          file_template: "templates/wikipedia_returned_value.j2"

+ 8 - 2
core/NeuronModule.py

@@ -63,6 +63,7 @@ class NeuronModule(object):
         """
         # get the child who called the class
         child_name = self.__class__.__name__
+        self.neuron_name = child_name
         logger.debug("NeuronModule called from class %s with parameters: %s" % (child_name, str(kwargs)))
 
         sl = SettingLoader.Instance()
@@ -181,7 +182,7 @@ class NeuronModule(object):
         # else:
         #     raise NoTemplateException("You must specify a say_template or a file_template")
 
-    def run_synapse_ny_name(self, name):
+    def run_synapse_by_name(self, name):
         SynapseLauncher.start_synapse(name=name, brain=self.brain)
 
     @staticmethod
@@ -217,4 +218,9 @@ class NeuronModule(object):
         oa = OrderListener(callback=callback)
         oa.start()
 
-
+    def get_neuron_name(self):
+        """
+        Return the name of the neuron who call the mother class
+        :return:
+        """
+        return self.neuron_name

+ 32 - 32
core/Tests/test_brain_loader.py

@@ -25,38 +25,38 @@ class TestBrainLoader(unittest.TestCase):
              '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_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']}}]

+ 2 - 0
install/files/python_requirements.txt

@@ -15,3 +15,5 @@ python-twitter==3.1
 flask==0.11.1
 Flask-Restful==0.3.5
 wikipedia==1.4.0
+requests==2.12.1
+httpretty==0.8.14

+ 8 - 8
manual_testing.py

@@ -1,22 +1,22 @@
 # coding: utf8
 import logging
 
-from core.ConfigurationManager.BrainLoader import BrainLoader
+from core import OrderAnalyser
+from core.ConfigurationManager import BrainLoader
 from core.ConfigurationManager.SettingLoader import SettingLoader
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger.setLevel(logging.DEBUG)
 
-# file_path = "core/Tests/brains/brain_test.yml"
-#
-# brainloader1 = BrainLoader.Instance(file_path=file_path)
-#
-# brainloader2 = BrainLoader.Instance(file_path=file_path)
+brain_to_test = "neurons/uri/tests/uri_test_brain.yml"
+bl = BrainLoader.Instance(file_path=brain_to_test)
 
-sl = SettingLoader.Instance(file_path="core/Tests/settings/settings_test.yml")
-print sl.yaml_config
+order = "test-delete-url"
 
+oa = OrderAnalyser(order, brain=bl.brain)
+
+oa.start()
 
 
 

+ 32 - 10
neurons/neurotransmitter/README.md

@@ -2,16 +2,17 @@
 
 ## Synopsis
 
-Link synapses together. Call a synapse from another one depending on the captured speech from the user.
+Link synapses together. Call a synapse directly or depending on the captured speech from the user.
 
 ## Options
 
-| parameter | required | default | choices | comment                                                                                        |
-|-----------|----------|---------|---------|------------------------------------------------------------------------------------------------|
-| links     | yes      |         |         | List of tuple synapse/answer object                                                            |
-| synapse   | yes      |         |         | Name of the synapse to launch if the captured audio from the STT is present in the answer list |
-| answers   | yes      |         |         | List of sentences that are valid for running the attached synapse                              |
-| default   | yes      |         |         | Name of the synapse to launch if the captured audio doesn't match any answers                  |
+| parameter        | required | default | choices | comment                                                                                           |
+|------------------|----------|---------|---------|---------------------------------------------------------------------------------------------------|
+| from_answer_link | NO       |         |         | Link a synapse depending on the answer of the user. Contain a list of tuple synapse/answer object |
+| direct_link      | NO       |         |         | Direct call to a synapse by the name of this one                                                  |
+| synapse          | NO       |         |         | Name of the synapse to launch if the captured audio from the STT is present in the answer list    |
+| answers          | NO       |         |         | List of sentences that are valid for running the attached synapse                                 |
+| default          | NO       |         |         | Name of the synapse to launch if the captured audio doesn't match any answers                     |
 
 ## Return Values
 
@@ -19,6 +20,26 @@ None
 
 ## Synapses example
 
+We call another synapse directly at the end of the first synapse
+```
+- name: "direct-link"
+    signals:
+      - order: "direct link"
+    neurons:
+      - say:
+          message: "I launch directly the synapse number 1"
+      - neurotransmitter:
+          direct_link: "synapse-1"
+
+  - name: "synapse-1"
+    signals:
+      - order: "synapse-direct-link-1"
+    neurons:
+      - say:
+          message: "Synapse 1 launched"
+```
+
+
 Here the synapse will ask the user if he likes french fries. If the user answer "yes" or "maybe", he will be redirected to the synapse2 that say something.
 If the user answer no, he will be redirected to another synapse that say something else.
 If the user say something that is not present in `answers`, he will be redirected to the synapse4.
@@ -31,7 +52,7 @@ If the user say something that is not present in `answers`, he will be redirecte
       - say:
           message: "do you like french fries?"
       - neurotransmitter:
-          links:
+          from_answer_link:
             - synapse: "synapse2"
               answers:
                 - "absolutely"
@@ -60,8 +81,9 @@ If the user say something that is not present in `answers`, he will be redirecte
       - order: "synapse4"
     neurons:
       - say:
-          message: "I havn't understood your answer"
+          message: "I haven't understood your answer"
 ```
 
-
+## Notes
+> When using the neuron neurotransmitter, you must set a `direct_link` or a `from_answer_link`, no both at the same time.
 

+ 34 - 20
neurons/neurotransmitter/neurotransmitter.py

@@ -1,6 +1,6 @@
 import logging
 
-from core.NeuronModule import NeuronModule, MissingParameterException
+from core.NeuronModule import NeuronModule, InvalidParameterException
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -10,13 +10,19 @@ class Neurotransmitter(NeuronModule):
     def __init__(self, **kwargs):
         super(Neurotransmitter, self).__init__(**kwargs)
 
-        # get links
-        self.links = kwargs.get('links', None)
+        # get parameters
+        self.from_answer_link = kwargs.get('from_answer_link', None)
         self.default = kwargs.get('default', None)
+        self.direct_link = kwargs.get('direct_link', None)
+
         # do some check
-        if self._links_content_ok():
-            # the brain seems fine, we call the stt to get an audio
-            self.get_audio_from_stt(callback=self.callback)
+        if self._is_parameters_ok():
+            if self.direct_link is not None:
+                logger.debug("Neurotransmitter directly call to the synapse name: %s" % self.direct_link)
+                self.run_synapse_by_name(self.direct_link)
+            else:
+                # the user is using a from_answer_link, we call the stt to get an audio
+                self.get_audio_from_stt(callback=self.callback)
 
     def callback(self, audio):
         """
@@ -28,32 +34,40 @@ class Neurotransmitter(NeuronModule):
         # print self.links
         # set a bool to know if we have found a valid answer
         found = False
-        for el in self.links:
+        for el in self.from_answer_link:
             if audio in el["answers"]:
                 found = True
-                self.run_synapse_ny_name(el["synapse"])
+                self.run_synapse_by_name(el["synapse"])
                 # we don't need to check to rest of answer
                 break
         if not found:
             # the answer do not correspond to any answer. We run the default synapse
-            self.run_synapse_ny_name(self.default)
+            self.run_synapse_by_name(self.default)
 
-    def _links_content_ok(self):
+    def _is_parameters_ok(self):
         """
         Check if received links are ok to perform operations
-        :return: true if links are ok, raise an exception otherwise
+        :return: true if the neuron is well configured, raise an exception otherwise
 
         .. raises:: MissingParameterException
         """
+        # with the neuron the user has the choice of a direct link that call another synapse,
+        #  or a link with an answer caught from the STT engine
+
+        # we cannot use at the same time a direct redirection and a link with question
+        if self.direct_link is not None and self.from_answer_link is not None:
+            raise InvalidParameterException("neurotransmitter cannot be used with both direct_link and from_answer_link")
+
+        if self.direct_link is None and self.from_answer_link is None:
+            raise InvalidParameterException("neurotransmitter must be used with direct_link or from_answer_link")
 
-        if self.links is None:
-            raise MissingParameterException("links parameter required and must contain at least one link")
-        if self.default is None:
-            raise MissingParameterException("default parameter is required and must contain a valid synapse name")
-        for el in self.links:
-            if "synapse" not in el:
-                raise MissingParameterException("Links must contain a synapse name: %s" % el)
-            if "answers" not in el:
-                raise MissingParameterException("Links must contain answers: %s" % el)
+        if self.from_answer_link is not None:
+            if self.default is None:
+                raise InvalidParameterException("default parameter is required and must contain a valid synapse name")
+            for el in self.from_answer_link:
+                if "synapse" not in el:
+                    raise InvalidParameterException("Links must contain a synapse name: %s" % el)
+                if "answers" not in el:
+                    raise InvalidParameterException("Links must contain answers: %s" % el)
 
         return True

+ 0 - 0
neurons/neurotransmitter/tests/__init__.py


+ 55 - 0
neurons/neurotransmitter/tests/neurotransmitter_test_brain.yml

@@ -0,0 +1,55 @@
+---
+  - name: "direct-link"
+    signals:
+      - order: "direct link"
+    neurons:
+      - say:
+          message: "je lance directement le synapse 1"
+      - neurotransmitter:
+          direct_link: "synapse-direct-link-1"
+
+  - name: "synapse-direct-link-1"
+    signals:
+      - order: "synapse-direct-link-1"
+    neurons:
+      - say:
+          message: "Synapse 1 lancé"
+
+  - name: "synapse1"
+    signals:
+      - order: "pose moi une question"
+    neurons:
+      - say:
+          message: "aimez vous les frites?"
+      - neurotransmitter:
+          from_answer_link:
+            - synapse: "synapse2"
+              answers:
+                - "absolument"
+                - "peut-être"
+            - synapse: "synapse3"
+              answers:
+                - "non"
+          default: "synapse4"
+
+  - name: "synapse2"
+    signals:
+      - order: "synapse2"
+    neurons:
+      - say:
+          message: "Vous aimez les frites! Moi aussi!"
+
+
+  - name: "synapse3"
+    signals:
+      - order: "synapse3"
+    neurons:
+      - say:
+          message: "vous n'aimez pas les frites. c'est pas grave."
+
+  - name: "synapse4"
+    signals:
+      - order: "synapse4"
+    neurons:
+      - say:
+          message: "Je n'ai pas compris votre réponse"

+ 155 - 0
neurons/uri/README.md

@@ -0,0 +1,155 @@
+# Uri
+
+## Synopsis
+
+Interacts with HTTP and HTTPS web services.
+
+## Options
+
+| parameter      | required | default | choices                                      | comment                                                                    |
+|----------------|----------|---------|----------------------------------------------|----------------------------------------------------------------------------|
+| url            | YES      |         |                                              | HTTP or HTTPS URL in the form (http|https)://host.domain[:port]/path       |
+| headers        | NO       |         | E.g: Content-Type: 'application/json'        | Add custom HTTP headers to a request in the format of a YAML hash          |
+| data           | NO       |         | E.g: "{\"title\": \"foo\"}"                  | JSON data to send to the server. You must escape quotes in the YAML file.  |
+| data_from_file | NO       |         | E.g: /path/to/my/file.json                   | JSON data loaded from a file.                                              |
+| method         | NO       | GET     | GET, POST, DELETE, PUT, HEAD, PATCH, OPTIONS | The HTTP method of the request or response. It MUST be uppercase.          |
+| user           | NO       |         |                                              | username for the basic authentication.                                     |
+| password       | NO       |         |                                              | passwordfor the basic authentication.                                      |
+| timeout        | NO       |         |                                              | The socket level timeout in seconds. Must be an integer without quotes     |
+
+
+## Return Values
+
+| Name            | Description                                                                            | Type   | sample                                                                                          |
+|-----------------|----------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------------------|
+| status_code     | Numeric, HTTP status code that signifies success of the request.                       | int    | 200                                                                                             |
+| content         | The body content of the server answer. May be JSON serialized and usable in a template | string | {"title": "foo", "body": "bar", "userId": 1}                                                    |
+| response_header | Response header from the server.                                                       | dict   | {'Date': 'Sat, 19 Nov 2016 11:17:56 GMT', 'Content-Length': '192', 'Content-Type': 'text/html'} |
+
+## Synapses example
+
+Simple call to a server. The default method is GET
+```
+  - name: "test-get-url"
+    signals:
+      - order: "test-get-url"
+    neurons:
+      - uri:
+          url: "http://host.domain/get/1"             
+```
+
+A simple call with authentication
+```
+- name: "test-get-url-with-auth"
+    signals:
+      - order: "test-get-url-with-auth"
+    neurons:
+      - uri:
+          url: "http://host.domain/get/1"        
+          user: "admin"
+          password: "secret"
+```
+
+A simple post with data inside the url 
+```
+- name: "test-post-url-with-auth"
+    signals:
+      - order: "test-post-url-with-data"
+    neurons:
+      - uri:
+          url: "http://host.domain/login?email=user@host.domain&password=foobar123" 
+          method: POST          
+```
+
+A post with json data. Note that we need to escape quotes from the payload.
+```
+- name: "test-post-url"
+    signals:
+      - order: "test-post-url"
+    neurons:
+      - uri:
+          url: "http://host.domain/posts"
+          method: POST
+          headers:            
+            Content-Type: 'application/json'      
+          data: "{\"id\": 1,\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}"         
+```
+
+A post with json data imported from a file and a custom header.
+```
+- name: "test-post-url"
+    signals:
+      - order: "test-post-url"
+    neurons:
+      - uri:
+          url: "http://host.domain/posts"
+          method: POST
+          headers:            
+            Content-Type: 'application/json'
+            user-agent: 'my-app/0.0.1'
+          data_from_file: "/path/to/my/payload.json"        
+```
+
+And the `payload.json` would be (note that we don't need to escape any character):
+```
+{"title": "foo", "body": "bar", "userId": 1}
+```
+
+A simple call to a deletion. Here we also ask Kalliope to tell use if request was a success through a template, depending on the returned status code.
+```
+  - name: "test-delete-url"
+    signals:
+      - order: "test-delete-url"
+    neurons:
+      - uri:
+          url: "http://host.domain/posts/1"
+          method: DELETE          
+          say_template:
+            - "{% if status_code==201 %}delete complete{% else %}fail to delete{% endif %}"
+```
+
+Call to an URL to get a resource and make kalliope speaking out loud a value of returned content.
+Here, we ask the server to return us the user with ID number 42. We know that the server will return a dict like the following
+```
+{
+  "id": 42,
+  "name": "Leanne Graham",
+  "username": "Bret",
+  "email": "Sincere@april.biz",
+  "phone": "1-770-736-8031 x56442",
+  "website": "hildegard.org",
+  "company": {
+    "name": "Romaguera-Crona",
+    "catchPhrase": "Multi-layered client-server neural-net",
+    "bs": "harness real-time e-markets"
+  }
+}
+```
+
+Here is the synapse we would use to make Kalliope speak out loud the name of the user and the name of his company
+```
+  - name: "test-get-url-with-template"
+    signals:
+      - order: "test-get-url-with-template"
+    neurons:
+      - uri:
+          url: "http://host.domain/users/42"                
+          say_template:
+            - "The user name is {{ content.name }} and his company's name is {{ content.company.name }}"
+```
+
+
+## Templates example 
+
+The following template will make kalliope read the content of the received json variable "title" from the remote server.
+```
+The title is {{ content.title }}
+```
+
+The following template will make Kalliope say if the request has been made with success or not depending on the returned status code.
+```
+{% if status_code==200 %}request complete{% else %}request failled{% endif %}
+```
+
+## Notes
+> When the parameter `data` is used, you need to escape character that could be interpreted by the YAML syntax.

+ 0 - 0
neurons/uri/__init__.py


+ 0 - 0
neurons/uri/tests/__init__.py


+ 1 - 0
neurons/uri/tests/data_post_test.json

@@ -0,0 +1 @@
+{"title": "foo", "body": "bar", "userId": 1}

+ 1 - 0
neurons/uri/tests/test_get_template.j2

@@ -0,0 +1 @@
+The title is {{ content.title }}

+ 203 - 0
neurons/uri/tests/test_uri_neuron.py

@@ -0,0 +1,203 @@
+import json
+import unittest
+
+from httpretty import httpretty
+
+from core.NeuronModule import InvalidParameterException
+from neurons.uri.uri import Uri
+
+
+class TestUri(unittest.TestCase):
+
+    def setUp(self):
+        self.test_url = "http://kalliope.fr/voices/"
+
+    def testGet(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.GET, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testGetRaw(self):
+        expected_content = 'raw line'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.GET, self.test_url, body=expected_content)
+        parameters = {
+            "url": self.test_url
+        }
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.content, expected_content)
+
+    def testPost(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.POST, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "POST"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testPut(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.PUT, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "PUT"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testDelete(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.DELETE, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "DELETE"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testOptions(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.OPTIONS, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "OPTIONS"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testHead(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.HEAD, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "HEAD"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.status_code, 200)
+
+    def testPatch(self):
+        expected_content = '{"voice": "nico"}'
+        httpretty.enable()
+        httpretty.register_uri(httpretty.PATCH, self.test_url, body=expected_content)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "PATCH"
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.text, expected_content)
+
+    def testParameters(self):
+        def run_test(parameters_to_test):
+            with self.assertRaises(InvalidParameterException):
+                Uri(**parameters_to_test)
+
+        parameters = dict()
+        run_test(parameters)
+
+        parameters = {
+            "url": self.test_url,
+            "headers": 1
+        }
+        run_test(parameters)
+
+        parameters = {
+            "url": self.test_url,
+            "timeout": "string"
+        }
+        run_test(parameters)
+
+        parameters = {
+            "url": self.test_url,
+            "data": "this is a data",
+            "data_from_file": "this is another data"
+        }
+        run_test(parameters)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "NONEXIST"
+        }
+        run_test(parameters)
+
+    def testPostJsonFromFile(self):
+        """
+        Test that we are able to send json data through a file
+        :return:
+        """
+        def request_callback(request, url, headers):
+            data = json.loads(request.body)
+            if "title" in data and "body" in data and "userId" in data:
+                return 200, headers, "all key received from URL %s" % url
+
+            return 400, headers, "server did not receive all keys from URL %s" % url
+
+        httpretty.enable()
+        httpretty.register_uri(httpretty.POST, self.test_url, body=request_callback)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "POST",
+            "data_from_file": "data_post_test.json",
+            "headers": {
+                "Content-Type": 'application/json'
+            }
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.status_code, 200)
+
+    def testPostJson(self):
+        """
+        Test that we are able to send json data directly from the data variable
+        :return:
+        """
+        def request_callback(request, url, headers):
+            data = json.loads(request.body)
+            if "title" in data and "body" in data and "userId" in data:
+                return 200, headers, "all key received from URL %s" % url
+
+            return 400, headers, "server did not receive all keys from URL %s" % url
+
+        httpretty.enable()
+        httpretty.register_uri(httpretty.POST, self.test_url, body=request_callback)
+
+        parameters = {
+            "url": self.test_url,
+            "method": "POST",
+            "data": "{\"id\": 1,\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}",
+            "headers": {
+                "Content-Type": 'application/json'
+            }
+        }
+
+        uri_neuron = Uri(**parameters)
+        self.assertEqual(uri_neuron.status_code, 200)
+
+if __name__ == '__main__':
+    unittest.main()

+ 71 - 0
neurons/uri/tests/uri_test_brain.yml

@@ -0,0 +1,71 @@
+---
+  # GET
+  - name: "test-get-url"
+    signals:
+      - order: "test-get-url"
+    neurons:
+      - uri:
+          url: "http://jsonplaceholder.typicode.com/posts/1"
+          file_template: "neurons/uri/tests/test_get_template.j2"
+
+  # POST
+  - name: "test-post-url"
+    signals:
+      - order: "test-post-url"
+    neurons:
+      - uri:
+          url: "http://jsonplaceholder.typicode.com/posts"
+          headers:
+            user-agent: 'my-app/0.0.1'
+            Content-Type: 'application/json'
+          timeout: 12
+          method: POST
+          data_from_file: "neurons/uri/tests/data_post_test.json"
+          say_template:
+            - "{{ content.id }}"
+
+  # POST with auth
+  - name: "test-post-url-with-auth"
+    signals:
+      - order: "test-post-url-with-auth"
+    neurons:
+      - uri:
+          url: "http://192.168.0.17:8000/app"
+          headers:
+            Content-Type: 'application/json'
+          method: POST
+          user: "admin"
+          password: "secret"
+          data_from_file: "neurons/uri/tests/data_post_test.json"
+
+  # PUT
+  - name: "test-put-url"
+    signals:
+      - order: "test-put-url"
+    neurons:
+      - uri:
+          url: "http://jsonplaceholder.typicode.com/posts/1"
+          method: PUT
+          data: "{\"id\": 1,\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}"
+
+  # PATCH
+  - name: "test-patch-url"
+    signals:
+      - order: "test-patch-url"
+    neurons:
+      - uri:
+          url: "http://jsonplaceholder.typicode.com/posts/1"
+          method: PATCH
+          data: "{\"title\": \"foo\"}"
+
+  # DELETE
+  - name: "test-delete-url"
+    signals:
+      - order: "test-delete-url"
+    neurons:
+      - uri:
+          url: "http://jsonplaceholder.typicode.com/posts/1"
+          method: DELETE
+          timeout: 2
+          say_template:
+            - "{% if status_code==200 %}delete complete{% else %}fail to delete{% endif %}"

+ 197 - 0
neurons/uri/uri.py

@@ -0,0 +1,197 @@
+import json
+import logging
+import os
+
+import requests
+
+from core.NeuronModule import NeuronModule, InvalidParameterException
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class Uri(NeuronModule):
+
+    def __init__(self, **kwargs):
+        super(Uri, self).__init__(**kwargs)
+
+        # input variables
+        self.url = kwargs.get('url', None)
+        self.headers = kwargs.get('headers', None)
+        self.data = kwargs.get('data', None)
+        self.data_from_file = kwargs.get('data_from_file', None)
+        self.method = kwargs.get('method', "GET")
+        self.user = kwargs.get('user', None)
+        self.password = kwargs.get('password', None)
+        self.timeout = kwargs.get('timeout', None)
+
+        # processing parameters
+        self.parameters = None
+
+        # output variable
+        self.status_code = None
+        self.content = None
+        self.text = None
+        self.response_header = None
+
+        # this is a switch case option
+        switch_case = {
+            "GET": self.do_get,
+            "POST": self.do_post,
+            "DELETE": self.do_delete,
+            "PUT": self.do_put,
+            "HEAD": self.do_head,
+            "PATCH": self.do_patch,
+            "OPTIONS": self.do_options
+        }
+
+        # check parameters
+        if self._is_parameters_ok():
+            # we get parameters that will be passed to the requests lib
+            self.parameters = self.get_parameters()
+            # we call the right method depending of the method selected
+            switch_case[self.method]()
+
+            message = {
+                "status_code": self.status_code,
+                "content": self.content,
+                "response_header": self.response_header
+            }
+
+            self.say(message)
+
+    def do_get(self):
+        logger.debug(self.neuron_name + " do_get method called")
+        r = requests.get(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_post(self):
+        logger.debug(self.neuron_name + " do_post method called")
+        r = requests.post(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_delete(self):
+        logger.debug(self.neuron_name + " do_delete method called")
+        r = requests.delete(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_put(self):
+        logger.debug(self.neuron_name + " do_put method called")
+        r = requests.put(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_head(self):
+        logger.debug(self.neuron_name + " do_head method called")
+        r = requests.head(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_patch(self):
+        logger.debug(self.neuron_name + " do_patch method called")
+        r = requests.patch(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def do_options(self):
+        logger.debug(self.neuron_name + " do_options method called")
+        r = requests.options(url=self.url, **self.parameters)
+        self.post_processing_request(r)
+
+    def get_parameters(self):
+        """
+
+        :return: Dict of parameters usable by the "requests" lib
+        """
+        returned_parameters = dict()
+
+        if self.headers is not None:
+            returned_parameters["headers"] = self.headers
+
+        if self.timeout is not None:
+            returned_parameters["timeout"] = self.timeout
+
+        if self.data is not None:
+            returned_parameters["data"] = self.data
+
+        if self.data_from_file is not None:
+            returned_parameters["data"] = self.data_from_file
+
+        if self.user is not None:
+            # this implicitly means that the password is set too, the check has been done in _is_parameters_ok
+            returned_parameters["auth"] = self.user, self.password
+
+        logger.debug(self.neuron_name + " parameters: %s" % returned_parameters)
+
+        return returned_parameters
+
+    def post_processing_request(self, r):
+        self.status_code = r.status_code
+        self.content = r.content
+        # we try to load into a json object the content. So Kalliope can use it to talk
+        try:
+            self.content = json.loads(self.content)
+        except ValueError:
+            logger.debug(self.neuron_name + "cannot get a valid json from returned content")
+            pass
+        self.text = r.text
+        self.response_header = r.headers
+
+        logger.debug(self.neuron_name + " status_code: %s" % self.status_code)
+        logger.debug(self.neuron_name + " content: %s" % self.content)
+        logger.debug(self.neuron_name + " response_header: %s" % self.response_header)
+
+    def _is_parameters_ok(self):
+        """
+        Check that all provided parameters in the neurons are valid
+        :return: True if all check passed
+        """
+        # URL is mandatory
+        if self.url is None:
+            raise InvalidParameterException("Uri needs an url")
+
+        # headers can be null, but if provided it must be a list
+        if self.headers is not None:
+            if not isinstance(self.headers, dict):
+                raise InvalidParameterException("headers must be a list of key: value")
+
+        # timeout in second must be an integer
+        if self.timeout is not None:
+            if not isinstance(self.timeout, int):
+                raise InvalidParameterException("timeout must be an integer")
+
+        # data must be loadable with json
+        if self.data is not None:
+            try:
+                json.loads(self.data)
+            except ValueError, e:
+                raise InvalidParameterException("error in \"data\" parameter: %s" % e)
+
+        # data_from_file path must exist and data inside must be loadable by json
+        if self.data_from_file is not None:
+            # check that the file exist
+            if not os.path.exists(self.data_from_file):
+                raise InvalidParameterException("error in \"data_file\". File does not exist: %s" % self.data_from_file)
+            # then try to load the json from the file
+            try:
+                self.data_from_file = self.readfile(self.data_from_file)
+            except ValueError, e:
+                raise InvalidParameterException("error in \"data\" parameter: %s" % e)
+
+        # we cannot provide both data and data from file
+        if self.data is not None and self.data_from_file is not None:
+            raise InvalidParameterException("URI can be used with data or data_from_file, not both in same time")
+
+        # the provided method must exist
+        allowed_method = ["GET", "POST", "DELETE", "PUT", "HEAD", "PATCH", "OPTIONS"]
+        if self.method not in allowed_method:
+            raise InvalidParameterException("method %s not in: %s" % (self.method, allowed_method))
+
+        return True
+
+    @staticmethod
+    def readfile(file_path):
+        """
+        return the content of the file <file_path>
+        :param file_path: File path to read
+        :return: Str content of the file
+        """
+        file_to_read = open(file_path, 'r')
+        return file_to_read.read()

+ 0 - 1
neurons/wikipedia/__init__.py

@@ -1 +0,0 @@
-from wikipedia import Wikipedia

+ 2 - 2
neurons/wikipedia/README.md → neurons/wikipedia_searcher/README.md

@@ -1,4 +1,4 @@
-# wikipedia
+# wikipedia_searcher
 
 ## Synopsis
 
@@ -36,7 +36,7 @@ This synapse will look for the {{ query }} spelt by the user on Wikipedia
   signals:
     - order: "look on wikipedia {{ query }}"
   neurons:
-    - wikipedia:
+    - wikipedia_searcher:
         language: "en"
         args:
           - query

+ 1 - 0
neurons/wikipedia_searcher/__init__.py

@@ -0,0 +1 @@
+from wikipedia_searcher import Wikipedia_searcher

+ 0 - 0
neurons/wikipedia_searcher/tests/__init__.py


+ 67 - 0
neurons/wikipedia_searcher/tests/test_wikipedia_searcher.py

@@ -0,0 +1,67 @@
+import unittest
+
+from core.NeuronModule import InvalidParameterException
+from neurons.wikipedia_searcher import Wikipedia_searcher
+
+
+class TestWikipediaSearcher(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_parameters(self):
+        def run_test(parameters_to_test):
+            with self.assertRaises(InvalidParameterException):
+                Wikipedia_searcher(**parameters_to_test)
+
+        parameters = dict()
+        run_test(parameters)
+
+        # sentences must be an integer
+        parameters = {
+            "language": "en",
+            "query": "this is the query",
+            "sentences": "invalid"
+
+        }
+        run_test(parameters)
+
+        # test non existing language
+        parameters = {
+            "language": "foo",
+            "query": "this is the query",
+            "sentences": 1
+
+        }
+        run_test(parameters)
+
+    def test_get_DisambiguationError(self):
+
+        parameters = {
+            "language": "fr",
+            "query": "bot",
+            "sentences": 1
+        }
+
+        wiki = Wikipedia_searcher(**parameters)
+        self.assertEqual(wiki.returncode, "DisambiguationError")
+
+    def test_page_error(self):
+        parameters = {
+            "language": "fr",
+            "query": "fudu foo bar non exist",
+            "sentences": 1
+        }
+
+        wiki = Wikipedia_searcher(**parameters)
+        self.assertEqual(wiki.returncode, "PageError")
+
+    def test_summary_found(self):
+        parameters = {
+            "language": "fr",
+            "query": "kalliope"
+        }
+        wiki = Wikipedia_searcher(**parameters)
+        self.assertEqual(wiki.returncode, "SummaryFound")
+
+if __name__ == '__main__':
+    unittest.main()

+ 10 - 5
neurons/wikipedia/wikipedia.py → neurons/wikipedia_searcher/wikipedia_searcher.py

@@ -7,14 +7,14 @@ logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
 
-class Wikipedia(NeuronModule):
+class Wikipedia_searcher(NeuronModule):
     def __init__(self, **kwargs):
         # we don't need the TTS cache for this neuron
         cache = kwargs.get('cache', None)
         if cache is None:
             cache = False
             kwargs["cache"] = cache
-        super(Wikipedia, self).__init__(**kwargs)
+        super(Wikipedia_searcher, self).__init__(**kwargs)
 
         # get parameters form the neuron
         self.query = kwargs.get('query', None)
@@ -23,6 +23,7 @@ class Wikipedia(NeuronModule):
 
         self.may_refer = None
         self.returncode = None
+        self.message = None
 
         # check parameters
         if self._is_parameters_ok():
@@ -46,14 +47,14 @@ class Wikipedia(NeuronModule):
                 self.returncode = "PageError"
                 summary = ""
 
-            message = {
+            self.message = {
                 "summary": summary,
                 "may_refer": self.may_refer,
                 "returncode": self.returncode
             }
-            logger.debug("Wikipedia returned message: %s" % str(message))
+            logger.debug("Wikipedia returned message: %s" % str(self.message))
 
-            self.say(message)
+            self.say(self.message)
 
     def _is_parameters_ok(self):
         """
@@ -72,4 +73,8 @@ class Wikipedia(NeuronModule):
         if self.language not in valid_language:
             raise InvalidParameterException("Wikipedia needs a valid language: %s" % valid_language)
 
+        if self.sentences is not None:
+            if not isinstance(self.sentences, int):
+                raise InvalidParameterException("Number of sentences must be an integer")
+
         return True