Skip to content

Commit f4c46b0

Browse files
authored
fix(client): repair list_orders pagination and add missing page param (#27)
The auto-paginator was emitting combined responses as `{"data": [...], "pagination": {...}}` (generic shape), but `OrderListResponse.from_dict` requires a `meta` block — every list_orders call hit `KeyError: 'meta'` after pagination collected its pages. Auto-paginator now synthesizes a StatusPro-shaped `meta` block describing the combined result as a single page, while keeping the existing `pagination` telemetry as additional properties. Separately, the OpenAPI spec for `GET /orders` was missing the `page` query parameter the StatusPro server already accepts. The Orders.list helper was forwarding `page=` to a generated function that did not declare it. Spec now declares `page`; client regenerated. Adds a regression test asserting the combined paginated response round-trips through OrderListResponse.from_dict.
1 parent e2640a1 commit f4c46b0

4 files changed

Lines changed: 115 additions & 3 deletions

File tree

docs/statuspro-openapi.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ paths:
310310
schema:
311311
type: string
312312
format: date
313+
- name: page
314+
in: query
315+
required: false
316+
description: Page number to return (1-based).
317+
schema:
318+
type: integer
319+
minimum: 1
313320
- name: per_page
314321
in: query
315322
required: false

statuspro_public_api_client/api/orders/list_orders.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def _get_kwargs(
2727
exclude_cancelled: bool | Unset = UNSET,
2828
due_date_from: datetime.date | Unset = UNSET,
2929
due_date_to: datetime.date | Unset = UNSET,
30+
page: int | Unset = UNSET,
3031
per_page: int | Unset = 15,
3132
) -> dict[str, Any]:
3233

@@ -78,6 +79,8 @@ def _get_kwargs(
7879
json_due_date_to = due_date_to.isoformat()
7980
params["due_date_to"] = json_due_date_to
8081

82+
params["page"] = page
83+
8184
params["per_page"] = per_page
8285

8386
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
@@ -148,6 +151,7 @@ def sync_detailed(
148151
exclude_cancelled: bool | Unset = UNSET,
149152
due_date_from: datetime.date | Unset = UNSET,
150153
due_date_to: datetime.date | Unset = UNSET,
154+
page: int | Unset = UNSET,
151155
per_page: int | Unset = 15,
152156
) -> Response[ErrorResponse | OrderListResponse | ValidationErrorResponse]:
153157
"""Retrieve a paginated list of orders
@@ -164,6 +168,7 @@ def sync_detailed(
164168
exclude_cancelled (bool | Unset):
165169
due_date_from (datetime.date | Unset):
166170
due_date_to (datetime.date | Unset):
171+
page (int | Unset):
167172
per_page (int | Unset): Default: 15.
168173
169174
@@ -186,6 +191,7 @@ def sync_detailed(
186191
exclude_cancelled=exclude_cancelled,
187192
due_date_from=due_date_from,
188193
due_date_to=due_date_to,
194+
page=page,
189195
per_page=per_page,
190196
)
191197

@@ -208,6 +214,7 @@ def sync(
208214
exclude_cancelled: bool | Unset = UNSET,
209215
due_date_from: datetime.date | Unset = UNSET,
210216
due_date_to: datetime.date | Unset = UNSET,
217+
page: int | Unset = UNSET,
211218
per_page: int | Unset = 15,
212219
) -> ErrorResponse | OrderListResponse | ValidationErrorResponse | None:
213220
"""Retrieve a paginated list of orders
@@ -224,6 +231,7 @@ def sync(
224231
exclude_cancelled (bool | Unset):
225232
due_date_from (datetime.date | Unset):
226233
due_date_to (datetime.date | Unset):
234+
page (int | Unset):
227235
per_page (int | Unset): Default: 15.
228236
229237
@@ -247,6 +255,7 @@ def sync(
247255
exclude_cancelled=exclude_cancelled,
248256
due_date_from=due_date_from,
249257
due_date_to=due_date_to,
258+
page=page,
250259
per_page=per_page,
251260
).parsed
252261

@@ -263,6 +272,7 @@ async def asyncio_detailed(
263272
exclude_cancelled: bool | Unset = UNSET,
264273
due_date_from: datetime.date | Unset = UNSET,
265274
due_date_to: datetime.date | Unset = UNSET,
275+
page: int | Unset = UNSET,
266276
per_page: int | Unset = 15,
267277
) -> Response[ErrorResponse | OrderListResponse | ValidationErrorResponse]:
268278
"""Retrieve a paginated list of orders
@@ -279,6 +289,7 @@ async def asyncio_detailed(
279289
exclude_cancelled (bool | Unset):
280290
due_date_from (datetime.date | Unset):
281291
due_date_to (datetime.date | Unset):
292+
page (int | Unset):
282293
per_page (int | Unset): Default: 15.
283294
284295
@@ -301,6 +312,7 @@ async def asyncio_detailed(
301312
exclude_cancelled=exclude_cancelled,
302313
due_date_from=due_date_from,
303314
due_date_to=due_date_to,
315+
page=page,
304316
per_page=per_page,
305317
)
306318

@@ -321,6 +333,7 @@ async def asyncio(
321333
exclude_cancelled: bool | Unset = UNSET,
322334
due_date_from: datetime.date | Unset = UNSET,
323335
due_date_to: datetime.date | Unset = UNSET,
336+
page: int | Unset = UNSET,
324337
per_page: int | Unset = 15,
325338
) -> ErrorResponse | OrderListResponse | ValidationErrorResponse | None:
326339
"""Retrieve a paginated list of orders
@@ -337,6 +350,7 @@ async def asyncio(
337350
exclude_cancelled (bool | Unset):
338351
due_date_from (datetime.date | Unset):
339352
due_date_to (datetime.date | Unset):
353+
page (int | Unset):
340354
per_page (int | Unset): Default: 15.
341355
342356
@@ -361,6 +375,7 @@ async def asyncio(
361375
exclude_cancelled=exclude_cancelled,
362376
due_date_from=due_date_from,
363377
due_date_to=due_date_to,
378+
page=page,
364379
per_page=per_page,
365380
)
366381
).parsed

statuspro_public_api_client/statuspro_client.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,13 +538,29 @@ async def _handle_paginated_request(self, request: httpx.Request) -> httpx.Respo
538538
# Original endpoint returned a raw JSON list - preserve that format
539539
combined_content = json.dumps(all_data).encode()
540540
else:
541-
combined_data: dict[str, Any] = {"data": all_data}
542-
# Add pagination metadata
541+
# StatusPro wrapped list shape: {"data": [...], "meta": {...}}.
542+
# Generated response models (e.g. OrderListResponse.from_dict)
543+
# require `meta`, so synthesize one describing the combined result
544+
# as a single page containing every collected item.
545+
total_items = len(all_data)
546+
combined_data: dict[str, Any] = {
547+
"data": all_data,
548+
"meta": {
549+
"current_page": 1,
550+
"last_page": 1,
551+
"per_page": total_items,
552+
"total": total_items,
553+
"from": 1 if total_items else None,
554+
"to": total_items if total_items else None,
555+
},
556+
}
557+
# Telemetry about the underlying pagination walk; lands in
558+
# additional_properties on the parsed model.
543559
if total_pages:
544560
combined_data["pagination"] = {
545561
"total_pages": total_pages,
546562
"collected_pages": page_num,
547-
"total_items": len(all_data),
563+
"total_items": total_items,
548564
"auto_paginated": True,
549565
}
550566
combined_content = json.dumps(combined_data).encode()

tests/test_statuspro_client.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,80 @@ async def mock_aread():
461461
assert combined_data["pagination"]["collected_pages"] == 3
462462
assert combined_data["pagination"]["auto_paginated"] is True
463463

464+
@pytest.mark.asyncio
465+
async def test_auto_pagination_emits_statuspro_meta_shape(
466+
self, transport, mock_wrapped_transport
467+
):
468+
"""Combined paginated response must include a `meta` block parseable by
469+
OrderListResponse.from_dict (StatusPro's wrapped list schema requires it).
470+
"""
471+
from statuspro_public_api_client.models.order_list_response import (
472+
OrderListResponse,
473+
)
474+
475+
page1 = {
476+
"data": [
477+
{"id": 1, "name": "#1", "order_number": "1"},
478+
{"id": 2, "name": "#2", "order_number": "2"},
479+
],
480+
"meta": {
481+
"current_page": 1,
482+
"last_page": 2,
483+
"per_page": 2,
484+
"total": 3,
485+
"from": 1,
486+
"to": 2,
487+
},
488+
}
489+
page2 = {
490+
"data": [{"id": 3, "name": "#3", "order_number": "3"}],
491+
"meta": {
492+
"current_page": 2,
493+
"last_page": 2,
494+
"per_page": 2,
495+
"total": 3,
496+
"from": 3,
497+
"to": 3,
498+
},
499+
}
500+
501+
def create_response(data):
502+
mock_resp = MagicMock(spec=httpx.Response)
503+
mock_resp.status_code = 200
504+
mock_resp.json.return_value = data
505+
mock_resp.headers = {}
506+
507+
async def mock_aread():
508+
pass
509+
510+
mock_resp.aread = mock_aread
511+
return mock_resp
512+
513+
mock_wrapped_transport.handle_async_request.side_effect = [
514+
create_response(page1),
515+
create_response(page2),
516+
]
517+
518+
request = httpx.Request(method="GET", url="https://api.example.com/orders")
519+
response = await transport.handle_async_request(request)
520+
521+
combined = json.loads(response.content)
522+
# All items concatenated
523+
assert [item["id"] for item in combined["data"]] == [1, 2, 3]
524+
# StatusPro `meta` block must be present and well-formed
525+
assert "meta" in combined, (
526+
"Combined paginated response must emit a `meta` block — "
527+
"OrderListResponse.from_dict() requires it."
528+
)
529+
assert combined["meta"]["current_page"] == 1
530+
assert combined["meta"]["last_page"] == 1
531+
assert combined["meta"]["per_page"] == 3
532+
assert combined["meta"]["total"] == 3
533+
# And the response must round-trip through the generated parser
534+
parsed = OrderListResponse.from_dict(combined)
535+
assert len(parsed.data) == 3
536+
assert parsed.meta.total == 3
537+
464538

465539
@pytest.mark.unit
466540
class TestStatusProClient:

0 commit comments

Comments
 (0)