Skip to content

Commit 922d486

Browse files
Merge pull request #1238 from allmightyspiff/advancedFilters
Advanced filters
2 parents 6c033f2 + 66d85aa commit 922d486

File tree

3 files changed

+107
-18
lines changed

3 files changed

+107
-18
lines changed

SoftLayer/CLI/call_api.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Call arbitrary API endpoints."""
2+
import json
3+
24
import click
35

46
from SoftLayer.CLI import environment
@@ -27,8 +29,7 @@ def _build_filters(_filters):
2729
if len(top_parts) == 2:
2830
break
2931
else:
30-
raise exceptions.CLIAbort('Failed to find valid operation for: %s'
31-
% _filter)
32+
raise exceptions.CLIAbort('Failed to find valid operation for: %s' % _filter)
3233

3334
key, value = top_parts
3435
current = root
@@ -68,25 +69,60 @@ def _build_python_example(args, kwargs):
6869
return call_str
6970

7071

72+
def _validate_filter(ctx, param, value): # pylint: disable=unused-argument
73+
"""Validates a JSON style object filter"""
74+
_filter = None
75+
if value:
76+
try:
77+
_filter = json.loads(value)
78+
if not isinstance(_filter, dict):
79+
raise exceptions.CLIAbort("\"{}\" should be a JSON object, but is a {} instead.".
80+
format(_filter, type(_filter)))
81+
except json.JSONDecodeError as error:
82+
raise exceptions.CLIAbort("\"{}\" is not valid JSON. {}".format(value, error))
83+
84+
return _filter
85+
86+
87+
def _validate_parameters(ctx, param, value): # pylint: disable=unused-argument
88+
"""Checks if value is a JSON string, and converts it to a datastructure if that is true"""
89+
90+
validated_values = []
91+
for parameter in value:
92+
if isinstance(parameter, str):
93+
# looks like a JSON string...
94+
if '{' in parameter or '[' in parameter:
95+
try:
96+
parameter = json.loads(parameter)
97+
except json.JSONDecodeError as error:
98+
click.secho("{} looked like json, but was invalid, passing to API as is. {}".
99+
format(parameter, error), fg='red')
100+
validated_values.append(parameter)
101+
return validated_values
102+
103+
71104
@click.command('call', short_help="Call arbitrary API endpoints.")
72105
@click.argument('service')
73106
@click.argument('method')
74-
@click.argument('parameters', nargs=-1)
107+
@click.argument('parameters', nargs=-1, callback=_validate_parameters)
75108
@click.option('--id', '_id', help="Init parameter")
76109
@helpers.multi_option('--filter', '-f', '_filters',
77-
help="Object filters. This should be of the form: "
78-
"'property=value' or 'nested.property=value'. Complex "
79-
"filters like betweenDate are not currently supported.")
110+
help="Object filters. This should be of the form: 'property=value' or 'nested.property=value'."
111+
"Complex filters should use --json-filter.")
80112
@click.option('--mask', help="String-based object mask")
81113
@click.option('--limit', type=click.INT, help="Result limit")
82114
@click.option('--offset', type=click.INT, help="Result offset")
83115
@click.option('--output-python / --no-output-python',
84116
help="Show python example code instead of executing the call")
117+
@click.option('--json-filter', callback=_validate_filter,
118+
help="A JSON string to be passed in as the object filter to the API call."
119+
"Remember to use double quotes (\") for variable names. Can NOT be used with --filter.")
85120
@environment.pass_env
86121
def cli(env, service, method, parameters, _id, _filters, mask, limit, offset,
87-
output_python=False):
122+
output_python=False, json_filter=None):
88123
"""Call arbitrary API endpoints with the given SERVICE and METHOD.
89124
125+
For parameters that require a datatype, use a JSON string for that parameter.
90126
Example::
91127
92128
slcli call-api Account getObject
@@ -100,12 +136,23 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset,
100136
--mask=id,hostname,datacenter.name,maxCpu
101137
slcli call-api Account getVirtualGuests \\
102138
-f 'virtualGuests.datacenter.name IN dal05,sng01'
139+
slcli call-api Account getVirtualGuests \\
140+
--json-filter '{"virtualGuests":{"hostname": {"operation": "^= test"}}}' --limit=10
141+
slcli -v call-api SoftLayer_User_Customer addBulkPortalPermission --id=1234567 \\
142+
'[{"keyName": "NETWORK_MESSAGE_DELIVERY_MANAGE"}]'
103143
"""
104144

145+
if _filters and json_filter:
146+
raise exceptions.CLIAbort("--filter and --json-filter cannot be used together.")
147+
148+
object_filter = _build_filters(_filters)
149+
if json_filter:
150+
object_filter.update(json_filter)
151+
105152
args = [service, method] + list(parameters)
106153
kwargs = {
107154
'id': _id,
108-
'filter': _build_filters(_filters),
155+
'filter': object_filter,
109156
'mask': mask,
110157
'limit': limit,
111158
'offset': offset,

docs/cli/commands.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
Call API
44
========
55

6+
This function allows you to easily call any API. The format is
7+
8+
`slcli call-api SoftLayer_Service method param1 param2 --id=1234 --mask="mask[id,name]"`
9+
10+
Parameters should be in the order they are presented on sldn.softlayer.com.
11+
Any complex parameters (those that link to other datatypes) should be presented as JSON strings. They need to be enclosed in single quotes (`'`), and variables and strings enclosed in double quotes (`"`).
12+
13+
For example: `{"hostname":"test",ssh_keys:[{"id":1234}]}`
614

715
.. click:: SoftLayer.CLI.call_api:cli
816
:prog: call-api

tests/CLI/modules/call_api_tests.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ def test_object_table(self):
158158
'None': None,
159159
'Bool': True}
160160

161-
result = self.run_command(['call-api', 'Service', 'method'],
162-
fmt='table')
161+
result = self.run_command(['call-api', 'Service', 'method'], fmt='table')
163162

164163
self.assert_no_fail(result)
165164
# NOTE(kmcdonald): Order is not guaranteed
@@ -179,8 +178,7 @@ def test_object_nested(self):
179178
result = self.run_command(['call-api', 'Service', 'method'])
180179

181180
self.assert_no_fail(result)
182-
self.assertEqual(json.loads(result.output),
183-
{'this': {'is': [{'pretty': 'nested'}]}})
181+
self.assertEqual(json.loads(result.output), {'this': {'is': [{'pretty': 'nested'}]}})
184182

185183
def test_list(self):
186184
mock = self.set_mock('SoftLayer_Service', 'method')
@@ -208,8 +206,7 @@ def test_list_table(self):
208206
'None': None,
209207
'Bool': True}]
210208

211-
result = self.run_command(['call-api', 'Service', 'method'],
212-
fmt='table')
209+
result = self.run_command(['call-api', 'Service', 'method'], fmt='table')
213210

214211
self.assert_no_fail(result)
215212
self.assertEqual(result.output,
@@ -224,12 +221,10 @@ def test_parameters(self):
224221
mock = self.set_mock('SoftLayer_Service', 'method')
225222
mock.return_value = {}
226223

227-
result = self.run_command(['call-api', 'Service', 'method',
228-
'arg1', '1234'])
224+
result = self.run_command(['call-api', 'Service', 'method', 'arg1', '1234'])
229225

230226
self.assert_no_fail(result)
231-
self.assert_called_with('SoftLayer_Service', 'method',
232-
args=('arg1', '1234'))
227+
self.assert_called_with('SoftLayer_Service', 'method', args=('arg1', '1234'))
233228

234229
def test_fixture_not_implemented(self):
235230
service = 'SoftLayer_Test'
@@ -264,3 +259,42 @@ def test_fixture_exception(self):
264259
self.assertIsInstance(result.exception, SoftLayerAPIError)
265260
output = '%s::%s fixture is not implemented' % (call_service, call_method)
266261
self.assertIn(output, result.exception.faultString)
262+
263+
def test_json_filter_validation(self):
264+
json_filter = '{"test":"something"}'
265+
result = call_api._validate_filter(None, None, json_filter)
266+
self.assertEqual(result['test'], 'something')
267+
268+
# Valid JSON, but we expect objects, not simple types
269+
with pytest.raises(exceptions.CLIAbort):
270+
call_api._validate_filter(None, None, '"test"')
271+
272+
# Invalid JSON
273+
with pytest.raises(exceptions.CLIAbort):
274+
call_api._validate_filter(None, None, 'test')
275+
276+
# Empty Request
277+
result = call_api._validate_filter(None, None, None)
278+
self.assertEqual(None, result)
279+
280+
def test_json_parameters_validation(self):
281+
json_params = ('{"test":"something"}', 'String', 1234, '[{"a":"b"}]', '{funky non [ Json')
282+
result = call_api._validate_parameters(None, None, json_params)
283+
self.assertEqual(result[0], {"test": "something"})
284+
self.assertEqual(result[1], "String")
285+
self.assertEqual(result[2], 1234)
286+
self.assertEqual(result[3], [{"a": "b"}])
287+
self.assertEqual(result[4], "{funky non [ Json")
288+
289+
def test_filter_with_filter(self):
290+
result = self.run_command(['call-api', 'Account', 'getObject', '--filter=nested.property=5432',
291+
'--json-filter={"test":"something"}'])
292+
self.assertEqual(2, result.exit_code)
293+
self.assertEqual(result.exception.message, "--filter and --json-filter cannot be used together.")
294+
self.assertIsInstance(result.exception, exceptions.CLIAbort)
295+
296+
def test_json_filter(self):
297+
pass
298+
result = self.run_command(['call-api', 'Account', 'getObject', '--json-filter={"test":"something"}'])
299+
self.assert_no_fail(result)
300+
self.assert_called_with('SoftLayer_Account', 'getObject', filter={"test": "something"})

0 commit comments

Comments
 (0)