mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-14 13:36:21 +08:00
bd32fecaf6
* WIP * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
470 lines
19 KiB
Python
470 lines
19 KiB
Python
import http.client
|
|
import inspect
|
|
import warnings
|
|
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
|
|
|
from fastapi import routing
|
|
from fastapi._compat import (
|
|
GenerateJsonSchema,
|
|
ModelField,
|
|
Undefined,
|
|
get_compat_model_name_map,
|
|
get_definitions,
|
|
get_schema_from_model_field,
|
|
lenient_issubclass,
|
|
)
|
|
from fastapi.datastructures import DefaultPlaceholder
|
|
from fastapi.dependencies.models import Dependant
|
|
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
|
|
from fastapi.encoders import jsonable_encoder
|
|
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
|
from fastapi.openapi.models import OpenAPI
|
|
from fastapi.params import Body, Param
|
|
from fastapi.responses import Response
|
|
from fastapi.types import ModelNameMap
|
|
from fastapi.utils import (
|
|
deep_dict_update,
|
|
generate_operation_id_for_path,
|
|
is_body_allowed_for_status_code,
|
|
)
|
|
from starlette.responses import JSONResponse
|
|
from starlette.routing import BaseRoute
|
|
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
validation_error_definition = {
|
|
"title": "ValidationError",
|
|
"type": "object",
|
|
"properties": {
|
|
"loc": {
|
|
"title": "Location",
|
|
"type": "array",
|
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
|
},
|
|
"msg": {"title": "Message", "type": "string"},
|
|
"type": {"title": "Error Type", "type": "string"},
|
|
},
|
|
"required": ["loc", "msg", "type"],
|
|
}
|
|
|
|
validation_error_response_definition = {
|
|
"title": "HTTPValidationError",
|
|
"type": "object",
|
|
"properties": {
|
|
"detail": {
|
|
"title": "Detail",
|
|
"type": "array",
|
|
"items": {"$ref": REF_PREFIX + "ValidationError"},
|
|
}
|
|
},
|
|
}
|
|
|
|
status_code_ranges: Dict[str, str] = {
|
|
"1XX": "Information",
|
|
"2XX": "Success",
|
|
"3XX": "Redirection",
|
|
"4XX": "Client Error",
|
|
"5XX": "Server Error",
|
|
"DEFAULT": "Default Response",
|
|
}
|
|
|
|
|
|
def get_openapi_security_definitions(
|
|
flat_dependant: Dependant,
|
|
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
|
security_definitions = {}
|
|
operation_security = []
|
|
for security_requirement in flat_dependant.security_requirements:
|
|
security_definition = jsonable_encoder(
|
|
security_requirement.security_scheme.model,
|
|
by_alias=True,
|
|
exclude_none=True,
|
|
)
|
|
security_name = security_requirement.security_scheme.scheme_name
|
|
security_definitions[security_name] = security_definition
|
|
operation_security.append({security_name: security_requirement.scopes})
|
|
return security_definitions, operation_security
|
|
|
|
|
|
def get_openapi_operation_parameters(
|
|
*,
|
|
all_route_params: Sequence[ModelField],
|
|
schema_generator: GenerateJsonSchema,
|
|
model_name_map: ModelNameMap,
|
|
) -> List[Dict[str, Any]]:
|
|
parameters = []
|
|
for param in all_route_params:
|
|
field_info = param.field_info
|
|
field_info = cast(Param, field_info)
|
|
if not field_info.include_in_schema:
|
|
continue
|
|
param_schema = get_schema_from_model_field(
|
|
field=param,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
parameter = {
|
|
"name": param.alias,
|
|
"in": field_info.in_.value,
|
|
"required": param.required,
|
|
"schema": param_schema,
|
|
}
|
|
if field_info.description:
|
|
parameter["description"] = field_info.description
|
|
if field_info.examples:
|
|
parameter["examples"] = jsonable_encoder(field_info.examples)
|
|
elif field_info.example != Undefined:
|
|
parameter["example"] = jsonable_encoder(field_info.example)
|
|
if field_info.deprecated:
|
|
parameter["deprecated"] = field_info.deprecated
|
|
parameters.append(parameter)
|
|
return parameters
|
|
|
|
|
|
def get_openapi_operation_request_body(
|
|
*,
|
|
body_field: Optional[ModelField],
|
|
schema_generator: GenerateJsonSchema,
|
|
model_name_map: ModelNameMap,
|
|
) -> Optional[Dict[str, Any]]:
|
|
if not body_field:
|
|
return None
|
|
assert isinstance(body_field, ModelField)
|
|
body_schema = get_schema_from_model_field(
|
|
field=body_field,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
field_info = cast(Body, body_field.field_info)
|
|
request_media_type = field_info.media_type
|
|
required = body_field.required
|
|
request_body_oai: Dict[str, Any] = {}
|
|
if required:
|
|
request_body_oai["required"] = required
|
|
request_media_content: Dict[str, Any] = {"schema": body_schema}
|
|
if field_info.examples:
|
|
request_media_content["examples"] = jsonable_encoder(field_info.examples)
|
|
elif field_info.example != Undefined:
|
|
request_media_content["example"] = jsonable_encoder(field_info.example)
|
|
request_body_oai["content"] = {request_media_type: request_media_content}
|
|
return request_body_oai
|
|
|
|
|
|
def generate_operation_id(
|
|
*, route: routing.APIRoute, method: str
|
|
) -> str: # pragma: nocover
|
|
warnings.warn(
|
|
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
|
"it is not used internally, and will be removed soon",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
if route.operation_id:
|
|
return route.operation_id
|
|
path: str = route.path_format
|
|
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
|
|
|
|
|
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
|
if route.summary:
|
|
return route.summary
|
|
return route.name.replace("_", " ").title()
|
|
|
|
|
|
def get_openapi_operation_metadata(
|
|
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
|
|
) -> Dict[str, Any]:
|
|
operation: Dict[str, Any] = {}
|
|
if route.tags:
|
|
operation["tags"] = route.tags
|
|
operation["summary"] = generate_operation_summary(route=route, method=method)
|
|
if route.description:
|
|
operation["description"] = route.description
|
|
operation_id = route.operation_id or route.unique_id
|
|
if operation_id in operation_ids:
|
|
message = (
|
|
f"Duplicate Operation ID {operation_id} for function "
|
|
+ f"{route.endpoint.__name__}"
|
|
)
|
|
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
|
if file_name:
|
|
message += f" at {file_name}"
|
|
warnings.warn(message, stacklevel=1)
|
|
operation_ids.add(operation_id)
|
|
operation["operationId"] = operation_id
|
|
if route.deprecated:
|
|
operation["deprecated"] = route.deprecated
|
|
return operation
|
|
|
|
|
|
def get_openapi_path(
|
|
*,
|
|
route: routing.APIRoute,
|
|
operation_ids: Set[str],
|
|
schema_generator: GenerateJsonSchema,
|
|
model_name_map: ModelNameMap,
|
|
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
|
path = {}
|
|
security_schemes: Dict[str, Any] = {}
|
|
definitions: Dict[str, Any] = {}
|
|
assert route.methods is not None, "Methods must be a list"
|
|
if isinstance(route.response_class, DefaultPlaceholder):
|
|
current_response_class: Type[Response] = route.response_class.value
|
|
else:
|
|
current_response_class = route.response_class
|
|
assert current_response_class, "A response class is needed to generate OpenAPI"
|
|
route_response_media_type: Optional[str] = current_response_class.media_type
|
|
if route.include_in_schema:
|
|
for method in route.methods:
|
|
operation = get_openapi_operation_metadata(
|
|
route=route, method=method, operation_ids=operation_ids
|
|
)
|
|
parameters: List[Dict[str, Any]] = []
|
|
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
|
security_definitions, operation_security = get_openapi_security_definitions(
|
|
flat_dependant=flat_dependant
|
|
)
|
|
if operation_security:
|
|
operation.setdefault("security", []).extend(operation_security)
|
|
if security_definitions:
|
|
security_schemes.update(security_definitions)
|
|
all_route_params = get_flat_params(route.dependant)
|
|
operation_parameters = get_openapi_operation_parameters(
|
|
all_route_params=all_route_params,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
parameters.extend(operation_parameters)
|
|
if parameters:
|
|
all_parameters = {
|
|
(param["in"], param["name"]): param for param in parameters
|
|
}
|
|
required_parameters = {
|
|
(param["in"], param["name"]): param
|
|
for param in parameters
|
|
if param.get("required")
|
|
}
|
|
# Make sure required definitions of the same parameter take precedence
|
|
# over non-required definitions
|
|
all_parameters.update(required_parameters)
|
|
operation["parameters"] = list(all_parameters.values())
|
|
if method in METHODS_WITH_BODY:
|
|
request_body_oai = get_openapi_operation_request_body(
|
|
body_field=route.body_field,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
if request_body_oai:
|
|
operation["requestBody"] = request_body_oai
|
|
if route.callbacks:
|
|
callbacks = {}
|
|
for callback in route.callbacks:
|
|
if isinstance(callback, routing.APIRoute):
|
|
(
|
|
cb_path,
|
|
cb_security_schemes,
|
|
cb_definitions,
|
|
) = get_openapi_path(
|
|
route=callback,
|
|
operation_ids=operation_ids,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
callbacks[callback.name] = {callback.path: cb_path}
|
|
operation["callbacks"] = callbacks
|
|
if route.status_code is not None:
|
|
status_code = str(route.status_code)
|
|
else:
|
|
# It would probably make more sense for all response classes to have an
|
|
# explicit default status_code, and to extract it from them, instead of
|
|
# doing this inspection tricks, that would probably be in the future
|
|
# TODO: probably make status_code a default class attribute for all
|
|
# responses in Starlette
|
|
response_signature = inspect.signature(current_response_class.__init__)
|
|
status_code_param = response_signature.parameters.get("status_code")
|
|
if status_code_param is not None:
|
|
if isinstance(status_code_param.default, int):
|
|
status_code = str(status_code_param.default)
|
|
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
|
"description"
|
|
] = route.response_description
|
|
if route_response_media_type and is_body_allowed_for_status_code(
|
|
route.status_code
|
|
):
|
|
response_schema = {"type": "string"}
|
|
if lenient_issubclass(current_response_class, JSONResponse):
|
|
if route.response_field:
|
|
response_schema = get_schema_from_model_field(
|
|
field=route.response_field,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
else:
|
|
response_schema = {}
|
|
operation.setdefault("responses", {}).setdefault(
|
|
status_code, {}
|
|
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
|
"schema"
|
|
] = response_schema
|
|
if route.responses:
|
|
operation_responses = operation.setdefault("responses", {})
|
|
for (
|
|
additional_status_code,
|
|
additional_response,
|
|
) in route.responses.items():
|
|
process_response = additional_response.copy()
|
|
process_response.pop("model", None)
|
|
status_code_key = str(additional_status_code).upper()
|
|
if status_code_key == "DEFAULT":
|
|
status_code_key = "default"
|
|
openapi_response = operation_responses.setdefault(
|
|
status_code_key, {}
|
|
)
|
|
assert isinstance(
|
|
process_response, dict
|
|
), "An additional response must be a dict"
|
|
field = route.response_fields.get(additional_status_code)
|
|
additional_field_schema: Optional[Dict[str, Any]] = None
|
|
if field:
|
|
additional_field_schema = get_schema_from_model_field(
|
|
field=field,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
media_type = route_response_media_type or "application/json"
|
|
additional_schema = (
|
|
process_response.setdefault("content", {})
|
|
.setdefault(media_type, {})
|
|
.setdefault("schema", {})
|
|
)
|
|
deep_dict_update(additional_schema, additional_field_schema)
|
|
status_text: Optional[str] = status_code_ranges.get(
|
|
str(additional_status_code).upper()
|
|
) or http.client.responses.get(int(additional_status_code))
|
|
description = (
|
|
process_response.get("description")
|
|
or openapi_response.get("description")
|
|
or status_text
|
|
or "Additional Response"
|
|
)
|
|
deep_dict_update(openapi_response, process_response)
|
|
openapi_response["description"] = description
|
|
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
|
|
if (all_route_params or route.body_field) and not any(
|
|
status in operation["responses"]
|
|
for status in [http422, "4XX", "default"]
|
|
):
|
|
operation["responses"][http422] = {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
|
}
|
|
},
|
|
}
|
|
if "ValidationError" not in definitions:
|
|
definitions.update(
|
|
{
|
|
"ValidationError": validation_error_definition,
|
|
"HTTPValidationError": validation_error_response_definition,
|
|
}
|
|
)
|
|
if route.openapi_extra:
|
|
deep_dict_update(operation, route.openapi_extra)
|
|
path[method.lower()] = operation
|
|
return path, security_schemes, definitions
|
|
|
|
|
|
def get_fields_from_routes(
|
|
routes: Sequence[BaseRoute],
|
|
) -> List[ModelField]:
|
|
body_fields_from_routes: List[ModelField] = []
|
|
responses_from_routes: List[ModelField] = []
|
|
request_fields_from_routes: List[ModelField] = []
|
|
callback_flat_models: List[ModelField] = []
|
|
for route in routes:
|
|
if getattr(route, "include_in_schema", None) and isinstance(
|
|
route, routing.APIRoute
|
|
):
|
|
if route.body_field:
|
|
assert isinstance(
|
|
route.body_field, ModelField
|
|
), "A request body must be a Pydantic Field"
|
|
body_fields_from_routes.append(route.body_field)
|
|
if route.response_field:
|
|
responses_from_routes.append(route.response_field)
|
|
if route.response_fields:
|
|
responses_from_routes.extend(route.response_fields.values())
|
|
if route.callbacks:
|
|
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
|
params = get_flat_params(route.dependant)
|
|
request_fields_from_routes.extend(params)
|
|
|
|
flat_models = callback_flat_models + list(
|
|
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
|
)
|
|
return flat_models
|
|
|
|
|
|
def get_openapi(
|
|
*,
|
|
title: str,
|
|
version: str,
|
|
openapi_version: str = "3.0.2",
|
|
description: Optional[str] = None,
|
|
routes: Sequence[BaseRoute],
|
|
tags: Optional[List[Dict[str, Any]]] = None,
|
|
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
|
terms_of_service: Optional[str] = None,
|
|
contact: Optional[Dict[str, Union[str, Any]]] = None,
|
|
license_info: Optional[Dict[str, Union[str, Any]]] = None,
|
|
) -> Dict[str, Any]:
|
|
info: Dict[str, Any] = {"title": title, "version": version}
|
|
if description:
|
|
info["description"] = description
|
|
if terms_of_service:
|
|
info["termsOfService"] = terms_of_service
|
|
if contact:
|
|
info["contact"] = contact
|
|
if license_info:
|
|
info["license"] = license_info
|
|
output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
|
|
if servers:
|
|
output["servers"] = servers
|
|
components: Dict[str, Dict[str, Any]] = {}
|
|
paths: Dict[str, Dict[str, Any]] = {}
|
|
operation_ids: Set[str] = set()
|
|
all_fields = get_fields_from_routes(routes)
|
|
model_name_map = get_compat_model_name_map(all_fields)
|
|
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
|
definitions = get_definitions(
|
|
fields=all_fields,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
for route in routes:
|
|
if isinstance(route, routing.APIRoute):
|
|
result = get_openapi_path(
|
|
route=route,
|
|
operation_ids=operation_ids,
|
|
schema_generator=schema_generator,
|
|
model_name_map=model_name_map,
|
|
)
|
|
if result:
|
|
path, security_schemes, path_definitions = result
|
|
if path:
|
|
paths.setdefault(route.path_format, {}).update(path)
|
|
if security_schemes:
|
|
components.setdefault("securitySchemes", {}).update(
|
|
security_schemes
|
|
)
|
|
if path_definitions:
|
|
definitions.update(path_definitions)
|
|
if definitions:
|
|
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
|
if components:
|
|
output["components"] = components
|
|
output["paths"] = paths
|
|
if tags:
|
|
output["tags"] = tags
|
|
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
|