1515"""
1616API module for interacting with result collections.
1717"""
18+ from functools import partial
1819from ._2to3 import STRTYPE
1920from .error import ResultException
2021from ._common_util import py_to_couch_validate , type_or_none
@@ -327,16 +328,16 @@ def _handle_result_by_key_slice(self, key_slice):
327328 def __iter__ (self ):
328329 """
329330 Provides iteration support, primarily for large data collections.
330- The iterator uses the ``skip`` and ``limit`` options to consume
331- data in chunks controlled by the ``page_size`` option. It retrieves
332- a batch of data from the result collection and then yields each
333- element.
331+ The iterator uses the ``startkey``, ``startkey_docid``, and ``limit``
332+ options to consume data in chunks controlled by the ``page_size``
333+ option. It retrieves a batch of data from the result collection
334+ and then yields each element.
334335
335336 See :class:`~cloudant.result.Result` for Result iteration examples.
336337
337338 :returns: Iterable data sequence
338339 """
339- invalid_options = ('skip ' , 'limit' )
340+ invalid_options = ('limit ' , )
340341 if any (x in invalid_options for x in self .options ):
341342 raise ResultException (103 , invalid_options , self .options )
342343
@@ -347,21 +348,60 @@ def __iter__(self):
347348 except ValueError :
348349 raise ResultException (104 , self ._page_size )
349350
350- skip = 0
351+ init_opts = {
352+ 'skip' : self .options .pop ('skip' , None ),
353+ 'startkey' : self .options .pop ('startkey' , None )
354+ }
355+
356+ self ._call = partial (self ._ref , #pylint: disable=attribute-defined-outside-init
357+ limit = self ._real_page_size ,
358+ ** self .options )
359+
360+ response = self ._call (** {k : v
361+ for k , v
362+ in init_opts .items ()
363+ if v is not None })
364+
365+ return self ._iterator (response )
366+
367+ @property
368+ def _real_page_size (self ):
369+ '''
370+ In views we paginate with N+1 items per page.
371+ https://docs.couchdb.org/en/stable/ddocs/views/pagination.html#paging-alternate-method
372+ '''
373+ return self ._page_size + 1
374+
375+ def _iterator (self , response ):
376+ '''
377+ Iterate through view data.
378+ '''
379+
351380 while True :
352- response = self ._ref (
353- limit = self ._page_size ,
354- skip = skip ,
355- ** self .options
356- )
357381 result = self ._parse_data (response )
358- skip += self ._page_size
359382 if result :
383+ doc_count = len (result )
384+ last = result .pop ()
360385 for row in result :
361386 yield row
362- if len (result ) < self ._page_size :
387+
388+ # We expect doc_count = self._page_size + 1 results, if
389+ # we have self._page_size or less it means we are on the
390+ # last page and need to return the last result.
391+ if doc_count < self ._real_page_size :
392+ yield last
363393 break
364394 del result
395+
396+ # if we are in a view, keys could be duplicate so we
397+ # need to start from the right docid
398+ if last ['id' ]:
399+ response = self ._call (startkey = last ['key' ],
400+ startkey_docid = last ['id' ])
401+ # reduce result keys are unique by definition
402+ else :
403+ response = self ._call (startkey = last ['key' ])
404+
365405 else :
366406 break
367407
@@ -510,3 +550,32 @@ def _parse_data(self, data):
510550 query result JSON response content
511551 """
512552 return data .get ('docs' , [])
553+
554+ @property
555+ def _real_page_size (self ):
556+ '''
557+ During queries iteration page size is user-specified
558+ '''
559+ return self ._page_size
560+
561+ def _iterator (self , response ):
562+ '''
563+ Iterate through query data.
564+ '''
565+
566+ while True :
567+ result = self ._parse_data (response )
568+ bookmark = response .get ('bookmark' )
569+ if result :
570+ for row in result :
571+ yield row
572+
573+ del result
574+
575+ if not bookmark :
576+ break
577+
578+ response = self ._call (bookmark = bookmark )
579+
580+ else :
581+ break
0 commit comments