Skip to content

Commit 3f3066b

Browse files
author
Mike Fedosin
committed
Extend images CLI v2 with new sorting syntax
This code enables new syntax for sorting output with multiple keys and directions based on API Working group sorting guidelines. It's a client code to consume API modified in change Ie4ccfefa0492a3ac94cc7e22201f2f2be5c1cdbb Example: glance --os-image-api-version 2 --sort name:desc,size:asc Implements-blueprint: glance-sorting-enhancements DocImpact Depends-On: Ie4ccfefa0492a3ac94cc7e22201f2f2be5c1cdbb Change-Id: I36a9fa9f0508fea1235de2ac3a0d6a093e1af635
1 parent bbd27d5 commit 3f3066b

File tree

4 files changed

+140
-22
lines changed

4 files changed

+140
-22
lines changed

glanceclient/v2/images.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ def _wrap(value):
4747
return [value]
4848
return value
4949

50+
@staticmethod
51+
def _validate_sort_param(sort):
52+
"""Validates sorting argument for invalid keys and directions values.
53+
54+
:param sort: comma-separated list of sort keys with optional <:dir>
55+
after each key
56+
"""
57+
for sort_param in sort.strip().split(','):
58+
key, _sep, dir = sort_param.partition(':')
59+
if dir and dir not in SORT_DIR_VALUES:
60+
msg = ('Invalid sort direction: %(sort_dir)s.'
61+
' It must be one of the following: %(available)s.'
62+
) % {'sort_dir': dir,
63+
'available': ', '.join(SORT_DIR_VALUES)}
64+
raise exc.HTTPBadRequest(msg)
65+
if key not in SORT_KEY_VALUES:
66+
msg = ('Invalid sort key: %(sort_key)s.'
67+
' It must be one of the following: %(available)s.'
68+
) % {'sort_key': key,
69+
'available': ', '.join(SORT_KEY_VALUES)}
70+
raise exc.HTTPBadRequest(msg)
71+
return sort
72+
5073
def list(self, **kwargs):
5174
"""Retrieve a listing of Image objects
5275
@@ -104,16 +127,6 @@ def paginate(url, page_size, limit=None):
104127
# the page_size as Glance's limit.
105128
filters['limit'] = page_size
106129

107-
sort_dir = self._wrap(kwargs.get('sort_dir', []))
108-
sort_key = self._wrap(kwargs.get('sort_key', []))
109-
110-
if len(sort_key) != len(sort_dir) and len(sort_dir) > 1:
111-
raise exc.HTTPBadRequest("Unexpected number of sort directions: "
112-
"provide only one default sorting "
113-
"direction for each key or make sure "
114-
"that sorting keys number matches with a "
115-
"number of sorting directions.")
116-
117130
tags = filters.pop('tag', [])
118131
tags_url_params = []
119132

@@ -130,11 +143,27 @@ def paginate(url, page_size, limit=None):
130143
for param in tags_url_params:
131144
url = '%s&%s' % (url, parse.urlencode(param))
132145

133-
for key in sort_key:
134-
url = '%s&sort_key=%s' % (url, key)
135-
136-
for dir in sort_dir:
137-
url = '%s&sort_dir=%s' % (url, dir)
146+
if 'sort' in kwargs:
147+
if 'sort_key' in kwargs or 'sort_dir' in kwargs:
148+
raise exc.HTTPBadRequest("The 'sort' argument is not supported"
149+
" with 'sort_key' or 'sort_dir'.")
150+
url = '%s&sort=%s' % (url,
151+
self._validate_sort_param(
152+
kwargs['sort']))
153+
else:
154+
sort_dir = self._wrap(kwargs.get('sort_dir', []))
155+
sort_key = self._wrap(kwargs.get('sort_key', []))
156+
157+
if len(sort_key) != len(sort_dir) and len(sort_dir) > 1:
158+
raise exc.HTTPBadRequest(
159+
"Unexpected number of sort directions: "
160+
"either provide a single sort direction or an equal "
161+
"number of sort keys and sort directions.")
162+
for key in sort_key:
163+
url = '%s&sort_key=%s' % (url, key)
164+
165+
for dir in sort_dir:
166+
url = '%s&sort_dir=%s' % (url, dir)
138167

139168
for image in paginate(url, page_size, limit):
140169
yield image

glanceclient/v2/shell.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ def do_image_update(gc, args):
131131
@utils.arg('--sort-dir', default=[], action='append',
132132
choices=images.SORT_DIR_VALUES,
133133
help='Sort image list in specified directions.')
134+
@utils.arg('--sort', metavar='<key>[:<direction>]', default=None,
135+
help=(("Comma-separated list of sort keys and directions in the "
136+
"form of <key>[:<asc|desc>]. Valid keys: %s. OPTIONAL: "
137+
"Default='name:asc'.") % ', '.join(images.SORT_KEY_VALUES)))
134138
def do_image_list(gc, args):
135139
"""List images you can access."""
136140
filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag']
@@ -148,14 +152,15 @@ def do_image_list(gc, args):
148152
kwargs['limit'] = args.limit
149153
if args.page_size is not None:
150154
kwargs['page_size'] = args.page_size
155+
151156
if args.sort_key:
152157
kwargs['sort_key'] = args.sort_key
153-
else:
154-
kwargs['sort_key'] = ['name']
155158
if args.sort_dir:
156159
kwargs['sort_dir'] = args.sort_dir
157-
else:
158-
kwargs['sort_dir'] = ['asc']
160+
if args.sort is not None:
161+
kwargs['sort'] = args.sort
162+
elif not args.sort_dir and not args.sort_key:
163+
kwargs['sort'] = 'name:asc'
159164

160165
images = gc.images.list(**kwargs)
161166
columns = ['ID', 'Name']

tests/v2/test_images.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,22 @@
473473
]},
474474
),
475475
},
476+
'/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc'
477+
% images.DEFAULT_PAGE_SIZE: {
478+
'GET': (
479+
{},
480+
{'images': [
481+
{
482+
'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
483+
'name': 'image-2',
484+
},
485+
{
486+
'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
487+
'name': 'image-1',
488+
},
489+
]},
490+
),
491+
},
476492
}
477493

478494
schema_fixtures = {
@@ -678,6 +694,13 @@ def test_list_images_with_multiple_sort_dirs(self):
678694
self.assertEqual(2, len(images))
679695
self.assertEqual('%s' % img_id1, images[1].id)
680696

697+
def test_list_images_with_new_sorting_syntax(self):
698+
img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
699+
sort = 'name:desc,size:asc'
700+
images = list(self.controller.list(sort=sort))
701+
self.assertEqual(2, len(images))
702+
self.assertEqual('%s' % img_id1, images[1].id)
703+
681704
def test_list_images_sort_dirs_fewer_than_keys(self):
682705
sort_key = ['name', 'id', 'created_at']
683706
sort_dir = ['desc', 'asc']
@@ -687,6 +710,31 @@ def test_list_images_sort_dirs_fewer_than_keys(self):
687710
sort_key=sort_key,
688711
sort_dir=sort_dir))
689712

713+
def test_list_images_combined_syntax(self):
714+
sort_key = ['name', 'id']
715+
sort_dir = ['desc', 'asc']
716+
sort = 'name:asc'
717+
self.assertRaises(exc.HTTPBadRequest,
718+
list,
719+
self.controller.list(
720+
sort=sort,
721+
sort_key=sort_key,
722+
sort_dir=sort_dir))
723+
724+
def test_list_images_new_sorting_syntax_invalid_key(self):
725+
sort = 'INVALID:asc'
726+
self.assertRaises(exc.HTTPBadRequest,
727+
list,
728+
self.controller.list(
729+
sort=sort))
730+
731+
def test_list_images_new_sorting_syntax_invalid_direction(self):
732+
sort = 'name:INVALID'
733+
self.assertRaises(exc.HTTPBadRequest,
734+
list,
735+
self.controller.list(
736+
sort=sort))
737+
690738
def test_list_images_for_property(self):
691739
filters = {'filters': dict([('os_distro', 'NixOS')])}
692740
images = list(self.controller.list(**filters))

tests/v2/test_shell_v2.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ def test_do_image_list(self):
7070
'tag': 'fake tag',
7171
'properties': [],
7272
'sort_key': ['name', 'id'],
73-
'sort_dir': ['desc', 'asc']
73+
'sort_dir': ['desc', 'asc'],
74+
'sort': None
7475
}
7576
args = self._make_args(input)
7677
with mock.patch.object(self.gc.images, 'list') as mocked_list:
@@ -102,7 +103,8 @@ def test_do_image_list_with_single_sort_key(self):
102103
'tag': 'fake tag',
103104
'properties': [],
104105
'sort_key': ['name'],
105-
'sort_dir': ['desc']
106+
'sort_dir': ['desc'],
107+
'sort': None
106108
}
107109
args = self._make_args(input)
108110
with mock.patch.object(self.gc.images, 'list') as mocked_list:
@@ -123,6 +125,39 @@ def test_do_image_list_with_single_sort_key(self):
123125
filters=exp_img_filters)
124126
utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
125127

128+
def test_do_image_list_new_sorting_syntax(self):
129+
input = {
130+
'limit': None,
131+
'page_size': 18,
132+
'visibility': True,
133+
'member_status': 'Fake',
134+
'owner': 'test',
135+
'checksum': 'fake_checksum',
136+
'tag': 'fake tag',
137+
'properties': [],
138+
'sort': 'name:desc,size:asc',
139+
'sort_key': [],
140+
'sort_dir': []
141+
}
142+
args = self._make_args(input)
143+
with mock.patch.object(self.gc.images, 'list') as mocked_list:
144+
mocked_list.return_value = {}
145+
146+
test_shell.do_image_list(self.gc, args)
147+
148+
exp_img_filters = {
149+
'owner': 'test',
150+
'member_status': 'Fake',
151+
'visibility': True,
152+
'checksum': 'fake_checksum',
153+
'tag': 'fake tag'
154+
}
155+
mocked_list.assert_called_once_with(
156+
page_size=18,
157+
sort='name:desc,size:asc',
158+
filters=exp_img_filters)
159+
utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
160+
126161
def test_do_image_list_with_property_filter(self):
127162
input = {
128163
'limit': None,
@@ -134,7 +169,8 @@ def test_do_image_list_with_property_filter(self):
134169
'tag': 'fake tag',
135170
'properties': ['os_distro=NixOS', 'architecture=x86_64'],
136171
'sort_key': ['name'],
137-
'sort_dir': ['desc']
172+
'sort_dir': ['desc'],
173+
'sort': None
138174
}
139175
args = self._make_args(input)
140176
with mock.patch.object(self.gc.images, 'list') as mocked_list:

0 commit comments

Comments
 (0)