From fff000ed2e3e13ff5fe3aefdb8d8b26932417ada Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 21 Jun 2026 12:58:32 -0600 Subject: [PATCH] =?UTF-8?q?plan(SP22):=20pre-flight=20fix=20=E2=80=94=20ad?= =?UTF-8?q?d=20=5Fextract=5Fitems=20helper=20for=20paginated=20API=20respo?= =?UTF-8?q?nses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-21-cyclone-pipeline-agent.md | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md b/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md index 286408e..7178332 100644 --- a/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md +++ b/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md @@ -751,6 +751,28 @@ BACKOFF_S = (1.0, 2.0, 4.0) # --- Response models --------------------------------------------------------- +class PaginatedResponse(BaseModel): + """Cyclone's list endpoints return a paginated wrapper: + `{"items": [...], "total": N, "returned": N, "has_more": bool}`. + Some endpoints (e.g., /api/ta1-acks) omit `returned` and `has_more`.""" + items: list[dict[str, Any]] = Field(default_factory=list) + total: int = 0 + returned: int | None = None + has_more: bool | None = None + + +def _extract_items(raw: dict[str, Any] | list[Any]) -> list[dict[str, Any]]: + """Unwrap a paginated response into a plain list. Tolerates both + the paginated wrapper and a bare list (so tests can use either).""" + if isinstance(raw, list): + return raw + if isinstance(raw, dict) and "items" in raw: + return list(raw["items"]) + raise ValueError( + f"Unexpected API response shape; expected list or paginated " + f"wrapper with 'items'. Got: {type(raw).__name__}" + ) + class HealthSnapshot(BaseModel): status: str version: str @@ -958,7 +980,10 @@ Add the methods to the `CycloneClient` class (below `scheduler_status`): "/api/claims", params=params ) resp.raise_for_status() - return [ClaimSummary.model_validate(c) for c in resp.json()] + return [ + ClaimSummary.model_validate(c) + for c in _extract_items(resp.json()) + ] except (httpx.HTTPStatusError, httpx.RequestError) as e: last_exc = e if attempt < MAX_RETRIES - 1: @@ -1163,7 +1188,10 @@ Add the methods to the `CycloneClient` class: params={"since": since}, ) resp.raise_for_status() - return [ProcessedFile.model_validate(f) for f in resp.json()] + return [ + ProcessedFile.model_validate(f) + for f in _extract_items(resp.json()) + ] except (httpx.HTTPStatusError, httpx.RequestError) as e: last_exc = e if attempt < MAX_RETRIES - 1: @@ -1295,7 +1323,10 @@ Add the methods to the `CycloneClient` class: "/api/ta1-acks", params={"since": since} ) resp.raise_for_status() - return [Ta1Ack.model_validate(a) for a in resp.json()] + return [ + Ta1Ack.model_validate(a) + for a in _extract_items(resp.json()) + ] async def list_acks(self, since: str) -> list[Ack999]: assert self._http is not None @@ -1303,7 +1334,10 @@ Add the methods to the `CycloneClient` class: "/api/acks", params={"since": since} ) resp.raise_for_status() - return [Ack999.model_validate(a) for a in resp.json()] + return [ + Ack999.model_validate(a) + for a in _extract_items(resp.json()) + ] async def list_remittances( self, since: str @@ -1314,7 +1348,8 @@ Add the methods to the `CycloneClient` class: ) resp.raise_for_status() return [ - RemittanceSummary.model_validate(r) for r in resp.json() + RemittanceSummary.model_validate(r) + for r in _extract_items(resp.json()) ] ```