diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index 2adab121c..c0f1efba7 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -104,3 +104,47 @@ def args_array_to_patch(op, attributes): else: raise exc.CommandError(_('Unknown PATCH operation: %s') % op) return patch + + +def common_params_for_list(args, fields, field_labels): + params = {} + if args.marker is not None: + params['marker'] = args.marker + if args.limit is not None: + params['limit'] = args.limit + + if args.sort_key is not None: + # Support using both heading and field name for sort_key + fields_map = dict(zip(field_labels, fields)) + fields_map.update(zip(fields, fields)) + try: + sort_key = fields_map[args.sort_key] + except KeyError: + raise exc.CommandError( + _("%(sort_key)s is not a valid field for sorting, " + "valid are %(valid)s") % + {'sort_key': args.sort_key, + 'valid': list(fields_map)}) + params['sort_key'] = sort_key + if args.sort_dir is not None: + if args.sort_dir not in ('asc', 'desc'): + raise exc.CommandError( + _("%s is not valid value for sort direction, " + "valid are 'asc' and 'desc'") % + args.sort_dir) + params['sort_dir'] = args.sort_dir + + return params + + +def common_filters(marker, limit, sort_key, sort_dir): + filters = [] + if isinstance(limit, int) and limit > 0: + filters.append('limit=%s' % limit) + if marker is not None: + filters.append('marker=%s' % marker) + if sort_key is not None: + filters.append('sort_key=%s' % sort_key) + if sort_dir is not None: + filters.append('sort_dir=%s' % sort_dir) + return filters diff --git a/ironicclient/tests/test_utils.py b/ironicclient/tests/test_utils.py index f6253044c..8795af336 100644 --- a/ironicclient/tests/test_utils.py +++ b/ironicclient/tests/test_utils.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from ironicclient.common import utils from ironicclient import exc from ironicclient.tests import utils as test_utils @@ -64,3 +66,50 @@ class UtilsTest(test_utils.BaseTestCase): my_args['attributes']) self.assertEqual([{'op': 'remove', 'path': '/foo'}, {'op': 'remove', 'path': '/extra/bar'}], patch) + + +class CommonParamsForListTest(test_utils.BaseTestCase): + def setUp(self): + super(CommonParamsForListTest, self).setUp() + self.args = mock.Mock(marker=None, limit=None, + sort_key=None, sort_dir=None) + + def test_nothing_set(self): + self.assertEqual({}, utils.common_params_for_list(self.args, [], [])) + + def test_marker_and_limit(self): + self.args.marker = 'foo' + self.args.limit = 42 + self.assertEqual({'marker': 'foo', 'limit': 42}, + utils.common_params_for_list(self.args, [], [])) + + def test_sort_key_and_sort_dir(self): + self.args.sort_key = 'field' + self.args.sort_dir = 'desc' + self.assertEqual({'sort_key': 'field', 'sort_dir': 'desc'}, + utils.common_params_for_list(self.args, + ['field'], + [])) + + def test_sort_key_allows_label(self): + self.args.sort_key = 'Label' + self.assertEqual({'sort_key': 'field'}, + utils.common_params_for_list(self.args, + ['field', 'field2'], + ['Label', 'Label2'])) + + def test_sort_key_invalid(self): + self.args.sort_key = 'something' + self.assertRaises(exc.CommandError, + utils.common_params_for_list, + self.args, + ['field', 'field2'], + []) + + def test_sort_dir_invalid(self): + self.args.sort_dir = 'something' + self.assertRaises(exc.CommandError, + utils.common_params_for_list, + self.args, + [], + []) diff --git a/ironicclient/tests/v1/test_chassis.py b/ironicclient/tests/v1/test_chassis.py index 75acb8bea..b81a6061d 100644 --- a/ironicclient/tests/v1/test_chassis.py +++ b/ironicclient/tests/v1/test_chassis.py @@ -125,6 +125,37 @@ fake_responses_pagination = { }, } +fake_responses_sorting = { + '/v1/chassis/?sort_key=updated_at': + { + 'GET': ( + {}, + {"chassis": [CHASSIS2]} + ), + }, + '/v1/chassis/?sort_dir=desc': + { + 'GET': ( + {}, + {"chassis": [CHASSIS2]} + ), + }, + '/v1/chassis/%s/nodes?sort_key=updated_at' % CHASSIS['uuid']: + { + 'GET': ( + {}, + {"nodes": [NODE]}, + ), + }, + '/v1/chassis/%s/nodes?sort_dir=desc' % CHASSIS['uuid']: + { + 'GET': ( + {}, + {"nodes": [NODE]}, + ), + }, +} + class ChassisManagerTest(testtools.TestCase): @@ -172,6 +203,26 @@ class ChassisManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertThat(chassis, HasLength(2)) + def test_chassis_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) + chassis = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/chassis/?sort_key=updated_at', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(chassis, HasLength(1)) + + def test_chassis_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) + chassis = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/chassis/?sort_dir=desc', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(chassis, HasLength(1)) + def test_chassis_show(self): chassis = self.mgr.get(CHASSIS['uuid']) expect = [ @@ -229,6 +280,32 @@ class ChassisManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE['uuid'], nodes[0].uuid) + def test_chassis_node_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) + nodes = self.mgr.list_nodes(CHASSIS['uuid'], sort_key='updated_at') + expect = [ + ('GET', + '/v1/chassis/%s/nodes?sort_key=updated_at' % CHASSIS['uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE['uuid'], nodes[0].uuid) + + def test_chassis_node_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) + nodes = self.mgr.list_nodes(CHASSIS['uuid'], sort_dir='desc') + expect = [ + ('GET', + '/v1/chassis/%s/nodes?sort_dir=desc' % CHASSIS['uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE['uuid'], nodes[0].uuid) + def test_chassis_node_list_marker(self): self.api = utils.FakeAPI(fake_responses_pagination) self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) diff --git a/ironicclient/tests/v1/test_node.py b/ironicclient/tests/v1/test_node.py index 15fe256cb..01b37ba6d 100644 --- a/ironicclient/tests/v1/test_node.py +++ b/ironicclient/tests/v1/test_node.py @@ -271,6 +271,37 @@ fake_responses_pagination = { }, } +fake_responses_sorting = { + '/v1/nodes/?sort_key=updated_at': + { + 'GET': ( + {}, + {"nodes": [NODE2, NODE1]} + ), + }, + '/v1/nodes/?sort_dir=desc': + { + 'GET': ( + {}, + {"nodes": [NODE2, NODE1]} + ), + }, + '/v1/nodes/%s/ports?sort_key=updated_at' % NODE1['uuid']: + { + 'GET': ( + {}, + {"ports": [PORT]}, + ), + }, + '/v1/nodes/%s/ports?sort_dir=desc' % NODE1['uuid']: + { + 'GET': ( + {}, + {"ports": [PORT]}, + ), + }, +} + class NodeManagerTest(testtools.TestCase): @@ -318,6 +349,26 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertEqual(2, len(nodes)) + def test_node_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + nodes = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/nodes/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(nodes)) + + def test_node_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + nodes = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/nodes/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(nodes)) + def test_node_list_associated(self): nodes = self.mgr.list(associated=True) expect = [ @@ -449,6 +500,32 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertThat(ports, HasLength(1)) + def test_node_port_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + ports = self.mgr.list_ports(NODE1['uuid'], sort_key='updated_at') + expect = [ + ('GET', '/v1/nodes/%s/ports?sort_key=updated_at' % NODE1['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(ports, HasLength(1)) + self.assertEqual(PORT['uuid'], ports[0].uuid) + self.assertEqual(PORT['address'], ports[0].address) + + def test_node_port_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + ports = self.mgr.list_ports(NODE1['uuid'], sort_dir='desc') + expect = [ + ('GET', '/v1/nodes/%s/ports?sort_dir=desc' % NODE1['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(ports, HasLength(1)) + self.assertEqual(PORT['uuid'], ports[0].uuid) + self.assertEqual(PORT['address'], ports[0].address) + def test_node_set_power_state(self): power_state = self.mgr.set_power_state(NODE1['uuid'], "on") body = {'target': 'power on'} diff --git a/ironicclient/tests/v1/test_port.py b/ironicclient/tests/v1/test_port.py index 4cb2571f9..f960ac245 100644 --- a/ironicclient/tests/v1/test_port.py +++ b/ironicclient/tests/v1/test_port.py @@ -104,6 +104,23 @@ fake_responses_pagination = { }, } +fake_responses_sorting = { + '/v1/ports/?sort_key=updated_at': + { + 'GET': ( + {}, + {"ports": [PORT2, PORT]} + ), + }, + '/v1/ports/?sort_dir=desc': + { + 'GET': ( + {}, + {"ports": [PORT2, PORT]} + ), + }, +} + class PortManagerTest(testtools.TestCase): @@ -151,6 +168,26 @@ class PortManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertThat(ports, HasLength(2)) + def test_ports_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.port.PortManager(self.api) + ports = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/ports/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(ports)) + + def test_ports_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.port.PortManager(self.api) + ports = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/ports/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(ports)) + def test_ports_show(self): port = self.mgr.get(PORT['uuid']) expect = [ diff --git a/ironicclient/v1/chassis.py b/ironicclient/v1/chassis.py index 809165413..0b88cb2be 100644 --- a/ironicclient/v1/chassis.py +++ b/ironicclient/v1/chassis.py @@ -15,6 +15,7 @@ # under the License. from ironicclient.common import base +from ironicclient.common import utils from ironicclient import exc @@ -33,7 +34,7 @@ class ChassisManager(base.Manager): def _path(id=None): return '/v1/chassis/%s' % id if id else '/v1/chassis' - def list(self, marker=None, limit=None): + def list(self, marker=None, limit=None, sort_key=None, sort_dir=None): """Retrieve a list of chassis. :param marker: Optional, the UUID of a chassis, eg the last @@ -48,17 +49,18 @@ class ChassisManager(base.Manager): returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of chassis. """ if limit is not None: limit = int(limit) - filters = [] - if isinstance(limit, int) and limit > 0: - filters.append('limit=%s' % limit) - if marker is not None: - filters.append('marker=%s' % marker) + filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = None if filters: @@ -70,7 +72,8 @@ class ChassisManager(base.Manager): return self._list_pagination(self._path(path), "chassis", limit=limit) - def list_nodes(self, chassis_id, marker=None, limit=None): + def list_nodes(self, chassis_id, marker=None, limit=None, + sort_key=None, sort_dir=None): """List all the nodes for a given chassis. :param chassis_id: The UUID of the chassis. @@ -86,17 +89,18 @@ class ChassisManager(base.Manager): returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of nodes. """ if limit is not None: limit = int(limit) - filters = [] - if isinstance(limit, int) and limit > 0: - filters.append('limit=%s' % limit) - if marker is not None: - filters.append('marker=%s' % marker) + filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = "%s/nodes" % chassis_id if filters: diff --git a/ironicclient/v1/chassis_shell.py b/ironicclient/v1/chassis_shell.py index 30b5a4203..1faa30232 100644 --- a/ironicclient/v1/chassis_shell.py +++ b/ironicclient/v1/chassis_shell.py @@ -45,17 +45,22 @@ def do_chassis_show(cc, args): help='Chassis UUID (e.g of the last chassis in the list ' 'from a previous request). Returns the list of chassis ' 'after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Chassis field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: one of "asc" (the default) or "desc".') def do_chassis_list(cc, args): """List chassis.""" - params = {} - if args.marker is not None: - params['marker'] = args.marker - if args.limit is not None: - params['limit'] = args.limit - - chassis = cc.chassis.list(**params) field_labels = ['UUID', 'Description'] fields = ['uuid', 'description'] + params = utils.common_params_for_list(args, fields, field_labels) + + chassis = cc.chassis.list(**params) cliutils.print_list(chassis, fields, field_labels=field_labels, sortby_index=None) @@ -130,19 +135,24 @@ def do_chassis_update(cc, args): help='Node UUID (e.g of the last node in the list from ' 'a previous request). Returns the list of nodes ' 'after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Node field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: one of "asc" (the default) or "desc".') @cliutils.arg('chassis', metavar='', help="UUID of chassis") def do_chassis_node_list(cc, args): """List the nodes contained in the chassis.""" - params = {} - if args.marker is not None: - params['marker'] = args.marker - if args.limit is not None: - params['limit'] = args.limit - - nodes = cc.chassis.list_nodes(args.chassis, **params) field_labels = ['UUID', 'Instance UUID', 'Power State', 'Provisioning State'] fields = ['uuid', 'instance_uuid', 'power_state', 'provision_state'] + params = utils.common_params_for_list(args, fields, field_labels) + + nodes = cc.chassis.list_nodes(args.chassis, **params) cliutils.print_list(nodes, fields, field_labels=field_labels, sortby_index=None) diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index d93d73e50..1224134c8 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -15,6 +15,7 @@ # under the License. from ironicclient.common import base +from ironicclient.common import utils from ironicclient import exc CREATION_ATTRIBUTES = ['chassis_uuid', 'driver', 'driver_info', 'extra', @@ -34,7 +35,7 @@ class NodeManager(base.Manager): return '/v1/nodes/%s' % id if id else '/v1/nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, - detail=False): + detail=False, sort_key=None, sort_dir=None): """Retrieve a list of nodes. :param associated: Optional, boolean whether to return a list of @@ -57,17 +58,18 @@ class NodeManager(base.Manager): :param detail: Optional, boolean whether to return detailed information about nodes. + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of nodes. """ if limit is not None: limit = int(limit) - filters = [] - if isinstance(limit, int) and limit > 0: - filters.append('limit=%s' % limit) - if marker is not None: - filters.append('marker=%s' % marker) + filters = utils.common_filters(marker, limit, sort_key, sort_dir) if associated is not None: filters.append('associated=%s' % associated) if maintenance is not None: @@ -85,7 +87,8 @@ class NodeManager(base.Manager): return self._list_pagination(self._path(path), "nodes", limit=limit) - def list_ports(self, node_id, marker=None, limit=None): + def list_ports(self, node_id, marker=None, limit=None, sort_key=None, + sort_dir=None): """List all the ports for a given node. :param node_id: The UUID of the node. @@ -101,17 +104,18 @@ class NodeManager(base.Manager): returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of ports. """ if limit is not None: limit = int(limit) - filters = [] - if isinstance(limit, int) and limit > 0: - filters.append('limit=%s' % limit) - if marker is not None: - filters.append('marker=%s' % marker) + filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = "%s/ports" % node_id if filters: diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index e9e12eb58..d4bb203f5 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -74,6 +74,15 @@ def do_node_show(cc, args): help='Node UUID (e.g of the last node in the list from ' 'a previous request). Returns the list of nodes ' 'after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Node field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: one of "asc" (the default) or "desc".') @cliutils.arg( '--maintenance', metavar='', @@ -97,22 +106,22 @@ def do_node_list(cc, args): params['associated'] = args.associated if args.maintenance is not None: params['maintenance'] = args.maintenance - if args.marker is not None: - params['marker'] = args.marker - if args.limit is not None: - params['limit'] = args.limit params['detail'] = args.detail - nodes = cc.node.list(**params) if args.detail: - cliutils.print_list(nodes, FIELDS, - field_labels=FIELD_LABELS, - sortby_index=None) + fields = FIELDS + field_labels = FIELD_LABELS else: - cliutils.print_list(nodes, - LIST_FIELDS, - field_labels=LIST_FIELD_LABELS, - sortby_index=None) + fields = LIST_FIELDS + field_labels = LIST_FIELD_LABELS + + params.update(utils.common_params_for_list(args, + fields, + field_labels)) + nodes = cc.node.list(**params) + cliutils.print_list(nodes, fields, + field_labels=field_labels, + sortby_index=None) @cliutils.arg( @@ -228,18 +237,23 @@ def do_node_vendor_passthru(cc, args): help='Port UUID (e.g of the last port in the list from ' 'a previous request). Returns the list of ports ' 'after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Port field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: one of "asc" (the default) or "desc".') @cliutils.arg('node', metavar='', help="UUID of node") def do_node_port_list(cc, args): """List the ports associated with the node.""" - params = {} - if args.marker is not None: - params['marker'] = args.marker - if args.limit is not None: - params['limit'] = args.limit - - ports = cc.node.list_ports(args.node, **params) field_labels = ['UUID', 'Address'] fields = ['uuid', 'address'] + params = utils.common_params_for_list(args, fields, field_labels) + + ports = cc.node.list_ports(args.node, **params) cliutils.print_list(ports, fields, field_labels=field_labels, sortby_index=None) diff --git a/ironicclient/v1/port.py b/ironicclient/v1/port.py index d2ec6088a..0687b7faf 100644 --- a/ironicclient/v1/port.py +++ b/ironicclient/v1/port.py @@ -15,6 +15,7 @@ # under the License. from ironicclient.common import base +from ironicclient.common import utils from ironicclient import exc CREATION_ATTRIBUTES = ['address', 'extra', 'node_uuid'] @@ -32,7 +33,7 @@ class PortManager(base.Manager): def _path(id=None): return '/v1/ports/%s' % id if id else '/v1/ports' - def list(self, limit=None, marker=None): + def list(self, limit=None, marker=None, sort_key=None, sort_dir=None): """Retrieve a list of port. :param marker: Optional, the UUID of a port, eg the last @@ -47,17 +48,18 @@ class PortManager(base.Manager): returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + :returns: A list of ports. """ if limit is not None: limit = int(limit) - filters = [] - if isinstance(limit, int) and limit > 0: - filters.append('limit=%s' % limit) - if marker is not None: - filters.append('marker=%s' % marker) + filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = None if filters: diff --git a/ironicclient/v1/port_shell.py b/ironicclient/v1/port_shell.py index c2f56408e..28fc46584 100644 --- a/ironicclient/v1/port_shell.py +++ b/ironicclient/v1/port_shell.py @@ -55,17 +55,22 @@ def do_port_show(cc, args): help='Port UUID (e.g of the last port in the list from ' 'a previous request). Returns the list of ports ' 'after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Port field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: one of "asc" (the default) or "desc".') def do_port_list(cc, args): """List ports.""" - params = {} - if args.marker is not None: - params['marker'] = args.marker - if args.limit is not None: - params['limit'] = args.limit - - port = cc.port.list(**params) field_labels = ['UUID', 'Address'] fields = ['uuid', 'address'] + params = utils.common_params_for_list(args, fields, field_labels) + + port = cc.port.list(**params) cliutils.print_list(port, fields, field_labels=field_labels, sortby_index=None)