|
11 | 11 |
|
12 | 12 | from apify_client import ApifyClient, ApifyClientAsync |
13 | 13 | from apify_client import _models as _models_module |
| 14 | +from apify_client._pagination import ( |
| 15 | + get_cursor_iterator, |
| 16 | + get_cursor_iterator_async, |
| 17 | + get_items_iterator, |
| 18 | + get_items_iterator_async, |
| 19 | +) |
14 | 20 | from apify_client._resource_clients import ( |
15 | 21 | ActorCollectionClient, |
16 | 22 | ActorCollectionClientAsync, |
@@ -617,3 +623,73 @@ async def test_rq_list_requests_iterable_async( |
617 | 623 | client: RequestQueueClientAsync = _CLIENT_FACTORIES[client_name](_make_async_client(pagination_server)) |
618 | 624 | returned_items = [dict(item) async for item in client.iterate_requests(**inputs)] |
619 | 625 | assert returned_items == expected_items |
| 626 | + |
| 627 | + |
| 628 | +class _FakeOffsetPage: |
| 629 | + """Offset-paginated page whose `count` (items scanned) may exceed `len(items)` when filters drop items.""" |
| 630 | + |
| 631 | + def __init__(self, items: list[dict[str, int]], count: int) -> None: |
| 632 | + self.items = items |
| 633 | + self.count = count |
| 634 | + |
| 635 | + |
| 636 | +class _FakeCursorPage: |
| 637 | + """Cursor-paginated page whose `count` (items scanned) may exceed `len(items)` when filters drop items.""" |
| 638 | + |
| 639 | + def __init__(self, items: list[dict[str, int]], count: int, next_cursor: str | None) -> None: |
| 640 | + self.items = items |
| 641 | + self.count = count |
| 642 | + self.next_cursor = next_cursor |
| 643 | + |
| 644 | + |
| 645 | +def test_items_iterator_continues_past_fully_filtered_page() -> None: |
| 646 | + """A fully-filtered page (`items=[]`, `count>0`) must not stop the offset iterator while more data was scanned.""" |
| 647 | + pages = { |
| 648 | + 0: _FakeOffsetPage(items=[], count=1000), |
| 649 | + 1000: _FakeOffsetPage(items=[{'id': 1}, {'id': 2}], count=2), |
| 650 | + } |
| 651 | + |
| 652 | + def _callback(*, limit: int | None = None, offset: int | None = None) -> _FakeOffsetPage: # noqa: ARG001 |
| 653 | + return pages.get(offset or 0, _FakeOffsetPage(items=[], count=0)) |
| 654 | + |
| 655 | + assert list(get_items_iterator(_callback, chunk_size=1000)) == [{'id': 1}, {'id': 2}] |
| 656 | + |
| 657 | + |
| 658 | +async def test_items_iterator_async_continues_past_fully_filtered_page() -> None: |
| 659 | + """A fully-filtered page (`items=[]`, `count>0`) must not stop the async offset iterator while more was scanned.""" |
| 660 | + pages = { |
| 661 | + 0: _FakeOffsetPage(items=[], count=1000), |
| 662 | + 1000: _FakeOffsetPage(items=[{'id': 1}, {'id': 2}], count=2), |
| 663 | + } |
| 664 | + |
| 665 | + async def _callback(*, limit: int | None = None, offset: int | None = None) -> _FakeOffsetPage: # noqa: ARG001 |
| 666 | + return pages.get(offset or 0, _FakeOffsetPage(items=[], count=0)) |
| 667 | + |
| 668 | + assert [item async for item in get_items_iterator_async(_callback, chunk_size=1000)] == [{'id': 1}, {'id': 2}] |
| 669 | + |
| 670 | + |
| 671 | +def test_cursor_iterator_continues_past_fully_filtered_page() -> None: |
| 672 | + """A fully-filtered page (`items=[]`, `count>0`) with a live cursor must not stop the cursor iterator.""" |
| 673 | + pages = { |
| 674 | + None: _FakeCursorPage(items=[], count=1000, next_cursor='c1'), |
| 675 | + 'c1': _FakeCursorPage(items=[{'id': 1}, {'id': 2}], count=2, next_cursor=None), |
| 676 | + } |
| 677 | + |
| 678 | + def _callback(*, limit: int | None = None, cursor: str | None = None) -> _FakeCursorPage: # noqa: ARG001 |
| 679 | + return pages[cursor] |
| 680 | + |
| 681 | + assert list(get_cursor_iterator(_callback, chunk_size=1000)) == [{'id': 1}, {'id': 2}] # ty: ignore[no-matching-overload] |
| 682 | + |
| 683 | + |
| 684 | +async def test_cursor_iterator_async_continues_past_fully_filtered_page() -> None: |
| 685 | + """A fully-filtered page (`items=[]`, `count>0`) with a live cursor must not stop the async cursor iterator.""" |
| 686 | + pages = { |
| 687 | + None: _FakeCursorPage(items=[], count=1000, next_cursor='c1'), |
| 688 | + 'c1': _FakeCursorPage(items=[{'id': 1}, {'id': 2}], count=2, next_cursor=None), |
| 689 | + } |
| 690 | + |
| 691 | + async def _callback(*, limit: int | None = None, cursor: str | None = None) -> _FakeCursorPage: # noqa: ARG001 |
| 692 | + return pages[cursor] |
| 693 | + |
| 694 | + collected = [item async for item in get_cursor_iterator_async(_callback, chunk_size=1000)] # ty: ignore[no-matching-overload] |
| 695 | + assert collected == [{'id': 1}, {'id': 2}] |
0 commit comments