Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/cloudflare/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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})


Expand All @@ -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})


Expand Down
62 changes: 62 additions & 0 deletions tests/test_pagination_fix.py
Original file line number Diff line number Diff line change
@@ -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