1import contextlib2import email.message3import functools4import inspect5import json6import types7from collections.abc import (8 AsyncIterator,9 Awaitable,10 Callable,11 Collection,12 Coroutine,13 Generator,14 Iterator,15 Mapping,16 Sequence,17)18from contextlib import (19 AbstractAsyncContextManager,20 AbstractContextManager,21 AsyncExitStack,22 asynccontextmanager,23)24from enum import Enum, IntEnum25from typing import (26 Annotated,27 Any,28 TypeVar,29 cast,30)3132import anyio33from annotated_doc import Doc34from anyio.abc import ObjectReceiveStream35from fastapi import params36from fastapi._compat import (37 ModelField,38 Undefined,39 lenient_issubclass,40)41from fastapi.datastructures import Default, DefaultPlaceholder42from fastapi.dependencies.models import Dependant43from fastapi.dependencies.utils import (44 _should_embed_body_fields,45 get_body_field,46 get_dependant,47 get_flat_dependant,48 get_parameterless_sub_dependant,49 get_stream_item_type,50 get_typed_return_annotation,51 solve_dependencies,52)53from fastapi.encoders import jsonable_encoder54from fastapi.exceptions import (55 EndpointContext,56 FastAPIError,57 RequestValidationError,58 ResponseValidationError,59 WebSocketRequestValidationError,60)61from fastapi.sse import (62 _PING_INTERVAL,63 KEEPALIVE_COMMENT,64 EventSourceResponse,65 ServerSentEvent,66 format_sse_event,67)68from fastapi.types import DecoratedCallable, IncEx69from fastapi.utils import (70 create_model_field,71 generate_unique_id,72 get_value_or_default,73 is_body_allowed_for_status_code,74)75from starlette import routing76from starlette._exception_handler import wrap_app_handling_exceptions77from starlette._utils import is_async_callable78from starlette.concurrency import iterate_in_threadpool, run_in_threadpool79from starlette.datastructures import FormData80from starlette.exceptions import HTTPException81from starlette.requests import Request82from starlette.responses import JSONResponse, Response, StreamingResponse83from starlette.routing import (84 BaseRoute,85 Match,86 compile_path,87 get_name,88)89from starlette.routing import Mount as Mount # noqa90from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send91from starlette.websockets import WebSocket92from typing_extensions import deprecated939495# Copy of starlette.routing.request_response modified to include the96# dependencies' AsyncExitStack97def request_response(98 func: Callable[[Request], Awaitable[Response] | Response],99) -> ASGIApp:100 """101 Takes a function or coroutine `func(request) -> response`,102 and returns an ASGI application.103 """104 f: Callable[[Request], Awaitable[Response]] = (105 func # type: ignore[assignment]106 if is_async_callable(func)107 else functools.partial(run_in_threadpool, func) # type: ignore[call-arg]108 ) # ty: ignore[invalid-assignment]109110 async def app(scope: Scope, receive: Receive, send: Send) -> None:111 request = Request(scope, receive, send)112113 async def app(scope: Scope, receive: Receive, send: Send) -> None:114 # Starts customization115 response_awaited = False116 async with AsyncExitStack() as request_stack:117 scope["fastapi_inner_astack"] = request_stack118 async with AsyncExitStack() as function_stack:119 scope["fastapi_function_astack"] = function_stack120 response = await f(request)121 await response(scope, receive, send)122 # Continues customization123 response_awaited = True124 if not response_awaited:125 raise FastAPIError(126 "Response not awaited. There's a high chance that the "127 "application code is raising an exception and a dependency with yield "128 "has a block with a bare except, or a block with except Exception, "129 "and is not raising the exception again. Read more about it in the "130 "docs: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-except"131 )132133 # Same as in Starlette134 await wrap_app_handling_exceptions(app, request)(scope, receive, send)135136 return app137138139# Copy of starlette.routing.websocket_session modified to include the140# dependencies' AsyncExitStack141def websocket_session(142 func: Callable[[WebSocket], Awaitable[None]],143) -> ASGIApp:144 """145 Takes a coroutine `func(session)`, and returns an ASGI application.146 """147 # assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async"148149 async def app(scope: Scope, receive: Receive, send: Send) -> None:150 session = WebSocket(scope, receive=receive, send=send)151152 async def app(scope: Scope, receive: Receive, send: Send) -> None:153 async with AsyncExitStack() as request_stack:154 scope["fastapi_inner_astack"] = request_stack155 async with AsyncExitStack() as function_stack:156 scope["fastapi_function_astack"] = function_stack157 await func(session)158159 # Same as in Starlette160 await wrap_app_handling_exceptions(app, session)(scope, receive, send)161162 return app163164165_T = TypeVar("_T")166167168# Vendored from starlette.routing to avoid importing private symbols169class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]):170 """171 Wraps a synchronous context manager to make it async.172173 This is vendored from Starlette to avoid importing private symbols.174 """175176 def __init__(self, cm: AbstractContextManager[_T]) -> None:177 self._cm = cm178179 async def __aenter__(self) -> _T:180 return self._cm.__enter__()181182 async def __aexit__(183 self,184 exc_type: type[BaseException] | None,185 exc_value: BaseException | None,186 traceback: types.TracebackType | None,187 ) -> bool | None:188 return self._cm.__exit__(exc_type, exc_value, traceback)189190191# Vendored from starlette.routing to avoid importing private symbols192def _wrap_gen_lifespan_context(193 lifespan_context: Callable[[Any], Generator[Any, Any, Any]],194) -> Callable[[Any], AbstractAsyncContextManager[Any]]:195 """196 Wrap a generator-based lifespan context into an async context manager.197198 This is vendored from Starlette to avoid importing private symbols.199 """200 cmgr = contextlib.contextmanager(lifespan_context)201202 @functools.wraps(cmgr)203 def wrapper(app: Any) -> _AsyncLiftContextManager[Any]:204 return _AsyncLiftContextManager(cmgr(app))205206 return wrapper207208209def _merge_lifespan_context(210 original_context: Lifespan[Any], nested_context: Lifespan[Any]211) -> Lifespan[Any]:212 @asynccontextmanager213 async def merged_lifespan(214 app: AppType,215 ) -> AsyncIterator[Mapping[str, Any] | None]:216 async with original_context(app) as maybe_original_state:217 async with nested_context(app) as maybe_nested_state:218 if maybe_nested_state is None and maybe_original_state is None:219 yield None # old ASGI compatibility220 else:221 yield {**(maybe_nested_state or {}), **(maybe_original_state or {})}222223 return merged_lifespan # type: ignore[return-value] # ty: ignore[invalid-return-type]224225226class _DefaultLifespan:227 """228 Default lifespan context manager that runs on_startup and on_shutdown handlers.229230 This is a copy of the Starlette _DefaultLifespan class that was removed231 in Starlette. FastAPI keeps it to maintain backward compatibility with232 on_startup and on_shutdown event handlers.233234 Ref: https://github.com/Kludex/starlette/pull/3117235 """236237 def __init__(self, router: "APIRouter") -> None:238 self._router = router239240 async def __aenter__(self) -> None:241 await self._router._startup()242243 async def __aexit__(self, *exc_info: object) -> None:244 await self._router._shutdown()245246 def __call__(self: _T, app: object) -> _T:247 return self248249250# Cache for endpoint context to avoid re-extracting on every request251_endpoint_context_cache: dict[int, EndpointContext] = {}252253254def _extract_endpoint_context(func: Any) -> EndpointContext:255 """Extract endpoint context with caching to avoid repeated file I/O."""256 func_id = id(func)257258 if func_id in _endpoint_context_cache:259 return _endpoint_context_cache[func_id]260261 try:262 ctx: EndpointContext = {}263264 if (source_file := inspect.getsourcefile(func)) is not None:265 ctx["file"] = source_file266 if (line_number := inspect.getsourcelines(func)[1]) is not None:267 ctx["line"] = line_number268 if (func_name := getattr(func, "__name__", None)) is not None:269 ctx["function"] = func_name270 except Exception:271 ctx = EndpointContext()272273 _endpoint_context_cache[func_id] = ctx274 return ctx275276277async def serialize_response(278 *,279 field: ModelField | None = None,280 response_content: Any,281 include: IncEx | None = None,282 exclude: IncEx | None = None,283 by_alias: bool = True,284 exclude_unset: bool = False,285 exclude_defaults: bool = False,286 exclude_none: bool = False,287 is_coroutine: bool = True,288 endpoint_ctx: EndpointContext | None = None,289 dump_json: bool = False,290) -> Any:291 if field:292 if is_coroutine:293 value, errors = field.validate(response_content, {}, loc=("response",))294 else:295 value, errors = await run_in_threadpool(296 field.validate, response_content, {}, loc=("response",)297 )298 if errors:299 ctx = endpoint_ctx or EndpointContext()300 raise ResponseValidationError(301 errors=errors,302 body=response_content,303 endpoint_ctx=ctx,304 )305 serializer = field.serialize_json if dump_json else field.serialize306 return serializer(307 value,308 include=include,309 exclude=exclude,310 by_alias=by_alias,311 exclude_unset=exclude_unset,312 exclude_defaults=exclude_defaults,313 exclude_none=exclude_none,314 )315316 else:317 return jsonable_encoder(response_content)318319320async def run_endpoint_function(321 *, dependant: Dependant, values: dict[str, Any], is_coroutine: bool322) -> Any:323 # Only called by get_request_handler. Has been split into its own function to324 # facilitate profiling endpoints, since inner functions are harder to profile.325 assert dependant.call is not None, "dependant.call must be a function"326327 if is_coroutine:328 return await dependant.call(**values)329 else:330 return await run_in_threadpool(dependant.call, **values)331332333def _build_response_args(334 *, status_code: int | None, solved_result: Any335) -> dict[str, Any]:336 response_args: dict[str, Any] = {337 "background": solved_result.background_tasks,338 }339 # If status_code was set, use it, otherwise use the default from the340 # response class, in the case of redirect it's 307341 current_status_code = (342 status_code if status_code else solved_result.response.status_code343 )344 if current_status_code is not None:345 response_args["status_code"] = current_status_code346 if solved_result.response.status_code:347 response_args["status_code"] = solved_result.response.status_code348 return response_args349350351def get_request_handler(352 dependant: Dependant,353 body_field: ModelField | None = None,354 status_code: int | None = None,355 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),356 response_field: ModelField | None = None,357 response_model_include: IncEx | None = None,358 response_model_exclude: IncEx | None = None,359 response_model_by_alias: bool = True,360 response_model_exclude_unset: bool = False,361 response_model_exclude_defaults: bool = False,362 response_model_exclude_none: bool = False,363 dependency_overrides_provider: Any | None = None,364 embed_body_fields: bool = False,365 strict_content_type: bool | DefaultPlaceholder = Default(True),366 stream_item_field: ModelField | None = None,367 is_json_stream: bool = False,368) -> Callable[[Request], Coroutine[Any, Any, Response]]:369 assert dependant.call is not None, "dependant.call must be a function"370 is_coroutine = dependant.is_coroutine_callable371 is_body_form = body_field and isinstance(body_field.field_info, params.Form)372 if isinstance(response_class, DefaultPlaceholder):373 actual_response_class: type[Response] = response_class.value374 else:375 actual_response_class = response_class376 is_sse_stream = lenient_issubclass(actual_response_class, EventSourceResponse)377 if isinstance(strict_content_type, DefaultPlaceholder):378 actual_strict_content_type: bool = strict_content_type.value379 else:380 actual_strict_content_type = strict_content_type381382 async def app(request: Request) -> Response:383 response: Response | None = None384 file_stack = request.scope.get("fastapi_middleware_astack")385 assert isinstance(file_stack, AsyncExitStack), (386 "fastapi_middleware_astack not found in request scope"387 )388389 # Extract endpoint context for error messages390 endpoint_ctx = (391 _extract_endpoint_context(dependant.call)392 if dependant.call393 else EndpointContext()394 )395396 if dependant.path:397 # For mounted sub-apps, include the mount path prefix398 mount_path = request.scope.get("root_path", "").rstrip("/")399 endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}"400401 # Read body and auto-close files402 try:403 body: Any = None404 if body_field:405 if is_body_form:406 body = await request.form()407 file_stack.push_async_callback(body.close)408 else:409 body_bytes = await request.body()410 if body_bytes:411 json_body: Any = Undefined412 content_type_value = request.headers.get("content-type")413 if not content_type_value:414 if not actual_strict_content_type:415 json_body = await request.json()416 else:417 message = email.message.Message()418 message["content-type"] = content_type_value419 if message.get_content_maintype() == "application":420 subtype = message.get_content_subtype()421 if subtype == "json" or subtype.endswith("+json"):422 json_body = await request.json()423 if json_body != Undefined:424 body = json_body425 else:426 body = body_bytes427 except json.JSONDecodeError as e:428 validation_error = RequestValidationError(429 [430 {431 "type": "json_invalid",432 "loc": ("body", e.pos),433 "msg": "JSON decode error",434 "input": {},435 "ctx": {"error": e.msg},436 }437 ],438 body=e.doc,439 endpoint_ctx=endpoint_ctx,440 )441 raise validation_error from e442 except HTTPException:443 # If a middleware raises an HTTPException, it should be raised again444 raise445 except Exception as e:446 http_error = HTTPException(447 status_code=400, detail="There was an error parsing the body"448 )449 raise http_error from e450451 # Solve dependencies and run path operation function, auto-closing dependencies452 errors: list[Any] = []453 async_exit_stack = request.scope.get("fastapi_inner_astack")454 assert isinstance(async_exit_stack, AsyncExitStack), (455 "fastapi_inner_astack not found in request scope"456 )457 solved_result = await solve_dependencies(458 request=request,459 dependant=dependant,460 body=cast(dict[str, Any] | FormData | bytes | None, body),461 dependency_overrides_provider=dependency_overrides_provider,462 async_exit_stack=async_exit_stack,463 embed_body_fields=embed_body_fields,464 )465 errors = solved_result.errors466 assert dependant.call # For types467 if not errors:468 # Shared serializer for stream items (JSONL and SSE).469 # Validates against stream_item_field when set, then470 # serializes to JSON bytes.471 def _serialize_data(data: Any) -> bytes:472 if stream_item_field:473 value, errors_ = stream_item_field.validate(474 data, {}, loc=("response",)475 )476 if errors_:477 ctx = endpoint_ctx or EndpointContext()478 raise ResponseValidationError(479 errors=errors_,480 body=data,481 endpoint_ctx=ctx,482 )483 return stream_item_field.serialize_json(484 value,485 include=response_model_include,486 exclude=response_model_exclude,487 by_alias=response_model_by_alias,488 exclude_unset=response_model_exclude_unset,489 exclude_defaults=response_model_exclude_defaults,490 exclude_none=response_model_exclude_none,491 )492 else:493 data = jsonable_encoder(data)494 return json.dumps(data).encode("utf-8")495496 if is_sse_stream:497 # Generator endpoint: stream as Server-Sent Events498 gen = dependant.call(**solved_result.values)499500 def _serialize_sse_item(item: Any) -> bytes:501 if isinstance(item, ServerSentEvent):502 # User controls the event structure.503 # Serialize the data payload if present.504 # For ServerSentEvent items we skip stream_item_field505 # validation (the user may mix types intentionally).506 if item.raw_data is not None:507 data_str: str | None = item.raw_data508 elif item.data is not None:509 if hasattr(item.data, "model_dump_json"):510 data_str = item.data.model_dump_json()511 else:512 data_str = json.dumps(jsonable_encoder(item.data))513 else:514 data_str = None515 return format_sse_event(516 data_str=data_str,517 event=item.event,518 id=item.id,519 retry=item.retry,520 comment=item.comment,521 )522 else:523 # Plain object: validate + serialize via524 # stream_item_field (if set) and wrap in data field525 return format_sse_event(526 data_str=_serialize_data(item).decode("utf-8")527 )528529 if dependant.is_async_gen_callable:530 sse_aiter: AsyncIterator[Any] = gen.__aiter__()531 else:532 sse_aiter = iterate_in_threadpool(gen)533534 @asynccontextmanager535 async def _sse_producer_cm() -> AsyncIterator[536 ObjectReceiveStream[bytes]537 ]:538 # Use a memory stream to decouple generator iteration539 # from the keepalive timer. A producer task pulls items540 # from the generator independently, so541 # `anyio.fail_after` never wraps the generator's542 # `__anext__` directly - avoiding CancelledError that543 # would finalize the generator and also working for sync544 # generators running in a thread pool.545 #546 # This context manager is entered on the request-scoped547 # AsyncExitStack so its __aexit__ (which cancels the548 # task group) is called by the exit stack after the549 # streaming response completes — not by async generator550 # finalization via GeneratorExit.551 # Ref: https://peps.python.org/pep-0789/552 send_stream, receive_stream = anyio.create_memory_object_stream[553 bytes554 ](max_buffer_size=1)555556 async def _producer() -> None:557 async with send_stream:558 async for raw_item in sse_aiter:559 await send_stream.send(_serialize_sse_item(raw_item))560561 send_keepalive, receive_keepalive = (562 anyio.create_memory_object_stream[bytes](max_buffer_size=1)563 )564565 async def _keepalive_inserter() -> None:566 """Read from the producer and forward to the output,567 inserting keepalive comments on timeout."""568 async with send_keepalive, receive_stream:569 try:570 while True:571 try:572 with anyio.fail_after(_PING_INTERVAL):573 data = await receive_stream.receive()574 await send_keepalive.send(data)575 except TimeoutError:576 await send_keepalive.send(KEEPALIVE_COMMENT)577 except anyio.EndOfStream:578 pass579580 async with anyio.create_task_group() as tg:581 tg.start_soon(_producer)582 tg.start_soon(_keepalive_inserter)583 yield receive_keepalive584 tg.cancel_scope.cancel()585586 # Enter the SSE context manager on the request-scoped587 # exit stack. The stack outlives the streaming response,588 # so __aexit__ runs via proper structured teardown, not589 # via GeneratorExit thrown into an async generator.590 sse_receive_stream = await async_exit_stack.enter_async_context(591 _sse_producer_cm()592 )593 # Ensure the receive stream is closed when the exit stack594 # unwinds, preventing ResourceWarning from __del__.595 async_exit_stack.push_async_callback(sse_receive_stream.aclose)596597 async def _sse_with_checkpoints(598 stream: ObjectReceiveStream[bytes],599 ) -> AsyncIterator[bytes]:600 async for data in stream:601 yield data602 # Guarantee a checkpoint so cancellation can be603 # delivered even when the producer is faster than604 # the consumer and receive() never suspends.605 await anyio.sleep(0)606607 sse_stream_content: AsyncIterator[bytes] | Iterator[bytes] = (608 _sse_with_checkpoints(sse_receive_stream)609 )610611 response = StreamingResponse(612 sse_stream_content,613 media_type="text/event-stream",614 background=solved_result.background_tasks,615 )616 response.headers["Cache-Control"] = "no-cache"617 # For Nginx proxies to not buffer server sent events618 response.headers["X-Accel-Buffering"] = "no"619 response.headers.raw.extend(solved_result.response.headers.raw)620 elif is_json_stream:621 # Generator endpoint: stream as JSONL622 gen = dependant.call(**solved_result.values)623624 def _serialize_item(item: Any) -> bytes:625 return _serialize_data(item) + b"\n"626627 if dependant.is_async_gen_callable:628629 async def _async_stream_jsonl() -> AsyncIterator[bytes]:630 async for item in gen:631 yield _serialize_item(item)632 # To allow for cancellation to trigger633 # Ref: https://github.com/fastapi/fastapi/issues/14680634 await anyio.sleep(0)635636 jsonl_stream_content: AsyncIterator[bytes] | Iterator[bytes] = (637 _async_stream_jsonl()638 )639 else:640641 def _sync_stream_jsonl() -> Iterator[bytes]:642 for item in gen: # ty: ignore[not-iterable]643 yield _serialize_item(item)644645 jsonl_stream_content = _sync_stream_jsonl()646647 response = StreamingResponse(648 jsonl_stream_content,649 media_type="application/jsonl",650 background=solved_result.background_tasks,651 )652 response.headers.raw.extend(solved_result.response.headers.raw)653 elif dependant.is_async_gen_callable or dependant.is_gen_callable:654 # Raw streaming with explicit response_class (e.g. StreamingResponse)655 gen = dependant.call(**solved_result.values)656 if dependant.is_async_gen_callable:657658 async def _async_stream_raw(659 async_gen: AsyncIterator[Any],660 ) -> AsyncIterator[Any]:661 async for chunk in async_gen:662 yield chunk663 # To allow for cancellation to trigger664 # Ref: https://github.com/fastapi/fastapi/issues/14680665 await anyio.sleep(0)666667 gen = _async_stream_raw(gen)668 response_args = _build_response_args(669 status_code=status_code, solved_result=solved_result670 )671 response = actual_response_class(content=gen, **response_args)672 response.headers.raw.extend(solved_result.response.headers.raw)673 else:674 raw_response = await run_endpoint_function(675 dependant=dependant,676 values=solved_result.values,677 is_coroutine=is_coroutine,678 )679 if isinstance(raw_response, Response):680 if raw_response.background is None:681 raw_response.background = solved_result.background_tasks682 response = raw_response683 else:684 response_args = _build_response_args(685 status_code=status_code, solved_result=solved_result686 )687 # Use the fast path (dump_json) when no custom response688 # class was set and a response field with a TypeAdapter689 # exists. Serializes directly to JSON bytes via Pydantic's690 # Rust core, skipping the intermediate Python dict +691 # json.dumps() step.692 use_dump_json = response_field is not None and isinstance(693 response_class, DefaultPlaceholder694 )695 content = await serialize_response(696 field=response_field,697 response_content=raw_response,698 include=response_model_include,699 exclude=response_model_exclude,700 by_alias=response_model_by_alias,701 exclude_unset=response_model_exclude_unset,702 exclude_defaults=response_model_exclude_defaults,703 exclude_none=response_model_exclude_none,704 is_coroutine=is_coroutine,705 endpoint_ctx=endpoint_ctx,706 dump_json=use_dump_json,707 )708 if use_dump_json:709 response = Response(710 content=content,711 media_type="application/json",712 **response_args,713 )714 else:715 response = actual_response_class(content, **response_args)716 if not is_body_allowed_for_status_code(response.status_code):717 response.body = b""718 response.headers.raw.extend(solved_result.response.headers.raw)719 if errors:720 validation_error = RequestValidationError(721 errors, body=body, endpoint_ctx=endpoint_ctx722 )723 raise validation_error724725 # Return response726 assert response727 return response728729 return app730731732def get_websocket_app(733 dependant: Dependant,734 dependency_overrides_provider: Any | None = None,735 embed_body_fields: bool = False,736) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:737 async def app(websocket: WebSocket) -> None:738 endpoint_ctx = (739 _extract_endpoint_context(dependant.call)740 if dependant.call741 else EndpointContext()742 )743 if dependant.path:744 # For mounted sub-apps, include the mount path prefix745 mount_path = websocket.scope.get("root_path", "").rstrip("/")746 endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}"747 async_exit_stack = websocket.scope.get("fastapi_inner_astack")748 assert isinstance(async_exit_stack, AsyncExitStack), (749 "fastapi_inner_astack not found in request scope"750 )751 solved_result = await solve_dependencies(752 request=websocket,753 dependant=dependant,754 dependency_overrides_provider=dependency_overrides_provider,755 async_exit_stack=async_exit_stack,756 embed_body_fields=embed_body_fields,757 )758 if solved_result.errors:759 raise WebSocketRequestValidationError(760 solved_result.errors,761 endpoint_ctx=endpoint_ctx,762 )763 assert dependant.call is not None, "dependant.call must be a function"764 await dependant.call(**solved_result.values)765766 return app767768769class APIWebSocketRoute(routing.WebSocketRoute):770 def __init__(771 self,772 path: str,773 endpoint: Callable[..., Any],774 *,775 name: str | None = None,776 dependencies: Sequence[params.Depends] | None = None,777 dependency_overrides_provider: Any | None = None,778 ) -> None:779 self.path = path780 self.endpoint = endpoint781 self.name = get_name(endpoint) if name is None else name782 self.dependencies = list(dependencies or [])783 self.path_regex, self.path_format, self.param_convertors = compile_path(path)784 self.dependant = get_dependant(785 path=self.path_format, call=self.endpoint, scope="function"786 )787 for depends in self.dependencies[::-1]:788 self.dependant.dependencies.insert(789 0,790 get_parameterless_sub_dependant(depends=depends, path=self.path_format),791 )792 self._flat_dependant = get_flat_dependant(self.dependant)793 self._embed_body_fields = _should_embed_body_fields(794 self._flat_dependant.body_params795 )796 self.app = websocket_session(797 get_websocket_app(798 dependant=self.dependant,799 dependency_overrides_provider=dependency_overrides_provider,800 embed_body_fields=self._embed_body_fields,801 )802 )803804 def matches(self, scope: Scope) -> tuple[Match, Scope]:805 match, child_scope = super().matches(scope)806 if match != Match.NONE:807 child_scope["route"] = self808 return match, child_scope809810811class APIRoute(routing.Route):812 def __init__(813 self,814 path: str,815 endpoint: Callable[..., Any],816 *,817 response_model: Any = Default(None),818 status_code: int | None = None,819 tags: list[str | Enum] | None = None,820 dependencies: Sequence[params.Depends] | None = None,821 summary: str | None = None,822 description: str | None = None,823 response_description: str = "Successful Response",824 responses: dict[int | str, dict[str, Any]] | None = None,825 deprecated: bool | None = None,826 name: str | None = None,827 methods: set[str] | list[str] | None = None,828 operation_id: str | None = None,829 response_model_include: IncEx | None = None,830 response_model_exclude: IncEx | None = None,831 response_model_by_alias: bool = True,832 response_model_exclude_unset: bool = False,833 response_model_exclude_defaults: bool = False,834 response_model_exclude_none: bool = False,835 include_in_schema: bool = True,836 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),837 dependency_overrides_provider: Any | None = None,838 callbacks: list[BaseRoute] | None = None,839 openapi_extra: dict[str, Any] | None = None,840 generate_unique_id_function: Callable[["APIRoute"], str]841 | DefaultPlaceholder = Default(generate_unique_id),842 strict_content_type: bool | DefaultPlaceholder = Default(True),843 ) -> None:844 self.path = path845 self.endpoint = endpoint846 self.stream_item_type: Any | None = None847 if isinstance(response_model, DefaultPlaceholder):848 return_annotation = get_typed_return_annotation(endpoint)849 if lenient_issubclass(return_annotation, Response):850 response_model = None851 else:852 stream_item = get_stream_item_type(return_annotation)853 if stream_item is not None:854 # Extract item type for JSONL or SSE streaming when855 # response_class is DefaultPlaceholder (JSONL) or856 # EventSourceResponse (SSE).857 # ServerSentEvent is excluded: it's a transport858 # wrapper, not a data model, so it shouldn't feed859 # into validation or OpenAPI schema generation.860 if (861 isinstance(response_class, DefaultPlaceholder)862 or lenient_issubclass(response_class, EventSourceResponse)863 ) and not lenient_issubclass(stream_item, ServerSentEvent):864 self.stream_item_type = stream_item865 response_model = None866 else:867 response_model = return_annotation868 self.response_model = response_model869 self.summary = summary870 self.response_description = response_description871 self.deprecated = deprecated872 self.operation_id = operation_id873 self.response_model_include = response_model_include874 self.response_model_exclude = response_model_exclude875 self.response_model_by_alias = response_model_by_alias876 self.response_model_exclude_unset = response_model_exclude_unset877 self.response_model_exclude_defaults = response_model_exclude_defaults878 self.response_model_exclude_none = response_model_exclude_none879 self.include_in_schema = include_in_schema880 self.response_class = response_class881 self.dependency_overrides_provider = dependency_overrides_provider882 self.callbacks = callbacks883 self.openapi_extra = openapi_extra884 self.generate_unique_id_function = generate_unique_id_function885 self.strict_content_type = strict_content_type886 self.tags = tags or []887 self.responses = responses or {}888 self.name = get_name(endpoint) if name is None else name889 self.path_regex, self.path_format, self.param_convertors = compile_path(path)890 if methods is None:891 methods = ["GET"]892 self.methods: set[str] = {method.upper() for method in methods}893 if isinstance(generate_unique_id_function, DefaultPlaceholder):894 current_generate_unique_id: Callable[[APIRoute], str] = (895 generate_unique_id_function.value896 )897 else:898 current_generate_unique_id = generate_unique_id_function899 self.unique_id = self.operation_id or current_generate_unique_id(self)900 # normalize enums e.g. http.HTTPStatus901 if isinstance(status_code, IntEnum):902 status_code = int(status_code)903 self.status_code = status_code904 if self.response_model:905 assert is_body_allowed_for_status_code(status_code), (906 f"Status code {status_code} must not have a response body"907 )908 response_name = "Response_" + self.unique_id909 self.response_field = create_model_field(910 name=response_name,911 type_=self.response_model,912 mode="serialization",913 )914 else:915 self.response_field = None # type: ignore[assignment]916 if self.stream_item_type:917 stream_item_name = "StreamItem_" + self.unique_id918 self.stream_item_field: ModelField | None = create_model_field(919 name=stream_item_name,920 type_=self.stream_item_type,921 mode="serialization",922 )923 else:924 self.stream_item_field = None925 self.dependencies = list(dependencies or [])926 self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")927 # if a "form feed" character (page break) is found in the description text,928 # truncate description text to the content preceding the first "form feed"929 self.description = self.description.split("\f")[0].strip()930 response_fields = {}931 for additional_status_code, response in self.responses.items():932 assert isinstance(response, dict), "An additional response must be a dict"933 model = response.get("model")934 if model:935 assert is_body_allowed_for_status_code(additional_status_code), (936 f"Status code {additional_status_code} must not have a response body"937 )938 response_name = f"Response_{additional_status_code}_{self.unique_id}"939 response_field = create_model_field(940 name=response_name, type_=model, mode="serialization"941 )942 response_fields[additional_status_code] = response_field943 if response_fields:944 self.response_fields: dict[int | str, ModelField] = response_fields945 else:946 self.response_fields = {}947948 assert callable(endpoint), "An endpoint must be a callable"949 self.dependant = get_dependant(950 path=self.path_format, call=self.endpoint, scope="function"951 )952 for depends in self.dependencies[::-1]:953 self.dependant.dependencies.insert(954 0,955 get_parameterless_sub_dependant(depends=depends, path=self.path_format),956 )957 self._flat_dependant = get_flat_dependant(self.dependant)958 self._embed_body_fields = _should_embed_body_fields(959 self._flat_dependant.body_params960 )961 self.body_field = get_body_field(962 flat_dependant=self._flat_dependant,963 name=self.unique_id,964 embed_body_fields=self._embed_body_fields,965 )966 # Detect generator endpoints that should stream as JSONL or SSE967 is_generator = (968 self.dependant.is_async_gen_callable or self.dependant.is_gen_callable969 )970 self.is_sse_stream = is_generator and lenient_issubclass(971 response_class, EventSourceResponse972 )973 self.is_json_stream = is_generator and isinstance(974 response_class, DefaultPlaceholder975 )976 self.app = request_response(self.get_route_handler())977978 def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:979 return get_request_handler(980 dependant=self.dependant,981 body_field=self.body_field,982 status_code=self.status_code,983 response_class=self.response_class,984 response_field=self.response_field,985 response_model_include=self.response_model_include,986 response_model_exclude=self.response_model_exclude,987 response_model_by_alias=self.response_model_by_alias,988 response_model_exclude_unset=self.response_model_exclude_unset,989 response_model_exclude_defaults=self.response_model_exclude_defaults,990 response_model_exclude_none=self.response_model_exclude_none,991 dependency_overrides_provider=self.dependency_overrides_provider,992 embed_body_fields=self._embed_body_fields,993 strict_content_type=self.strict_content_type,994 stream_item_field=self.stream_item_field,995 is_json_stream=self.is_json_stream,996 )997998 def matches(self, scope: Scope) -> tuple[Match, Scope]:999 match, child_scope = super().matches(scope)1000 if match != Match.NONE:1001 child_scope["route"] = self1002 return match, child_scope100310041005class APIRouter(routing.Router):1006 """1007 `APIRouter` class, used to group *path operations*, for example to structure1008 an app in multiple files. It would then be included in the `FastAPI` app, or1009 in another `APIRouter` (ultimately included in the app).10101011 Read more about it in the1012 [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/).10131014 ## Example10151016 ```python1017 from fastapi import APIRouter, FastAPI10181019 app = FastAPI()1020 router = APIRouter()102110221023 @router.get("/users/", tags=["users"])1024 async def read_users():1025 return [{"username": "Rick"}, {"username": "Morty"}]102610271028 app.include_router(router)1029 ```1030 """10311032 def __init__(1033 self,1034 *,1035 prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "",1036 tags: Annotated[1037 list[str | Enum] | None,1038 Doc(1039 """1040 A list of tags to be applied to all the *path operations* in this1041 router.10421043 It will be added to the generated OpenAPI (e.g. visible at `/docs`).10441045 Read more about it in the1046 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1047 """1048 ),1049 ] = None,1050 dependencies: Annotated[1051 Sequence[params.Depends] | None,1052 Doc(1053 """1054 A list of dependencies (using `Depends()`) to be applied to all the1055 *path operations* in this router.10561057 Read more about it in the1058 [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).1059 """1060 ),1061 ] = None,1062 default_response_class: Annotated[1063 type[Response],1064 Doc(1065 """1066 The default response class to be used.10671068 Read more in the1069 [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class).1070 """1071 ),1072 ] = Default(JSONResponse),1073 responses: Annotated[1074 dict[int | str, dict[str, Any]] | None,1075 Doc(1076 """1077 Additional responses to be shown in OpenAPI.10781079 It will be added to the generated OpenAPI (e.g. visible at `/docs`).10801081 Read more about it in the1082 [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/).10831084 And in the1085 [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).1086 """1087 ),1088 ] = None,1089 callbacks: Annotated[1090 list[BaseRoute] | None,1091 Doc(1092 """1093 OpenAPI callbacks that should apply to all *path operations* in this1094 router.10951096 It will be added to the generated OpenAPI (e.g. visible at `/docs`).10971098 Read more about it in the1099 [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/).1100 """1101 ),1102 ] = None,1103 routes: Annotated[1104 list[BaseRoute] | None,1105 Doc(1106 """1107 **Note**: you probably shouldn't use this parameter, it is inherited1108 from Starlette and supported for compatibility.11091110 ---11111112 A list of routes to serve incoming HTTP and WebSocket requests.1113 """1114 ),1115 deprecated(1116 """1117 You normally wouldn't use this parameter with FastAPI, it is inherited1118 from Starlette and supported for compatibility.11191120 In FastAPI, you normally would use the *path operation methods*,1121 like `router.get()`, `router.post()`, etc.1122 """1123 ),1124 ] = None,1125 redirect_slashes: Annotated[1126 bool,1127 Doc(1128 """1129 Whether to detect and redirect slashes in URLs when the client doesn't1130 use the same format.1131 """1132 ),1133 ] = True,1134 default: Annotated[1135 ASGIApp | None,1136 Doc(1137 """1138 Default function handler for this router. Used to handle1139 404 Not Found errors.1140 """1141 ),1142 ] = None,1143 dependency_overrides_provider: Annotated[1144 Any | None,1145 Doc(1146 """1147 Only used internally by FastAPI to handle dependency overrides.11481149 You shouldn't need to use it. It normally points to the `FastAPI` app1150 object.1151 """1152 ),1153 ] = None,1154 route_class: Annotated[1155 type[APIRoute],1156 Doc(1157 """1158 Custom route (*path operation*) class to be used by this router.11591160 Read more about it in the1161 [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router).1162 """1163 ),1164 ] = APIRoute,1165 on_startup: Annotated[1166 Sequence[Callable[[], Any]] | None,1167 Doc(1168 """1169 A list of startup event handler functions.11701171 You should instead use the `lifespan` handlers.11721173 Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).1174 """1175 ),1176 ] = None,1177 on_shutdown: Annotated[1178 Sequence[Callable[[], Any]] | None,1179 Doc(1180 """1181 A list of shutdown event handler functions.11821183 You should instead use the `lifespan` handlers.11841185 Read more in the1186 [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).1187 """1188 ),1189 ] = None,1190 # the generic to Lifespan[AppType] is the type of the top level application1191 # which the router cannot know statically, so we use typing.Any1192 lifespan: Annotated[1193 Lifespan[Any] | None,1194 Doc(1195 """1196 A `Lifespan` context manager handler. This replaces `startup` and1197 `shutdown` functions with a single context manager.11981199 Read more in the1200 [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).1201 """1202 ),1203 ] = None,1204 deprecated: Annotated[1205 bool | None,1206 Doc(1207 """1208 Mark all *path operations* in this router as deprecated.12091210 It will be added to the generated OpenAPI (e.g. visible at `/docs`).12111212 Read more about it in the1213 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1214 """1215 ),1216 ] = None,1217 include_in_schema: Annotated[1218 bool,1219 Doc(1220 """1221 To include (or not) all the *path operations* in this router in the1222 generated OpenAPI.12231224 This affects the generated OpenAPI (e.g. visible at `/docs`).12251226 Read more about it in the1227 [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-parameters-from-openapi).1228 """1229 ),1230 ] = True,1231 generate_unique_id_function: Annotated[1232 Callable[[APIRoute], str],1233 Doc(1234 """1235 Customize the function used to generate unique IDs for the *path1236 operations* shown in the generated OpenAPI.12371238 This is particularly useful when automatically generating clients or1239 SDKs for your API.12401241 Read more about it in the1242 [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function).1243 """1244 ),1245 ] = Default(generate_unique_id),1246 strict_content_type: Annotated[1247 bool,1248 Doc(1249 """1250 Enable strict checking for request Content-Type headers.12511252 When `True` (the default), requests with a body that do not include1253 a `Content-Type` header will **not** be parsed as JSON.12541255 This prevents potential cross-site request forgery (CSRF) attacks1256 that exploit the browser's ability to send requests without a1257 Content-Type header, bypassing CORS preflight checks. In particular1258 applicable for apps that need to be run locally (in localhost).12591260 When `False`, requests without a `Content-Type` header will have1261 their body parsed as JSON, which maintains compatibility with1262 certain clients that don't send `Content-Type` headers.12631264 Read more about it in the1265 [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).1266 """1267 ),1268 ] = Default(True),1269 ) -> None:1270 # Determine the lifespan context to use1271 if lifespan is None:1272 # Use the default lifespan that runs on_startup/on_shutdown handlers1273 lifespan_context: Lifespan[Any] = _DefaultLifespan(self)1274 elif inspect.isasyncgenfunction(lifespan):1275 lifespan_context = asynccontextmanager(lifespan)1276 elif inspect.isgeneratorfunction(lifespan):1277 lifespan_context = _wrap_gen_lifespan_context(lifespan)1278 else:1279 lifespan_context = lifespan1280 self.lifespan_context = lifespan_context12811282 super().__init__(1283 routes=routes,1284 redirect_slashes=redirect_slashes,1285 default=default,1286 lifespan=lifespan_context,1287 )1288 if prefix:1289 assert prefix.startswith("/"), "A path prefix must start with '/'"1290 assert not prefix.endswith("/"), (1291 "A path prefix must not end with '/', as the routes will start with '/'"1292 )12931294 # Handle on_startup/on_shutdown locally since Starlette removed support1295 # Ref: https://github.com/Kludex/starlette/pull/31171296 # TODO: deprecate this once the lifespan (or alternative) interface is improved1297 self.on_startup: list[Callable[[], Any]] = (1298 [] if on_startup is None else list(on_startup)1299 )1300 self.on_shutdown: list[Callable[[], Any]] = (1301 [] if on_shutdown is None else list(on_shutdown)1302 )13031304 self.prefix = prefix1305 self.tags: list[str | Enum] = tags or []1306 self.dependencies = list(dependencies or [])1307 self.deprecated = deprecated1308 self.include_in_schema = include_in_schema1309 self.responses = responses or {}1310 self.callbacks = callbacks or []1311 self.dependency_overrides_provider = dependency_overrides_provider1312 self.route_class = route_class1313 self.default_response_class = default_response_class1314 self.generate_unique_id_function = generate_unique_id_function1315 self.strict_content_type = strict_content_type13161317 def route(1318 self,1319 path: str,1320 methods: Collection[str] | None = None,1321 name: str | None = None,1322 include_in_schema: bool = True,1323 ) -> Callable[[DecoratedCallable], DecoratedCallable]:1324 def decorator(func: DecoratedCallable) -> DecoratedCallable:1325 self.add_route(1326 path,1327 func,1328 methods=methods,1329 name=name,1330 include_in_schema=include_in_schema,1331 )1332 return func13331334 return decorator13351336 def add_api_route(1337 self,1338 path: str,1339 endpoint: Callable[..., Any],1340 *,1341 response_model: Any = Default(None),1342 status_code: int | None = None,1343 tags: list[str | Enum] | None = None,1344 dependencies: Sequence[params.Depends] | None = None,1345 summary: str | None = None,1346 description: str | None = None,1347 response_description: str = "Successful Response",1348 responses: dict[int | str, dict[str, Any]] | None = None,1349 deprecated: bool | None = None,1350 methods: set[str] | list[str] | None = None,1351 operation_id: str | None = None,1352 response_model_include: IncEx | None = None,1353 response_model_exclude: IncEx | None = None,1354 response_model_by_alias: bool = True,1355 response_model_exclude_unset: bool = False,1356 response_model_exclude_defaults: bool = False,1357 response_model_exclude_none: bool = False,1358 include_in_schema: bool = True,1359 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),1360 name: str | None = None,1361 route_class_override: type[APIRoute] | None = None,1362 callbacks: list[BaseRoute] | None = None,1363 openapi_extra: dict[str, Any] | None = None,1364 generate_unique_id_function: Callable[[APIRoute], str]1365 | DefaultPlaceholder = Default(generate_unique_id),1366 strict_content_type: bool | DefaultPlaceholder = Default(True),1367 ) -> None:1368 route_class = route_class_override or self.route_class1369 responses = responses or {}1370 combined_responses = {**self.responses, **responses}1371 current_response_class = get_value_or_default(1372 response_class, self.default_response_class1373 )1374 current_tags = self.tags.copy()1375 if tags:1376 current_tags.extend(tags)1377 current_dependencies = self.dependencies.copy()1378 if dependencies:1379 current_dependencies.extend(dependencies)1380 current_callbacks = self.callbacks.copy()1381 if callbacks:1382 current_callbacks.extend(callbacks)1383 current_generate_unique_id = get_value_or_default(1384 generate_unique_id_function, self.generate_unique_id_function1385 )1386 route = route_class(1387 self.prefix + path,1388 endpoint=endpoint,1389 response_model=response_model,1390 status_code=status_code,1391 tags=current_tags,1392 dependencies=current_dependencies,1393 summary=summary,1394 description=description,1395 response_description=response_description,1396 responses=combined_responses,1397 deprecated=deprecated or self.deprecated,1398 methods=methods,1399 operation_id=operation_id,1400 response_model_include=response_model_include,1401 response_model_exclude=response_model_exclude,1402 response_model_by_alias=response_model_by_alias,1403 response_model_exclude_unset=response_model_exclude_unset,1404 response_model_exclude_defaults=response_model_exclude_defaults,1405 response_model_exclude_none=response_model_exclude_none,1406 include_in_schema=include_in_schema and self.include_in_schema,1407 response_class=current_response_class,1408 name=name,1409 dependency_overrides_provider=self.dependency_overrides_provider,1410 callbacks=current_callbacks,1411 openapi_extra=openapi_extra,1412 generate_unique_id_function=current_generate_unique_id,1413 strict_content_type=get_value_or_default(1414 strict_content_type, self.strict_content_type1415 ),1416 )1417 self.routes.append(route)14181419 def api_route(1420 self,1421 path: str,1422 *,1423 response_model: Any = Default(None),1424 status_code: int | None = None,1425 tags: list[str | Enum] | None = None,1426 dependencies: Sequence[params.Depends] | None = None,1427 summary: str | None = None,1428 description: str | None = None,1429 response_description: str = "Successful Response",1430 responses: dict[int | str, dict[str, Any]] | None = None,1431 deprecated: bool | None = None,1432 methods: list[str] | None = None,1433 operation_id: str | None = None,1434 response_model_include: IncEx | None = None,1435 response_model_exclude: IncEx | None = None,1436 response_model_by_alias: bool = True,1437 response_model_exclude_unset: bool = False,1438 response_model_exclude_defaults: bool = False,1439 response_model_exclude_none: bool = False,1440 include_in_schema: bool = True,1441 response_class: type[Response] = Default(JSONResponse),1442 name: str | None = None,1443 callbacks: list[BaseRoute] | None = None,1444 openapi_extra: dict[str, Any] | None = None,1445 generate_unique_id_function: Callable[[APIRoute], str] = Default(1446 generate_unique_id1447 ),1448 ) -> Callable[[DecoratedCallable], DecoratedCallable]:1449 def decorator(func: DecoratedCallable) -> DecoratedCallable:1450 self.add_api_route(1451 path,1452 func,1453 response_model=response_model,1454 status_code=status_code,1455 tags=tags,1456 dependencies=dependencies,1457 summary=summary,1458 description=description,1459 response_description=response_description,1460 responses=responses,1461 deprecated=deprecated,1462 methods=methods,1463 operation_id=operation_id,1464 response_model_include=response_model_include,1465 response_model_exclude=response_model_exclude,1466 response_model_by_alias=response_model_by_alias,1467 response_model_exclude_unset=response_model_exclude_unset,1468 response_model_exclude_defaults=response_model_exclude_defaults,1469 response_model_exclude_none=response_model_exclude_none,1470 include_in_schema=include_in_schema,1471 response_class=response_class,1472 name=name,1473 callbacks=callbacks,1474 openapi_extra=openapi_extra,1475 generate_unique_id_function=generate_unique_id_function,1476 )1477 return func14781479 return decorator14801481 def add_api_websocket_route(1482 self,1483 path: str,1484 endpoint: Callable[..., Any],1485 name: str | None = None,1486 *,1487 dependencies: Sequence[params.Depends] | None = None,1488 ) -> None:1489 current_dependencies = self.dependencies.copy()1490 if dependencies:1491 current_dependencies.extend(dependencies)14921493 route = APIWebSocketRoute(1494 self.prefix + path,1495 endpoint=endpoint,1496 name=name,1497 dependencies=current_dependencies,1498 dependency_overrides_provider=self.dependency_overrides_provider,1499 )1500 self.routes.append(route)15011502 def websocket(1503 self,1504 path: Annotated[1505 str,1506 Doc(1507 """1508 WebSocket path.1509 """1510 ),1511 ],1512 name: Annotated[1513 str | None,1514 Doc(1515 """1516 A name for the WebSocket. Only used internally.1517 """1518 ),1519 ] = None,1520 *,1521 dependencies: Annotated[1522 Sequence[params.Depends] | None,1523 Doc(1524 """1525 A list of dependencies (using `Depends()`) to be used for this1526 WebSocket.15271528 Read more about it in the1529 [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/).1530 """1531 ),1532 ] = None,1533 ) -> Callable[[DecoratedCallable], DecoratedCallable]:1534 """1535 Decorate a WebSocket function.15361537 Read more about it in the1538 [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/).15391540 **Example**15411542 ## Example15431544 ```python1545 from fastapi import APIRouter, FastAPI, WebSocket15461547 app = FastAPI()1548 router = APIRouter()15491550 @router.websocket("/ws")1551 async def websocket_endpoint(websocket: WebSocket):1552 await websocket.accept()1553 while True:1554 data = await websocket.receive_text()1555 await websocket.send_text(f"Message text was: {data}")15561557 app.include_router(router)1558 ```1559 """15601561 def decorator(func: DecoratedCallable) -> DecoratedCallable:1562 self.add_api_websocket_route(1563 path, func, name=name, dependencies=dependencies1564 )1565 return func15661567 return decorator15681569 def websocket_route(1570 self, path: str, name: str | None = None1571 ) -> Callable[[DecoratedCallable], DecoratedCallable]:1572 def decorator(func: DecoratedCallable) -> DecoratedCallable:1573 self.add_websocket_route(path, func, name=name)1574 return func15751576 return decorator15771578 def include_router(1579 self,1580 router: Annotated["APIRouter", Doc("The `APIRouter` to include.")],1581 *,1582 prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "",1583 tags: Annotated[1584 list[str | Enum] | None,1585 Doc(1586 """1587 A list of tags to be applied to all the *path operations* in this1588 router.15891590 It will be added to the generated OpenAPI (e.g. visible at `/docs`).15911592 Read more about it in the1593 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1594 """1595 ),1596 ] = None,1597 dependencies: Annotated[1598 Sequence[params.Depends] | None,1599 Doc(1600 """1601 A list of dependencies (using `Depends()`) to be applied to all the1602 *path operations* in this router.16031604 Read more about it in the1605 [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).1606 """1607 ),1608 ] = None,1609 default_response_class: Annotated[1610 type[Response],1611 Doc(1612 """1613 The default response class to be used.16141615 Read more in the1616 [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class).1617 """1618 ),1619 ] = Default(JSONResponse),1620 responses: Annotated[1621 dict[int | str, dict[str, Any]] | None,1622 Doc(1623 """1624 Additional responses to be shown in OpenAPI.16251626 It will be added to the generated OpenAPI (e.g. visible at `/docs`).16271628 Read more about it in the1629 [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/).16301631 And in the1632 [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).1633 """1634 ),1635 ] = None,1636 callbacks: Annotated[1637 list[BaseRoute] | None,1638 Doc(1639 """1640 OpenAPI callbacks that should apply to all *path operations* in this1641 router.16421643 It will be added to the generated OpenAPI (e.g. visible at `/docs`).16441645 Read more about it in the1646 [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/).1647 """1648 ),1649 ] = None,1650 deprecated: Annotated[1651 bool | None,1652 Doc(1653 """1654 Mark all *path operations* in this router as deprecated.16551656 It will be added to the generated OpenAPI (e.g. visible at `/docs`).16571658 Read more about it in the1659 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1660 """1661 ),1662 ] = None,1663 include_in_schema: Annotated[1664 bool,1665 Doc(1666 """1667 Include (or not) all the *path operations* in this router in the1668 generated OpenAPI schema.16691670 This affects the generated OpenAPI (e.g. visible at `/docs`).1671 """1672 ),1673 ] = True,1674 generate_unique_id_function: Annotated[1675 Callable[[APIRoute], str],1676 Doc(1677 """1678 Customize the function used to generate unique IDs for the *path1679 operations* shown in the generated OpenAPI.16801681 This is particularly useful when automatically generating clients or1682 SDKs for your API.16831684 Read more about it in the1685 [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function).1686 """1687 ),1688 ] = Default(generate_unique_id),1689 ) -> None:1690 """1691 Include another `APIRouter` in the same current `APIRouter`.16921693 Read more about it in the1694 [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/).16951696 ## Example16971698 ```python1699 from fastapi import APIRouter, FastAPI17001701 app = FastAPI()1702 internal_router = APIRouter()1703 users_router = APIRouter()17041705 @users_router.get("/users/")1706 def read_users():1707 return [{"name": "Rick"}, {"name": "Morty"}]17081709 internal_router.include_router(users_router)1710 app.include_router(internal_router)1711 ```1712 """1713 assert self is not router, (1714 "Cannot include the same APIRouter instance into itself. "1715 "Did you mean to include a different router?"1716 )1717 if prefix:1718 assert prefix.startswith("/"), "A path prefix must start with '/'"1719 assert not prefix.endswith("/"), (1720 "A path prefix must not end with '/', as the routes will start with '/'"1721 )1722 else:1723 for r in router.routes:1724 path = getattr(r, "path") # noqa: B0091725 name = getattr(r, "name", "unknown")1726 if path is not None and not path:1727 raise FastAPIError(1728 f"Prefix and path cannot be both empty (path operation: {name})"1729 )1730 if responses is None:1731 responses = {}1732 for route in router.routes:1733 if isinstance(route, APIRoute):1734 combined_responses = {**responses, **route.responses}1735 use_response_class = get_value_or_default(1736 route.response_class,1737 router.default_response_class,1738 default_response_class,1739 self.default_response_class,1740 )1741 current_tags = []1742 if tags:1743 current_tags.extend(tags)1744 if route.tags:1745 current_tags.extend(route.tags)1746 current_dependencies: list[params.Depends] = []1747 if dependencies:1748 current_dependencies.extend(dependencies)1749 if route.dependencies:1750 current_dependencies.extend(route.dependencies)1751 current_callbacks = []1752 if callbacks:1753 current_callbacks.extend(callbacks)1754 if route.callbacks:1755 current_callbacks.extend(route.callbacks)1756 current_generate_unique_id = get_value_or_default(1757 route.generate_unique_id_function,1758 router.generate_unique_id_function,1759 generate_unique_id_function,1760 self.generate_unique_id_function,1761 )1762 self.add_api_route(1763 prefix + route.path,1764 route.endpoint,1765 response_model=route.response_model,1766 status_code=route.status_code,1767 tags=current_tags,1768 dependencies=current_dependencies,1769 summary=route.summary,1770 description=route.description,1771 response_description=route.response_description,1772 responses=combined_responses,1773 deprecated=route.deprecated or deprecated or self.deprecated,1774 methods=route.methods,1775 operation_id=route.operation_id,1776 response_model_include=route.response_model_include,1777 response_model_exclude=route.response_model_exclude,1778 response_model_by_alias=route.response_model_by_alias,1779 response_model_exclude_unset=route.response_model_exclude_unset,1780 response_model_exclude_defaults=route.response_model_exclude_defaults,1781 response_model_exclude_none=route.response_model_exclude_none,1782 include_in_schema=route.include_in_schema1783 and self.include_in_schema1784 and include_in_schema,1785 response_class=use_response_class,1786 name=route.name,1787 route_class_override=type(route),1788 callbacks=current_callbacks,1789 openapi_extra=route.openapi_extra,1790 generate_unique_id_function=current_generate_unique_id,1791 strict_content_type=get_value_or_default(1792 route.strict_content_type,1793 router.strict_content_type,1794 self.strict_content_type,1795 ),1796 )1797 elif isinstance(route, routing.Route):1798 methods = list(route.methods or [])1799 self.add_route(1800 prefix + route.path,1801 route.endpoint,1802 methods=methods,1803 include_in_schema=route.include_in_schema,1804 name=route.name,1805 )1806 elif isinstance(route, APIWebSocketRoute):1807 current_dependencies = []1808 if dependencies:1809 current_dependencies.extend(dependencies)1810 if route.dependencies:1811 current_dependencies.extend(route.dependencies)1812 self.add_api_websocket_route(1813 prefix + route.path,1814 route.endpoint,1815 dependencies=current_dependencies,1816 name=route.name,1817 )1818 elif isinstance(route, routing.WebSocketRoute):1819 self.add_websocket_route(1820 prefix + route.path, route.endpoint, name=route.name1821 )1822 for handler in router.on_startup:1823 self.add_event_handler("startup", handler)1824 for handler in router.on_shutdown:1825 self.add_event_handler("shutdown", handler)1826 self.lifespan_context = _merge_lifespan_context(1827 self.lifespan_context,1828 router.lifespan_context,1829 )18301831 def get(1832 self,1833 path: Annotated[1834 str,1835 Doc(1836 """1837 The URL path to be used for this *path operation*.18381839 For example, in `http://example.com/items`, the path is `/items`.1840 """1841 ),1842 ],1843 *,1844 response_model: Annotated[1845 Any,1846 Doc(1847 """1848 The type to use for the response.18491850 It could be any valid Pydantic *field* type. So, it doesn't have to1851 be a Pydantic model, it could be other things, like a `list`, `dict`,1852 etc.18531854 It will be used for:18551856 * Documentation: the generated OpenAPI (and the UI at `/docs`) will1857 show it as the response (JSON Schema).1858 * Serialization: you could return an arbitrary object and the1859 `response_model` would be used to serialize that object into the1860 corresponding JSON.1861 * Filtering: the JSON sent to the client will only contain the data1862 (fields) defined in the `response_model`. If you returned an object1863 that contains an attribute `password` but the `response_model` does1864 not include that field, the JSON sent to the client would not have1865 that `password`.1866 * Validation: whatever you return will be serialized with the1867 `response_model`, converting any data as necessary to generate the1868 corresponding JSON. But if the data in the object returned is not1869 valid, that would mean a violation of the contract with the client,1870 so it's an error from the API developer. So, FastAPI will raise an1871 error and return a 500 error code (Internal Server Error).18721873 Read more about it in the1874 [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).1875 """1876 ),1877 ] = Default(None),1878 status_code: Annotated[1879 int | None,1880 Doc(1881 """1882 The default status code to be used for the response.18831884 You could override the status code by returning a response directly.18851886 Read more about it in the1887 [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/).1888 """1889 ),1890 ] = None,1891 tags: Annotated[1892 list[str | Enum] | None,1893 Doc(1894 """1895 A list of tags to be applied to the *path operation*.18961897 It will be added to the generated OpenAPI (e.g. visible at `/docs`).18981899 Read more about it in the1900 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags).1901 """1902 ),1903 ] = None,1904 dependencies: Annotated[1905 Sequence[params.Depends] | None,1906 Doc(1907 """1908 A list of dependencies (using `Depends()`) to be applied to the1909 *path operation*.19101911 Read more about it in the1912 [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).1913 """1914 ),1915 ] = None,1916 summary: Annotated[1917 str | None,1918 Doc(1919 """1920 A summary for the *path operation*.19211922 It will be added to the generated OpenAPI (e.g. visible at `/docs`).19231924 Read more about it in the1925 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1926 """1927 ),1928 ] = None,1929 description: Annotated[1930 str | None,1931 Doc(1932 """1933 A description for the *path operation*.19341935 If not provided, it will be extracted automatically from the docstring1936 of the *path operation function*.19371938 It can contain Markdown.19391940 It will be added to the generated OpenAPI (e.g. visible at `/docs`).19411942 Read more about it in the1943 [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).1944 """1945 ),1946 ] = None,1947 response_description: Annotated[1948 str,1949 Doc(1950 """1951 The description for the default response.19521953 It will be added to the generated OpenAPI (e.g. visible at `/docs`).1954 """1955 ),1956 ] = "Successful Response",1957 responses: Annotated[1958 dict[int | str, dict[str, Any]] | None,1959 Doc(1960 """1961 Additional responses that could be returned by this *path operation*.19621963 It will be added to the generated OpenAPI (e.g. visible at `/docs`).1964 """1965 ),1966 ] = None,1967 deprecated: Annotated[1968 bool | None,1969 Doc(1970 """1971 Mark this *path operation* as deprecated.19721973 It will be added to the generated OpenAPI (e.g. visible at `/docs`).1974 """1975 ),1976 ] = None,1977 operation_id: Annotated[1978 str | None,1979 Doc(1980 """1981 Custom operation ID to be used by this *path operation*.19821983 By default, it is generated automatically.19841985 If you provide a custom operation ID, you need to make sure it is1986 unique for the whole API.19871988 You can customize the1989 operation ID generation with the parameter1990 `generate_unique_id_function` in the `FastAPI` class.19911992 Read more about it in the1993 [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function).1994 """1995 ),1996 ] = None,1997 response_model_include: Annotated[1998 IncEx | None,1999 Doc(2000 """
Findings
✓ No findings reported for this file.