合并 agent-mvp 到 master #1
@@ -8,8 +8,10 @@ from functools import wraps
|
|||||||
from typing import Any, Generic, TypeVar, get_args, get_origin
|
from typing import Any, Generic, TypeVar, get_args, get_origin
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from pydantic import BaseModel, JsonValue, create_model
|
from pydantic import BaseModel, JsonValue, create_model
|
||||||
|
from starlette.responses import Response
|
||||||
|
|
||||||
from app.api.problem_details import ProblemDetails
|
from app.api.problem_details import ProblemDetails
|
||||||
from app.api.v1.router import api_router as handler_api_router
|
from app.api.v1.router import api_router as handler_api_router
|
||||||
@@ -263,6 +265,21 @@ def _with_pagination(endpoint):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _with_jsonable_response(endpoint):
|
||||||
|
"""Normalize untyped handler results before JsonValue validation."""
|
||||||
|
|
||||||
|
@wraps(endpoint)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
result = endpoint(*args, **kwargs)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
result = await result
|
||||||
|
if isinstance(result, Response):
|
||||||
|
return result
|
||||||
|
return jsonable_encoder(result)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def _adapt_route(route: APIRoute) -> APIRoute:
|
def _adapt_route(route: APIRoute) -> APIRoute:
|
||||||
methods = route.methods or set()
|
methods = route.methods or set()
|
||||||
if len(methods) != 1:
|
if len(methods) != 1:
|
||||||
@@ -286,6 +303,9 @@ def _adapt_route(route: APIRoute) -> APIRoute:
|
|||||||
|
|
||||||
endpoint = _with_header_project_context(route.endpoint, route.name)
|
endpoint = _with_header_project_context(route.endpoint, route.name)
|
||||||
response_model = route.response_model
|
response_model = route.response_model
|
||||||
|
has_untyped_response = response_model is None
|
||||||
|
if has_untyped_response:
|
||||||
|
endpoint = _with_jsonable_response(endpoint)
|
||||||
if get_origin(response_model) is list:
|
if get_origin(response_model) is list:
|
||||||
item_type = get_args(response_model)[0] if get_args(response_model) else JsonValue
|
item_type = get_args(response_model)[0] if get_args(response_model) else JsonValue
|
||||||
response_model = Page[item_type]
|
response_model = Page[item_type]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -282,3 +283,35 @@ def test_rest_runtime_wraps_handler_paginated_list() -> None:
|
|||||||
"limit": 2,
|
"limit": 2,
|
||||||
"offset": 1,
|
"offset": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_runtime_json_encodes_untyped_datetime_response() -> None:
|
||||||
|
source_router = APIRouter()
|
||||||
|
|
||||||
|
@source_router.get("/simulation-result")
|
||||||
|
async def simulation_result():
|
||||||
|
return {
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"time": datetime(2026, 7, 30, 4, tzinfo=timezone.utc),
|
||||||
|
"id": "4277",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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/simulation-result"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"time": "2026-07-30T04:00:00+00:00",
|
||||||
|
"id": "4277",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user