Merge "Implement iterator paging"

This commit is contained in:
Jenkins
2014-11-21 19:14:01 +00:00
committed by Gerrit Code Review
3 changed files with 78 additions and 37 deletions

View File

@@ -46,6 +46,8 @@ class FloatingIP(resource.Resource):
'fields': cls.id_attribute, 'fields': cls.id_attribute,
} }
info = cls.list(session, **args) info = cls.list(session, **args)
if len(info) > 0: try:
return info[0] return next(info)
raise exceptions.ResourceNotFound("No available floating ips exist.") except StopIteration:
msg = "No available floating ips exist."
raise exceptions.ResourceNotFound(msg)

View File

@@ -425,33 +425,49 @@ class Resource(collections.MutableMapping):
@classmethod @classmethod
def list(cls, session, limit=None, marker=None, path_args=None, **params): def list(cls, session, limit=None, marker=None, path_args=None, **params):
"""Get a list of resources as an array of objects.""" """Return a generator that will page through results of GET requests.
# NOTE(jamielennox): Is it possible we can return a generator from here This method starts at `limit` and `marker` (both defaulting to None),
# and allow us to keep paging rather than limit and marker? advances the marker to the last item received, and continues paging
until no results are returned.
"""
if not cls.allow_list: if not cls.allow_list:
raise exceptions.MethodNotSupported('list') raise exceptions.MethodNotSupported('list')
filters = {} more_data = True
if limit: while more_data:
filters['limit'] = limit filters = {}
if marker:
filters['marker'] = marker
if path_args: if limit:
url = cls.base_path % path_args filters['limit'] = limit
else: if marker:
url = cls.base_path filters['marker'] = marker
if filters:
url = '%s?%s' % (url, url_parse.urlencode(filters))
resp = session.get(url, service=cls.service, params=params).body if path_args:
url = cls.base_path % path_args
else:
url = cls.base_path
if filters:
url = '%s?%s' % (url, url_parse.urlencode(filters))
if cls.resources_key: resp = session.get(url, service=cls.service, params=params).body
resp = resp[cls.resources_key]
return [cls.existing(**data) for data in resp] if cls.resources_key:
resp = resp[cls.resources_key]
# TODO(briancurtin): Although there are a few different ways
# across services, we can know from a response if it's the end
# without doing an extra request to get an empty response.
# Resources should probably carry something like a `_should_page`
# method to handle their service's pagination style.
if not resp:
more_data = False
for data in resp:
value = cls.existing(**data)
marker = value.id
yield value
@classmethod @classmethod
def find(cls, session, name_or_id, path_args=None): def find(cls, session, name_or_id, path_args=None):
@@ -463,20 +479,36 @@ class Resource(collections.MutableMapping):
'path_args': path_args, 'path_args': path_args,
} }
info = cls.list(session, **args) info = cls.list(session, **args)
if len(info) == 1: # If there is exactly one result available, return it.
return info[0] result = None
try:
result = next(info)
next(info)
except StopIteration:
if result is not None:
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 = {cls.name_attribute: name_or_id,
'fields': cls.id_attribute} 'fields': cls.id_attribute}
info = cls.list(session, path_args=path_args, **params) info = cls.list(session, path_args=path_args, **params)
if len(info) == 1: result = None
return info[0] # Take the first value as our result. If another value is,
if len(info) > 1: # available then raise DuplicateResource.
try:
result = next(info)
next(info)
msg = "More than one %s exists with the name '%s'." msg = "More than one %s exists with the name '%s'."
msg = (msg % (cls.get_resource_name(), name_or_id)) msg = (msg % (cls.get_resource_name(), name_or_id))
raise exceptions.DuplicateResource(msg) raise exceptions.DuplicateResource(msg)
except StopIteration:
# We got here because `info` either gave us the result
# or it was empty.
if result is not None:
return result
msg = ("No %s with a name or ID of '%s' exists." % msg = ("No %s with a name or ID of '%s' exists." %
(cls.get_resource_name(), name_or_id)) (cls.get_resource_name(), name_or_id))
raise exceptions.ResourceNotFound(msg) raise exceptions.ResourceNotFound(msg)

View File

@@ -259,14 +259,19 @@ class ResourceTests(base.TestTransportBase):
for i in range(len(results)): for i in range(len(results)):
results[i]['id'] = fake_id + i results[i]['id'] = fake_id + i
marker = "marker=%d" % results[-1]['id']
self.stub_url(httpretty.GET,
path=[fake_path + "?" + marker],
json={fake_resources: []},
match_querystring=True)
self.stub_url(httpretty.GET, self.stub_url(httpretty.GET,
path=[fake_path], path=[fake_path],
json={fake_resources: results}) json={fake_resources: results})
objs = FakeResource.list(self.session, marker='x', objs = FakeResource.list(self.session, limit=1,
path_args=fake_arguments) path_args=fake_arguments)
objs = list(objs)
self.assertIn('marker=x', httpretty.last_request().path) self.assertIn(marker, httpretty.last_request().path)
self.assertEqual(3, len(objs)) self.assertEqual(3, len(objs))
for obj in objs: for obj in objs:
@@ -325,18 +330,19 @@ class TestFind(base.TestCase):
self.assertEqual(self.ID, result.id) self.assertEqual(self.ID, result.id)
p = {'fields': 'id', 'name': self.NAME} p = {'fields': 'id', 'name': self.NAME}
self.mock_get.assert_called_with(fake_path, params=p, service=None) self.mock_get.assert_any_call(fake_path, params=p, service=None)
def test_id(self): def test_id(self):
resp = FakeResponse({FakeResource.resources_key: [self.matrix]}) self.mock_get.side_effect = [
self.mock_get.return_value = resp FakeResponse({FakeResource.resources_key: [self.matrix]})
]
result = FakeResource.find(self.mock_session, self.ID, result = FakeResource.find(self.mock_session, self.ID,
path_args=fake_arguments) path_args=fake_arguments)
self.assertEqual(self.ID, result.id) self.assertEqual(self.ID, result.id)
p = {'fields': 'id', 'id': self.ID} p = {'fields': 'id', 'id': self.ID}
self.mock_get.assert_called_with(fake_path, params=p, service=None) self.mock_get.assert_any_call(fake_path, params=p, service=None)
def test_nameo(self): def test_nameo(self):
self.mock_get.side_effect = [ self.mock_get.side_effect = [
@@ -351,7 +357,7 @@ class TestFind(base.TestCase):
FakeResource.name_attribute = 'name' FakeResource.name_attribute = 'name'
self.assertEqual(self.ID, result.id) self.assertEqual(self.ID, result.id)
p = {'fields': 'id', 'nameo': self.NAME} p = {'fields': 'id', 'nameo': self.NAME}
self.mock_get.assert_called_with(fake_path, params=p, service=None) self.mock_get.assert_any_call(fake_path, params=p, service=None)
def test_dups(self): def test_dups(self):
dup = {'id': 'Larry'} dup = {'id': 'Larry'}
@@ -363,8 +369,9 @@ class TestFind(base.TestCase):
def test_id_attribute_find(self): def test_id_attribute_find(self):
floater = {'ip_address': "127.0.0.1"} floater = {'ip_address': "127.0.0.1"}
resp = FakeResponse({FakeResource.resources_key: [floater]}) self.mock_get.side_effect = [
self.mock_get.return_value = resp FakeResponse({FakeResource.resources_key: [floater]})
]
FakeResource.id_attribute = 'ip_address' FakeResource.id_attribute = 'ip_address'
result = FakeResource.find(self.mock_session, "127.0.0.1", result = FakeResource.find(self.mock_session, "127.0.0.1",
@@ -373,7 +380,7 @@ class TestFind(base.TestCase):
FakeResource.id_attribute = 'id' FakeResource.id_attribute = 'id'
p = {'fields': 'ip_address', 'ip_address': "127.0.0.1"} p = {'fields': 'ip_address', 'ip_address': "127.0.0.1"}
self.mock_get.assert_called_with(fake_path, params=p, service=None) self.mock_get.assert_any_call(fake_path, params=p, service=None)
def test_nada(self): def test_nada(self):
resp = FakeResponse({FakeResource.resources_key: []}) resp = FakeResponse({FakeResource.resources_key: []})