Skip to content

Commit 8c1cd82

Browse files
Add paginator class
1 parent 96858fd commit 8c1cd82

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

seam/paginator.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from typing import Callable, Dict, Any, Tuple, Generator, List
2+
3+
4+
class Pagination:
5+
def __init__(
6+
self,
7+
has_next_page: bool,
8+
next_page_cursor: str | None,
9+
next_page_url: str | None,
10+
):
11+
self.has_next_page = has_next_page
12+
self.next_page_cursor = next_page_cursor
13+
self.next_page_url = next_page_url
14+
15+
16+
class Paginator:
17+
"""
18+
Handles pagination for API list endpoints.
19+
20+
Iterates through pages of results returned by a callable function.
21+
"""
22+
23+
_FIRST_PAGE = "FIRST_PAGE"
24+
25+
def __init__(self, callable_func: Callable, params: Dict[str, Any] = None):
26+
"""
27+
Initializes the Paginator.
28+
29+
Args:
30+
callable_func: The function to call to fetch a page of data.
31+
params: Initial parameters to pass to the callable function.
32+
"""
33+
self._callable_func = callable_func
34+
self._params = params or {}
35+
self._pagination_cache: Dict[str, Pagination] = {}
36+
37+
def _cache_pagination(self, response: Dict[str, Any], page_key: str) -> None:
38+
"""Extracts pagination dict from response, creates Pagination object, and caches it."""
39+
pagination_dict = response.get("pagination")
40+
41+
if isinstance(pagination_dict, dict):
42+
pagination_obj = Pagination(
43+
has_next_page=pagination_dict.get("has_next_page", False),
44+
next_page_cursor=pagination_dict.get("next_page_cursor"),
45+
next_page_url=pagination_dict.get("next_page_url"),
46+
)
47+
self._pagination_cache[page_key] = pagination_obj
48+
49+
def first_page(self) -> Tuple[List[Any], Pagination | None]:
50+
"""Fetches the first page of results."""
51+
params = self._params.copy()
52+
53+
params["on_response"] = lambda response: self._cache_pagination(
54+
response, self._FIRST_PAGE
55+
)
56+
57+
data = self._callable_func(**params)
58+
pagination = self._pagination_cache.get(self._FIRST_PAGE)
59+
60+
return data, pagination
61+
62+
def next_page(self, next_page_cursor: str) -> Tuple[List[Any], Pagination | None]:
63+
"""Fetches the next page of results using a cursor."""
64+
if not next_page_cursor:
65+
return [], None
66+
67+
params = self._params.copy()
68+
params["page_cursor"] = next_page_cursor
69+
params["on_response"] = lambda response: self._cache_pagination(
70+
response, next_page_cursor
71+
)
72+
73+
data = self._callable_func(**params)
74+
pagination = self._pagination_cache.get(next_page_cursor)
75+
76+
return data, pagination
77+
78+
def flatten_to_list(self) -> List[Any]:
79+
"""Fetches all pages and returns all items as a single list."""
80+
all_items = []
81+
current_items, pagination = self.first_page()
82+
83+
if current_items:
84+
all_items.extend(current_items)
85+
86+
while pagination and pagination.has_next_page and pagination.next_page_cursor:
87+
current_items, pagination = self.next_page(pagination.next_page_cursor)
88+
if current_items:
89+
all_items.extend(current_items)
90+
91+
return all_items
92+
93+
def flatten(self) -> Generator[Any, None, None]:
94+
"""Fetches all pages and yields items one by one using a generator."""
95+
current_items, pagination = self.first_page()
96+
if current_items:
97+
yield from current_items
98+
99+
while pagination and pagination.has_next_page and pagination.next_page_cursor:
100+
current_items, pagination = self.next_page(pagination.next_page_cursor)
101+
if current_items:
102+
yield from current_items

0 commit comments

Comments
 (0)