Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| from collections.abc import Mapping | |
| from typing import Any, Generic, Literal, TypeVar | |
| from uuid import uuid4 | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field | |
| from starlette.responses import Response | |
| T = TypeVar("T") | |
| class ApiMeta(BaseModel): | |
| request_id: str = Field(min_length=1) | |
| class ApiErrorDetail(BaseModel): | |
| code: str = Field(min_length=1) | |
| message: str = Field(min_length=1) | |
| details: Any | None = None | |
| class ApiSuccessResponse(BaseModel, Generic[T]): | |
| success: Literal[True] = True | |
| data: T | |
| message: str | None = None | |
| meta: ApiMeta | |
| class ApiErrorResponse(BaseModel): | |
| success: Literal[False] = False | |
| error: ApiErrorDetail | |
| meta: ApiMeta | |
| ERROR_CODE_BY_STATUS: dict[int, str] = { | |
| 400: "invalid_request", | |
| 401: "unauthorized", | |
| 403: "forbidden", | |
| 404: "not_found", | |
| 413: "payload_too_large", | |
| 429: "too_many_requests", | |
| 422: "validation_error", | |
| 500: "internal_error", | |
| 503: "service_unavailable", | |
| } | |
| def error_code_from_status(status_code: int) -> str: | |
| if status_code in ERROR_CODE_BY_STATUS: | |
| return ERROR_CODE_BY_STATUS[status_code] | |
| if status_code >= 500: | |
| return "internal_error" | |
| return "invalid_request" | |
| def build_request_id() -> str: | |
| return f"req-{uuid4()}" | |
| def get_or_set_request_id(request: Request) -> str: | |
| request_id = getattr(request.state, "request_id", None) | |
| if isinstance(request_id, str) and request_id: | |
| return request_id | |
| request_id = build_request_id() | |
| request.state.request_id = request_id | |
| return request_id | |
| def build_success_payload( | |
| request: Request, | |
| data: T, | |
| message: str | None = None, | |
| ) -> ApiSuccessResponse[T]: | |
| return ApiSuccessResponse[T]( | |
| data=data, | |
| message=message, | |
| meta=ApiMeta(request_id=get_or_set_request_id(request)), | |
| ) | |
| def build_error_payload( | |
| request: Request, | |
| *, | |
| status_code: int, | |
| message: str, | |
| details: Any | None = None, | |
| code: str | None = None, | |
| ) -> ApiErrorResponse: | |
| return ApiErrorResponse( | |
| error=ApiErrorDetail( | |
| code=code or error_code_from_status(status_code), | |
| message=message, | |
| details=details, | |
| ), | |
| meta=ApiMeta(request_id=get_or_set_request_id(request)), | |
| ) | |
| def build_error_response( | |
| request: Request, | |
| *, | |
| status_code: int, | |
| message: str, | |
| details: Any | None = None, | |
| code: str | None = None, | |
| headers: Mapping[str, str] | None = None, | |
| ) -> JSONResponse: | |
| payload = build_error_payload( | |
| request, | |
| status_code=status_code, | |
| message=message, | |
| details=details, | |
| code=code, | |
| ) | |
| return JSONResponse( | |
| status_code=status_code, | |
| content=payload.model_dump(), | |
| headers=dict(headers or {}), | |
| ) | |
| def normalize_exception_detail( | |
| detail: Any, | |
| *, | |
| default_message: str, | |
| ) -> tuple[str, Any | None]: | |
| if isinstance(detail, str): | |
| return detail, None | |
| if isinstance(detail, Mapping): | |
| raw_message = detail.get("message") | |
| if isinstance(raw_message, str) and raw_message: | |
| extra = {k: v for k, v in detail.items() if k != "message"} | |
| return raw_message, extra or None | |
| return default_message, dict(detail) | |
| if isinstance(detail, list): | |
| return default_message, detail | |
| if detail is None: | |
| return default_message, None | |
| return str(detail), None | |
| def attach_meta_to_json_response(response: Response, request_id: str) -> Response: | |
| content_type = response.headers.get("content-type", "").lower() | |
| if "application/json" not in content_type: | |
| return response | |
| body = getattr(response, "body", None) | |
| if not isinstance(body, (bytes, bytearray)): | |
| return response | |
| try: | |
| payload = json.loads(body) | |
| except json.JSONDecodeError: | |
| return response | |
| if not isinstance(payload, dict): | |
| return response | |
| raw_meta = payload.get("meta") | |
| if isinstance(raw_meta, Mapping): | |
| if raw_meta.get("request_id") == request_id: | |
| return response | |
| payload["meta"] = {**raw_meta, "request_id": request_id} | |
| else: | |
| payload["meta"] = {"request_id": request_id} | |
| encoded = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode( | |
| "utf-8" | |
| ) | |
| response.body = encoded | |
| response.headers["content-length"] = str(len(encoded)) | |
| return response | |