Skip to content

Commit 0d03d35

Browse files
Allow PaginatedList to use metadata for pagination (#614)
* Update `PaginatedList` Based on #605, `PaginatedList` could not process requests which return pagination info in the response body. This update checks for `Link` headers before checking the `meta` property in the response body. * Update based on review * Update pag list test with no header no next meta to run properly. Update changelog. --------- Co-authored-by: Matthew Emond <me@ucf.edu>
1 parent ac2fc96 commit 0d03d35

File tree

4 files changed

+116
-5
lines changed

4 files changed

+116
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### General
6+
7+
- Added support for pagination with metadata when headers are missing (Thanks, [@bennettscience](https://github.com/bennettscience))
8+
59
## [3.1.0] - 2023-04-21
610

711
### New Endpoint Coverage

canvasapi/paginated_list.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,22 @@ def _get_next_page(self):
5757
)
5858
data = response.json()
5959
self._next_url = None
60+
# Check the response headers first. This is the normal Canvas convention
61+
# for pagination, but there are endpoints which return a `meta` property
62+
# for pagination instead.
63+
# See https://github.com/ucfopen/canvasapi/discussions/605
64+
if response.links:
65+
next_link = response.links.get("next")
66+
elif isinstance(data, dict) and "meta" in data:
67+
# requests parses headers into dicts, this returns the same
68+
# structure so the regex will still work.
69+
try:
70+
next_link = {"url": data["meta"]["pagination"]["next"], "rel": "next"}
71+
except KeyError:
72+
next_link = None
73+
else:
74+
next_link = None
6075

61-
next_link = response.links.get("next")
6276
regex = r"{}(.*)".format(re.escape(self._requester.base_url))
6377

6478
self._next_url = (
@@ -73,8 +87,9 @@ def _get_next_page(self):
7387
try:
7488
data = data[self._root]
7589
except KeyError:
76-
# TODO: Fix this message to make more sense to an end user.
77-
raise ValueError("Invalid root value specified.")
90+
raise ValueError(
91+
"The key <{}> does not exist in the response.".format(self._root)
92+
)
7893

7994
for element in data:
8095
if element is not None:

tests/fixtures/paginated_list.json

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,66 @@
114114
}
115115
],
116116
"status_code": 200
117-
}
118-
}
117+
},
118+
"no_header_4_2_pages_p1": {
119+
"method": "ANY",
120+
"endpoint": "no_header_four_objects_two_pages",
121+
"data": {
122+
"assessments": [
123+
{
124+
"id": "1",
125+
"name": "object 1"
126+
},
127+
{
128+
"id": "2",
129+
"name": "object 2"
130+
}
131+
],
132+
"meta": {
133+
"pagination": {
134+
"next": "https://example.com/api/v1/no_header_four_objects_two_pages?page=2"
135+
}
136+
}
137+
},
138+
"status_code": 200
139+
},
140+
"no_header_4_2_pages_p2": {
141+
"method": "ANY",
142+
"endpoint": "no_header_four_objects_two_pages?page=2",
143+
"data": {
144+
"assessments": [
145+
{
146+
"id": "3",
147+
"name": "object 3"
148+
},
149+
{
150+
"id": "4",
151+
"name": "object 4"
152+
}
153+
]
154+
},
155+
"status_code": 200
156+
},
157+
"no_header_no_next_key": {
158+
"method": "ANY",
159+
"endpoint": "no_header_no_next_key",
160+
"data": {
161+
"assessments": [
162+
{
163+
"id": "1",
164+
"name": "object 1"
165+
},
166+
{
167+
"id": "2",
168+
"name": "object 2"
169+
}
170+
],
171+
"meta": {
172+
"pagination": {
173+
"prev": "https://example.com/api/v1/previous"
174+
}
175+
}
176+
}
177+
},
178+
"status_code": 200
179+
}

tests/test_paginated_list.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ def test_root_element_incorrect(self, m):
156156

157157
with self.assertRaises(ValueError):
158158
pag_list[0]
159+
self.assertEqual(
160+
pag_list[0], "The key <wrong> does not exist in the response."
161+
)
159162

160163
def test_root_element(self, m):
161164
register_uris({"account": ["get_enrollment_terms"]}, m)
@@ -202,3 +205,31 @@ def test_negative_index_for_slice_end(self, m):
202205

203206
with self.assertRaises(IndexError):
204207
pag_list[:-1]
208+
209+
def test_paginated_list_no_header(self, m):
210+
register_uris(
211+
{"paginated_list": ["no_header_4_2_pages_p1", "no_header_4_2_pages_p2"]}, m
212+
)
213+
214+
pag_list = PaginatedList(
215+
User,
216+
self.requester,
217+
"GET",
218+
"no_header_four_objects_two_pages",
219+
_root="assessments",
220+
)
221+
222+
self.assertIsInstance(pag_list, PaginatedList)
223+
self.assertEqual(len(list(pag_list)), 4)
224+
self.assertIsInstance(pag_list[0], User)
225+
226+
def test_paginated_list_no_header_no_next(self, m):
227+
register_uris({"paginated_list": ["no_header_no_next_key"]}, m)
228+
229+
pag_list = PaginatedList(
230+
User, self.requester, "GET", "no_header_no_next_key", _root="assessments"
231+
)
232+
233+
self.assertIsInstance(pag_list, PaginatedList)
234+
self.assertEqual(len(list(pag_list)), 2)
235+
self.assertIsInstance(pag_list[0], User)

0 commit comments

Comments
 (0)