Repository: softlayer/softlayer-python
Pull request #728 (2016-05-25 21:38:51)

Adds support for the 'in' filter to slcli call-api

sudorandom: Resolves #727
coveralls: 
[![Coverage Status](https://coveralls.io/builds/6335620/badge)](https://coveralls.io/builds/6335620)

Coverage increased (+0.01%) to 79.802% when pulling **3e58fc908a9640ded3cef9236762766be09ecc75 on sudorandom:call-api-in-filter** into **def654a3bc8cff0c535d37e2b0e8b010f3eab145 on softlayer:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/6335684/badge)](https://coveralls.io/builds/6335684)

Coverage increased (+0.01%) to 79.802% when pulling **3e58fc908a9640ded3cef9236762766be09ecc75 on sudorandom:call-api-in-filter** into **def654a3bc8cff0c535d37e2b0e8b010f3eab145 on softlayer:master**.

thekuffs: lgtm :shipit:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/6335847/badge)](https://coveralls.io/builds/6335847)

Coverage increased (+0.01%) to 79.802% when pulling **24b701148736d38e4f9e7a591af50454e962cbb5 on sudorandom:call-api-in-filter** into **def654a3bc8cff0c535d37e2b0e8b010f3eab145 on softlayer:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/6335869/badge)](https://coveralls.io/builds/6335869)

Coverage increased (+0.01%) to 79.802% when pulling **24b701148736d38e4f9e7a591af50454e962cbb5 on sudorandom:call-api-in-filter** into **def654a3bc8cff0c535d37e2b0e8b010f3eab145 on softlayer:master**.

camporter: A more backwards-compatible way of changing this would be to add a new keyword. The docs for the _build_filters method says "This only supports the equals keyword at the moment", which seems to indicate that additional keywords would be added in the future.

There could be cases, such as various names on objects (images, ssh keys, etc), that may have commas in them.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/6732307/badge)](https://coveralls.io/builds/6732307)

Coverage increased (+0.04%) to 79.836% when pulling **f09debfc9a6d9825dbd7449507d5b2ec0f0b5aae on sudorandom:call-api-in-filter** into **62515bd07eb51982ed4271b574ecb96a167fe82f on softlayer:master**.


Patch:
diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py
index 31b0a76c..0adb4fa3 100644
--- a/SoftLayer/CLI/call_api.py
+++ b/SoftLayer/CLI/call_api.py
@@ -2,10 +2,16 @@
 import click
 
 from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
 from SoftLayer.CLI import formatting
 from SoftLayer.CLI import helpers
 from SoftLayer import utils
 
+SPLIT_TOKENS = [
+    ('in', ' IN '),
+    ('eq', '='),
+]
+
 
 def _build_filters(_filters):
     """Builds filters using the filter options passed into the CLI.
@@ -14,9 +20,17 @@ def _build_filters(_filters):
     """
     root = utils.NestedDict({})
     for _filter in _filters:
-        # split "some.key=value" into ["some.key", "value"]
-        key, value = _filter.split('=', 1)
-
+        operation = None
+        for operation, token in SPLIT_TOKENS:
+            # split "some.key=value" into ["some.key", "value"]
+            top_parts = _filter.split(token, 1)
+            if len(top_parts) == 2:
+                break
+        else:
+            raise exceptions.CLIAbort('Failed to find valid operation for: %s'
+                                      % _filter)
+
+        key, value = top_parts
         current = root
         # split "some.key" into ["some", "key"]
         parts = [part.strip() for part in key.split('.')]
@@ -24,7 +38,17 @@ def _build_filters(_filters):
         # Actually drill down and add the filter
         for part in parts[:-1]:
             current = current[part]
-        current[parts[-1]] = utils.query_filter(value.strip())
+
+        if operation == 'eq':
+            current[parts[-1]] = utils.query_filter(value.strip())
+        elif operation == 'in':
+            current[parts[-1]] = {
+                'operation': 'in',
+                'options': [{
+                    'name': 'data',
+                    'value': [p.strip() for p in value.split(',')],
+                }],
+            }
 
     return root.to_dict()
 
@@ -74,6 +98,8 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset,
         -f 'virtualGuests.datacenter.name=dal05' \\
         -f 'virtualGuests.maxCpu=4' \\
         --mask=id,hostname,datacenter.name,maxCpu
+    slcli call-api Account getVirtualGuests \\
+        -f 'virtualGuests.datacenter.name IN dal05,sng01'
     """
 
     args = [service, method] + list(parameters)


Test patch:
diff --git a/tests/CLI/modules/call_api_tests.py b/tests/CLI/modules/call_api_tests.py
index 84dd6901..a6ce0d76 100644
--- a/tests/CLI/modules/call_api_tests.py
+++ b/tests/CLI/modules/call_api_tests.py
@@ -7,8 +7,11 @@
 import json
 
 from SoftLayer.CLI import call_api
+from SoftLayer.CLI import exceptions
 from SoftLayer import testing
 
+import pytest
+
 
 class BuildFilterTests(testing.TestCase):
 
@@ -30,6 +33,48 @@ def test_multi(self):
             'prop2': {'operation': '_= prop2'},
         }
 
+    def test_in(self):
+        result = call_api._build_filters(['prop IN value1,value2'])
+        assert result == {
+            'prop': {
+                'operation': 'in',
+                'options': [{'name': 'data', 'value': ['value1', 'value2']}],
+            }
+        }
+
+    def test_in_multi(self):
+        result = call_api._build_filters([
+            'prop_a IN a_val1,a_val2',
+            'prop_b IN b_val1,b_val2',
+        ])
+        assert result == {
+            'prop_a': {
+                'operation': 'in',
+                'options': [{'name': 'data', 'value': ['a_val1', 'a_val2']}],
+            },
+            'prop_b': {
+                'operation': 'in',
+                'options': [{'name': 'data', 'value': ['b_val1', 'b_val2']}],
+            },
+        }
+
+    def test_in_with_whitespace(self):
+        result = call_api._build_filters(['prop IN value1 ,  value2  '])
+        assert result == {
+            'prop': {
+                'operation': 'in',
+                'options': [{'name': 'data', 'value': ['value1', 'value2']}],
+            }
+        }
+
+    def test_invalid_operation(self):
+        with pytest.raises(exceptions.CLIAbort):
+            call_api._build_filters(['prop N/A value1'])
+
+    def test_only_whitespace(self):
+        with pytest.raises(exceptions.CLIAbort):
+            call_api._build_filters([' '])
+
 
 class CallCliTests(testing.TestCase):