fix(api): wrap pre-paginated list responses
Server CI/CD / docker-image (push) Failing after 28s
Server CI/CD / deploy-fallback-log (push) Successful in 1s

This commit is contained in:
2026-07-30 21:50:21 +08:00
parent ba947b616b
commit 1d88f8efbe
2 changed files with 71 additions and 21 deletions
+42 -20
View File
@@ -201,18 +201,39 @@ def _with_header_project_context(endpoint, route_name: str):
def _with_pagination(endpoint):
signature = inspect.signature(endpoint)
if "limit" in signature.parameters or "offset" in signature.parameters:
return endpoint
handler_limit_parameter = "limit" if "limit" in signature.parameters else None
handler_offset_parameter = next(
(
parameter_name
for parameter_name in ("offset", "skip")
if parameter_name in signature.parameters
),
None,
)
handler_handles_pagination = bool(
handler_limit_parameter or handler_offset_parameter
)
@wraps(endpoint)
async def wrapper(*args, **kwargs):
limit = kwargs.pop("_rest_limit")
offset = kwargs.pop("_rest_offset")
if handler_handles_pagination:
limit = kwargs.get(handler_limit_parameter, 0)
offset = kwargs.get(handler_offset_parameter, 0)
else:
limit = kwargs.pop("_rest_limit")
offset = kwargs.pop("_rest_offset")
result = endpoint(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
if not isinstance(result, list):
return result
if handler_handles_pagination:
return Page(
items=result,
total=offset + len(result),
limit=limit or len(result),
offset=offset,
)
return Page(
items=result[offset : offset + limit],
total=len(result),
@@ -221,22 +242,23 @@ def _with_pagination(endpoint):
)
parameters = list(signature.parameters.values())
parameters.extend(
[
inspect.Parameter(
"_rest_limit",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=Query(100, ge=1, le=1000, alias="limit"),
),
inspect.Parameter(
"_rest_offset",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=Query(0, ge=0, alias="offset"),
),
]
)
if not handler_handles_pagination:
parameters.extend(
[
inspect.Parameter(
"_rest_limit",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=Query(100, ge=1, le=1000, alias="limit"),
),
inspect.Parameter(
"_rest_offset",
kind=inspect.Parameter.KEYWORD_ONLY,
annotation=int,
default=Query(0, ge=0, alias="offset"),
),
]
)
wrapper.__signature__ = signature.replace(parameters=parameters)
return wrapper
+29 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi import APIRouter, FastAPI, Query
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
@@ -254,3 +254,31 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
assert response.json()["items"] == [
{"scheme_name": "burst_case", "scheme_type": "burst_analysis"}
]
def test_rest_runtime_wraps_handler_paginated_list() -> None:
source_router = APIRouter()
@source_router.get("/records", response_model=list[int])
async def list_records(
skip: int = Query(0, ge=0),
limit: int = Query(2, ge=1, le=10),
) -> list[int]:
records = [10, 20, 30, 40]
return records[skip : skip + limit]
app = FastAPI(redirect_slashes=False)
app.include_router(build_rest_router(source_router.routes), prefix="/api/v1")
response = TestClient(app, raise_server_exceptions=False).get(
"/api/v1/records",
params={"skip": 1, "limit": 2},
)
assert response.status_code == 200
assert response.json() == {
"items": [20, 30],
"total": 3,
"limit": 2,
"offset": 1,
}