|
3 | 3 | import httpx |
4 | 4 | import pytest |
5 | 5 |
|
| 6 | +from kernel import Kernel, AsyncKernel |
| 7 | +from kernel._models import FinalRequestOptions |
6 | 8 | from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination |
7 | 9 |
|
8 | 10 | PageClass = Union[Type[SyncOffsetPagination[Any]], Type[AsyncOffsetPagination[Any]]] |
|
12 | 14 | both_classes = pytest.mark.parametrize("cls", [SyncOffsetPagination, AsyncOffsetPagination]) |
13 | 15 |
|
14 | 16 |
|
15 | | -def _page(cls: PageClass, *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]) -> Any: |
| 17 | +def client_for(cls: PageClass) -> Any: |
| 18 | + if cls is SyncOffsetPagination: |
| 19 | + return Kernel(api_key="test-api-key", base_url="http://127.0.0.1") |
| 20 | + return AsyncKernel(api_key="test-api-key", base_url="http://127.0.0.1") |
| 21 | + |
| 22 | + |
| 23 | +def _page( |
| 24 | + cls: PageClass, |
| 25 | + *, |
| 26 | + items: List[Any], |
| 27 | + next_offset: Optional[int], |
| 28 | + has_more: Optional[bool], |
| 29 | + request_offset: Optional[int] = None, |
| 30 | +) -> Any: |
16 | 31 | headers: dict[str, str] = {} |
17 | 32 | if next_offset is not None: |
18 | 33 | headers["X-Next-Offset"] = str(next_offset) |
19 | 34 | if has_more is not None: |
20 | 35 | headers["X-Has-More"] = "true" if has_more else "false" |
21 | 36 | response = httpx.Response(200, headers=headers) |
22 | | - return cls.build(response=response, data=items) |
| 37 | + page = cls.build(response=response, data=items) |
| 38 | + params = {} if request_offset is None else {"offset": request_offset} |
| 39 | + options = FinalRequestOptions(method="get", url="/proxies", params=params) |
| 40 | + page._set_private_attributes(client=client_for(cls), model=object, options=options) # type: ignore[arg-type] |
| 41 | + return page |
23 | 42 |
|
24 | 43 |
|
25 | 44 | @both_classes |
@@ -54,8 +73,31 @@ def test_stops_on_next_offset_zero_sentinel(cls: PageClass) -> None: |
54 | 73 |
|
55 | 74 |
|
56 | 75 | @both_classes |
57 | | -def test_stops_on_empty_page(cls: PageClass) -> None: |
58 | | - page = _page(cls, items=[], next_offset=300, has_more=True) |
| 76 | +def test_continues_past_empty_page_while_x_has_more_true(cls: PageClass) -> None: |
| 77 | + # A server-side filter can leave a page empty with more records ahead. |
| 78 | + page = _page(cls, items=[], next_offset=300, has_more=True, request_offset=200) |
| 79 | + assert page.has_next_page() is True |
| 80 | + info = page.next_page_info() |
| 81 | + assert info is not None |
| 82 | + assert info.params == {"offset": 300} |
| 83 | + |
| 84 | + |
| 85 | +@both_classes |
| 86 | +def test_stops_on_empty_page_when_x_has_more_false(cls: PageClass) -> None: |
| 87 | + page = _page(cls, items=[], next_offset=0, has_more=False, request_offset=200) |
| 88 | + assert page.has_next_page() is False |
| 89 | + |
| 90 | + |
| 91 | +@both_classes |
| 92 | +def test_stops_when_next_offset_does_not_advance(cls: PageClass) -> None: |
| 93 | + page = _page(cls, items=[], next_offset=200, has_more=True, request_offset=200) |
| 94 | + assert page.next_page_info() is None |
| 95 | + assert page.has_next_page() is False |
| 96 | + |
| 97 | + |
| 98 | +@both_classes |
| 99 | +def test_stops_on_empty_first_page_without_pagination_headers(cls: PageClass) -> None: |
| 100 | + page = _page(cls, items=[], next_offset=None, has_more=None) |
59 | 101 | assert page.has_next_page() is False |
60 | 102 |
|
61 | 103 |
|
|
0 commit comments