-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add seam.create_paginator
#311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
8c1cd82
Add paginator class
andrii-balitskyi 17c7581
Disable generation, generate manually
andrii-balitskyi cabf72b
Test paginator
andrii-balitskyi a7935df
ci: Format code
seambot ca6e425
Update type
andrii-balitskyi d844cf6
Rename callable_func to request
andrii-balitskyi 78e49d0
Update how params are passed to create_paginator in test
andrii-balitskyi 284ce7e
Simplify flatten_to_list check, default response pagination to {}
andrii-balitskyi d3662fe
next_page should throw on null next_page_cursor
andrii-balitskyi a0dba87
Use literal spread syntax
andrii-balitskyi 1e056b6
Update seam/paginator.py
andrii-balitskyi 6e55aac
Fix var name
andrii-balitskyi b071eab
Move private method to the end of class body
andrii-balitskyi 691d8ee
Use niquests hooks instead of on_response callback
andrii-balitskyi f3ac72b
Reenable generation
andrii-balitskyi bd859a6
ci: Generate code
seambot da984d1
Update docstring
andrii-balitskyi 61bf8b2
Move pagination class into a separate file
andrii-balitskyi c6df47b
Positional only
andrii-balitskyi 42ad7de
Change param order, use better naming
andrii-balitskyi 760e80a
Add readme section, rename Paginator to SeamPaginator
andrii-balitskyi 1ef305b
ci: Generate code
seambot 3d9e582
Improve readme code snippets
andrii-balitskyi 7881fbb
Update seam/paginator.py
andrii-balitskyi 43afa81
ci: Format code
seambot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from typing import Callable, Dict, Any, Tuple, Generator, List | ||
from .client import SeamHttpClient | ||
from niquests import Response, JSONDecodeError | ||
|
||
|
||
class Pagination: | ||
def __init__( | ||
self, | ||
has_next_page: bool, | ||
next_page_cursor: str | None, | ||
next_page_url: str | None, | ||
): | ||
self.has_next_page = has_next_page | ||
self.next_page_cursor = next_page_cursor | ||
self.next_page_url = next_page_url | ||
|
||
|
||
class Paginator: | ||
""" | ||
Handles pagination for API list endpoints. | ||
|
||
Iterates through pages of results returned by a callable function. | ||
""" | ||
|
||
_FIRST_PAGE = "FIRST_PAGE" | ||
|
||
def __init__( | ||
self, | ||
request: Callable, | ||
http_client: SeamHttpClient, | ||
andrii-balitskyi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
params: Dict[str, Any] = None, | ||
): | ||
""" | ||
Initializes the Paginator. | ||
|
||
Args: | ||
request: The function to call to fetch a page of data. | ||
http_client: The Seam HTTP client used in the request. | ||
params: Initial parameters to pass to the callable function. | ||
""" | ||
self._request = request | ||
self._http_client = http_client | ||
self._params = params or {} | ||
self._pagination_cache: Dict[str, Pagination] = {} | ||
|
||
def first_page(self) -> Tuple[List[Any], Pagination | None]: | ||
"""Fetches the first page of results.""" | ||
self._http_client.hooks["response"].append( | ||
lambda response: self._cache_pagination(response, self._FIRST_PAGE) | ||
) | ||
data = self._request(**self._params) | ||
self._http_client.hooks["response"].pop() | ||
|
||
pagination = self._pagination_cache.get(self._FIRST_PAGE) | ||
|
||
return data, pagination | ||
|
||
def next_page(self, next_page_cursor: str) -> Tuple[List[Any], Pagination | None]: | ||
andrii-balitskyi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Fetches the next page of results using a cursor.""" | ||
if not next_page_cursor: | ||
raise ValueError("Cannot get the next page with a null next_page_cursor.") | ||
|
||
params = { | ||
**self._params, | ||
"page_cursor": next_page_cursor, | ||
} | ||
|
||
self._http_client.hooks["response"].append( | ||
lambda response: self._cache_pagination(response, next_page_cursor) | ||
) | ||
data = self._request(**params) | ||
self._http_client.hooks["response"].pop() | ||
|
||
pagination = self._pagination_cache.get(next_page_cursor) | ||
|
||
return data, pagination | ||
|
||
def flatten_to_list(self) -> List[Any]: | ||
"""Fetches all pages and returns all items as a single list.""" | ||
all_items = [] | ||
current_items, pagination = self.first_page() | ||
|
||
if current_items: | ||
all_items.extend(current_items) | ||
|
||
while pagination.has_next_page: | ||
current_items, pagination = self.next_page(pagination.next_page_cursor) | ||
if current_items: | ||
all_items.extend(current_items) | ||
|
||
return all_items | ||
|
||
def flatten(self) -> Generator[Any, None, None]: | ||
"""Fetches all pages and yields items one by one using a generator.""" | ||
current_items, pagination = self.first_page() | ||
if current_items: | ||
yield from current_items | ||
|
||
while pagination and pagination.has_next_page and pagination.next_page_cursor: | ||
andrii-balitskyi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
current_items, pagination = self.next_page(pagination.next_page_cursor) | ||
if current_items: | ||
yield from current_items | ||
|
||
def _cache_pagination(self, response: Response, page_key: str) -> None: | ||
"""Extracts pagination dict from response, creates Pagination object, and caches it.""" | ||
try: | ||
response_json = response.json() | ||
pagination = response_json.get("pagination", {}) | ||
except JSONDecodeError: | ||
pagination = {} | ||
|
||
if isinstance(pagination, dict): | ||
self._pagination_cache[page_key] = Pagination( | ||
has_next_page=pagination.get("has_next_page", False), | ||
next_page_cursor=pagination.get("next_page_cursor"), | ||
next_page_url=pagination.get("next_page_url"), | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from seam import Seam | ||
|
||
|
||
def test_paginator_first_page(seam: Seam): | ||
paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 2}) | ||
connected_accounts, pagination = paginator.first_page() | ||
|
||
assert isinstance(connected_accounts, list) | ||
assert len(connected_accounts) == 2 | ||
assert pagination is not None | ||
assert pagination.has_next_page is True | ||
assert pagination.next_page_cursor is not None | ||
assert pagination.next_page_url is not None | ||
|
||
|
||
def test_paginator_next_page(seam: Seam): | ||
paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 2}) | ||
first_page_accounts, first_pagination = paginator.first_page() | ||
|
||
assert len(first_page_accounts) == 2 | ||
assert first_pagination.has_next_page is True | ||
|
||
next_page_accounts, _ = paginator.next_page(first_pagination.next_page_cursor) | ||
|
||
assert isinstance(next_page_accounts, list) | ||
assert len(next_page_accounts) == 1 | ||
|
||
|
||
def test_paginator_flatten_to_list(seam: Seam): | ||
all_connected_accounts = seam.connected_accounts.list() | ||
|
||
paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 1}) | ||
paginated_accounts = paginator.flatten_to_list() | ||
|
||
assert len(paginated_accounts) > 1 | ||
assert len(paginated_accounts) == len(all_connected_accounts) | ||
|
||
|
||
def test_paginator_flatten(seam: Seam): | ||
all_connected_accounts = seam.connected_accounts.list() | ||
|
||
paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 1}) | ||
|
||
collected_accounts = [] | ||
for account in paginator.flatten(): | ||
collected_accounts.append(account) | ||
|
||
assert len(collected_accounts) > 1 | ||
assert len(collected_accounts) == len(all_connected_accounts) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.