Skip to content

Commit db650b7

Browse files
committed
Removing Iterator and Page subclasses.
Instead require `Iterator` takes: - a well-formed path for the request - a callable to convert a JSON item to native obj. - (optional) the key in a response holding all items - (optional) a `page_start` (acts as proxy for `Page.__init__`)
1 parent 0aca3f6 commit db650b7

File tree

8 files changed

+407
-485
lines changed

8 files changed

+407
-485
lines changed

core/google/cloud/iterator.py

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,35 @@
1717
These iterators simplify the process of paging through API responses
1818
where the response is a list of results with a ``nextPageToken``.
1919
20-
To make an iterator work, you may need to override the
21-
``ITEMS_KEY`` class attribute so that a given response (containing a page of
22-
results) can be parsed into an iterable page of the actual objects you want::
23-
24-
class MyIterator(Iterator):
25-
26-
ITEMS_KEY = 'blocks'
27-
28-
def _item_to_value(self, item):
29-
my_item = MyItemClass(other_arg=True)
30-
my_item._set_properties(item)
31-
return my_item
32-
33-
You then can use this to get **all** the results from a resource::
34-
35-
>>> iterator = MyIterator(...)
20+
To make an iterator work, you'll need to provide a way to convert a JSON
21+
item returned from the API into the object of your choice (via
22+
``item_to_value``). You also may need to specify a custom ``items_key`` so
23+
that a given response (containing a page of results) can be parsed into an
24+
iterable page of the actual objects you want. You then can use this to get
25+
**all** the results from a resource::
26+
27+
>>> def item_to_value(iterator, item):
28+
... my_item = MyItemClass(iterator.client, other_arg=True)
29+
... my_item._set_properties(item)
30+
... return my_item
31+
...
32+
>>> iterator = Iterator(..., items_key='blocks',
33+
... item_to_value=item_to_value)
3634
>>> list(iterator) # Convert to a list (consumes all values).
3735
3836
Or you can walk your way through items and call off the search early if
3937
you find what you're looking for (resulting in possibly fewer
4038
requests)::
4139
42-
>>> for my_item in MyIterator(...):
40+
>>> for my_item in Iterator(...):
4341
... print(my_item.name)
4442
... if not my_item.is_valid:
4543
... break
4644
4745
When iterating, not every new item will send a request to the server.
4846
To monitor these requests, track the current page of the iterator::
4947
50-
>>> iterator = MyIterator(...)
48+
>>> iterator = Iterator(...)
5149
>>> iterator.page_number
5250
0
5351
>>> next(iterator)
@@ -58,6 +56,8 @@ def _item_to_value(self, item):
5856
1
5957
>>> next(iterator)
6058
<MyItemClass at 0x7f1d3cccfe90>
59+
>>> iterator.page_number
60+
1
6161
>>> iterator.page.remaining
6262
0
6363
>>> next(iterator)
@@ -70,7 +70,7 @@ def _item_to_value(self, item):
7070
It's also possible to consume an entire page and handle the paging process
7171
manually::
7272
73-
>>> iterator = MyIterator(...)
73+
>>> iterator = Iterator(...)
7474
>>> # Manually pull down the first page.
7575
>>> iterator.update_page()
7676
>>> items = list(iterator.page)
@@ -96,6 +96,8 @@ def _item_to_value(self, item):
9696
]
9797
>>>
9898
>>> # When there are no more results
99+
>>> iterator.next_page_token is None
100+
True
99101
>>> iterator.update_page()
100102
>>> iterator.page is None
101103
True
@@ -113,6 +115,41 @@ def _item_to_value(self, item):
113115
_PAGE_ERR_TEMPLATE = (
114116
'Tried to update the page while current page (%r) still has %d '
115117
'items remaining.')
118+
DEFAULT_ITEMS_KEY = 'items'
119+
"""The dictionary key used to retrieve items from each response."""
120+
121+
122+
def _not_implemented_item_to_value(iterator, item):
123+
"""Helper to convert an item into the native object.
124+
125+
This is a virtual stand-in as the default value, effectively
126+
causing callers to pass in their own callable.
127+
128+
:type iterator: :class:`Iterator`
129+
:param iterator: An iterator that holds some request info.
130+
131+
:type item: dict
132+
:param item: A JSON object to be converted into a native object.
133+
134+
:raises NotImplementedError: Always.
135+
"""
136+
raise NotImplementedError
137+
138+
139+
def _do_nothing_page_start(iterator, page, response):
140+
"""Helper to provide custom behavior after a :class:`Page` is started.
141+
142+
This is a do-nothing stand-in as the default value.
143+
144+
:type iterator: :class:`Iterator`
145+
:param iterator: An iterator that holds some request info.
146+
147+
:type page: :class:`Page`
148+
:param page: The page that was just created.
149+
150+
:type response: dict
151+
:param response: The JSON API response for a page.
152+
"""
116153

117154

118155
class Page(object):
@@ -127,15 +164,21 @@ class Page(object):
127164
:type items_key: str
128165
:param items_key: The dictionary key used to retrieve items
129166
from the response.
167+
168+
:type item_to_value: callable
169+
:param item_to_value: Callable to convert an item from JSON
170+
into the native object. Assumed signature
171+
takes an :class:`Iterator` and a dictionary
172+
holding a single item.
130173
"""
131174

132-
def __init__(self, parent, response, items_key):
175+
def __init__(self, parent, response, items_key, item_to_value):
133176
self._parent = parent
134177
items = response.get(items_key, ())
135178
self._num_items = len(items)
136179
self._remaining = self._num_items
137180
self._item_iter = iter(items)
138-
self.response = response
181+
self._item_to_value = item_to_value
139182

140183
@property
141184
def num_items(self):
@@ -162,7 +205,7 @@ def __iter__(self):
162205
def next(self):
163206
"""Get the next value in the page."""
164207
item = six.next(self._item_iter)
165-
result = self._parent._item_to_value(item)
208+
result = self._item_to_value(self._parent, item)
166209
# Since we've successfully got the next value from the
167210
# iterator, we update the number of remaining.
168211
self._remaining -= 1
@@ -175,12 +218,23 @@ def next(self):
175218
class Iterator(object):
176219
"""A generic class for iterating through Cloud JSON APIs list responses.
177220
178-
Sub-classes need to over-write :attr:`ITEMS_KEY` and to define
179-
:meth:`_item_to_value`.
180-
181221
:type client: :class:`~google.cloud.client.Client`
182222
:param client: The client, which owns a connection to make requests.
183223
224+
:type path: str
225+
:param path: The path to query for the list of items. Defaults
226+
to :attr:`PATH` on the current iterator class.
227+
228+
:type items_key: str
229+
:param items_key: The key used to grab retrieved items from an API
230+
response. Defaults to :data:`DEFAULT_ITEMS_KEY`.
231+
232+
:type item_to_value: callable
233+
:param item_to_value: (Optional) Callable to convert an item from JSON
234+
into the native object. Assumed signature
235+
takes an :class:`Iterator` and a dictionary
236+
holding a single item.
237+
184238
:type page_token: str
185239
:param page_token: (Optional) A token identifying a page in a result set.
186240
@@ -191,26 +245,32 @@ class Iterator(object):
191245
:param extra_params: (Optional) Extra query string parameters for the
192246
API call.
193247
194-
:type path: str
195-
:param path: (Optional) The path to query for the list of items. Defaults
196-
to :attr:`PATH` on the current iterator class.
248+
:type page_start: callable
249+
:param page_start: (Optional) Callable to provide any special behavior
250+
after a new page has been created. Assumed signature
251+
takes the :class:`Iterator` that started the page,
252+
the :class:`Page` that was started and the dictionary
253+
containing the page response.
197254
"""
198255

199-
PAGE_TOKEN = 'pageToken'
200-
MAX_RESULTS = 'maxResults'
201-
RESERVED_PARAMS = frozenset([PAGE_TOKEN, MAX_RESULTS])
202-
PATH = None
203-
ITEMS_KEY = 'items'
204-
"""The dictionary key used to retrieve items from each response."""
205-
_PAGE_CLASS = Page
206-
207-
def __init__(self, client, page_token=None, max_results=None,
208-
extra_params=None, path=None):
209-
self.extra_params = extra_params or {}
210-
self._verify_params()
211-
self.max_results = max_results
256+
_PAGE_TOKEN = 'pageToken'
257+
_MAX_RESULTS = 'maxResults'
258+
_RESERVED_PARAMS = frozenset([_PAGE_TOKEN, _MAX_RESULTS])
259+
260+
def __init__(self, client, path, items_key=DEFAULT_ITEMS_KEY,
261+
item_to_value=_not_implemented_item_to_value,
262+
page_token=None, max_results=None, extra_params=None,
263+
page_start=_do_nothing_page_start):
212264
self.client = client
213-
self.path = path or self.PATH
265+
self.path = path
266+
self._items_key = items_key
267+
self._item_to_value = item_to_value
268+
self.max_results = max_results
269+
self.extra_params = extra_params
270+
self._page_start = page_start
271+
if self.extra_params is None:
272+
self.extra_params = {}
273+
self._verify_params()
214274
# The attributes below will change over the life of the iterator.
215275
self.page_number = 0
216276
self.next_page_token = page_token
@@ -222,7 +282,7 @@ def _verify_params(self):
222282
223283
:raises ValueError: If a reserved parameter is used.
224284
"""
225-
reserved_in_use = self.RESERVED_PARAMS.intersection(
285+
reserved_in_use = self._RESERVED_PARAMS.intersection(
226286
self.extra_params)
227287
if reserved_in_use:
228288
raise ValueError('Using a reserved parameter',
@@ -275,26 +335,16 @@ def update_page(self, require_empty=True):
275335
if page_empty:
276336
if self._has_next_page():
277337
response = self._get_next_page_response()
278-
self._page = self._PAGE_CLASS(self, response, self.ITEMS_KEY)
338+
self._page = Page(self, response, self._items_key,
339+
self._item_to_value)
340+
self._page_start(self, self._page, response)
279341
else:
280342
self._page = None
281343
else:
282344
if require_empty:
283345
msg = _PAGE_ERR_TEMPLATE % (self._page, self.page.remaining)
284346
raise ValueError(msg)
285347

286-
def _item_to_value(self, item):
287-
"""Get the next item in the page.
288-
289-
Subclasses will need to implement this method.
290-
291-
:type item: dict
292-
:param item: An item to be converted to a native object.
293-
294-
:raises NotImplementedError: Always
295-
"""
296-
raise NotImplementedError
297-
298348
def next(self):
299349
"""Get the next item from the request."""
300350
self.update_page(require_empty=False)
@@ -330,9 +380,9 @@ def _get_query_params(self):
330380
"""
331381
result = {}
332382
if self.next_page_token is not None:
333-
result[self.PAGE_TOKEN] = self.next_page_token
383+
result[self._PAGE_TOKEN] = self.next_page_token
334384
if self.max_results is not None:
335-
result[self.MAX_RESULTS] = self.max_results - self.num_results
385+
result[self._MAX_RESULTS] = self.max_results - self.num_results
336386
result.update(self.extra_params)
337387
return result
338388

0 commit comments

Comments
 (0)