Skip to content

Commit d021428

Browse files
authored
Refactor: Add standalone paginate helper (#120)
2 parents a44e25a + b662228 commit d021428

4 files changed

Lines changed: 120 additions & 33 deletions

File tree

tests/test_pagination.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from trakt.pagination import paginate
2+
3+
4+
class FakeClient:
5+
def __init__(self, pages):
6+
self.pages = list(pages)
7+
self.urls = []
8+
9+
def get(self, url, include_headers=False):
10+
self.urls.append((url, include_headers))
11+
return self.pages.pop(0)
12+
13+
14+
def test_paginate_fetches_all_list_pages():
15+
client = FakeClient([
16+
([{'title': 'One'}], {'X-Pagination-Page-Count': '3'}),
17+
([{'title': 'Two'}], {'X-Pagination-Page-Count': '3'}),
18+
([{'title': 'Three'}], {'X-Pagination-Page-Count': '3'}),
19+
])
20+
21+
result = paginate(
22+
'users/{user}/watched/movies',
23+
api=client,
24+
user='sean',
25+
limit=2,
26+
)
27+
28+
assert result == [
29+
{'title': 'One'},
30+
{'title': 'Two'},
31+
{'title': 'Three'},
32+
]
33+
assert client.urls == [
34+
('users/sean/watched/movies?limit=2', True),
35+
('users/sean/watched/movies?page=2&limit=2', True),
36+
('users/sean/watched/movies?page=3&limit=2', True),
37+
]
38+
39+
40+
def test_paginate_stops_after_one_page_without_valid_page_count():
41+
client = FakeClient([
42+
([{'title': 'One'}], {'X-Pagination-Page-Count': 'invalid'}),
43+
([{'title': 'Two'}], {'X-Pagination-Page-Count': '2'}),
44+
])
45+
46+
result = paginate('movies/popular', api=client)
47+
48+
assert result == [{'title': 'One'}]
49+
assert client.urls == [('movies/popular', True)]
50+
51+
52+
def test_paginate_stops_after_one_page_without_page_count():
53+
client = FakeClient([
54+
([{'title': 'One'}], {}),
55+
([{'title': 'Two'}], {'X-Pagination-Page-Count': '2'}),
56+
])
57+
58+
result = paginate('movies/popular', api=client)
59+
60+
assert result == [{'title': 'One'}]
61+
assert client.urls == [('movies/popular', True)]
62+
63+
64+
def test_paginate_skips_none_extends_lists_and_appends_objects():
65+
client = FakeClient([
66+
(None, {'X-Pagination-Page-Count': '3'}),
67+
([{'title': 'Two'}], {'X-Pagination-Page-Count': '3'}),
68+
({'title': 'Three'}, {'X-Pagination-Page-Count': '3'}),
69+
])
70+
71+
result = paginate('movies/popular', api=client)
72+
73+
assert result == [{'title': 'Two'}, {'title': 'Three'}]

trakt/api.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from trakt.core import TIMEOUT
1313
from trakt.errors import (BadRequestException, BadResponseException,
1414
OAuthException, OAuthRefreshException)
15-
from trakt.utils import build_uri
1615

1716
__author__ = 'Elan Ruusamäe'
1817

@@ -80,35 +79,6 @@ def put(self, url: str, data):
8079
"""
8180
return self.request('put', url, data=data)
8281

83-
def iter_pages(self, url, **params):
84-
"""Yield successive pages for a paginated GET endpoint."""
85-
page = 1
86-
while True:
87-
page_url = build_uri(url, **params) if page == 1 else build_uri(
88-
url, page=page, **params
89-
)
90-
page_data, headers = self.get(page_url, include_headers=True)
91-
yield page_data
92-
try:
93-
page_count = int(headers.get('X-Pagination-Page-Count', 1))
94-
except (TypeError, ValueError):
95-
page_count = 1
96-
if page >= page_count:
97-
break
98-
page += 1
99-
100-
def paginate(self, url, **params):
101-
"""Return a flattened list from all pages of a paginated GET endpoint."""
102-
results = []
103-
for page_data in self.iter_pages(url, **params):
104-
if page_data is None:
105-
continue
106-
if isinstance(page_data, list):
107-
results.extend(page_data)
108-
else:
109-
results.append(page_data)
110-
return results
111-
11282
@property
11383
def auth(self):
11484
"""

trakt/pagination.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Pagination helpers for Trakt API clients."""
2+
3+
from trakt.core import api as factory
4+
from trakt.utils import build_uri
5+
6+
__author__ = 'Elan Ruusamäe'
7+
__all__ = ['paginate']
8+
9+
10+
def page_count(headers):
11+
try:
12+
return int(headers.get('X-Pagination-Page-Count', 1))
13+
except (TypeError, ValueError):
14+
return 1
15+
16+
17+
def iter_pages(client, url: str, **params):
18+
"""Yield successive pages for a paginated GET endpoint."""
19+
params.pop('page', None)
20+
page = 1
21+
while True:
22+
page_url = build_uri(url, **params) if page == 1 else build_uri(
23+
url, page=page, **params
24+
)
25+
page_data, headers = client.get(page_url, include_headers=True)
26+
yield page_data
27+
28+
if page >= page_count(headers):
29+
break
30+
page += 1
31+
32+
33+
def paginate(url: str, api=None, **params):
34+
"""Return a flattened list from all pages of a paginated GET endpoint."""
35+
results = []
36+
client = api or factory()
37+
for page_data in iter_pages(client, url, **params):
38+
if page_data is None:
39+
continue
40+
if isinstance(page_data, list):
41+
results.extend(page_data)
42+
else:
43+
results.append(page_data)
44+
return results

trakt/users.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from dataclasses import dataclass, fields
55
from typing import Any, NamedTuple, Optional, Union
66

7-
from trakt.core import api, delete, get, post
7+
from trakt.core import delete, get, post
88
from trakt.mixins import DataClassMixin, IdsMixin
99
from trakt.movies import Movie
10+
from trakt.pagination import paginate
1011
from trakt.people import Person
1112
from trakt.tv import TVEpisode, TVSeason, TVShow
1213
from trakt.utils import build_uri, slugify
@@ -548,8 +549,7 @@ def watched_movies(self):
548549
collection. Automatically fetches all pages.
549550
"""
550551
if self._watched_movies is None:
551-
client = api()
552-
all_movies = client.paginate(
552+
all_movies = paginate(
553553
'users/{user}/watched/movies',
554554
user=slugify(self.username),
555555
)

0 commit comments

Comments
 (0)