Source code for action0.github.pagination
"""
Iterating over all pages of a listing.
Pagination itself is execution-model-agnostic data: every listing returns
a :py:class:`~action0.github.models.page.Page` whose ``next`` is the
ready-to-send operation for the following page. The helpers here are the
flattening sugar on top — one per execution model, like action0-client's
retry wrappers, because "loop over pages" is spelled differently in sync,
async and Twisted code.
Each helper keeps sending ``page.next`` through the client until the last
page; one GitHub request per page, so mind the rate limit on large
listings (cap via ``per_page``/starting ``page`` if needed).
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from collections.abc import Awaitable
from collections.abc import Iterator
from typing import TYPE_CHECKING
from typing import cast
from action0.client import APIClient
from action0.client import Backend
from action0.client import Operation
from action0.req import Response
from .models.page import ItemT
from .models.page import Page
if TYPE_CHECKING:
# twisted is an optional dependency: only the type checker sees this
from twisted.internet.defer import Deferred
[docs]
def all_items(
client: APIClient[Backend[Response]],
operation: Operation[Page[ItemT]],
) -> Iterator[ItemT]:
"""
Iterate over all items of a listing, lazily following the pages
(sync backends).
:param client: the client to send through (any sync backend)
:param operation: the first page's operation
:return: the items, page by page — each page is fetched only when
the iteration reaches it
"""
page = client.send(operation)
while True:
yield from page
if page.next is None:
return
page = client.send(page.next)
[docs]
async def all_items_async(
client: APIClient[Backend[Awaitable[Response]]],
operation: Operation[Page[ItemT]],
) -> AsyncIterator[ItemT]:
"""
Iterate over all items of a listing, lazily following the pages
(async backends) — consume with ``async for``.
:param client: the client to send through (any async backend)
:param operation: the first page's operation
:return: the items, page by page — each page is fetched only when
the iteration reaches it
"""
page = await client.send(operation)
while True:
for item in page:
yield item
if page.next is None:
return
page = await client.send(page.next)
[docs]
def all_items_deferred(
client: APIClient[Backend[Deferred[Response]]],
operation: Operation[Page[ItemT]],
) -> Deferred[list[ItemT]]:
"""
Collect all items of a listing, following the pages (Twisted
backends). Unlike the sync/async helpers this gathers everything into
one list — a Deferred cannot stream lazily.
:param client: the client to send through (a Twisted backend)
:param operation: the first page's operation
:return: a Deferred firing with the items of all pages
"""
items: list[ItemT] = []
def collect(page: Page[ItemT]) -> object:
# returning a Deferred from a callback chains it: the outer
# Deferred fires only once the whole page chain is done
items.extend(page.items)
if page.next is None:
return items
return client.send(page.next).addCallback(collect)
chained: Deferred[object] = client.send(operation).addCallback(collect)
# Deferred chaining unwraps the callback's Deferreds at runtime, so
# this fires with the item list — inexpressible in twisted's stubs
return cast("Deferred[list[ItemT]]", chained)