Ensure functions have docstrings for documentation
def get_openapi_operation_request_body(
1import copy2import http.client3import inspect4import warnings5from collections.abc import Sequence6from dataclasses import dataclass, field7from typing import Any, Literal, cast89from fastapi import routing10from fastapi._compat import (11 ModelField,12 get_definitions,13 get_flat_models_from_fields,14 get_model_name_map,15 get_schema_from_model_field,16 lenient_issubclass,17)18from fastapi.datastructures import DefaultPlaceholder, _Unset19from fastapi.dependencies.models import (20 Dependant,21 _get_cache_key,22 _get_oauth_scopes,23 _get_security_scheme,24 _is_security_scheme,25 _UsesScopesCache,26)27from fastapi.dependencies.utils import (28 _get_flat_fields_from_params,29 get_flat_params,30 get_validation_alias,31)32from fastapi.encoders import jsonable_encoder33from fastapi.exceptions import FastAPIDeprecationWarning34from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX35from fastapi.openapi.models import OpenAPI36from fastapi.params import Body, ParamTypes37from fastapi.responses import Response38from fastapi.sse import _SSE_EVENT_SCHEMA39from fastapi.types import DependencyCacheKey, ModelNameMap40from fastapi.utils import (41 deep_dict_update,42 generate_operation_id_for_path,43 is_body_allowed_for_status_code,44)45from pydantic import BaseModel46from starlette.responses import JSONResponse47from starlette.routing import BaseRoute4849validation_error_definition = {50 "title": "ValidationError",51 "type": "object",52 "properties": {53 "loc": {54 "title": "Location",55 "type": "array",56 "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},57 },58 "msg": {"title": "Message", "type": "string"},59 "type": {"title": "Error Type", "type": "string"},60 "input": {"title": "Input"},61 "ctx": {"title": "Context", "type": "object"},62 },63 "required": ["loc", "msg", "type"],64}6566validation_error_response_definition = {67 "title": "HTTPValidationError",68 "type": "object",69 "properties": {70 "detail": {71 "title": "Detail",72 "type": "array",73 "items": {"$ref": REF_PREFIX + "ValidationError"},74 }75 },76}7778status_code_ranges: dict[str, str] = {79 "1XX": "Information",80 "2XX": "Success",81 "3XX": "Redirection",82 "4XX": "Client Error",83 "5XX": "Server Error",84 "DEFAULT": "Default Response",85}868788@dataclass89class _OpenAPIDependencyData:90 path_params: list[ModelField] = field(default_factory=list)91 query_params: list[ModelField] = field(default_factory=list)92 header_params: list[ModelField] = field(default_factory=list)93 cookie_params: list[ModelField] = field(default_factory=list)94 security_dependencies: list[tuple[Dependant, list[str]]] = field(95 default_factory=list96 )979899def _get_openapi_dependency_data(dependant: Dependant) -> _OpenAPIDependencyData:100 dependency_data = _OpenAPIDependencyData()101 visited: list[DependencyCacheKey] = []102 uses_scopes_cache: _UsesScopesCache = {}103 dependants: list[tuple[Dependant, list[str], bool]] = [(dependant, [], True)]104 while dependants:105 current_dependant, parent_oauth_scopes, is_root = dependants.pop()106 cache_key = _get_cache_key(107 dependant=current_dependant,108 uses_scopes_cache=uses_scopes_cache,109 )110 if cache_key in visited:111 continue112 visited.append(cache_key)113 dependency_data.path_params.extend(current_dependant.path_params)114 dependency_data.query_params.extend(current_dependant.query_params)115 dependency_data.header_params.extend(current_dependant.header_params)116 dependency_data.cookie_params.extend(current_dependant.cookie_params)117 oauth_scopes = parent_oauth_scopes.copy()118 for scope in _get_oauth_scopes(dependant=current_dependant):119 if scope not in oauth_scopes:120 oauth_scopes.append(scope)121 if not is_root and _is_security_scheme(dependant=current_dependant):122 dependency_data.security_dependencies.append(123 (current_dependant, oauth_scopes)124 )125 dependants.extend(126 (sub_dependant, oauth_scopes, False)127 for sub_dependant in reversed(current_dependant.dependencies)128 )129 return dependency_data130131132def _get_openapi_security_definitions(133 security_dependencies: list[tuple[Dependant, list[str]]],134) -> tuple[dict[str, Any], list[dict[str, Any]]]:135 security_definitions = {}136 # Use a dict to merge scopes for same security scheme137 operation_security_dict: dict[str, list[str]] = {}138 for security_dependency, oauth_scopes in security_dependencies:139 security_scheme = _get_security_scheme(dependant=security_dependency)140 security_definition = jsonable_encoder(141 security_scheme.model,142 by_alias=True,143 exclude_none=True,144 )145 security_name = security_scheme.scheme_name146 security_definitions[security_name] = security_definition147 # Merge scopes for the same security scheme148 if security_name not in operation_security_dict:149 operation_security_dict[security_name] = []150 for scope in oauth_scopes:151 if scope not in operation_security_dict[security_name]:152 operation_security_dict[security_name].append(scope)153 operation_security = [154 {name: scopes} for name, scopes in operation_security_dict.items()155 ]156 return security_definitions, operation_security157158159def _get_openapi_operation_parameters(160 *,161 dependency_data: _OpenAPIDependencyData,162 model_name_map: ModelNameMap,163 field_mapping: dict[164 tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]165 ],166 separate_input_output_schemas: bool = True,167) -> list[dict[str, Any]]:168 parameters = []169 path_params = _get_flat_fields_from_params(dependency_data.path_params)170 query_params = _get_flat_fields_from_params(dependency_data.query_params)171 header_params = _get_flat_fields_from_params(dependency_data.header_params)172 cookie_params = _get_flat_fields_from_params(dependency_data.cookie_params)173 parameter_groups = [174 (ParamTypes.path, path_params),175 (ParamTypes.query, query_params),176 (ParamTypes.header, header_params),177 (ParamTypes.cookie, cookie_params),178 ]179 default_convert_underscores = True180 if len(dependency_data.header_params) == 1:181 first_field = dependency_data.header_params[0]182 if lenient_issubclass(first_field.field_info.annotation, BaseModel):183 default_convert_underscores = getattr(184 first_field.field_info, "convert_underscores", True185 )186 for param_type, param_group in parameter_groups:187 for param in param_group:188 field_info = param.field_info189 # field_info = cast(Param, field_info)190 if not getattr(field_info, "include_in_schema", True):191 continue192 param_schema = get_schema_from_model_field(193 field=param,194 model_name_map=model_name_map,195 field_mapping=field_mapping,196 separate_input_output_schemas=separate_input_output_schemas,197 )198 name = get_validation_alias(param)199 convert_underscores = getattr(200 param.field_info,201 "convert_underscores",202 default_convert_underscores,203 )204 if (205 param_type == ParamTypes.header206 and name == param.name207 and convert_underscores208 ):209 name = param.name.replace("_", "-")210211 parameter = {212 "name": name,213 "in": param_type.value,214 "required": param.field_info.is_required(),215 "schema": param_schema,216 }217 if field_info.description:218 parameter["description"] = field_info.description219 openapi_examples = getattr(field_info, "openapi_examples", None)220 example = getattr(field_info, "example", None)221 if openapi_examples:222 parameter["examples"] = jsonable_encoder(openapi_examples)223 elif example is not _Unset:224 parameter["example"] = jsonable_encoder(example)225 if getattr(field_info, "deprecated", None):226 parameter["deprecated"] = True227 parameters.append(parameter)228 return parameters229230231def get_openapi_operation_request_body(232 *,233 body_field: ModelField | None,234 model_name_map: ModelNameMap,235 field_mapping: dict[236 tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]237 ],238 separate_input_output_schemas: bool = True,239) -> dict[str, Any] | None:240 if not body_field:241 return None242 assert isinstance(body_field, ModelField)243 body_schema = get_schema_from_model_field(244 field=body_field,245 model_name_map=model_name_map,246 field_mapping=field_mapping,247 separate_input_output_schemas=separate_input_output_schemas,248 )249 field_info = cast(Body, body_field.field_info)250 request_media_type = field_info.media_type251 required = body_field.field_info.is_required()252 request_body_oai: dict[str, Any] = {}253 if required:254 request_body_oai["required"] = required255 request_media_content: dict[str, Any] = {"schema": body_schema}256 if field_info.openapi_examples:257 request_media_content["examples"] = jsonable_encoder(258 field_info.openapi_examples259 )260 elif field_info.example is not _Unset:261 request_media_content["example"] = jsonable_encoder(field_info.example)262 request_body_oai["content"] = {request_media_type: request_media_content}263 return request_body_oai264265266def generate_operation_id(267 *, route: routing._APIRouteLike, method: str268) -> str: # pragma: nocover269 warnings.warn(270 message="fastapi.openapi.utils.generate_operation_id() was deprecated, "271 "it is not used internally, and will be removed soon",272 category=FastAPIDeprecationWarning,273 stacklevel=2,274 )275 if route.operation_id:276 return route.operation_id277 path: str = route.path_format278 return generate_operation_id_for_path(name=route.name, path=path, method=method)279280281def generate_operation_summary(*, route: routing._APIRouteLike, method: str) -> str:282 if route.summary:283 return route.summary284 return route.name.replace("_", " ").title()285286287def get_openapi_operation_metadata(288 *, route: routing._APIRouteLike, method: str, operation_ids: set[str]289) -> dict[str, Any]:290 operation: dict[str, Any] = {}291 if route.tags:292 operation["tags"] = route.tags293 operation["summary"] = generate_operation_summary(route=route, method=method)294 if route.description:295 operation["description"] = route.description296 operation_id = route.operation_id or route.unique_id297 if operation_id in operation_ids:298 endpoint_name = getattr(route.endpoint, "__name__", "<unnamed_endpoint>")299 message = f"Duplicate Operation ID {operation_id} for function {endpoint_name}"300 file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")301 if file_name:302 message += f" at {file_name}"303 warnings.warn(message, stacklevel=1)304 operation_ids.add(operation_id)305 operation["operationId"] = operation_id306 if route.deprecated:307 operation["deprecated"] = route.deprecated308 return operation309310311def get_openapi_path(312 *,313 route: routing._APIRouteLike,314 operation_ids: set[str],315 model_name_map: ModelNameMap,316 field_mapping: dict[317 tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]318 ],319 separate_input_output_schemas: bool = True,320) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:321 path = {}322 security_schemes: dict[str, Any] = {}323 definitions: dict[str, Any] = {}324 assert route.methods is not None, "Methods must be a list"325 if isinstance(route.response_class, DefaultPlaceholder):326 current_response_class: type[Response] = route.response_class.value327 else:328 current_response_class = route.response_class329 assert current_response_class, "A response class is needed to generate OpenAPI"330 route_response_media_type: str | None = current_response_class.media_type331 if route.include_in_schema:332 dependency_data = _get_openapi_dependency_data(route.dependant)333 all_route_params = [334 field335 for fields in (336 dependency_data.path_params,337 dependency_data.query_params,338 dependency_data.header_params,339 dependency_data.cookie_params,340 )341 for field in _get_flat_fields_from_params(fields)342 ]343 for method in route.methods:344 operation = get_openapi_operation_metadata(345 route=route, method=method, operation_ids=operation_ids346 )347 parameters: list[dict[str, Any]] = []348 security_definitions, operation_security = (349 _get_openapi_security_definitions(350 security_dependencies=dependency_data.security_dependencies351 )352 )353 if operation_security:354 operation.setdefault("security", []).extend(operation_security)355 if security_definitions:356 security_schemes.update(security_definitions)357 operation_parameters = _get_openapi_operation_parameters(358 dependency_data=dependency_data,359 model_name_map=model_name_map,360 field_mapping=field_mapping,361 separate_input_output_schemas=separate_input_output_schemas,362 )363 parameters.extend(operation_parameters)364 if parameters:365 all_parameters = {366 (param["in"], param["name"]): param for param in parameters367 }368 required_parameters = {369 (param["in"], param["name"]): param370 for param in parameters371 if param.get("required")372 }373 # Make sure required definitions of the same parameter take precedence374 # over non-required definitions375 all_parameters.update(required_parameters)376 operation["parameters"] = list(all_parameters.values())377 if method in METHODS_WITH_BODY:378 request_body_oai = get_openapi_operation_request_body(379 body_field=route.body_field,380 model_name_map=model_name_map,381 field_mapping=field_mapping,382 separate_input_output_schemas=separate_input_output_schemas,383 )384 if request_body_oai:385 operation["requestBody"] = request_body_oai386 if route.callbacks:387 callbacks = {}388 for callback in route.callbacks:389 if isinstance(callback, routing.APIRoute):390 (391 cb_path,392 cb_security_schemes,393 cb_definitions,394 ) = get_openapi_path(395 route=cast(routing._APIRouteLike, callback),396 operation_ids=operation_ids,397 model_name_map=model_name_map,398 field_mapping=field_mapping,399 separate_input_output_schemas=separate_input_output_schemas,400 )401 callbacks[callback.name] = {callback.path: cb_path}402 operation["callbacks"] = callbacks403 if route.status_code is not None:404 status_code = str(route.status_code)405 else:406 # It would probably make more sense for all response classes to have an407 # explicit default status_code, and to extract it from them, instead of408 # doing this inspection tricks, that would probably be in the future409 # TODO: probably make status_code a default class attribute for all410 # responses in Starlette411 response_signature = inspect.signature(current_response_class.__init__)412 status_code_param = response_signature.parameters.get("status_code")413 if status_code_param is not None:414 if isinstance(status_code_param.default, int):415 status_code = str(status_code_param.default)416 operation.setdefault("responses", {}).setdefault(status_code, {})[417 "description"418 ] = route.response_description419 if is_body_allowed_for_status_code(route.status_code):420 # Check for JSONL streaming (generator endpoints)421 if route.is_json_stream:422 jsonl_content: dict[str, Any] = {}423 if route.stream_item_field:424 item_schema = get_schema_from_model_field(425 field=route.stream_item_field,426 model_name_map=model_name_map,427 field_mapping=field_mapping,428 separate_input_output_schemas=separate_input_output_schemas,429 )430 jsonl_content["itemSchema"] = item_schema431 else:432 jsonl_content["itemSchema"] = {}433 operation.setdefault("responses", {}).setdefault(434 status_code, {}435 ).setdefault("content", {})["application/jsonl"] = jsonl_content436 elif route.is_sse_stream:437 sse_content: dict[str, Any] = {}438 item_schema = copy.deepcopy(_SSE_EVENT_SCHEMA)439 if route.stream_item_field:440 content_schema = get_schema_from_model_field(441 field=route.stream_item_field,442 model_name_map=model_name_map,443 field_mapping=field_mapping,444 separate_input_output_schemas=separate_input_output_schemas,445 )446 item_schema["required"] = ["data"]447 item_schema["properties"]["data"] = {448 "type": "string",449 "contentMediaType": "application/json",450 "contentSchema": content_schema,451 }452 sse_content["itemSchema"] = item_schema453 operation.setdefault("responses", {}).setdefault(454 status_code, {}455 ).setdefault("content", {})["text/event-stream"] = sse_content456 elif route_response_media_type:457 response_schema = {"type": "string"}458 if lenient_issubclass(current_response_class, JSONResponse):459 if route.response_field:460 response_schema = get_schema_from_model_field(461 field=route.response_field,462 model_name_map=model_name_map,463 field_mapping=field_mapping,464 separate_input_output_schemas=separate_input_output_schemas,465 )466 else:467 response_schema = {}468 operation.setdefault("responses", {}).setdefault(469 status_code, {}470 ).setdefault("content", {}).setdefault(471 route_response_media_type, {}472 )["schema"] = response_schema473 if route.responses:474 operation_responses = operation.setdefault("responses", {})475 for (476 additional_status_code,477 additional_response,478 ) in route.responses.items():479 process_response = copy.deepcopy(additional_response)480 process_response.pop("model", None)481 status_code_key = str(additional_status_code).upper()482 if status_code_key == "DEFAULT":483 status_code_key = "default"484 openapi_response = operation_responses.setdefault(485 status_code_key, {}486 )487 assert isinstance(process_response, dict), (488 "An additional response must be a dict"489 )490 field = route.response_fields.get(additional_status_code)491 additional_field_schema: dict[str, Any] | None = None492 if field:493 additional_field_schema = get_schema_from_model_field(494 field=field,495 model_name_map=model_name_map,496 field_mapping=field_mapping,497 separate_input_output_schemas=separate_input_output_schemas,498 )499 media_type = route_response_media_type or "application/json"500 additional_schema = (501 process_response.setdefault("content", {})502 .setdefault(media_type, {})503 .setdefault("schema", {})504 )505 deep_dict_update(additional_schema, additional_field_schema)506 status_text: str | None = status_code_ranges.get(507 str(additional_status_code).upper()508 ) or http.client.responses.get(int(additional_status_code))509 description = (510 process_response.get("description")511 or openapi_response.get("description")512 or status_text513 or "Additional Response"514 )515 deep_dict_update(openapi_response, process_response)516 openapi_response["description"] = description517 http422 = "422"518 if (all_route_params or route.body_field) and not any(519 status in operation["responses"]520 for status in [http422, "4XX", "default"]521 ):522 operation["responses"][http422] = {523 "description": "Validation Error",524 "content": {525 "application/json": {526 "schema": {"$ref": REF_PREFIX + "HTTPValidationError"}527 }528 },529 }530 if "ValidationError" not in definitions:531 definitions.update(532 {533 "ValidationError": validation_error_definition,534 "HTTPValidationError": validation_error_response_definition,535 }536 )537 if route.openapi_extra:538 deep_dict_update(operation, route.openapi_extra)539 path[method.lower()] = operation540 return path, security_schemes, definitions541542543def _get_api_route_for_openapi(544 route_context: routing.RouteContext,545) -> routing._APIRouteLike | None:546 if isinstance(route_context.original_route, routing.APIRoute):547 return cast(routing._APIRouteLike, route_context)548 return None549550551def get_fields_from_routes(552 routes: Sequence[BaseRoute | routing.RouteContext],553) -> list[ModelField]:554 body_fields_from_routes: list[ModelField] = []555 responses_from_routes: list[ModelField] = []556 request_fields_from_routes: list[ModelField] = []557 callback_flat_models: list[ModelField] = []558 for route_context in routing.iter_route_contexts(routes):559 api_route = _get_api_route_for_openapi(route_context)560 if api_route is None:561 continue562 if api_route.include_in_schema:563 if api_route.body_field:564 assert isinstance(api_route.body_field, ModelField), (565 "A request body must be a Pydantic Field"566 )567 body_fields_from_routes.append(api_route.body_field)568 if api_route.response_field:569 responses_from_routes.append(api_route.response_field)570 if api_route.response_fields:571 responses_from_routes.extend(api_route.response_fields.values())572 if api_route.stream_item_field:573 responses_from_routes.append(api_route.stream_item_field)574 if api_route.callbacks:575 callback_flat_models.extend(get_fields_from_routes(api_route.callbacks))576 params = get_flat_params(api_route.dependant)577 request_fields_from_routes.extend(params)578579 flat_models = callback_flat_models + list(580 body_fields_from_routes + responses_from_routes + request_fields_from_routes581 )582 return flat_models583584585def get_openapi(586 *,587 title: str,588 version: str,589 openapi_version: str = "3.1.0",590 summary: str | None = None,591 description: str | None = None,592 routes: Sequence[BaseRoute | routing.RouteContext],593 webhooks: Sequence[BaseRoute | routing.RouteContext] | None = None,594 tags: list[dict[str, Any]] | None = None,595 servers: list[dict[str, str | Any]] | None = None,596 terms_of_service: str | None = None,597 contact: dict[str, str | Any] | None = None,598 license_info: dict[str, str | Any] | None = None,599 separate_input_output_schemas: bool = True,600 external_docs: dict[str, Any] | None = None,601) -> dict[str, Any]:602 info: dict[str, Any] = {"title": title, "version": version}603 if summary:604 info["summary"] = summary605 if description:606 info["description"] = description607 if terms_of_service:608 info["termsOfService"] = terms_of_service609 if contact:610 info["contact"] = contact611 if license_info:612 info["license"] = license_info613 output: dict[str, Any] = {"openapi": openapi_version, "info": info}614 if servers:615 output["servers"] = servers616 components: dict[str, dict[str, Any]] = {}617 paths: dict[str, dict[str, Any]] = {}618 webhook_paths: dict[str, dict[str, Any]] = {}619 operation_ids: set[str] = set()620 all_fields = get_fields_from_routes(list(routes) + list(webhooks or []))621 flat_models = get_flat_models_from_fields(all_fields, known_models=set())622 model_name_map = get_model_name_map(flat_models)623 field_mapping, definitions = get_definitions(624 fields=all_fields,625 model_name_map=model_name_map,626 separate_input_output_schemas=separate_input_output_schemas,627 )628 for route_context in routing.iter_route_contexts(routes):629 api_route = _get_api_route_for_openapi(route_context)630 if api_route is not None:631 result = get_openapi_path(632 route=api_route,633 operation_ids=operation_ids,634 model_name_map=model_name_map,635 field_mapping=field_mapping,636 separate_input_output_schemas=separate_input_output_schemas,637 )638 if result:639 path, security_schemes, path_definitions = result640 if path:641 paths.setdefault(api_route.path_format, {}).update(path)642 if security_schemes:643 components.setdefault("securitySchemes", {}).update(644 security_schemes645 )646 if path_definitions:647 definitions.update(path_definitions)648 for webhook_context in routing.iter_route_contexts(webhooks or []):649 api_webhook = _get_api_route_for_openapi(webhook_context)650 if api_webhook is not None:651 result = get_openapi_path(652 route=api_webhook,653 operation_ids=operation_ids,654 model_name_map=model_name_map,655 field_mapping=field_mapping,656 separate_input_output_schemas=separate_input_output_schemas,657 )658 if result:659 path, security_schemes, path_definitions = result660 if path:661 webhook_paths.setdefault(api_webhook.path_format, {}).update(path)662 if security_schemes:663 components.setdefault("securitySchemes", {}).update(664 security_schemes665 )666 if path_definitions:667 definitions.update(path_definitions)668 if definitions:669 components["schemas"] = {k: definitions[k] for k in sorted(definitions)}670 if components:671 output["components"] = components672 output["paths"] = paths673 if webhook_paths:674 output["webhooks"] = webhook_paths675 if tags:676 output["tags"] = tags677 if external_docs:678 output["externalDocs"] = external_docs679 return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore[no-any-return]
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.
Generated by searchcode.com · powered by code_get_file + code_get_findings