Repository: jddeal/python-cmr

Issue:
Param() method that accepts keyword args
This might be kind of a neat alternative to using the different methods directly. For example:
```python
# current way
api.short_name("AST_L1T").version("006").point(-100, 42).query()

# param() way
api.params(
    short_name="AST_L1T",
    version="006",
    point=(-100, 42)
).query()
```

And then in params() the class could inspect itself for those methods and delegate the parameters to the appropriate methods that we've already made. I'm thinking about this as an alternative to how we do it, not a replacement.

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 91f2785..f2fb6d5 100644
--- a/README.rst
+++ b/README.rst
@@ -125,6 +125,22 @@ Collection searches support these methods (in addition to the shared methods abo
     >>> api.keyword("M*D09")
 
 
+As an alternative to chaining methods together to set the parameters of your query, a
+method exists to allow you to pass your parameters as keyword arguments:
+
+::
+
+    # search for AST_L1T version 003 granules at latitude 42, longitude -100
+    >>> api.parameters(
+        short_name="AST_L1T",
+        version="003",
+        point=(-100, 42)
+    )
+
+Note: the kwarg key should match the name of a method from the above examples, and the value
+should be a tuple if it's a parameter that requires multiple values.
+
+
 To inspect and retreive results from the API, the following methods are available:
 
 ::
diff --git a/cmr/queries.py b/cmr/queries.py
index bfc187c..7f56c22 100644
--- a/cmr/queries.py
+++ b/cmr/queries.py
@@ -8,6 +8,7 @@ except ImportError:
     from urllib import pathname2url as quote
 
 from datetime import datetime
+from inspect import getmembers, ismethod
 from re import search
 from requests import get, exceptions
 
@@ -99,6 +100,35 @@ class Query(object):
 
         return self.get(self.hits())
 
+    def parameters(self, **kwargs):
+        """
+        Provide query parameters as keyword arguments. The keyword needs to match the name
+        of the method, and the value should either be the value or a tuple of values.
+
+        Example: parameters(short_name="AST_L1T", point=(42.5, -101.25))
+
+        :returns: Query instance
+        """
+
+        # build a dictionary of method names and their reference
+        methods = {}
+        for name, func in getmembers(self, predicate=ismethod):
+            methods[name] = func
+
+        for key, val in kwargs.items():
+
+            # verify the key matches one of our methods
+            if key not in methods:
+                raise ValueError("Unknown key {}".format(key))
+
+            # call the method
+            if isinstance(val, tuple):
+                methods[key](*val)
+            else:
+                methods[key](val)
+
+        return self
+
     def format(self, output_format="json"):
         """
         Sets the format for the returned results.


Test patch:
diff --git a/tests/test_granule.py b/tests/test_granule.py
index 11b481b..338ca10 100644
--- a/tests/test_granule.py
+++ b/tests/test_granule.py
@@ -350,6 +350,22 @@ class TestGranuleClass(unittest.TestCase):
         with self.assertRaises(ValueError):
             query = GranuleQuery(None)
     
+    def test_valid_parameters(self):
+        query = GranuleQuery()
+
+        query.parameters(short_name="AST_L1T", version="003", point=(-100, 42))
+
+        self.assertEqual(query.params["short_name"], "AST_L1T")
+        self.assertEqual(query.params["version"], "003")
+        self.assertEqual(query.params["point"], "-100.0,42.0")
+    
+    def test_invalid_parameters(self):
+        query = GranuleQuery()
+
+        with self.assertRaises(ValueError):
+            query.parameters(fake=123)
+            query.parameters(point=(-100, "badvalue"))
+
     def test_valid_formats(self):
         query = GranuleQuery()
         formats = ["json", "xml", "echo10", "iso", "iso19115", "csv", "atom", "kml", "native"]