fastapi/openapi/utils.py PYTHON 680 lines View on github.com → Search inside
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]

Code quality findings 6

Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_openapi_operation_request_body(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def generate_operation_id(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def generate_operation_summary(*, route: routing._APIRouteLike, method: str) -> str:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_openapi_operation_metadata(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_openapi_path(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_fields_from_routes(

Get this view in your editor

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