Repository: bird-house/birdy

Issue:
native client: how to return multiple values?
## Description

In the current *native* client implementation we either return a single value or a list of values. I would find it more descriptive to return a dictionary with: 
```
key=parameter identifier
value=parameter value
```

@huard @davidcaron 
What do you think?

Here a link from a quick google search:
https://stackoverflow.com/questions/354883/how-do-you-return-multiple-values-in-python

## Environment


## Steps to Reproduce



## Additional Information




Discussion:

Patch:
diff --git a/birdy/client/base.py b/birdy/client/base.py
index ba82dbb..20bbe60 100644
--- a/birdy/client/base.py
+++ b/birdy/client/base.py
@@ -1,5 +1,6 @@
 import types
 from collections import OrderedDict
+from collections import namedtuple
 from copy import copy
 from textwrap import dedent
 import threading
@@ -12,7 +13,8 @@ from owslib.wps import WPS_DEFAULT_VERSION, WebProcessingService, SYNC, ASYNC
 from birdy.exceptions import UnauthorizedException
 from birdy.client import utils
 from birdy.client.converters import default_converters
-from collections import namedtuple
+from birdy.utils import sanitize, delist
+
 import logging
 
 
@@ -214,8 +216,8 @@ class WPSClient(object):
             raise
 
         # Output type conversion
-        output = namedtuple(pid, [o.identifier for o in resp.processOutputs])
-        return output(**{o.identifier: self._process_output(o, pid) for o in resp.processOutputs})
+        Output = namedtuple('Output', [sanitize(o.identifier) for o in resp.processOutputs])
+        return Output._make([self._process_output(o, pid) for o in resp.processOutputs])
 
     def _notebook_monitor(self, execution, sleep=3):
         """Monitor the execution of a process using a notebook progress bar widget.
@@ -329,7 +331,7 @@ class WPSClient(object):
             if data_type is None:
                 data_type = self._outputs[pid][output.identifier].dataType
             data = [utils.convert_output_value(d, data_type) for d in output.data]
-            return utils.delist(data)
+            return delist(data)
 
         if self._convert_objects:
             # Try to convert the bytes to an object.
diff --git a/birdy/client/utils.py b/birdy/client/utils.py
index 3df6319..af09bfa 100644
--- a/birdy/client/utils.py
+++ b/birdy/client/utils.py
@@ -119,16 +119,6 @@ def convert_output_value(value, data_type):
     return value
 
 
-def delist(data):
-    """If data is a sequence with a single element, returns this element, otherwise return a namedtuple."""
-    from collections import Iterable
-
-    if (isinstance(data, Iterable) and not isinstance(data, six.string_types) and len(data) == 1):
-        return data[0]
-
-    return data
-
-
 def is_notebook():
     """Return whether or not this function is executed in a notebook environment."""
     try:
diff --git a/birdy/utils.py b/birdy/utils.py
index d9964a0..f12002d 100644
--- a/birdy/utils.py
+++ b/birdy/utils.py
@@ -1,3 +1,7 @@
+import re
+import collections
+
+import six
 from six.moves.urllib.parse import urlparse
 
 
@@ -9,3 +13,17 @@ def is_url(url):
         return False
     else:
         return True
+
+
+def sanitize(name):
+    """Lower-case name and replace all non-ascii chars by `_`."""
+    return re.sub(r'\W|^(?=\d)', '_', name.lower())
+
+
+def delist(data):
+    """If data is a sequence with a single element, returns this element, otherwise return the sequence."""
+    if (
+        isinstance(data, collections.Iterable) and not isinstance(data, six.string_types) and len(data) == 1
+    ):
+        return data[0]
+    return data
diff --git a/requirements_extra.txt b/requirements_extra.txt
index cb4981f..912a4c4 100644
--- a/requirements_extra.txt
+++ b/requirements_extra.txt
@@ -1,3 +1,3 @@
 netCDF4
 ipywidgets
-ipython
\ No newline at end of file
+ipython


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 2fa5a26..6df4a4a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -53,7 +53,7 @@ def test_interactive(capsys):
     assert m.hello("david").output == "Hello david"
     captured = capsys.readouterr()
     assert captured.out.startswith(str(datetime.date.today()))
-    assert m.binaryoperatorfornumbers() == 5
+    assert m.binaryoperatorfornumbers()[0] == 5
 
 
 @pytest.mark.skip(reason="Complex Output is not working.")
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a120e18..f86b260 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,3 +5,14 @@ def test_is_url():
     assert utils.is_url("http://localhost:5000/wps")
     assert utils.is_url("file:///path/to/my/file.txt")
     assert not utils.is_url("myfile.txt")
+
+
+def test_sanitize():
+    assert utils.sanitize('output') == 'output'
+    assert utils.sanitize('My Output 1') == 'my_output_1'
+
+
+def test_delist():
+    assert utils.delist(['one', 'two']) == ['one', 'two']
+    assert utils.delist(['one', ]) == 'one'
+    assert utils.delist('one') == 'one'