Skip to content

Commit f2aa180

Browse files
author
Jon Wayne Parrott
authored
Add google.api.page_iterator.GRPCIterator (#3843)
1 parent 31a9b6b commit f2aa180

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

packages/google-cloud-core/google/api/core/page_iterator.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,90 @@ def _next_page(self):
423423
return page
424424
except StopIteration:
425425
return None
426+
427+
428+
class GRPCIterator(Iterator):
429+
"""A generic class for iterating through gRPC list responses.
430+
431+
.. note:: The class does not take a ``page_token`` argument because it can
432+
just be specified in the ``request``.
433+
434+
Args:
435+
client (google.cloud.client.Client): The API client. This unused by
436+
this class, but kept to satisfy the :class:`Iterator` interface.
437+
method (Callable[protobuf.Message]): A bound gRPC method that should
438+
take a single message for the request.
439+
request (protobuf.Message): The request message.
440+
items_field (str): The field in the response message that has the
441+
items for the page.
442+
item_to_value (Callable[Iterator, Any]): Callable to convert an item
443+
from the type in the JSON response into a native object. Will
444+
be called with the iterator and a single item.
445+
request_token_field (str): The field in the request message used to
446+
specify the page token.
447+
response_token_field (str): The field in the response message that has
448+
the token for the next page.
449+
max_results (int): The maximum number of results to fetch.
450+
451+
.. autoattribute:: pages
452+
"""
453+
454+
_DEFAULT_REQUEST_TOKEN_FIELD = 'page_token'
455+
_DEFAULT_RESPONSE_TOKEN_FIELD = 'next_page_token'
456+
457+
def __init__(
458+
self,
459+
client,
460+
method,
461+
request,
462+
items_field,
463+
item_to_value=_item_to_value_identity,
464+
request_token_field=_DEFAULT_REQUEST_TOKEN_FIELD,
465+
response_token_field=_DEFAULT_RESPONSE_TOKEN_FIELD,
466+
max_results=None):
467+
super(GRPCIterator, self).__init__(
468+
client, item_to_value, max_results=max_results)
469+
self._method = method
470+
self._request = request
471+
self._items_field = items_field
472+
self._request_token_field = request_token_field
473+
self._response_token_field = response_token_field
474+
475+
def _next_page(self):
476+
"""Get the next page in the iterator.
477+
478+
Returns:
479+
Page: The next page in the iterator or :data:`None` if there are no
480+
pages left.
481+
"""
482+
if not self._has_next_page():
483+
return None
484+
485+
if self.next_page_token is not None:
486+
setattr(
487+
self._request, self._request_token_field, self.next_page_token)
488+
489+
response = self._method(self._request)
490+
491+
self.next_page_token = getattr(response, self._response_token_field)
492+
items = getattr(response, self._items_field)
493+
page = Page(self, items, self._item_to_value)
494+
495+
return page
496+
497+
def _has_next_page(self):
498+
"""Determines whether or not there are more pages with results.
499+
500+
Returns:
501+
bool: Whether the iterator has more pages.
502+
"""
503+
if self.page_number == 0:
504+
return True
505+
506+
if self.max_results is not None:
507+
if self.num_results >= self.max_results:
508+
return False
509+
510+
# Note: intentionally a falsy check instead of a None check. The RPC
511+
# can return an empty string indicating no more pages.
512+
return True if self.next_page_token else False

packages/google-cloud-core/tests/unit/api_core/test_page_iterator.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,90 @@ def test__get_next_page_bad_http_method(self):
408408
iterator._get_next_page_response()
409409

410410

411+
class TestGRPCIterator(object):
412+
413+
def test_constructor(self):
414+
client = mock.sentinel.client
415+
items_field = 'items'
416+
iterator = page_iterator.GRPCIterator(
417+
client, mock.sentinel.method, mock.sentinel.request, items_field)
418+
419+
assert not iterator._started
420+
assert iterator.client is client
421+
assert iterator.max_results is None
422+
assert iterator._method == mock.sentinel.method
423+
assert iterator._request == mock.sentinel.request
424+
assert iterator._items_field == items_field
425+
assert iterator._item_to_value is page_iterator._item_to_value_identity
426+
assert (iterator._request_token_field ==
427+
page_iterator.GRPCIterator._DEFAULT_REQUEST_TOKEN_FIELD)
428+
assert (iterator._response_token_field ==
429+
page_iterator.GRPCIterator._DEFAULT_RESPONSE_TOKEN_FIELD)
430+
# Changing attributes.
431+
assert iterator.page_number == 0
432+
assert iterator.next_page_token is None
433+
assert iterator.num_results == 0
434+
435+
def test_constructor_options(self):
436+
client = mock.sentinel.client
437+
items_field = 'items'
438+
request_field = 'request'
439+
response_field = 'response'
440+
iterator = page_iterator.GRPCIterator(
441+
client, mock.sentinel.method, mock.sentinel.request, items_field,
442+
item_to_value=mock.sentinel.item_to_value,
443+
request_token_field=request_field,
444+
response_token_field=response_field,
445+
max_results=42)
446+
447+
assert iterator.client is client
448+
assert iterator.max_results == 42
449+
assert iterator._method == mock.sentinel.method
450+
assert iterator._request == mock.sentinel.request
451+
assert iterator._items_field == items_field
452+
assert iterator._item_to_value is mock.sentinel.item_to_value
453+
assert iterator._request_token_field == request_field
454+
assert iterator._response_token_field == response_field
455+
456+
def test_iterate(self):
457+
request = mock.Mock(spec=['page_token'], page_token=None)
458+
response1 = mock.Mock(items=['a', 'b'], next_page_token='1')
459+
response2 = mock.Mock(items=['c'], next_page_token='2')
460+
response3 = mock.Mock(items=['d'], next_page_token='')
461+
method = mock.Mock(side_effect=[response1, response2, response3])
462+
iterator = page_iterator.GRPCIterator(
463+
mock.sentinel.client, method, request, 'items')
464+
465+
assert iterator.num_results == 0
466+
467+
items = list(iterator)
468+
assert items == ['a', 'b', 'c', 'd']
469+
470+
method.assert_called_with(request)
471+
assert method.call_count == 3
472+
assert request.page_token == '2'
473+
474+
def test_iterate_with_max_results(self):
475+
request = mock.Mock(spec=['page_token'], page_token=None)
476+
response1 = mock.Mock(items=['a', 'b'], next_page_token='1')
477+
response2 = mock.Mock(items=['c'], next_page_token='2')
478+
response3 = mock.Mock(items=['d'], next_page_token='')
479+
method = mock.Mock(side_effect=[response1, response2, response3])
480+
iterator = page_iterator.GRPCIterator(
481+
mock.sentinel.client, method, request, 'items', max_results=3)
482+
483+
assert iterator.num_results == 0
484+
485+
items = list(iterator)
486+
487+
assert items == ['a', 'b', 'c']
488+
assert iterator.num_results == 3
489+
490+
method.assert_called_with(request)
491+
assert method.call_count == 2
492+
assert request.page_token is '1'
493+
494+
411495
class GAXPageIterator(object):
412496
"""Fake object that matches gax.PageIterator"""
413497
def __init__(self, pages, page_token=None):

0 commit comments

Comments
 (0)