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