Coverage for src/pipeline_analytics/pagination.py: 100%
39 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-16 19:27 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-16 19:27 +0000
1"""A real iterator over ``GET /api/repos``, the only endpoint in
2pipeline-analytics' spec with pagination -- ``limit``/``offset`` query
3params and a ``hasMore`` flag on the response. Walks every page
4transparently rather than handing back one page and a raw offset for the
5caller to loop on by hand (rules/sdk-generation.md's "Client shape").
6"""
8from __future__ import annotations
10from collections.abc import AsyncIterator, Iterator
12from ._generated.api.repos import list_repos as _list_repos
13from ._generated.client import Client as _GeneratedClient
14from ._generated.models.forge import Forge
15from ._generated.models.repo import Repo
16from ._generated.models.repo_list import RepoList
17from ._generated.types import UNSET, Unset
18from .errors import decode_error
20#: Used when the caller leaves ``limit`` unset -- ``GET /api/repos`` never
21#: paginates on its own (``RepoList.has_more`` is always false with no
22#: limit), so an iterator needs a default to actually walk pages.
23DEFAULT_PAGE_SIZE = 50
26def iter_repos(
27 client: _GeneratedClient,
28 *,
29 forge: Forge | Unset = UNSET,
30 limit: int = DEFAULT_PAGE_SIZE,
31 offset: int = 0,
32) -> Iterator[Repo]:
33 """Iterate every tracked repo matching *forge*, walking ``GET
34 /api/repos``'s offset pages transparently.
36 *client* is the generated client -- pass ``PipelineAnalyticsClient.raw``
37 or any ``pipeline_analytics._generated.client.Client``/
38 ``AuthenticatedClient``. Raises
39 :class:`~pipeline_analytics.errors.APIError` if any page's response is
40 an error, and whatever the underlying ``httpx`` transport raises on a
41 transport failure that exhausts its retries.
43 Usage::
45 for repo in iter_repos(client.raw):
46 print(repo.identifier, repo.ingestion_status)
47 """
48 while True:
49 response = _list_repos.sync_detailed(client=client, forge=forge, limit=limit, offset=offset)
50 if error := decode_error(response):
51 raise error
53 page = response.parsed
54 if page is None:
55 return
56 if not isinstance(page, RepoList):
57 # decode_error already raised for status_code >= 400, so a
58 # parsed body at this point should always be RepoList -- this
59 # only guards against a future spec change adding another
60 # documented 2xx shape to listRepos.
61 raise TypeError(f"unexpected listRepos response body: {page!r}")
63 yield from page.repos
65 if not page.has_more:
66 return
67 offset += len(page.repos)
70async def aiter_repos(
71 client: _GeneratedClient,
72 *,
73 forge: Forge | Unset = UNSET,
74 limit: int = DEFAULT_PAGE_SIZE,
75 offset: int = 0,
76) -> AsyncIterator[Repo]:
77 """Async counterpart of :func:`iter_repos`, for
78 ``PipelineAnalyticsClient`` used with ``async with``/``await``."""
79 while True:
80 response = await _list_repos.asyncio_detailed(client=client, forge=forge, limit=limit, offset=offset)
81 if error := decode_error(response):
82 raise error
84 page = response.parsed
85 if page is None:
86 return
87 if not isinstance(page, RepoList):
88 raise TypeError(f"unexpected listRepos response body: {page!r}")
90 for repo in page.repos:
91 yield repo
93 if not page.has_more:
94 return
95 offset += len(page.repos)