plan(SP22): pre-flight fix — add _extract_items helper for paginated API responses

This commit is contained in:
Tyler
2026-06-21 12:58:32 -06:00
parent 134eb4f404
commit fff000ed2e
@@ -751,6 +751,28 @@ BACKOFF_S = (1.0, 2.0, 4.0)
# --- Response models --------------------------------------------------------- # --- 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): class HealthSnapshot(BaseModel):
status: str status: str
version: str version: str
@@ -958,7 +980,10 @@ Add the methods to the `CycloneClient` class (below `scheduler_status`):
"/api/claims", params=params "/api/claims", params=params
) )
resp.raise_for_status() 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: except (httpx.HTTPStatusError, httpx.RequestError) as e:
last_exc = e last_exc = e
if attempt < MAX_RETRIES - 1: if attempt < MAX_RETRIES - 1:
@@ -1163,7 +1188,10 @@ Add the methods to the `CycloneClient` class:
params={"since": since}, params={"since": since},
) )
resp.raise_for_status() 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: except (httpx.HTTPStatusError, httpx.RequestError) as e:
last_exc = e last_exc = e
if attempt < MAX_RETRIES - 1: if attempt < MAX_RETRIES - 1:
@@ -1295,7 +1323,10 @@ Add the methods to the `CycloneClient` class:
"/api/ta1-acks", params={"since": since} "/api/ta1-acks", params={"since": since}
) )
resp.raise_for_status() 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]: async def list_acks(self, since: str) -> list[Ack999]:
assert self._http is not None assert self._http is not None
@@ -1303,7 +1334,10 @@ Add the methods to the `CycloneClient` class:
"/api/acks", params={"since": since} "/api/acks", params={"since": since}
) )
resp.raise_for_status() 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( async def list_remittances(
self, since: str self, since: str
@@ -1314,7 +1348,8 @@ Add the methods to the `CycloneClient` class:
) )
resp.raise_for_status() resp.raise_for_status()
return [ return [
RemittanceSummary.model_validate(r) for r in resp.json() RemittanceSummary.model_validate(r)
for r in _extract_items(resp.json())
] ]
``` ```