diff --git a/src/cloudflare/pagination.py b/src/cloudflare/pagination.py index 947aca4121a..7be9f8c7612 100644 --- a/src/cloudflare/pagination.py +++ b/src/cloudflare/pagination.py @@ -84,6 +84,11 @@ class V4PagePaginationArrayResultInfo(BaseModel): per_page: Optional[int] = None + # Added missing fields present in V4 API + total_pages: Optional[int] = None + total_count: Optional[int] = None + count: Optional[int] = None + class SyncV4PagePaginationArray(BaseSyncPage[_T], BasePage[_T], Generic[_T]): result: List[_T] @@ -100,6 +105,10 @@ def _get_page_items(self) -> List[_T]: def next_page_info(self) -> Optional[PageInfo]: last_page = cast("int | None", self._options.params.get("page")) or 1 + # Guard against infinite loops where API returns data past the last page + if self.result_info and self.result_info.total_pages is not None and last_page >= self.result_info.total_pages: + return None + return PageInfo(params={"page": last_page + 1}) @@ -118,6 +127,9 @@ def _get_page_items(self) -> List[_T]: def next_page_info(self) -> Optional[PageInfo]: last_page = cast("int | None", self._options.params.get("page")) or 1 + # Guard against infinite loops where API returns data past the last page + if self.result_info and self.result_info.total_pages is not None and last_page >= self.result_info.total_pages: + return None return PageInfo(params={"page": last_page + 1}) diff --git a/tests/test_pagination_fix.py b/tests/test_pagination_fix.py new file mode 100644 index 00000000000..ab54ff685c5 --- /dev/null +++ b/tests/test_pagination_fix.py @@ -0,0 +1,62 @@ +import pytest +from cloudflare.pagination import AsyncV4PagePaginationArray, V4PagePaginationArrayResultInfo + +class MockOptions: + """Mock object to simulate the options/params passed to the paginator.""" + def __init__(self, page_number: int): + self.params = {"page": page_number} + +@pytest.mark.asyncio +async def test_async_pagination_stops_iteration_when_total_pages_reached() -> None: + """ + Ensures the AsyncV4PagePaginationArray iterator correctly terminates when + the current page number matches the 'total_pages' field in the response metadata. + + This prevents infinite loops when the API returns the last page's data + repeatedly for out-of-bound page requests. + """ + result_info = V4PagePaginationArrayResultInfo( + page=5, + per_page=20, + total_pages=5, + total_count=100, + count=20 + ) + + paginator = AsyncV4PagePaginationArray( + result=[], + result_info=result_info + ) + + # Manually inject private _options to simulate the current page state + object.__setattr__(paginator, "_options", MockOptions(page_number=5)) + + next_info = paginator.next_page_info() + + assert next_info is None + +@pytest.mark.asyncio +async def test_async_pagination_continues_when_more_pages_exist() -> None: + """ + Ensures the iterator calculates the next page parameters correctly + when the current page is less than 'total_pages'. + """ + result_info = V4PagePaginationArrayResultInfo( + page=1, + per_page=20, + total_pages=5, + total_count=100, + count=20 + ) + + paginator = AsyncV4PagePaginationArray( + result=[], + result_info=result_info, + ) + + object.__setattr__(paginator, "_options", MockOptions(page_number=1)) + + next_info = paginator.next_page_info() + + assert next_info is not None + assert next_info.params["page"] == 2