Skip to content

Commit 35eea3d

Browse files
authored
Merge pull request #138 from Kinto/136-add-pages-to-paginated-methods
Add pages options for pagination (fixes #136)
2 parents 9114595 + 2536ab7 commit 35eea3d

File tree

4 files changed

+94
-10
lines changed

4 files changed

+94
-10
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This document describes changes between each past release.
1212
- Keep tracks of Backoff headers and raise an ``BackoffException`` if
1313
we are not waiting enough between two calls. (#53)
1414
- Add ``--retry`` and ``--retry-after`` to CLI utils helpers (fixes #126)
15+
- Fetch only one page when ``_limit`` is specified and allow to override this
16+
with a ``pages`` argument (fixes #136)
1517

1618
**Bug fixes**
1719

README.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,26 @@ It is possible (but not recommended) to force this value in the clients:
367367
retry=10,
368368
retry_after=5)
369369
370+
Pagination
371+
----------
372+
373+
When the server responses are paginated, the client will download every pages and
374+
merge them transparently.
375+
376+
However, it is possible to specify a limit for the number of items to be retrieved
377+
in one page:
378+
379+
.. code-block:: python
380+
381+
records = client.get_records(_limit=10)
382+
383+
In order to retrieve every available pages with a limited number of items in each
384+
of them, you can specify the number of pages:
385+
386+
.. code-block:: python
387+
388+
records = client.get_records(_limit=10, pages=float('inf')) # Infinity
389+
370390
371391
Generating endpoint paths
372392
-------------------------

kinto_http/__init__.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,16 @@ def get_endpoint(self, name, bucket=None, group=None, collection=None, id=None):
121121
}
122122
return self.endpoints.get(name, **kwargs)
123123

124-
def _paginated(self, endpoint, records=None, if_none_match=None, **kwargs):
124+
def _paginated(self, endpoint, records=None, if_none_match=None, pages=None, **kwargs):
125125
if records is None:
126126
records = collections.OrderedDict()
127127
headers = {}
128128
if if_none_match is not None:
129129
headers['If-None-Match'] = utils.quote(if_none_match)
130130

131+
if pages is None:
132+
pages = 1 if '_limit' in kwargs else float('inf')
133+
131134
record_resp, headers = self.session.request(
132135
'get', endpoint, headers=headers, params=kwargs)
133136

@@ -139,12 +142,13 @@ def _paginated(self, endpoint, records=None, if_none_match=None, **kwargs):
139142
records_tuples = [(r['id'], r) for r in record_resp['data']]
140143
records.update(collections.OrderedDict(records_tuples))
141144

142-
if 'next-page' in map(str.lower, headers.keys()):
145+
if pages > 1 and 'next-page' in map(str.lower, headers.keys()):
143146
# Paginated wants a relative URL, but the returned one is
144147
# absolute.
145148
next_page = headers['Next-Page']
146149
return self._paginated(next_page, records,
147-
if_none_match=if_none_match)
150+
if_none_match=if_none_match,
151+
pages=pages - 1)
148152
return list(records.values())
149153

150154
def _get_cache_headers(self, safe, data=None, if_match=None):
@@ -239,9 +243,9 @@ def patch_bucket(self, *args, **kwargs):
239243
kwargs['method'] = 'patch'
240244
return self.update_bucket(*args, **kwargs)
241245

242-
def get_buckets(self):
246+
def get_buckets(self, **kwargs):
243247
endpoint = self.get_endpoint('buckets')
244-
return self._paginated(endpoint)
248+
return self._paginated(endpoint, **kwargs)
245249

246250
def get_bucket(self, bucket=None):
247251
endpoint = self.get_endpoint('bucket', bucket=bucket)
@@ -279,9 +283,9 @@ def delete_buckets(self, safe=True, if_match=None):
279283

280284
# Groups
281285

282-
def get_groups(self, bucket=None):
286+
def get_groups(self, bucket=None, **kwargs):
283287
endpoint = self.get_endpoint('groups', bucket=bucket)
284-
return self._paginated(endpoint)
288+
return self._paginated(endpoint, **kwargs)
285289

286290
def create_group(self, group, bucket=None,
287291
data=None, permissions=None,
@@ -373,9 +377,9 @@ def delete_groups(self, bucket=None, safe=True, if_match=None):
373377

374378
# Collections
375379

376-
def get_collections(self, bucket=None):
380+
def get_collections(self, bucket=None, **kwargs):
377381
endpoint = self.get_endpoint('collections', bucket=bucket)
378-
return self._paginated(endpoint)
382+
return self._paginated(endpoint, **kwargs)
379383

380384
def create_collection(self, collection=None, bucket=None,
381385
data=None, permissions=None, safe=True,

kinto_http/tests/test_client.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,10 +674,14 @@ def test_pagination_is_followed(self):
674674
[{'id': '1', 'value': 'item1'},
675675
{'id': '2', 'value': 'item2'}, ],
676676
{'Next-Page': link}),
677-
# Second one returns a list of items without a pagination token.
678677
build_response(
679678
[{'id': '3', 'value': 'item3'},
680679
{'id': '4', 'value': 'item4'}, ],
680+
{'Next-Page': link}),
681+
# Second one returns a list of items without a pagination token.
682+
build_response(
683+
[{'id': '5', 'value': 'item5'},
684+
{'id': '6', 'value': 'item6'}, ],
681685
),
682686
]
683687
records = self.client.get_records('bucket', 'collection')
@@ -687,6 +691,60 @@ def test_pagination_is_followed(self):
687691
{'id': '2', 'value': 'item2'},
688692
{'id': '3', 'value': 'item3'},
689693
{'id': '4', 'value': 'item4'},
694+
{'id': '5', 'value': 'item5'},
695+
{'id': '6', 'value': 'item6'},
696+
]
697+
698+
def test_pagination_is_followed_for_number_of_pages(self):
699+
# Mock the calls to request.
700+
link = ('http://example.org/buckets/buck/collections/coll/records/'
701+
'?token=1234')
702+
703+
self.session.request.side_effect = [
704+
# First one returns a list of items with a pagination token.
705+
build_response(
706+
[{'id': '1', 'value': 'item1'},
707+
{'id': '2', 'value': 'item2'}, ],
708+
{'Next-Page': link}),
709+
build_response(
710+
[{'id': '3', 'value': 'item3'},
711+
{'id': '4', 'value': 'item4'}, ],
712+
{'Next-Page': link}),
713+
# Second one returns a list of items without a pagination token.
714+
build_response(
715+
[{'id': '5', 'value': 'item5'},
716+
{'id': '6', 'value': 'item6'}, ],
717+
),
718+
]
719+
records = self.client.get_records('bucket', 'collection', pages=2)
720+
721+
assert list(records) == [
722+
{'id': '1', 'value': 'item1'},
723+
{'id': '2', 'value': 'item2'},
724+
{'id': '3', 'value': 'item3'},
725+
{'id': '4', 'value': 'item4'},
726+
]
727+
728+
def test_pagination_is_not_followed_if_limit_is_specified(self):
729+
# Mock the calls to request.
730+
link = ('http://example.org/buckets/buck/collections/coll/records/'
731+
'?token=1234')
732+
733+
self.session.request.side_effect = [
734+
build_response(
735+
[{'id': '1', 'value': 'item1'},
736+
{'id': '2', 'value': 'item2'}, ],
737+
{'Next-Page': link}),
738+
build_response(
739+
[{'id': '3', 'value': 'item3'},
740+
{'id': '4', 'value': 'item4'}, ],
741+
),
742+
]
743+
records = self.client.get_records('bucket', 'collection', _limit=2)
744+
745+
assert list(records) == [
746+
{'id': '1', 'value': 'item1'},
747+
{'id': '2', 'value': 'item2'}
690748
]
691749

692750
def test_pagination_supports_if_none_match(self):

0 commit comments

Comments
 (0)