Add support for OpenAPI 3.1.0 (#9770)

*  Update OpenAPI models for JSON Schema 2020-12 and OpenAPI 3.1.0

*  Add support for summary and webhooks

*  Update JSON Schema for UploadFiles

* ️ Revert making paths optional, to ensure always correctness

* ️ Keep UploadFile as format: binary for compatibility with the rest of Pydantic bytes fields in v1

*  Update version of OpenAPI generated to 3.1.0

*  Update the version of Swagger UI

* 📝 Update docs about extending OpenAPI

* 📝 Update docs and links to refer to OpenAPI 3.1.0

*  Update logic for handling webhooks

* ♻️ Update parameter functions and classes, deprecate example and make examples the main field

*  Update tests for OpenAPI 3.1.0

* 📝 Update examples for OpenAPI metadata

*  Add and update tests for OpenAPI metadata

* 📝 Add source example for webhooks

* 📝 Update docs for metadata

* 📝 Update docs for Schema extra

* 📝 Add docs for webhooks

* 🔧 Add webhooks docs to MkDocs

*  Update tests for extending OpenAPI

*  Add tests for webhooks

* ♻️ Refactor generation of OpenAPI and JSON Schema with params

* 📝 Update source examples for field examples

*  Update tests for examples

*  Make sure the minimum version of typing-extensions installed has deprecated() (already a dependency of Pydantic)

* ✏️ Fix typo in Webhooks example code

* 🔥 Remove commented out code of removed nullable field

* 🗑️ Add deprecation warnings for example argument

*  Update tests to check for deprecation warnings

*  Add test for webhooks with security schemes, for coverage

* 🍱 Update image for metadata, with new summary

* 🍱 Add docs image for Webhooks

* 📝 Update docs for webhooks, add docs UI image
This commit is contained in:
Sebastián Ramírez
2023-06-30 20:25:16 +02:00
committed by GitHub
parent 02fc9e8a63
commit 7dad5a820b
335 changed files with 1533 additions and 891 deletions

View File

@@ -17,8 +17,8 @@ def get_swagger_ui_html(
*,
openapi_url: str,
title: str,
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
oauth2_redirect_url: Optional[str] = None,
init_oauth: Optional[Dict[str, Any]] = None,

View File

@@ -1,9 +1,10 @@
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal
from typing_extensions import Annotated, Literal
from typing_extensions import deprecated as typing_deprecated
try:
import email_validator # type: ignore
@@ -37,6 +38,7 @@ class Contact(BaseModel):
class License(BaseModel):
name: str
identifier: Optional[str] = None
url: Optional[AnyUrl] = None
class Config:
@@ -45,6 +47,7 @@ class License(BaseModel):
class Info(BaseModel):
title: str
summary: Optional[str] = None
description: Optional[str] = None
termsOfService: Optional[str] = None
contact: Optional[Contact] = None
@@ -56,7 +59,7 @@ class Info(BaseModel):
class ServerVariable(BaseModel):
enum: Optional[List[str]] = None
enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
default: str
description: Optional[str] = None
@@ -102,9 +105,42 @@ class ExternalDocumentation(BaseModel):
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")
vocabulary: Optional[str] = Field(default=None, alias="$vocabulary")
id: Optional[str] = Field(default=None, alias="$id")
anchor: Optional[str] = Field(default=None, alias="$anchor")
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
ref: Optional[str] = Field(default=None, alias="$ref")
title: Optional[str] = None
multipleOf: Optional[float] = None
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs")
comment: Optional[str] = Field(default=None, alias="$comment")
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
# A Vocabulary for Applying Subschemas
allOf: Optional[List["Schema"]] = None
anyOf: Optional[List["Schema"]] = None
oneOf: Optional[List["Schema"]] = None
not_: Optional["Schema"] = Field(default=None, alias="not")
if_: Optional["Schema"] = Field(default=None, alias="if")
then: Optional["Schema"] = None
else_: Optional["Schema"] = Field(default=None, alias="else")
dependentSchemas: Optional[Dict[str, "Schema"]] = None
prefixItems: Optional[List["Schema"]] = None
items: Optional[Union["Schema", List["Schema"]]] = None
contains: Optional["Schema"] = None
properties: Optional[Dict[str, "Schema"]] = None
patternProperties: Optional[Dict[str, "Schema"]] = None
additionalProperties: Optional["Schema"] = None
propertyNames: Optional["Schema"] = None
unevaluatedItems: Optional["Schema"] = None
unevaluatedProperties: Optional["Schema"] = 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[str] = None
enum: Optional[List[Any]] = None
const: Optional[Any] = None
multipleOf: Optional[float] = Field(default=None, gt=0)
maximum: Optional[float] = None
exclusiveMaximum: Optional[float] = None
minimum: Optional[float] = None
@@ -115,29 +151,41 @@ class Schema(BaseModel):
maxItems: Optional[int] = Field(default=None, ge=0)
minItems: Optional[int] = Field(default=None, ge=0)
uniqueItems: Optional[bool] = None
maxContains: Optional[int] = Field(default=None, ge=0)
minContains: Optional[int] = Field(default=None, ge=0)
maxProperties: Optional[int] = Field(default=None, ge=0)
minProperties: Optional[int] = Field(default=None, ge=0)
required: Optional[List[str]] = None
enum: Optional[List[Any]] = None
type: Optional[str] = None
allOf: Optional[List["Schema"]] = None
oneOf: Optional[List["Schema"]] = None
anyOf: Optional[List["Schema"]] = None
not_: Optional["Schema"] = Field(default=None, alias="not")
items: Optional[Union["Schema", List["Schema"]]] = None
properties: Optional[Dict[str, "Schema"]] = None
additionalProperties: Optional[Union["Schema", Reference, bool]] = None
description: Optional[str] = None
dependentRequired: Optional[Dict[str, Set[str]]] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c
# Vocabularies for Semantic Content With "format"
format: Optional[str] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten
# A Vocabulary for the Contents of String-Encoded Data
contentEncoding: Optional[str] = None
contentMediaType: Optional[str] = None
contentSchema: Optional["Schema"] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
# A Vocabulary for Basic Meta-Data Annotations
title: Optional[str] = None
description: Optional[str] = None
default: Optional[Any] = None
nullable: Optional[bool] = None
discriminator: Optional[Discriminator] = None
deprecated: Optional[bool] = None
readOnly: Optional[bool] = None
writeOnly: Optional[bool] = None
examples: Optional[List[Any]] = None
# Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
# Schema Object
discriminator: Optional[Discriminator] = None
xml: Optional[XML] = None
externalDocs: Optional[ExternalDocumentation] = None
example: Optional[Any] = None
deprecated: Optional[bool] = None
example: Annotated[
Optional[Any],
typing_deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = None
class Config:
extra: str = "allow"
@@ -248,7 +296,7 @@ class Operation(BaseModel):
parameters: Optional[List[Union[Parameter, Reference]]] = None
requestBody: Optional[Union[RequestBody, Reference]] = None
# Using Any for Specification Extensions
responses: Dict[str, Union[Response, Any]]
responses: Optional[Dict[str, Union[Response, Any]]] = None
callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None
deprecated: Optional[bool] = None
security: Optional[List[Dict[str, List[str]]]] = None
@@ -375,6 +423,7 @@ class Components(BaseModel):
links: Optional[Dict[str, Union[Link, Reference]]] = None
# Using Any for Specification Extensions
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
class Config:
extra = "allow"
@@ -392,9 +441,11 @@ class Tag(BaseModel):
class OpenAPI(BaseModel):
openapi: str
info: Info
jsonSchemaDialect: Optional[str] = None
servers: Optional[List[Server]] = None
# Using Any for Specification Extensions
paths: Dict[str, Union[PathItem, Any]]
paths: Optional[Dict[str, Union[PathItem, Any]]] = None
webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None
components: Optional[Components] = None
security: Optional[List[Dict[str, List[str]]]] = None
tags: Optional[List[Tag]] = None

View File

@@ -106,9 +106,7 @@ def get_openapi_operation_parameters(
}
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:
if field_info.example != Undefined:
parameter["example"] = jsonable_encoder(field_info.example)
if field_info.deprecated:
parameter["deprecated"] = field_info.deprecated
@@ -134,9 +132,7 @@ def get_openapi_operation_request_body(
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:
if 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
@@ -392,9 +388,11 @@ def get_openapi(
*,
title: str,
version: str,
openapi_version: str = "3.0.2",
openapi_version: str = "3.1.0",
summary: Optional[str] = None,
description: Optional[str] = None,
routes: Sequence[BaseRoute],
webhooks: Optional[Sequence[BaseRoute]] = None,
tags: Optional[List[Dict[str, Any]]] = None,
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
terms_of_service: Optional[str] = None,
@@ -402,6 +400,8 @@ def get_openapi(
license_info: Optional[Dict[str, Union[str, Any]]] = None,
) -> Dict[str, Any]:
info: Dict[str, Any] = {"title": title, "version": version}
if summary:
info["summary"] = summary
if description:
info["description"] = description
if terms_of_service:
@@ -415,13 +415,14 @@ def get_openapi(
output["servers"] = servers
components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {}
webhook_paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
flat_models = get_flat_models_from_routes(routes)
flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
model_name_map = get_model_name_map(flat_models)
definitions = get_model_definitions(
flat_models=flat_models, model_name_map=model_name_map
)
for route in routes:
for route in routes or []:
if isinstance(route, routing.APIRoute):
result = get_openapi_path(
route=route, model_name_map=model_name_map, operation_ids=operation_ids
@@ -436,11 +437,30 @@ def get_openapi(
)
if path_definitions:
definitions.update(path_definitions)
for webhook in webhooks or []:
if isinstance(webhook, routing.APIRoute):
result = get_openapi_path(
route=webhook,
model_name_map=model_name_map,
operation_ids=operation_ids,
)
if result:
path, security_schemes, path_definitions = result
if path:
webhook_paths.setdefault(webhook.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 webhook_paths:
output["webhooks"] = webhook_paths
if tags:
output["tags"] = tags
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore