main commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-16 16:30:25 +09:00
parent 91c7e04474
commit 537e7b363f
1146 changed files with 45926 additions and 77196 deletions

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, Optional
from fastapi.encoders import jsonable_encoder
from starlette.responses import HTMLResponse
from typing_extensions import Annotated, Doc
from typing_extensions import Annotated, Doc # type: ignore [attr-defined]
swagger_ui_default_parameters: Annotated[
Dict[str, Any],
@@ -53,7 +53,7 @@ def get_swagger_ui_html(
It is normally set to a CDN URL.
"""
),
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
swagger_css_url: Annotated[
str,
Doc(
@@ -63,7 +63,7 @@ def get_swagger_ui_html(
It is normally set to a CDN URL.
"""
),
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css",
swagger_favicon_url: Annotated[
str,
Doc(
@@ -188,7 +188,7 @@ def get_redoc_html(
It is normally set to a CDN URL.
"""
),
] = "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js",
] = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
redoc_favicon_url: Annotated[
str,
Doc(

View File

@@ -55,7 +55,11 @@ except ImportError: # pragma: no cover
return with_info_plain_validator_function(cls._validate)
class BaseModelWithConfig(BaseModel):
class Contact(BaseModel):
name: Optional[str] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
@@ -65,19 +69,21 @@ class BaseModelWithConfig(BaseModel):
extra = "allow"
class Contact(BaseModelWithConfig):
name: Optional[str] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
class License(BaseModelWithConfig):
class License(BaseModel):
name: str
identifier: Optional[str] = None
url: Optional[AnyUrl] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Info(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Info(BaseModel):
title: str
summary: Optional[str] = None
description: Optional[str] = None
@@ -86,18 +92,42 @@ class Info(BaseModelWithConfig):
license: Optional[License] = None
version: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class ServerVariable(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class ServerVariable(BaseModel):
enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
default: str
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Server(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Server(BaseModel):
url: Union[AnyUrl, str]
description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Reference(BaseModel):
ref: str = Field(alias="$ref")
@@ -108,26 +138,36 @@ class Discriminator(BaseModel):
mapping: Optional[Dict[str, str]] = None
class XML(BaseModelWithConfig):
class XML(BaseModel):
name: Optional[str] = None
namespace: Optional[str] = None
prefix: Optional[str] = None
attribute: Optional[bool] = None
wrapped: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class ExternalDocumentation(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class ExternalDocumentation(BaseModel):
description: Optional[str] = None
url: AnyUrl
if PYDANTIC_V2:
model_config = {"extra": "allow"}
# Ref JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation#name-type
SchemaType = Literal[
"array", "boolean", "integer", "null", "number", "object", "string"
]
else:
class Config:
extra = "allow"
class Schema(BaseModelWithConfig):
class Schema(BaseModel):
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu
# Core Vocabulary
schema_: Optional[str] = Field(default=None, alias="$schema")
@@ -151,7 +191,7 @@ class Schema(BaseModelWithConfig):
dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
prefixItems: Optional[List["SchemaOrBool"]] = None
# TODO: uncomment and remove below when deprecating Pydantic v1
# It generates a list of schemas for tuples, before prefixItems was available
# It generales a list of schemas for tuples, before prefixItems was available
# items: Optional["SchemaOrBool"] = None
items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
contains: Optional["SchemaOrBool"] = None
@@ -163,7 +203,7 @@ class Schema(BaseModelWithConfig):
unevaluatedProperties: Optional["SchemaOrBool"] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
# A Vocabulary for Structural Validation
type: Optional[Union[SchemaType, List[SchemaType]]] = None
type: Optional[str] = None
enum: Optional[List[Any]] = None
const: Optional[Any] = None
multipleOf: Optional[float] = Field(default=None, gt=0)
@@ -213,6 +253,14 @@ class Schema(BaseModelWithConfig):
),
] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
# A JSON Schema MUST be an object or a boolean.
@@ -241,22 +289,38 @@ class ParameterInType(Enum):
cookie = "cookie"
class Encoding(BaseModelWithConfig):
class Encoding(BaseModel):
contentType: Optional[str] = None
headers: Optional[Dict[str, Union["Header", Reference]]] = None
style: Optional[str] = None
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class MediaType(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class MediaType(BaseModel):
schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema")
example: Optional[Any] = None
examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class ParameterBase(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class ParameterBase(BaseModel):
description: Optional[str] = None
required: Optional[bool] = None
deprecated: Optional[bool] = None
@@ -270,6 +334,14 @@ class ParameterBase(BaseModelWithConfig):
# Serialization rules for more complex scenarios
content: Optional[Dict[str, MediaType]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Parameter(ParameterBase):
name: str
@@ -280,13 +352,21 @@ class Header(ParameterBase):
pass
class RequestBody(BaseModelWithConfig):
class RequestBody(BaseModel):
description: Optional[str] = None
content: Dict[str, MediaType]
required: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Link(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Link(BaseModel):
operationRef: Optional[str] = None
operationId: Optional[str] = None
parameters: Optional[Dict[str, Union[Any, str]]] = None
@@ -294,15 +374,31 @@ class Link(BaseModelWithConfig):
description: Optional[str] = None
server: Optional[Server] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Response(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Response(BaseModel):
description: str
headers: Optional[Dict[str, Union[Header, Reference]]] = None
content: Optional[Dict[str, MediaType]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Operation(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Operation(BaseModel):
tags: Optional[List[str]] = None
summary: Optional[str] = None
description: Optional[str] = None
@@ -317,8 +413,16 @@ class Operation(BaseModelWithConfig):
security: Optional[List[Dict[str, List[str]]]] = None
servers: Optional[List[Server]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class PathItem(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class PathItem(BaseModel):
ref: Optional[str] = Field(default=None, alias="$ref")
summary: Optional[str] = None
description: Optional[str] = None
@@ -333,6 +437,14 @@ class PathItem(BaseModelWithConfig):
servers: Optional[List[Server]] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class SecuritySchemeType(Enum):
apiKey = "apiKey"
@@ -341,10 +453,18 @@ class SecuritySchemeType(Enum):
openIdConnect = "openIdConnect"
class SecurityBase(BaseModelWithConfig):
class SecurityBase(BaseModel):
type_: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class APIKeyIn(Enum):
query = "query"
@@ -368,10 +488,18 @@ class HTTPBearer(HTTPBase):
bearerFormat: Optional[str] = None
class OAuthFlow(BaseModelWithConfig):
class OAuthFlow(BaseModel):
refreshUrl: Optional[str] = None
scopes: Dict[str, str] = {}
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class OAuthFlowImplicit(OAuthFlow):
authorizationUrl: str
@@ -390,12 +518,20 @@ class OAuthFlowAuthorizationCode(OAuthFlow):
tokenUrl: str
class OAuthFlows(BaseModelWithConfig):
class OAuthFlows(BaseModel):
implicit: Optional[OAuthFlowImplicit] = None
password: Optional[OAuthFlowPassword] = None
clientCredentials: Optional[OAuthFlowClientCredentials] = None
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class OAuth2(SecurityBase):
type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type")
@@ -412,7 +548,7 @@ class OpenIdConnect(SecurityBase):
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
class Components(BaseModelWithConfig):
class Components(BaseModel):
schemas: Optional[Dict[str, Union[Schema, Reference]]] = None
responses: Optional[Dict[str, Union[Response, Reference]]] = None
parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None
@@ -425,14 +561,30 @@ class Components(BaseModelWithConfig):
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class Tag(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class Tag(BaseModel):
name: str
description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
class OpenAPI(BaseModelWithConfig):
else:
class Config:
extra = "allow"
class OpenAPI(BaseModel):
openapi: str
info: Info
jsonSchemaDialect: Optional[str] = None
@@ -445,6 +597,14 @@ class OpenAPI(BaseModelWithConfig):
tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
_model_rebuild(Schema)
_model_rebuild(Operation)

View File

@@ -16,15 +16,11 @@ from fastapi._compat import (
)
from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
_get_flat_fields_from_params,
get_flat_dependant,
get_flat_params,
)
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, ParamTypes
from fastapi.params import Body, Param
from fastapi.responses import Response
from fastapi.types import ModelNameMap
from fastapi.utils import (
@@ -32,9 +28,9 @@ from fastapi.utils import (
generate_operation_id_for_path,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from typing_extensions import Literal
validation_error_definition = {
@@ -91,9 +87,9 @@ def get_openapi_security_definitions(
return security_definitions, operation_security
def _get_openapi_operation_parameters(
def get_openapi_operation_parameters(
*,
dependant: Dependant,
all_route_params: Sequence[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
@@ -102,67 +98,33 @@ def _get_openapi_operation_parameters(
separate_input_output_schemas: bool = True,
) -> List[Dict[str, Any]]:
parameters = []
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
parameter_groups = [
(ParamTypes.path, path_params),
(ParamTypes.query, query_params),
(ParamTypes.header, header_params),
(ParamTypes.cookie, cookie_params),
]
default_convert_underscores = True
if len(flat_dependant.header_params) == 1:
first_field = flat_dependant.header_params[0]
if lenient_issubclass(first_field.type_, BaseModel):
default_convert_underscores = getattr(
first_field.field_info, "convert_underscores", True
)
for param_type, param_group in parameter_groups:
for param in param_group:
field_info = param.field_info
# field_info = cast(Param, field_info)
if not getattr(field_info, "include_in_schema", True):
continue
param_schema = get_schema_from_model_field(
field=param,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
name = param.alias
convert_underscores = getattr(
param.field_info,
"convert_underscores",
default_convert_underscores,
)
if (
param_type == ParamTypes.header
and param.alias == param.name
and convert_underscores
):
name = param.name.replace("_", "-")
parameter = {
"name": name,
"in": param_type.value,
"required": param.required,
"schema": param_schema,
}
if field_info.description:
parameter["description"] = field_info.description
openapi_examples = getattr(field_info, "openapi_examples", None)
example = getattr(field_info, "example", None)
if openapi_examples:
parameter["examples"] = jsonable_encoder(openapi_examples)
elif example != Undefined:
parameter["example"] = jsonable_encoder(example)
if getattr(field_info, "deprecated", None):
parameter["deprecated"] = True
parameters.append(parameter)
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,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
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.openapi_examples:
parameter["examples"] = jsonable_encoder(field_info.openapi_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
@@ -285,8 +247,9 @@ def get_openapi_path(
operation.setdefault("security", []).extend(operation_security)
if security_definitions:
security_schemes.update(security_definitions)
operation_parameters = _get_openapi_operation_parameters(
dependant=route.dependant,
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,
field_mapping=field_mapping,
@@ -384,9 +347,9 @@ def get_openapi_path(
openapi_response = operation_responses.setdefault(
status_code_key, {}
)
assert isinstance(process_response, dict), (
"An additional response must be a dict"
)
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:
@@ -415,8 +378,7 @@ def get_openapi_path(
)
deep_dict_update(openapi_response, process_response)
openapi_response["description"] = description
http422 = "422"
all_route_params = get_flat_params(route.dependant)
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"]
@@ -454,9 +416,9 @@ def get_fields_from_routes(
route, routing.APIRoute
):
if route.body_field:
assert isinstance(route.body_field, ModelField), (
"A request body must be a Pydantic 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)
@@ -488,7 +450,6 @@ def get_openapi(
contact: Optional[Dict[str, Union[str, Any]]] = None,
license_info: Optional[Dict[str, Union[str, Any]]] = None,
separate_input_output_schemas: bool = True,
external_docs: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
info: Dict[str, Any] = {"title": title, "version": version}
if summary:
@@ -566,6 +527,4 @@ def get_openapi(
output["webhooks"] = webhook_paths
if tags:
output["tags"] = tags
if external_docs:
output["externalDocs"] = external_docs
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore