Skip to content

Commit c1e352f

Browse files
committed
iter page
1 parent 76de4a8 commit c1e352f

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from collections.abc import AsyncIterator
2+
from typing import Protocol
3+
4+
5+
class GetPageCallable(Protocol):
6+
async def __call__(
7+
self, *args: object, offset: int, limit: int, **kwargs: object
8+
) -> tuple[list, int]:
9+
...
10+
11+
12+
async def iter_pages(
13+
get_page_and_total_count: GetPageCallable,
14+
*args,
15+
offset: int = 0,
16+
limit: int = 100,
17+
**kwargs,
18+
) -> AsyncIterator[list]:
19+
"""
20+
Asynchronous generator that yields offsets for paginated API calls
21+
"""
22+
total_number_of_items = None
23+
24+
if limit <= 0:
25+
msg = f"{limit=} must be positive"
26+
raise ValueError(msg)
27+
if offset < 0:
28+
msg = f"{offset=} must be non-negative"
29+
raise ValueError(msg)
30+
31+
while total_number_of_items is None or offset < total_number_of_items:
32+
items, total_number_of_items = await get_page_and_total_count(
33+
*args, offset=offset, limit=limit, **kwargs
34+
)
35+
36+
assert len(items) <= limit # nosec
37+
assert len(items) <= total_number_of_items # nosec
38+
39+
yield items
40+
41+
offset += limit
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
6+
import asyncio
7+
from collections.abc import Callable
8+
9+
import pytest
10+
from common_library.iter_tools import iter_pages
11+
12+
13+
@pytest.fixture
14+
def all_items() -> list[int]:
15+
return list(range(11))
16+
17+
18+
@pytest.fixture
19+
async def get_page(all_items: list[int]) -> Callable:
20+
async def _get_page(offset, limit) -> tuple[list[int], int]:
21+
await asyncio.sleep(0)
22+
return all_items[offset : offset + limit], len(all_items)
23+
24+
return _get_page
25+
26+
27+
@pytest.mark.parametrize("limit", [2, 3, 5])
28+
@pytest.mark.parametrize("offset", [0, 1, 5])
29+
async def test_iter_pages(
30+
limit: int, offset: int, get_page: Callable, all_items: list[int]
31+
):
32+
33+
last_page = [None] * 2
34+
async for page_items in iter_pages(get_page, offset=offset, limit=limit):
35+
assert len(page_items) <= limit
36+
37+
assert set(last_page) != set(page_items)
38+
last_page = list(page_items)
39+
40+
# contains sub-sequence
41+
assert str(page_items)[1:-1] in str(all_items)[1:-1]
42+
43+
44+
@pytest.mark.parametrize("limit", [-1, 0])
45+
@pytest.mark.parametrize("offset", [-1])
46+
async def test_iter_pages_invalid(limit: int, offset: int, get_page: Callable):
47+
48+
with pytest.raises(ValueError, match="must be"): # noqa: PT012
49+
async for _ in iter_pages(get_page, offset=offset, limit=limit):
50+
pass

0 commit comments

Comments
 (0)