Convert list and find to use params parameter
We were doing a last minute switch to params in the page method, but we need to leave it up to the user to set limit, marker, fields, and search criteria. What is supported by the servers varies a lot. Closes-Bug: #1466175 Change-Id: I6b5932a812e2d0821f0ae5ed2d93b44dbd10b19a
This commit is contained in:
@@ -22,7 +22,7 @@ def run_list(opts):
|
|||||||
path_args = None
|
path_args = None
|
||||||
if opts.data:
|
if opts.data:
|
||||||
path_args = common.get_data_option(opts)
|
path_args = common.get_data_option(opts)
|
||||||
for obj in cls.list(sess, path_args=path_args):
|
for obj in cls.list(sess, path_args=path_args, paginated=True):
|
||||||
print(str(obj))
|
print(str(obj))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@@ -50,11 +50,11 @@ class FloatingIP(resource.Resource):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_available(cls, session):
|
def find_available(cls, session):
|
||||||
args = {
|
params = {
|
||||||
'port_id': '',
|
'port_id': '',
|
||||||
'fields': cls.id_attribute,
|
'fields': cls.id_attribute,
|
||||||
}
|
}
|
||||||
info = cls.list(session, **args)
|
info = cls.list(session, params=params)
|
||||||
try:
|
try:
|
||||||
return next(info)
|
return next(info)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
@@ -36,7 +36,6 @@ import itertools
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from six.moves.urllib import parse as url_parse
|
|
||||||
|
|
||||||
from openstack import exceptions
|
from openstack import exceptions
|
||||||
from openstack import utils
|
from openstack import utils
|
||||||
@@ -813,29 +812,14 @@ class Resource(collections.MutableMapping):
|
|||||||
self.delete_by_id(session, self.id, path_args=self)
|
self.delete_by_id(session, self.id, path_args=self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list(cls, session, limit=None, marker=None, path_args=None,
|
def list(cls, session, path_args=None, paginated=False, params=None):
|
||||||
paginated=False, **params):
|
"""This method is a generator which yields resource objects.
|
||||||
"""Get a response that is a list of objects.
|
|
||||||
|
|
||||||
This method starts at ``limit`` and ``marker`` (both defaulting to
|
This resource object list generator handles pagination and takes query
|
||||||
None), advances the marker to the last item received in each response,
|
params for response filtering.
|
||||||
and continues making requests for more resources until no results
|
|
||||||
are returned.
|
|
||||||
|
|
||||||
When no ``limit`` is provided, the server will return its maximum
|
|
||||||
amount of items per page. After that response, the limit will be
|
|
||||||
determined and sent in subsequent requests to the server. That limit
|
|
||||||
will be used to calculate when all items have been received.
|
|
||||||
|
|
||||||
:param session: The session to use for making this request.
|
:param session: The session to use for making this request.
|
||||||
:type session: :class:`~openstack.session.Session`
|
:type session: :class:`~openstack.session.Session`
|
||||||
:param limit: The maximum amount of results to retrieve.
|
|
||||||
The default is ``None``, which means no limit will be
|
|
||||||
set, and the underlying API will return its default
|
|
||||||
amount of responses.
|
|
||||||
:param marker: The position in the list to begin requests from.
|
|
||||||
The type of value to use for ``marker`` depends on
|
|
||||||
the API being called.
|
|
||||||
:param dict path_args: A dictionary of arguments to construct
|
:param dict path_args: A dictionary of arguments to construct
|
||||||
a compound URL.
|
a compound URL.
|
||||||
See `How path_args are used`_ for details.
|
See `How path_args are used`_ for details.
|
||||||
@@ -845,8 +829,10 @@ class Resource(collections.MutableMapping):
|
|||||||
**When paginated is False only one
|
**When paginated is False only one
|
||||||
page of data will be returned regardless
|
page of data will be returned regardless
|
||||||
of the API's support of pagination.**
|
of the API's support of pagination.**
|
||||||
:param dict params: Parameters to be passed into the underlying
|
:param dict params: Query parameters to be passed into the underlying
|
||||||
:meth:`~openstack.session.Session.get` method.
|
:meth:`~openstack.session.Session.get` method.
|
||||||
|
Values that the server may support include `limit`
|
||||||
|
and `marker`.
|
||||||
|
|
||||||
:return: A generator of :class:`Resource` objects.
|
:return: A generator of :class:`Resource` objects.
|
||||||
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
|
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
|
||||||
@@ -856,9 +842,12 @@ class Resource(collections.MutableMapping):
|
|||||||
raise exceptions.MethodNotSupported(cls, 'list')
|
raise exceptions.MethodNotSupported(cls, 'list')
|
||||||
|
|
||||||
more_data = True
|
more_data = True
|
||||||
|
params = {} if params is None else params
|
||||||
|
url = cls._get_url(path_args)
|
||||||
while more_data:
|
while more_data:
|
||||||
resp = cls.page(session, limit, marker, path_args, **params)
|
resp = session.get(url, service=cls.service, params=params).body
|
||||||
|
if cls.resources_key:
|
||||||
|
resp = resp[cls.resources_key]
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
more_data = False
|
more_data = False
|
||||||
@@ -869,55 +858,16 @@ class Resource(collections.MutableMapping):
|
|||||||
yielded = 0
|
yielded = 0
|
||||||
for data in resp:
|
for data in resp:
|
||||||
value = cls.existing(**data)
|
value = cls.existing(**data)
|
||||||
marker = value.id
|
new_marker = value.id
|
||||||
yielded += 1
|
yielded += 1
|
||||||
yield value
|
yield value
|
||||||
|
|
||||||
if not paginated or limit and yielded < limit:
|
if not paginated:
|
||||||
more_data = False
|
return
|
||||||
|
if 'limit' in params and yielded < params['limit']:
|
||||||
limit = yielded
|
return
|
||||||
|
params['limit'] = yielded
|
||||||
@classmethod
|
params['marker'] = new_marker
|
||||||
def page(cls, session, limit=None, marker=None, path_args=None, **params):
|
|
||||||
"""Get a one page response.
|
|
||||||
|
|
||||||
This method gets starting at ``marker`` with a maximum of ``limit``
|
|
||||||
records.
|
|
||||||
|
|
||||||
:param session: The session to use for making this request.
|
|
||||||
:type session: :class:`~openstack.session.Session`
|
|
||||||
:param limit: The maximum amount of results to retrieve. The default
|
|
||||||
is to retrieve as many results as the service allows.
|
|
||||||
:param marker: The position in the list to begin requests from.
|
|
||||||
The type of value to use for ``marker`` depends on
|
|
||||||
the API being called.
|
|
||||||
:param dict path_args: A dictionary of arguments to construct
|
|
||||||
a compound URL.
|
|
||||||
See `How path_args are used`_ for details.
|
|
||||||
:param dict params: Parameters to be passed into the underlying
|
|
||||||
:meth:`~openstack.session.Session.get` method.
|
|
||||||
|
|
||||||
:return: A list of dictionaries returned in the response body.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters = {}
|
|
||||||
|
|
||||||
if limit:
|
|
||||||
filters['limit'] = limit
|
|
||||||
if marker:
|
|
||||||
filters['marker'] = marker
|
|
||||||
|
|
||||||
url = cls._get_url(path_args)
|
|
||||||
if filters:
|
|
||||||
url = '%s?%s' % (url, url_parse.urlencode(filters))
|
|
||||||
|
|
||||||
resp = session.get(url, service=cls.service, params=params).body
|
|
||||||
|
|
||||||
if cls.resources_key:
|
|
||||||
resp = resp[cls.resources_key]
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find(cls, session, name_or_id, path_args=None, ignore_missing=True):
|
def find(cls, session, name_or_id, path_args=None, ignore_missing=True):
|
||||||
@@ -943,9 +893,18 @@ class Resource(collections.MutableMapping):
|
|||||||
:raises: :class:`openstack.exceptions.ResourceNotFound` if nothing
|
:raises: :class:`openstack.exceptions.ResourceNotFound` if nothing
|
||||||
is found and ignore_missing is ``False``.
|
is found and ignore_missing is ``False``.
|
||||||
"""
|
"""
|
||||||
def get_one_match(data, attr):
|
def get_one_match(generator, attr, raise_exception):
|
||||||
if len(data) == 1:
|
try:
|
||||||
result = cls.existing(**data[0])
|
first = next(generator)
|
||||||
|
if any(generator):
|
||||||
|
if raise_exception:
|
||||||
|
msg = "More than one %s exists with the name '%s'."
|
||||||
|
msg = (msg % (cls.get_resource_name(), name_or_id))
|
||||||
|
raise exceptions.DuplicateResource(msg)
|
||||||
|
return None
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
result = cls.existing(**first)
|
||||||
value = getattr(result, attr, False)
|
value = getattr(result, attr, False)
|
||||||
if value == name_or_id:
|
if value == name_or_id:
|
||||||
return result
|
return result
|
||||||
@@ -954,24 +913,20 @@ class Resource(collections.MutableMapping):
|
|||||||
try:
|
try:
|
||||||
if cls.allow_retrieve:
|
if cls.allow_retrieve:
|
||||||
return cls.get_by_id(session, name_or_id, path_args=path_args)
|
return cls.get_by_id(session, name_or_id, path_args=path_args)
|
||||||
args = {cls.id_attribute: name_or_id}
|
params = {'limit': 2}
|
||||||
info = cls.page(session, limit=2, path_args=path_args, **args)
|
params[cls.id_attribute] = name_or_id
|
||||||
result = get_one_match(info, cls.id_attribute)
|
info = cls.list(session, path_args=path_args, params=params)
|
||||||
|
result = get_one_match(info, cls.id_attribute, False)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
except exceptions.HttpException:
|
except exceptions.HttpException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if cls.name_attribute:
|
if cls.name_attribute:
|
||||||
params = {cls.name_attribute: name_or_id}
|
params = {'limit': 2}
|
||||||
info = cls.page(session, limit=2, path_args=path_args, **params)
|
params[cls.name_attribute] = name_or_id
|
||||||
|
info = cls.list(session, path_args=path_args, params=params)
|
||||||
if len(info) > 1:
|
result = get_one_match(info, cls.name_attribute, True)
|
||||||
msg = "More than one %s exists with the name '%s'."
|
|
||||||
msg = (msg % (cls.get_resource_name(), name_or_id))
|
|
||||||
raise exceptions.DuplicateResource(msg)
|
|
||||||
|
|
||||||
result = get_one_match(info, cls.name_attribute)
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@@ -737,7 +737,6 @@ class ResourceTests(base.TestTransportBase):
|
|||||||
results = [fake_data.copy(), fake_data.copy(), fake_data.copy()]
|
results = [fake_data.copy(), fake_data.copy(), fake_data.copy()]
|
||||||
for i in range(len(results)):
|
for i in range(len(results)):
|
||||||
results[i]['id'] = fake_id + i
|
results[i]['id'] = fake_id + i
|
||||||
|
|
||||||
if resource_class.resources_key is not None:
|
if resource_class.resources_key is not None:
|
||||||
body = {resource_class.resources_key:
|
body = {resource_class.resources_key:
|
||||||
self._get_expected_results()}
|
self._get_expected_results()}
|
||||||
@@ -745,19 +744,15 @@ class ResourceTests(base.TestTransportBase):
|
|||||||
else:
|
else:
|
||||||
body = self._get_expected_results()
|
body = self._get_expected_results()
|
||||||
sentinel = []
|
sentinel = []
|
||||||
|
|
||||||
marker = "marker=%d" % results[-1]['id']
|
|
||||||
|
|
||||||
self.session.get.side_effect = [mock.Mock(body=body),
|
self.session.get.side_effect = [mock.Mock(body=body),
|
||||||
mock.Mock(body=sentinel)]
|
mock.Mock(body=sentinel)]
|
||||||
|
|
||||||
objs = resource_class.list(self.session, limit=1,
|
objs = list(resource_class.list(self.session, path_args=fake_arguments,
|
||||||
path_args=fake_arguments,
|
paginated=True))
|
||||||
paginated=True)
|
|
||||||
objs = list(objs)
|
|
||||||
self.assertIn(marker, self.session.get.call_args[0][0])
|
|
||||||
self.assertEqual(3, len(objs))
|
|
||||||
|
|
||||||
|
params = {'limit': 3, 'marker': results[-1]['id']}
|
||||||
|
self.assertEqual(params, self.session.get.call_args[1]['params'])
|
||||||
|
self.assertEqual(3, len(objs))
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
self.assertIn(obj.id, range(fake_id, fake_id + 3))
|
self.assertIn(obj.id, range(fake_id, fake_id + 3))
|
||||||
self.assertEqual(fake_name, obj['name'])
|
self.assertEqual(fake_name, obj['name'])
|
||||||
@@ -783,7 +778,7 @@ class ResourceTests(base.TestTransportBase):
|
|||||||
attrs = {"get.return_value": body}
|
attrs = {"get.return_value": body}
|
||||||
session = mock.Mock(**attrs)
|
session = mock.Mock(**attrs)
|
||||||
|
|
||||||
list(FakeResource.list(session, limit=len(results) + 1,
|
list(FakeResource.list(session, params={'limit': len(results) + 1},
|
||||||
path_args=fake_arguments,
|
path_args=fake_arguments,
|
||||||
paginated=paginated))
|
paginated=paginated))
|
||||||
|
|
||||||
@@ -798,53 +793,26 @@ class ResourceTests(base.TestTransportBase):
|
|||||||
# When we call with paginated=False, make sure we made one call
|
# When we call with paginated=False, make sure we made one call
|
||||||
self._test_list_call_count(False)
|
self._test_list_call_count(False)
|
||||||
|
|
||||||
@mock.patch("openstack.resource.Resource.page")
|
def test_determine_limit(self):
|
||||||
def test_determine_limit(self, fake_page):
|
|
||||||
full_page = [fake_data.copy(), fake_data.copy(), fake_data.copy()]
|
full_page = [fake_data.copy(), fake_data.copy(), fake_data.copy()]
|
||||||
last_page = [fake_data.copy()]
|
last_page = [fake_data.copy()]
|
||||||
|
|
||||||
session = mock.MagicMock()
|
session = mock.MagicMock()
|
||||||
pages = [full_page, full_page, last_page]
|
session.get = mock.MagicMock()
|
||||||
fake_page.side_effect = pages
|
full_response = mock.MagicMock()
|
||||||
|
full_response.body = {FakeResource.resources_key: full_page}
|
||||||
|
last_response = mock.MagicMock()
|
||||||
|
last_response.body = {FakeResource.resources_key: last_page}
|
||||||
|
pages = [full_response, full_response, last_response]
|
||||||
|
session.get.side_effect = pages
|
||||||
|
|
||||||
# Don't specify a limit. Resource.list will determine the limit
|
# Don't specify a limit. Resource.list will determine the limit
|
||||||
# is 3 based on the first `full_page`.
|
# is 3 based on the first `full_page`.
|
||||||
results = list(FakeResource.list(session, path_args=fake_arguments,
|
results = list(FakeResource.list(session, path_args=fake_arguments,
|
||||||
paginated=True))
|
paginated=True))
|
||||||
|
|
||||||
self.assertEqual(fake_page.call_count, len(pages))
|
self.assertEqual(session.get.call_count, len(pages))
|
||||||
self.assertEqual(len(results), sum([len(page) for page in pages]))
|
self.assertEqual(len(full_page + full_page + last_page), len(results))
|
||||||
|
|
||||||
def test_page(self):
|
|
||||||
session = mock.Mock()
|
|
||||||
session.get = mock.Mock()
|
|
||||||
records = [{'id': 'squid'}]
|
|
||||||
response = FakeResponse({FakeResource.resources_key: records})
|
|
||||||
session.get.return_value = response
|
|
||||||
|
|
||||||
objs = FakeResource.page(session, 1, None, path_args=fake_arguments)
|
|
||||||
|
|
||||||
self.assertEqual(records, objs)
|
|
||||||
path = fake_path + '?limit=1'
|
|
||||||
session.get.assert_called_with(path, params={}, service=None)
|
|
||||||
|
|
||||||
objs = FakeResource.page(session, None, 'a', path_args=fake_arguments)
|
|
||||||
|
|
||||||
self.assertEqual(records, objs)
|
|
||||||
path = fake_path + '?marker=a'
|
|
||||||
session.get.assert_called_with(path, params={}, service=None)
|
|
||||||
|
|
||||||
objs = FakeResource.page(session, None, None, path_args=fake_arguments)
|
|
||||||
|
|
||||||
self.assertEqual(records, objs)
|
|
||||||
path = fake_path
|
|
||||||
session.get.assert_called_with(path, params={}, service=None)
|
|
||||||
|
|
||||||
objs = FakeResource.page(session)
|
|
||||||
|
|
||||||
self.assertEqual(records, objs)
|
|
||||||
path = fake_base_path
|
|
||||||
session.get.assert_called_with(path, params={}, service=None)
|
|
||||||
|
|
||||||
def test_attrs_name(self):
|
def test_attrs_name(self):
|
||||||
obj = FakeResource()
|
obj = FakeResource()
|
||||||
@@ -1200,9 +1168,8 @@ class TestFind(base.TestCase):
|
|||||||
self.assertEqual(self.NAME, result.name)
|
self.assertEqual(self.NAME, result.name)
|
||||||
self.assertEqual(self.PROP, result.prop)
|
self.assertEqual(self.PROP, result.prop)
|
||||||
|
|
||||||
p = {'name': self.NAME}
|
p = {'limit': 2, 'name': self.NAME}
|
||||||
path = fake_path + "?limit=2"
|
self.assertEqual(p, self.mock_get.call_args[1]['params'])
|
||||||
self.mock_get.assert_any_call(path, params=p, service=None)
|
|
||||||
|
|
||||||
def test_id(self):
|
def test_id(self):
|
||||||
self.mock_get.side_effect = [
|
self.mock_get.side_effect = [
|
||||||
@@ -1232,9 +1199,8 @@ class TestFind(base.TestCase):
|
|||||||
self.assertEqual(self.ID, result.id)
|
self.assertEqual(self.ID, result.id)
|
||||||
self.assertEqual(self.PROP, result.prop)
|
self.assertEqual(self.PROP, result.prop)
|
||||||
|
|
||||||
p = {'id': self.ID}
|
p = {'id': self.ID, 'limit': 2}
|
||||||
path = fake_path + "?limit=2"
|
self.assertEqual(p, self.mock_get.call_args[1]['params'])
|
||||||
self.mock_get.assert_any_call(path, params=p, service=None)
|
|
||||||
|
|
||||||
def test_dups(self):
|
def test_dups(self):
|
||||||
dup = {'id': 'Larry'}
|
dup = {'id': 'Larry'}
|
||||||
|
Reference in New Issue
Block a user