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,26 @@ 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+ # pylint: disable=unused-argument
123+ def _do_nothing_page_start (iterator , page , response ):
124+ """Helper to provide custom behavior after a :class:`Page` is started.
125+
126+ This is a do-nothing stand-in as the default value.
127+
128+ :type iterator: :class:`Iterator`
129+ :param iterator: An iterator that holds some request info.
130+
131+ :type page: :class:`Page`
132+ :param page: The page that was just created.
133+
134+ :type response: dict
135+ :param response: The JSON API response for a page.
136+ """
137+ # pylint: enable=unused-argument
116138
117139
118140class Page (object ):
@@ -127,15 +149,21 @@ class Page(object):
127149 :type items_key: str
128150 :param items_key: The dictionary key used to retrieve items
129151 from the response.
152+
153+ :type item_to_value: callable
154+ :param item_to_value: Callable to convert an item from JSON
155+ into the native object. Assumed signature
156+ takes an :class:`Iterator` and a dictionary
157+ holding a single item.
130158 """
131159
132- def __init__ (self , parent , response , items_key ):
160+ def __init__ (self , parent , response , items_key , item_to_value ):
133161 self ._parent = parent
134162 items = response .get (items_key , ())
135163 self ._num_items = len (items )
136164 self ._remaining = self ._num_items
137165 self ._item_iter = iter (items )
138- self .response = response
166+ self ._item_to_value = item_to_value
139167
140168 @property
141169 def num_items (self ):
@@ -162,7 +190,7 @@ def __iter__(self):
162190 def next (self ):
163191 """Get the next value in the page."""
164192 item = six .next (self ._item_iter )
165- result = self ._parent . _item_to_value (item )
193+ result = self ._item_to_value (self . _parent , item )
166194 # Since we've successfully got the next value from the
167195 # iterator, we update the number of remaining.
168196 self ._remaining -= 1
@@ -175,12 +203,23 @@ def next(self):
175203class Iterator (object ):
176204 """A generic class for iterating through Cloud JSON APIs list responses.
177205
178- Sub-classes need to over-write :attr:`ITEMS_KEY` and to define
179- :meth:`_item_to_value`.
180-
181206 :type client: :class:`~google.cloud.client.Client`
182207 :param client: The client, which owns a connection to make requests.
183208
209+ :type path: str
210+ :param path: The path to query for the list of items. Defaults
211+ to :attr:`PATH` on the current iterator class.
212+
213+ :type item_to_value: callable
214+ :param item_to_value: Callable to convert an item from JSON
215+ into the native object. Assumed signature
216+ takes an :class:`Iterator` and a dictionary
217+ holding a single item.
218+
219+ :type items_key: str
220+ :param items_key: (Optional) The key used to grab retrieved items from an
221+ API response. Defaults to :data:`DEFAULT_ITEMS_KEY`.
222+
184223 :type page_token: str
185224 :param page_token: (Optional) A token identifying a page in a result set.
186225
@@ -191,26 +230,32 @@ class Iterator(object):
191230 :param extra_params: (Optional) Extra query string parameters for the
192231 API call.
193232
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.
233+ :type page_start: callable
234+ :param page_start: (Optional) Callable to provide any special behavior
235+ after a new page has been created. Assumed signature
236+ takes the :class:`Iterator` that started the page,
237+ the :class:`Page` that was started and the dictionary
238+ containing the page response.
197239 """
198240
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
241+ _PAGE_TOKEN = 'pageToken'
242+ _MAX_RESULTS = 'maxResults'
243+ _RESERVED_PARAMS = frozenset ([_PAGE_TOKEN , _MAX_RESULTS ])
244+
245+ def __init__ (self , client , path , item_to_value ,
246+ items_key = DEFAULT_ITEMS_KEY ,
247+ page_token = None , max_results = None , extra_params = None ,
248+ page_start = _do_nothing_page_start ):
212249 self .client = client
213- self .path = path or self .PATH
250+ self .path = path
251+ self ._items_key = items_key
252+ self ._item_to_value = item_to_value
253+ self .max_results = max_results
254+ self .extra_params = extra_params
255+ self ._page_start = page_start
256+ if self .extra_params is None :
257+ self .extra_params = {}
258+ self ._verify_params ()
214259 # The attributes below will change over the life of the iterator.
215260 self .page_number = 0
216261 self .next_page_token = page_token
@@ -222,7 +267,7 @@ def _verify_params(self):
222267
223268 :raises ValueError: If a reserved parameter is used.
224269 """
225- reserved_in_use = self .RESERVED_PARAMS .intersection (
270+ reserved_in_use = self ._RESERVED_PARAMS .intersection (
226271 self .extra_params )
227272 if reserved_in_use :
228273 raise ValueError ('Using a reserved parameter' ,
@@ -275,26 +320,16 @@ def update_page(self, require_empty=True):
275320 if page_empty :
276321 if self ._has_next_page ():
277322 response = self ._get_next_page_response ()
278- self ._page = self ._PAGE_CLASS (self , response , self .ITEMS_KEY )
323+ self ._page = Page (self , response , self ._items_key ,
324+ self ._item_to_value )
325+ self ._page_start (self , self ._page , response )
279326 else :
280327 self ._page = None
281328 else :
282329 if require_empty :
283330 msg = _PAGE_ERR_TEMPLATE % (self ._page , self .page .remaining )
284331 raise ValueError (msg )
285332
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-
298333 def next (self ):
299334 """Get the next item from the request."""
300335 self .update_page (require_empty = False )
@@ -330,9 +365,9 @@ def _get_query_params(self):
330365 """
331366 result = {}
332367 if self .next_page_token is not None :
333- result [self .PAGE_TOKEN ] = self .next_page_token
368+ result [self ._PAGE_TOKEN ] = self .next_page_token
334369 if self .max_results is not None :
335- result [self .MAX_RESULTS ] = self .max_results - self .num_results
370+ result [self ._MAX_RESULTS ] = self .max_results - self .num_results
336371 result .update (self .extra_params )
337372 return result
338373
0 commit comments