Skip to content

Commit 0970940

Browse files
✨ Add iter_pages to turn operation methods to async iterators.
1 parent bf58da5 commit 0970940

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and the format of this file is based on [Keep a Changelog](https://keepachangelo
1010

1111
### Added
1212
- Accept session_factory in `ClientBase.__init__`.
13+
- Helper function to iterate over pages.
1314

1415
### Fixed
1516
- Handling collections in request bodies.

src/lapidary/runtime/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'delete',
2727
'get',
2828
'head',
29+
'iter_pages',
2930
'patch',
3031
'post',
3132
'put',
@@ -38,4 +39,5 @@
3839
from .model.error import HttpErrorResponse, LapidaryError, LapidaryResponseError, UnexpectedResponse
3940
from .model.param_serialization import Form, FormExplode, SimpleMultimap, SimpleString
4041
from .operation import delete, get, head, patch, post, put, trace
42+
from .paging import iter_pages
4143
from .types_ import ClientArgs, NamedAuth, SecurityRequirements, SessionFactory

src/lapidary/runtime/paging.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from collections.abc import AsyncIterable, Awaitable, Callable
2+
from typing import Optional, TypeVar
3+
4+
from typing_extensions import ParamSpec, Unpack
5+
6+
P = ParamSpec('P')
7+
R = TypeVar('R')
8+
C = TypeVar('C')
9+
10+
11+
def iter_pages(
12+
fn: Callable[P, Awaitable[R]],
13+
cursor_param_name: str,
14+
get_cursor: Callable[[R], Optional[C]],
15+
) -> Callable[P, AsyncIterable[R]]:
16+
"""
17+
Create a function that returns an async iterator over pages from the async operation function :param:`fn`.
18+
19+
The returned function can be called with the same parameters as :param:`fn` (except for the cursor parameter),
20+
and returns an async iterator that yields results from :param:`fn`, handling pagination automatically.
21+
22+
The function :param:`fn` will be called initially without the cursor parameter and then called with the cursor parameter
23+
as long as :param:`get_cursor` can extract a cursor from the result.
24+
25+
**Example:**
26+
27+
.. code:: python
28+
29+
async for page in iter_pages(client.fn, 'cursor', extractor_fn)(parameter=value):
30+
# Process page
31+
32+
Typically, an API will use the same paging pattern for all operations supporting it, so it's a good idea to write a shortcut function:
33+
34+
.. code:: python
35+
36+
from lapidary.runtime import iter_pages as _iter_pages
37+
38+
def iter_pages[P, R](fn: Callable[P, Awaitable[R]]) -> Callable[P, AsyncIterable[R]]:
39+
return _iter_pages(fn, 'cursor', lambda result: ...)
40+
41+
:param fn: An async function that retrieves a page of data.
42+
:param cursor_param_name: The name of the cursor parameter in :param:`fn`.
43+
:param get_cursor: A function that extracts a cursor value from the result of :param:`fn`. Return `None` to end the iteration.
44+
"""
45+
46+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> AsyncIterable[R]:
47+
result = await fn(*args, **kwargs) # type: ignore[call-arg]
48+
yield result
49+
cursor = get_cursor(result)
50+
51+
while cursor:
52+
kwargs[cursor_param_name] = cursor
53+
result = await fn(*args, **kwargs) # type: ignore[call-arg]
54+
yield result
55+
56+
cursor = get_cursor(result)
57+
58+
return wrapper

0 commit comments

Comments
 (0)