1717These iterators simplify the process of paging through API responses
1818where 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
3836Or you can walk your way through items and call off the search early if
3937you find what you're looking for (resulting in possibly fewer
4038requests)::
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
4745When iterating, not every new item will send a request to the server.
4846To 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):
7070It's also possible to consume an entire page and handle the paging process
7171manually::
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
118155class 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):
175218class 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