Compare commits

..

1 Commits

Author SHA1 Message Date
Rodrigo Rodrigues
b230eb0227
Merge 8c37dadb51 into c5916a3b66 2026-01-20 10:47:46 +00:00
3 changed files with 23 additions and 38 deletions

View File

@ -6,14 +6,13 @@ Usage example for iterating paginated responses:
```python
from thousandeyes_sdk.core import PaginatorIterator
from thousandeyes_sdk.dashboards.api.dashboards_api import DashboardsApi
from thousandeyes_sdk.usage.api.usage_api import UsageApi
dashboards_api = DashboardsApi()
for widget_data in PaginatorIterator(
dashboards_api.get_dashboard_widget_data,
lambda response: response.data.tests if response.data else [],
dashboard_id="dashboard-id",
widget_id="widget-id",
usage_api = UsageApi()
for page in PaginatorIterator(
usage_api.get_enterprise_agents_units_usage,
start_date="2024-01-01T00:00:00Z",
end_date="2024-01-31T23:59:59Z",
):
print(widget_data)
print(page)
```

View File

@ -16,21 +16,19 @@
from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator
from typing import Any, Mapping, Optional, TypeVar, Generic
from typing import Any, Callable, Iterator, Mapping, Optional, TypeVar, Generic
from urllib.parse import parse_qs, urlparse
from typing_extensions import ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
I = TypeVar("I")
class PaginatorIterator(Generic[P, R, I]):
class PaginatorIterator(Generic[P, R]):
"""Iterate over cursor-paginated responses.
Calls ``method`` repeatedly, passing a cursor parameter between calls,
and yields items obtained from ``items_getter``.
Calls ``method`` repeatedly, passing a cursor parameter between calls.
The next cursor is derived from ``response.data.links`` or ``response.data._links``
(or mapping equivalents), supporting these link formats:
@ -44,24 +42,21 @@ class PaginatorIterator(Generic[P, R, I]):
def __init__(
self,
method: Callable[P, R],
items_getter: Callable[[R], Iterable[I]],
*,
cursor_param: str = "cursor",
**params: P.kwargs,
) -> None:
self._method = method
self._items_getter = items_getter
self._cursor_param = cursor_param
self._params: dict[str, Any] = dict(params)
def __iter__(self) -> Iterator[I]:
def __iter__(self) -> Iterator[R]:
params = dict(self._params)
last_cursor = params.get(self._cursor_param)
while True:
response = self._method(**params)
for item in self._items_getter(response):
yield item
yield response
next_cursor = self._next_cursor_from_response(response)
if not next_cursor or next_cursor == last_cursor:

View File

@ -1,6 +1,6 @@
from types import SimpleNamespace
from thousandeyes_sdk.core.pagination_iterator import PaginatorIterator
from thousandeyes_sdk.core.iterable import PaginatorIterator
def test_iterator_uses_cursor_from_next_href():
@ -10,16 +10,14 @@ def test_iterator_uses_cursor_from_next_href():
calls.append(params.copy())
if params.get("cursor") is None:
links = SimpleNamespace(next="https://example.com/items?cursor=abc")
items = ["first", "second"]
else:
links = SimpleNamespace(next=None)
items = ["third"]
data = SimpleNamespace(links=links)
return SimpleNamespace(data=data, items=items)
return SimpleNamespace(data=data)
responses = list(PaginatorIterator(method, lambda response: response.items))
responses = list(PaginatorIterator(method))
assert responses == ["first", "second", "third"]
assert len(responses) == 2
assert calls == [{}, {"cursor": "abc"}]
@ -30,15 +28,12 @@ def test_iterator_reads_cursor_from_links_mapping():
calls.append(params.copy())
if params.get("pageCursor") is None:
data = {"_links": {"next": {"href": "https://example.com?foo=1&pageCursor=xyz"}}}
items = ["alpha"]
else:
data = {"_links": {"next": None}}
items = ["beta"]
return SimpleNamespace(data=data, items=items)
return SimpleNamespace(data=data)
responses = list(PaginatorIterator(method, lambda response: response.items, cursor_param="pageCursor"))
list(PaginatorIterator(method, cursor_param="pageCursor"))
assert responses == ["alpha", "beta"]
assert calls == [{}, {"pageCursor": "xyz"}]
@ -49,15 +44,12 @@ def test_iterator_stops_when_no_cursor_param_present():
calls.append(params.copy())
if params.get("cursor") is None:
data = {"links": {"next": "/next/page"}}
items = ["one"]
else:
data = {"links": {"next": None}}
items = ["two"]
return SimpleNamespace(data=data, items=items)
return SimpleNamespace(data=data)
responses = list(PaginatorIterator(method, lambda response: response.items))
list(PaginatorIterator(method))
assert responses == ["one"]
assert calls == [{}]
@ -67,9 +59,8 @@ def test_iterator_stops_on_repeated_cursor():
def method(**params):
calls.append(params.copy())
data = {"links": {"next": "https://example.com?cursor=same"}}
return SimpleNamespace(data=data, items=["only"])
return SimpleNamespace(data=data)
responses = list(PaginatorIterator(method, lambda response: response.items, cursor="same"))
list(PaginatorIterator(method, cursor="same"))
assert responses == ["only"]
assert calls == [{"cursor": "same"}]