Skip to content

Commit bbaf562

Browse files
committed
Merge pull request softlayer#659 from sudorandom/call-api-filters-python-example
Adjusts how filters work with call-api. Adds --output-python option
2 parents 9477810 + 9abb249 commit bbaf562

File tree

3 files changed

+124
-24
lines changed

3 files changed

+124
-24
lines changed

SoftLayer/CLI/call_api.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,66 @@
11
"""Call arbitrary API endpoints."""
2-
import json
3-
42
import click
53

64
from SoftLayer.CLI import environment
75
from SoftLayer.CLI import formatting
6+
from SoftLayer.CLI import helpers
7+
from SoftLayer import utils
8+
9+
10+
def _build_filters(_filters):
11+
"""Builds filters using the filter options passed into the CLI.
12+
13+
This only supports the equals keyword at the moment.
14+
"""
15+
root = utils.NestedDict({})
16+
for _filter in _filters:
17+
# split "some.key=value" into ["some.key", "value"]
18+
key, value = _filter.split('=', 1)
19+
20+
current = root
21+
# split "some.key" into ["some", "key"]
22+
parts = [part.strip() for part in key.split('.')]
823

9-
# pylint: disable=unused-argument
24+
# Actually drill down and add the filter
25+
for part in parts[:-1]:
26+
current = current[part]
27+
current[parts[-1]] = utils.query_filter(value.strip())
1028

29+
return root.to_dict()
1130

12-
def validate_filter(ctx, param, value):
13-
"""Try to parse the given filter as a JSON string."""
14-
try:
15-
if value:
16-
return json.loads(value)
17-
except ValueError:
18-
raise click.BadParameter('filters need to be in JSON format')
31+
32+
def _build_python_example(args, kwargs):
33+
sorted_kwargs = sorted(kwargs.items())
34+
35+
call_str = 'import SoftLayer\n\n'
36+
call_str += 'client = SoftLayer.create_client_from_env()\n'
37+
call_str += 'result = client.call('
38+
arg_list = [repr(arg) for arg in args]
39+
arg_list += [key + '=' + repr(value)
40+
for key, value in sorted_kwargs if value]
41+
call_str += ',\n '.join(arg_list)
42+
call_str += ')'
43+
44+
return call_str
1945

2046

2147
@click.command('call', short_help="Call arbitrary API endpoints.")
2248
@click.argument('service')
2349
@click.argument('method')
2450
@click.argument('parameters', nargs=-1)
2551
@click.option('--id', '_id', help="Init parameter")
26-
@click.option('--filter', '_filter',
27-
callback=validate_filter,
28-
help="Object filter in a JSON string")
52+
@helpers.multi_option('--filter', '-f', '_filters',
53+
help="Object filters. This should be of the form: "
54+
"'property=value' or 'nested.property=value'. Complex "
55+
"filters like betweenDate are not currently supported.")
2956
@click.option('--mask', help="String-based object mask")
3057
@click.option('--limit', type=click.INT, help="Result limit")
3158
@click.option('--offset', type=click.INT, help="Result offset")
59+
@click.option('--output-python / --no-output-python',
60+
help="Show python example code instead of executing the call")
3261
@environment.pass_env
33-
def cli(env, service, method, parameters, _id, _filter, mask, limit, offset):
62+
def cli(env, service, method, parameters, _id, _filters, mask, limit, offset,
63+
output_python=False):
3464
"""Call arbitrary API endpoints with the given SERVICE and METHOD.
3565
3666
\b
@@ -40,11 +70,23 @@ def cli(env, service, method, parameters, _id, _filter, mask, limit, offset):
4070
slcli call-api Virtual_Guest getObject --id=12345
4171
slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\
4272
"2015-01-01 00:00:00" "2015-01-1 12:00:00" public
73+
slcli call-api Account getVirtualGuests \\
74+
-f 'virtualGuests.datacenter.name=dal05' \\
75+
-f 'virtualGuests.maxCpu=4' \\
76+
--mask=id,hostname,datacenter.name,maxCpu
4377
"""
44-
result = env.client.call(service, method, *parameters,
45-
id=_id,
46-
filter=_filter,
47-
mask=mask,
48-
limit=limit,
49-
offset=offset)
50-
env.fout(formatting.iter_to_table(result))
78+
79+
args = [service, method] + list(parameters)
80+
kwargs = {
81+
'id': _id,
82+
'filter': _build_filters(_filters),
83+
'mask': mask,
84+
'limit': limit,
85+
'offset': offset,
86+
}
87+
88+
if output_python:
89+
env.out(_build_python_example(args, kwargs))
90+
else:
91+
result = env.client.call(*args, **kwargs)
92+
env.fout(formatting.iter_to_table(result))

tests/CLI/modules/call_api_tests.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,62 @@
44
55
:license: MIT, see LICENSE for more details.
66
"""
7+
import json
8+
9+
from SoftLayer.CLI import call_api
710
from SoftLayer import testing
811

9-
import json
12+
13+
class BuildFilterTests(testing.TestCase):
14+
15+
def test_empty(self):
16+
assert call_api._build_filters([]) == {}
17+
18+
def test_basic(self):
19+
result = call_api._build_filters(['property=value'])
20+
assert result == {'property': {'operation': '_= value'}}
21+
22+
def test_nested(self):
23+
result = call_api._build_filters(['nested.property=value'])
24+
assert result == {'nested': {'property': {'operation': '_= value'}}}
25+
26+
def test_multi(self):
27+
result = call_api._build_filters(['prop1=value1', 'prop2=prop2'])
28+
assert result == {
29+
'prop1': {'operation': '_= value1'},
30+
'prop2': {'operation': '_= prop2'},
31+
}
1032

1133

1234
class CallCliTests(testing.TestCase):
1335

36+
def test_python_output(self):
37+
result = self.run_command(['call-api', 'Service', 'method',
38+
'--mask=some.mask',
39+
'--limit=20',
40+
'--offset=40',
41+
'--id=100',
42+
'-f nested.property=5432',
43+
'--output-python'])
44+
45+
self.assertEqual(result.exit_code, 0)
46+
# NOTE(kmcdonald): Python 3 no longer inserts 'u' before unicode
47+
# string literals but python 2 does. These are stripped out to make
48+
# this test pass on both python versions.
49+
stripped_output = result.output.replace("u'", "'")
50+
self.assertIsNotNone(stripped_output, """import SoftLayer
51+
52+
client = SoftLayer.create_client_from_env()
53+
result = client.call(u'Service',
54+
u'method',
55+
filter={u'nested': {u'property': {'operation': 5432}}},
56+
id=u'100',
57+
limit=20,
58+
mask=u'some.mask',
59+
offset=40)
60+
""")
61+
self.assertEqual(self.calls(), [], "no API calls were made")
62+
1463
def test_options(self):
1564
mock = self.set_mock('SoftLayer_Service', 'method')
1665
mock.return_value = 'test'
@@ -19,15 +68,21 @@ def test_options(self):
1968
'--mask=some.mask',
2069
'--limit=20',
2170
'--offset=40',
22-
'--id=100'])
71+
'--id=100',
72+
'-f property=1234',
73+
'-f nested.property=5432'])
2374

2475
self.assertEqual(result.exit_code, 0)
2576
self.assertEqual(json.loads(result.output), 'test')
2677
self.assert_called_with('SoftLayer_Service', 'method',
2778
mask='mask[some.mask]',
2879
limit=20,
2980
offset=40,
30-
identifier='100')
81+
identifier='100',
82+
filter={
83+
'property': {'operation': 1234},
84+
'nested': {'property': {'operation': 5432}}
85+
})
3186

3287
def test_object(self):
3388
mock = self.set_mock('SoftLayer_Service', 'method')

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import logging
2+
3+
logging.basicConfig(level=logging.DEBUG)

0 commit comments

Comments
 (0)