pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
8.0 KiB
238 lines
8.0 KiB
import contextlib
|
|
import functools
|
|
import inspect
|
|
import types
|
|
from collections.abc import (
|
|
AsyncIterator,
|
|
Awaitable,
|
|
Callable,
|
|
Generator,
|
|
Mapping,
|
|
)
|
|
from contextlib import (
|
|
AbstractAsyncContextManager,
|
|
AbstractContextManager,
|
|
AsyncExitStack,
|
|
asynccontextmanager,
|
|
)
|
|
from typing import Any, TypeVar, cast
|
|
|
|
from fastapi._compat import ModelField
|
|
from fastapi.dependencies.models import Dependant
|
|
from fastapi.encoders import jsonable_encoder
|
|
from fastapi.exceptions import (
|
|
EndpointContext,
|
|
FastAPIError,
|
|
ResponseValidationError,
|
|
)
|
|
from fastapi.types import IncEx
|
|
from starlette._exception_handler import wrap_app_handling_exceptions
|
|
from starlette._utils import is_async_callable
|
|
from starlette.concurrency import run_in_threadpool
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
|
|
from starlette.websockets import WebSocket
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
|
|
def request_response(
|
|
func: Callable[[Request], Awaitable[Response] | Response],
|
|
) -> ASGIApp:
|
|
if is_async_callable(func):
|
|
f: Callable[[Request], Awaitable[Response]] = cast(
|
|
Callable[[Request], Awaitable[Response]], func
|
|
)
|
|
else:
|
|
sync_func = cast(Callable[[Request], Response], func)
|
|
|
|
async def f(request: Request) -> Response:
|
|
return await run_in_threadpool(sync_func, request)
|
|
|
|
async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
request = Request(scope, receive, send)
|
|
|
|
async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
response_awaited = False
|
|
async with AsyncExitStack() as request_stack:
|
|
scope["fastapi_inner_astack"] = request_stack
|
|
async with AsyncExitStack() as function_stack:
|
|
scope["fastapi_function_astack"] = function_stack
|
|
response = await f(request)
|
|
await response(scope, receive, send)
|
|
response_awaited = True
|
|
if not response_awaited:
|
|
raise FastAPIError(
|
|
"Response not awaited. There's a high chance that the "
|
|
"application code is raising an exception and a dependency with yield "
|
|
"has a block with a bare except, or a block with except Exception, "
|
|
"and is not raising the exception again. Read more about it in the "
|
|
"docs: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-except"
|
|
)
|
|
|
|
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
|
|
|
return app
|
|
|
|
|
|
def websocket_session(
|
|
func: Callable[[WebSocket], Awaitable[None]],
|
|
) -> ASGIApp:
|
|
async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
session = WebSocket(scope, receive=receive, send=send)
|
|
|
|
async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
async with AsyncExitStack() as request_stack:
|
|
scope["fastapi_inner_astack"] = request_stack
|
|
async with AsyncExitStack() as function_stack:
|
|
scope["fastapi_function_astack"] = function_stack
|
|
await func(session)
|
|
|
|
await wrap_app_handling_exceptions(app, session)(scope, receive, send)
|
|
|
|
return app
|
|
|
|
|
|
class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]):
|
|
def __init__(self, cm: AbstractContextManager[_T]) -> None:
|
|
self._cm = cm
|
|
|
|
async def __aenter__(self) -> _T:
|
|
return self._cm.__enter__()
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_value: BaseException | None,
|
|
traceback: types.TracebackType | None,
|
|
) -> bool | None:
|
|
return self._cm.__exit__(exc_type, exc_value, traceback)
|
|
|
|
|
|
def _wrap_gen_lifespan_context(
|
|
lifespan_context: Callable[[Any], Generator[Any, Any, Any]],
|
|
) -> Callable[[Any], AbstractAsyncContextManager[Any]]:
|
|
cmgr = contextlib.contextmanager(lifespan_context)
|
|
|
|
@functools.wraps(cmgr)
|
|
def wrapper(app: Any) -> _AsyncLiftContextManager[Any]:
|
|
return _AsyncLiftContextManager(cmgr(app))
|
|
|
|
return wrapper
|
|
|
|
|
|
def _merge_lifespan_context(
|
|
original_context: Lifespan[Any], nested_context: Lifespan[Any]
|
|
) -> Lifespan[Any]:
|
|
@asynccontextmanager
|
|
async def merged_lifespan(
|
|
app: AppType,
|
|
) -> AsyncIterator[Mapping[str, Any] | None]:
|
|
async with original_context(app) as maybe_original_state:
|
|
async with nested_context(app) as maybe_nested_state:
|
|
if maybe_nested_state is None and maybe_original_state is None:
|
|
yield None
|
|
else:
|
|
yield {**(maybe_nested_state or {}), **(maybe_original_state or {})}
|
|
|
|
return merged_lifespan # type: ignore[return-value]
|
|
|
|
|
|
class _DefaultLifespan:
|
|
def __init__(self, router: Any) -> None:
|
|
self._router = router
|
|
|
|
async def __aenter__(self) -> None:
|
|
await self._router._startup()
|
|
|
|
async def __aexit__(self, *exc_info: object) -> None:
|
|
await self._router._shutdown()
|
|
|
|
def __call__(self: _T, app: object) -> _T:
|
|
return self
|
|
|
|
|
|
_endpoint_context_cache: dict[int, EndpointContext] = {}
|
|
|
|
|
|
def extract_endpoint_context(func: Any) -> EndpointContext:
|
|
func_id = id(func)
|
|
if func_id in _endpoint_context_cache:
|
|
return _endpoint_context_cache[func_id]
|
|
try:
|
|
ctx: EndpointContext = {}
|
|
if (source_file := inspect.getsourcefile(func)) is not None:
|
|
ctx["file"] = source_file
|
|
if (line_number := inspect.getsourcelines(func)[1]) is not None:
|
|
ctx["line"] = line_number
|
|
if (func_name := getattr(func, "__name__", None)) is not None:
|
|
ctx["function"] = func_name
|
|
except Exception:
|
|
ctx = EndpointContext()
|
|
_endpoint_context_cache[func_id] = ctx
|
|
return ctx
|
|
|
|
|
|
async def serialize_response(
|
|
*,
|
|
field: ModelField | None = None,
|
|
response_content: Any,
|
|
include: IncEx | None = None,
|
|
exclude: IncEx | None = None,
|
|
by_alias: bool = True,
|
|
exclude_unset: bool = False,
|
|
exclude_defaults: bool = False,
|
|
exclude_none: bool = False,
|
|
is_coroutine: bool = True,
|
|
endpoint_ctx: EndpointContext | None = None,
|
|
dump_json: bool = False,
|
|
) -> Any:
|
|
if field:
|
|
if is_coroutine:
|
|
value, errors = field.validate(response_content, {}, loc=("response",))
|
|
else:
|
|
value, errors = await run_in_threadpool(
|
|
field.validate, response_content, {}, loc=("response",)
|
|
)
|
|
if errors:
|
|
ctx = endpoint_ctx or EndpointContext()
|
|
raise ResponseValidationError(
|
|
errors=errors,
|
|
body=response_content,
|
|
endpoint_ctx=ctx,
|
|
)
|
|
serializer = field.serialize_json if dump_json else field.serialize
|
|
return serializer(
|
|
value,
|
|
include=include,
|
|
exclude=exclude,
|
|
by_alias=by_alias,
|
|
exclude_unset=exclude_unset,
|
|
exclude_defaults=exclude_defaults,
|
|
exclude_none=exclude_none,
|
|
)
|
|
return jsonable_encoder(response_content)
|
|
|
|
|
|
async def run_endpoint_function(
|
|
*, dependant: Dependant, values: dict[str, Any], is_coroutine: bool
|
|
) -> Any:
|
|
assert dependant.call is not None, "dependant.call must be a function"
|
|
if is_coroutine:
|
|
return await dependant.call(**values)
|
|
return await run_in_threadpool(dependant.call, **values)
|
|
|
|
|
|
def build_response_args(
|
|
*, status_code: int | None, solved_result: Any
|
|
) -> dict[str, Any]:
|
|
response_args: dict[str, Any] = {"background": solved_result.background_tasks}
|
|
current_status_code = (
|
|
status_code if status_code else solved_result.response.status_code
|
|
)
|
|
if current_status_code is not None:
|
|
response_args["status_code"] = current_status_code
|
|
if solved_result.response.status_code:
|
|
response_args["status_code"] = solved_result.response.status_code
|
|
return response_args
|
|
|