Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ print(page.has_more()) # Whether more pages exist
print(page.after) # Cursor for the next page
```

> **Upgrading to v11?** Iterating a page directly no longer auto-paginates.
> In v11, `for x in page` and `async for x in page` yield only the current
> page's items and make no further requests; `auto_paging_iter()` iterates
> forward across all pages. To migrate, replace `for x in page` with
> `for x in page.auto_paging_iter()` (and `async for x in page` with
> `async for x in page.auto_paging_iter()`).

## Error Handling

All API errors map to typed exception classes with rich context:
Expand Down
8 changes: 5 additions & 3 deletions docs/V6_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ List endpoints now return typed page wrappers with cursor metadata and built-in
```python
page = client.organizations.list_organizations()

for organization in page:
for organization in page.auto_paging_iter():
print(organization.id)

assert page.before is None or isinstance(page.before, str)
Expand All @@ -273,12 +273,14 @@ assert page.after is None or isinstance(page.after, str)
```python
page = await async_client.organizations.list_organizations()

items = [organization async for organization in page]
items = [organization async for organization in page.auto_paging_iter()]
```

**Affected users:** Any code that expected a handwritten list wrapper or manually handled pagination state differently.

**Migration:** Update pagination code to work with `SyncPage` or `AsyncPage`, and use `page.data`, `page.before`, `page.after`, or iteration over the page as needed.
**Migration:** Update pagination code to work with `SyncPage` or `AsyncPage`, and use `page.data`, `page.before`, `page.after`, or `auto_paging_iter()` as needed.

**Breaking change (v11):** Iterating a page directly (`for organization in page`, `async for`) yields only the current page's `data` and makes no further requests. In v6–v10 it auto-paginates; in v11, call `page.auto_paging_iter()` to iterate across all pages.
Comment thread
greptile-apps[bot] marked this conversation as resolved.

### Requests now retry by default

Expand Down
11 changes: 6 additions & 5 deletions src/workos/_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ def auto_paging_iter(self) -> Iterator[T]:
page = page._fetch_page(after=page.after)

def __iter__(self) -> Iterator[T]:
"""Iterate through all items across all pages."""
return self.auto_paging_iter()
"""Iterate this page's items only; use auto_paging_iter() to cross pages."""
return iter(self.data)


@dataclass
Expand Down Expand Up @@ -109,6 +109,7 @@ async def auto_paging_iter(self) -> AsyncIterator[T]:
break
page = await page._fetch_page(after=page.after)

def __aiter__(self) -> AsyncIterator[T]:
"""Iterate through all items across all pages."""
return self.auto_paging_iter()
async def __aiter__(self) -> AsyncIterator[T]:
"""Iterate this page's items only; use auto_paging_iter() to cross pages."""
for item in self.data:
yield item
4 changes: 2 additions & 2 deletions tests/test_http_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,13 @@ def handler(request: Any) -> Any:
page = await client.user_management.list_users(
email="alice@example.com", limit=7
)
users = [user async for user in page]
users = [user async for user in page.auto_paging_iter()]
else:
sync_client = WorkOSClient(api_key=API_KEY, http_client=http_client)
sync_page = sync_client.user_management.list_users(
email="alice@example.com", limit=7
)
users = list(sync_page)
users = list(sync_page.auto_paging_iter())
finally:
if asynchronous:
await http_client.aclose()
Expand Down
69 changes: 57 additions & 12 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ async def _fetch(after=None):
assert [i.id for i in items] == ["1", "2", "3"]


ORG_BASE = {
"object": "organization",
"domains": [],
"metadata": {},
"external_id": None,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
}


class TestPaginationHTTPIntegration:
"""Integration test verifying auto_paging_iter fetches multiple pages via httpx."""

Expand All @@ -118,7 +128,7 @@ def test_forward_pagination_clears_before(self, workos, httpx_mock):
httpx_mock.add_response(json={"data": [{"id": "2"}], "list_metadata": {}})
params = {"before": "initial", "limit": 1, "organization_id": "org_1"}
page = workos.request_page("GET", ["items"], model=FakeItem, params=params)
assert [item.id for item in page] == ["1", "2"]
assert [item.id for item in page.auto_paging_iter()] == ["1", "2"]
requests = httpx_mock.get_requests()
assert dict(requests[0].url.params) == {
"before": "initial",
Expand All @@ -144,7 +154,7 @@ async def test_async_forward_pagination_clears_before(
page = await async_workos.request_page(
"GET", ["items"], model=FakeItem, params=params
)
assert [item.id async for item in page] == ["1", "2"]
assert [item.id async for item in page.auto_paging_iter()] == ["1", "2"]
requests = httpx_mock.get_requests()
assert dict(requests[0].url.params) == {
"before": "initial",
Expand All @@ -159,20 +169,12 @@ async def test_async_forward_pagination_clears_before(
assert params == {"before": "initial", "limit": 1, "organization_id": "org_1"}

def test_auto_paging_iter_fetches_two_pages(self, workos, httpx_mock):
org_base = {
"object": "organization",
"domains": [],
"metadata": {},
"external_id": None,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
}
page1_json = {
"data": [{"id": "org_1", "name": "Org 1", **org_base}],
"data": [{"id": "org_1", "name": "Org 1", **ORG_BASE}],
"list_metadata": {"after": "cursor_page2"},
}
page2_json = {
"data": [{"id": "org_2", "name": "Org 2", **org_base}],
"data": [{"id": "org_2", "name": "Org 2", **ORG_BASE}],
"list_metadata": {},
}
httpx_mock.add_response(json=page1_json)
Expand All @@ -187,3 +189,46 @@ def test_auto_paging_iter_fetches_two_pages(self, workos, httpx_mock):
requests = httpx_mock.get_requests()
assert len(requests) == 2
assert "after=cursor_page2" in str(requests[1].url)


class TestPageLocalIteration:
"""Iterating a page yields only that page's data and makes no extra request."""

def test_iter_yields_current_page_only(self, workos, httpx_mock):
httpx_mock.add_response(
json={
"data": [
{"id": "org_1", "name": "Org 1", **ORG_BASE},
{"id": "org_2", "name": "Org 2", **ORG_BASE},
],
"list_metadata": {"after": "cursor_page2"},
}
)

page = workos.organizations.list_organizations(
request_options={"max_retries": 0}
)
items = list(page)

assert [item.id for item in items] == ["org_1", "org_2"]
assert len(httpx_mock.get_requests()) == 1

@pytest.mark.asyncio
async def test_aiter_yields_current_page_only(self, async_workos, httpx_mock):
httpx_mock.add_response(
json={
"data": [
{"id": "org_1", "name": "Org 1", **ORG_BASE},
{"id": "org_2", "name": "Org 2", **ORG_BASE},
],
"list_metadata": {"after": "cursor_page2"},
}
)

page = await async_workos.organizations.list_organizations(
request_options={"max_retries": 0}
)
items = [item async for item in page]

assert [item.id for item in items] == ["org_1", "org_2"]
assert len(httpx_mock.get_requests()) == 1
Loading