fastapi 0.128.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi/__init__.py +25 -0
- fastapi/__main__.py +3 -0
- fastapi/_compat/__init__.py +41 -0
- fastapi/_compat/shared.py +206 -0
- fastapi/_compat/v2.py +568 -0
- fastapi/applications.py +4669 -0
- fastapi/background.py +60 -0
- fastapi/cli.py +13 -0
- fastapi/concurrency.py +41 -0
- fastapi/datastructures.py +183 -0
- fastapi/dependencies/__init__.py +0 -0
- fastapi/dependencies/models.py +193 -0
- fastapi/dependencies/utils.py +1021 -0
- fastapi/encoders.py +346 -0
- fastapi/exception_handlers.py +34 -0
- fastapi/exceptions.py +246 -0
- fastapi/logger.py +3 -0
- fastapi/middleware/__init__.py +1 -0
- fastapi/middleware/asyncexitstack.py +18 -0
- fastapi/middleware/cors.py +1 -0
- fastapi/middleware/gzip.py +1 -0
- fastapi/middleware/httpsredirect.py +3 -0
- fastapi/middleware/trustedhost.py +3 -0
- fastapi/middleware/wsgi.py +1 -0
- fastapi/openapi/__init__.py +0 -0
- fastapi/openapi/constants.py +3 -0
- fastapi/openapi/docs.py +344 -0
- fastapi/openapi/models.py +438 -0
- fastapi/openapi/utils.py +567 -0
- fastapi/param_functions.py +2369 -0
- fastapi/params.py +755 -0
- fastapi/py.typed +0 -0
- fastapi/requests.py +2 -0
- fastapi/responses.py +48 -0
- fastapi/routing.py +4508 -0
- fastapi/security/__init__.py +15 -0
- fastapi/security/api_key.py +318 -0
- fastapi/security/base.py +6 -0
- fastapi/security/http.py +423 -0
- fastapi/security/oauth2.py +663 -0
- fastapi/security/open_id_connect_url.py +94 -0
- fastapi/security/utils.py +10 -0
- fastapi/staticfiles.py +1 -0
- fastapi/templating.py +1 -0
- fastapi/testclient.py +1 -0
- fastapi/types.py +11 -0
- fastapi/utils.py +164 -0
- fastapi/websockets.py +3 -0
- fastapi-0.128.0.dist-info/METADATA +645 -0
- fastapi-0.128.0.dist-info/RECORD +53 -0
- fastapi-0.128.0.dist-info/WHEEL +4 -0
- fastapi-0.128.0.dist-info/entry_points.txt +5 -0
- fastapi-0.128.0.dist-info/licenses/LICENSE +21 -0
fastapi/openapi/utils.py
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import http.client
|
|
2
|
+
import inspect
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any, Optional, Union, cast
|
|
6
|
+
|
|
7
|
+
from fastapi import routing
|
|
8
|
+
from fastapi._compat import (
|
|
9
|
+
ModelField,
|
|
10
|
+
Undefined,
|
|
11
|
+
get_compat_model_name_map,
|
|
12
|
+
get_definitions,
|
|
13
|
+
get_schema_from_model_field,
|
|
14
|
+
lenient_issubclass,
|
|
15
|
+
)
|
|
16
|
+
from fastapi.datastructures import DefaultPlaceholder
|
|
17
|
+
from fastapi.dependencies.models import Dependant
|
|
18
|
+
from fastapi.dependencies.utils import (
|
|
19
|
+
_get_flat_fields_from_params,
|
|
20
|
+
get_flat_dependant,
|
|
21
|
+
get_flat_params,
|
|
22
|
+
get_validation_alias,
|
|
23
|
+
)
|
|
24
|
+
from fastapi.encoders import jsonable_encoder
|
|
25
|
+
from fastapi.exceptions import FastAPIDeprecationWarning
|
|
26
|
+
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
|
27
|
+
from fastapi.openapi.models import OpenAPI
|
|
28
|
+
from fastapi.params import Body, ParamTypes
|
|
29
|
+
from fastapi.responses import Response
|
|
30
|
+
from fastapi.types import ModelNameMap
|
|
31
|
+
from fastapi.utils import (
|
|
32
|
+
deep_dict_update,
|
|
33
|
+
generate_operation_id_for_path,
|
|
34
|
+
is_body_allowed_for_status_code,
|
|
35
|
+
)
|
|
36
|
+
from pydantic import BaseModel
|
|
37
|
+
from starlette.responses import JSONResponse
|
|
38
|
+
from starlette.routing import BaseRoute
|
|
39
|
+
from typing_extensions import Literal
|
|
40
|
+
|
|
41
|
+
validation_error_definition = {
|
|
42
|
+
"title": "ValidationError",
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"loc": {
|
|
46
|
+
"title": "Location",
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
|
49
|
+
},
|
|
50
|
+
"msg": {"title": "Message", "type": "string"},
|
|
51
|
+
"type": {"title": "Error Type", "type": "string"},
|
|
52
|
+
},
|
|
53
|
+
"required": ["loc", "msg", "type"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validation_error_response_definition = {
|
|
57
|
+
"title": "HTTPValidationError",
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"detail": {
|
|
61
|
+
"title": "Detail",
|
|
62
|
+
"type": "array",
|
|
63
|
+
"items": {"$ref": REF_PREFIX + "ValidationError"},
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
status_code_ranges: dict[str, str] = {
|
|
69
|
+
"1XX": "Information",
|
|
70
|
+
"2XX": "Success",
|
|
71
|
+
"3XX": "Redirection",
|
|
72
|
+
"4XX": "Client Error",
|
|
73
|
+
"5XX": "Server Error",
|
|
74
|
+
"DEFAULT": "Default Response",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_openapi_security_definitions(
|
|
79
|
+
flat_dependant: Dependant,
|
|
80
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
81
|
+
security_definitions = {}
|
|
82
|
+
# Use a dict to merge scopes for same security scheme
|
|
83
|
+
operation_security_dict: dict[str, list[str]] = {}
|
|
84
|
+
for security_dependency in flat_dependant._security_dependencies:
|
|
85
|
+
security_definition = jsonable_encoder(
|
|
86
|
+
security_dependency._security_scheme.model,
|
|
87
|
+
by_alias=True,
|
|
88
|
+
exclude_none=True,
|
|
89
|
+
)
|
|
90
|
+
security_name = security_dependency._security_scheme.scheme_name
|
|
91
|
+
security_definitions[security_name] = security_definition
|
|
92
|
+
# Merge scopes for the same security scheme
|
|
93
|
+
if security_name not in operation_security_dict:
|
|
94
|
+
operation_security_dict[security_name] = []
|
|
95
|
+
for scope in security_dependency.oauth_scopes or []:
|
|
96
|
+
if scope not in operation_security_dict[security_name]:
|
|
97
|
+
operation_security_dict[security_name].append(scope)
|
|
98
|
+
operation_security = [
|
|
99
|
+
{name: scopes} for name, scopes in operation_security_dict.items()
|
|
100
|
+
]
|
|
101
|
+
return security_definitions, operation_security
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_openapi_operation_parameters(
|
|
105
|
+
*,
|
|
106
|
+
dependant: Dependant,
|
|
107
|
+
model_name_map: ModelNameMap,
|
|
108
|
+
field_mapping: dict[
|
|
109
|
+
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
|
|
110
|
+
],
|
|
111
|
+
separate_input_output_schemas: bool = True,
|
|
112
|
+
) -> list[dict[str, Any]]:
|
|
113
|
+
parameters = []
|
|
114
|
+
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
|
115
|
+
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
|
116
|
+
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
|
117
|
+
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
|
118
|
+
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
|
|
119
|
+
parameter_groups = [
|
|
120
|
+
(ParamTypes.path, path_params),
|
|
121
|
+
(ParamTypes.query, query_params),
|
|
122
|
+
(ParamTypes.header, header_params),
|
|
123
|
+
(ParamTypes.cookie, cookie_params),
|
|
124
|
+
]
|
|
125
|
+
default_convert_underscores = True
|
|
126
|
+
if len(flat_dependant.header_params) == 1:
|
|
127
|
+
first_field = flat_dependant.header_params[0]
|
|
128
|
+
if lenient_issubclass(first_field.type_, BaseModel):
|
|
129
|
+
default_convert_underscores = getattr(
|
|
130
|
+
first_field.field_info, "convert_underscores", True
|
|
131
|
+
)
|
|
132
|
+
for param_type, param_group in parameter_groups:
|
|
133
|
+
for param in param_group:
|
|
134
|
+
field_info = param.field_info
|
|
135
|
+
# field_info = cast(Param, field_info)
|
|
136
|
+
if not getattr(field_info, "include_in_schema", True):
|
|
137
|
+
continue
|
|
138
|
+
param_schema = get_schema_from_model_field(
|
|
139
|
+
field=param,
|
|
140
|
+
model_name_map=model_name_map,
|
|
141
|
+
field_mapping=field_mapping,
|
|
142
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
143
|
+
)
|
|
144
|
+
name = get_validation_alias(param)
|
|
145
|
+
convert_underscores = getattr(
|
|
146
|
+
param.field_info,
|
|
147
|
+
"convert_underscores",
|
|
148
|
+
default_convert_underscores,
|
|
149
|
+
)
|
|
150
|
+
if (
|
|
151
|
+
param_type == ParamTypes.header
|
|
152
|
+
and name == param.name
|
|
153
|
+
and convert_underscores
|
|
154
|
+
):
|
|
155
|
+
name = param.name.replace("_", "-")
|
|
156
|
+
|
|
157
|
+
parameter = {
|
|
158
|
+
"name": name,
|
|
159
|
+
"in": param_type.value,
|
|
160
|
+
"required": param.required,
|
|
161
|
+
"schema": param_schema,
|
|
162
|
+
}
|
|
163
|
+
if field_info.description:
|
|
164
|
+
parameter["description"] = field_info.description
|
|
165
|
+
openapi_examples = getattr(field_info, "openapi_examples", None)
|
|
166
|
+
example = getattr(field_info, "example", None)
|
|
167
|
+
if openapi_examples:
|
|
168
|
+
parameter["examples"] = jsonable_encoder(openapi_examples)
|
|
169
|
+
elif example != Undefined:
|
|
170
|
+
parameter["example"] = jsonable_encoder(example)
|
|
171
|
+
if getattr(field_info, "deprecated", None):
|
|
172
|
+
parameter["deprecated"] = True
|
|
173
|
+
parameters.append(parameter)
|
|
174
|
+
return parameters
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_openapi_operation_request_body(
|
|
178
|
+
*,
|
|
179
|
+
body_field: Optional[ModelField],
|
|
180
|
+
model_name_map: ModelNameMap,
|
|
181
|
+
field_mapping: dict[
|
|
182
|
+
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
|
|
183
|
+
],
|
|
184
|
+
separate_input_output_schemas: bool = True,
|
|
185
|
+
) -> Optional[dict[str, Any]]:
|
|
186
|
+
if not body_field:
|
|
187
|
+
return None
|
|
188
|
+
assert isinstance(body_field, ModelField)
|
|
189
|
+
body_schema = get_schema_from_model_field(
|
|
190
|
+
field=body_field,
|
|
191
|
+
model_name_map=model_name_map,
|
|
192
|
+
field_mapping=field_mapping,
|
|
193
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
194
|
+
)
|
|
195
|
+
field_info = cast(Body, body_field.field_info)
|
|
196
|
+
request_media_type = field_info.media_type
|
|
197
|
+
required = body_field.required
|
|
198
|
+
request_body_oai: dict[str, Any] = {}
|
|
199
|
+
if required:
|
|
200
|
+
request_body_oai["required"] = required
|
|
201
|
+
request_media_content: dict[str, Any] = {"schema": body_schema}
|
|
202
|
+
if field_info.openapi_examples:
|
|
203
|
+
request_media_content["examples"] = jsonable_encoder(
|
|
204
|
+
field_info.openapi_examples
|
|
205
|
+
)
|
|
206
|
+
elif field_info.example != Undefined:
|
|
207
|
+
request_media_content["example"] = jsonable_encoder(field_info.example)
|
|
208
|
+
request_body_oai["content"] = {request_media_type: request_media_content}
|
|
209
|
+
return request_body_oai
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def generate_operation_id(
|
|
213
|
+
*, route: routing.APIRoute, method: str
|
|
214
|
+
) -> str: # pragma: nocover
|
|
215
|
+
warnings.warn(
|
|
216
|
+
message="fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
|
217
|
+
"it is not used internally, and will be removed soon",
|
|
218
|
+
category=FastAPIDeprecationWarning,
|
|
219
|
+
stacklevel=2,
|
|
220
|
+
)
|
|
221
|
+
if route.operation_id:
|
|
222
|
+
return route.operation_id
|
|
223
|
+
path: str = route.path_format
|
|
224
|
+
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
|
228
|
+
if route.summary:
|
|
229
|
+
return route.summary
|
|
230
|
+
return route.name.replace("_", " ").title()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_openapi_operation_metadata(
|
|
234
|
+
*, route: routing.APIRoute, method: str, operation_ids: set[str]
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
operation: dict[str, Any] = {}
|
|
237
|
+
if route.tags:
|
|
238
|
+
operation["tags"] = route.tags
|
|
239
|
+
operation["summary"] = generate_operation_summary(route=route, method=method)
|
|
240
|
+
if route.description:
|
|
241
|
+
operation["description"] = route.description
|
|
242
|
+
operation_id = route.operation_id or route.unique_id
|
|
243
|
+
if operation_id in operation_ids:
|
|
244
|
+
message = (
|
|
245
|
+
f"Duplicate Operation ID {operation_id} for function "
|
|
246
|
+
+ f"{route.endpoint.__name__}"
|
|
247
|
+
)
|
|
248
|
+
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
|
249
|
+
if file_name:
|
|
250
|
+
message += f" at {file_name}"
|
|
251
|
+
warnings.warn(message, stacklevel=1)
|
|
252
|
+
operation_ids.add(operation_id)
|
|
253
|
+
operation["operationId"] = operation_id
|
|
254
|
+
if route.deprecated:
|
|
255
|
+
operation["deprecated"] = route.deprecated
|
|
256
|
+
return operation
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_openapi_path(
|
|
260
|
+
*,
|
|
261
|
+
route: routing.APIRoute,
|
|
262
|
+
operation_ids: set[str],
|
|
263
|
+
model_name_map: ModelNameMap,
|
|
264
|
+
field_mapping: dict[
|
|
265
|
+
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
|
|
266
|
+
],
|
|
267
|
+
separate_input_output_schemas: bool = True,
|
|
268
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
269
|
+
path = {}
|
|
270
|
+
security_schemes: dict[str, Any] = {}
|
|
271
|
+
definitions: dict[str, Any] = {}
|
|
272
|
+
assert route.methods is not None, "Methods must be a list"
|
|
273
|
+
if isinstance(route.response_class, DefaultPlaceholder):
|
|
274
|
+
current_response_class: type[Response] = route.response_class.value
|
|
275
|
+
else:
|
|
276
|
+
current_response_class = route.response_class
|
|
277
|
+
assert current_response_class, "A response class is needed to generate OpenAPI"
|
|
278
|
+
route_response_media_type: Optional[str] = current_response_class.media_type
|
|
279
|
+
if route.include_in_schema:
|
|
280
|
+
for method in route.methods:
|
|
281
|
+
operation = get_openapi_operation_metadata(
|
|
282
|
+
route=route, method=method, operation_ids=operation_ids
|
|
283
|
+
)
|
|
284
|
+
parameters: list[dict[str, Any]] = []
|
|
285
|
+
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
|
286
|
+
security_definitions, operation_security = get_openapi_security_definitions(
|
|
287
|
+
flat_dependant=flat_dependant
|
|
288
|
+
)
|
|
289
|
+
if operation_security:
|
|
290
|
+
operation.setdefault("security", []).extend(operation_security)
|
|
291
|
+
if security_definitions:
|
|
292
|
+
security_schemes.update(security_definitions)
|
|
293
|
+
operation_parameters = _get_openapi_operation_parameters(
|
|
294
|
+
dependant=route.dependant,
|
|
295
|
+
model_name_map=model_name_map,
|
|
296
|
+
field_mapping=field_mapping,
|
|
297
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
298
|
+
)
|
|
299
|
+
parameters.extend(operation_parameters)
|
|
300
|
+
if parameters:
|
|
301
|
+
all_parameters = {
|
|
302
|
+
(param["in"], param["name"]): param for param in parameters
|
|
303
|
+
}
|
|
304
|
+
required_parameters = {
|
|
305
|
+
(param["in"], param["name"]): param
|
|
306
|
+
for param in parameters
|
|
307
|
+
if param.get("required")
|
|
308
|
+
}
|
|
309
|
+
# Make sure required definitions of the same parameter take precedence
|
|
310
|
+
# over non-required definitions
|
|
311
|
+
all_parameters.update(required_parameters)
|
|
312
|
+
operation["parameters"] = list(all_parameters.values())
|
|
313
|
+
if method in METHODS_WITH_BODY:
|
|
314
|
+
request_body_oai = get_openapi_operation_request_body(
|
|
315
|
+
body_field=route.body_field,
|
|
316
|
+
model_name_map=model_name_map,
|
|
317
|
+
field_mapping=field_mapping,
|
|
318
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
319
|
+
)
|
|
320
|
+
if request_body_oai:
|
|
321
|
+
operation["requestBody"] = request_body_oai
|
|
322
|
+
if route.callbacks:
|
|
323
|
+
callbacks = {}
|
|
324
|
+
for callback in route.callbacks:
|
|
325
|
+
if isinstance(callback, routing.APIRoute):
|
|
326
|
+
(
|
|
327
|
+
cb_path,
|
|
328
|
+
cb_security_schemes,
|
|
329
|
+
cb_definitions,
|
|
330
|
+
) = get_openapi_path(
|
|
331
|
+
route=callback,
|
|
332
|
+
operation_ids=operation_ids,
|
|
333
|
+
model_name_map=model_name_map,
|
|
334
|
+
field_mapping=field_mapping,
|
|
335
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
336
|
+
)
|
|
337
|
+
callbacks[callback.name] = {callback.path: cb_path}
|
|
338
|
+
operation["callbacks"] = callbacks
|
|
339
|
+
if route.status_code is not None:
|
|
340
|
+
status_code = str(route.status_code)
|
|
341
|
+
else:
|
|
342
|
+
# It would probably make more sense for all response classes to have an
|
|
343
|
+
# explicit default status_code, and to extract it from them, instead of
|
|
344
|
+
# doing this inspection tricks, that would probably be in the future
|
|
345
|
+
# TODO: probably make status_code a default class attribute for all
|
|
346
|
+
# responses in Starlette
|
|
347
|
+
response_signature = inspect.signature(current_response_class.__init__)
|
|
348
|
+
status_code_param = response_signature.parameters.get("status_code")
|
|
349
|
+
if status_code_param is not None:
|
|
350
|
+
if isinstance(status_code_param.default, int):
|
|
351
|
+
status_code = str(status_code_param.default)
|
|
352
|
+
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
|
353
|
+
"description"
|
|
354
|
+
] = route.response_description
|
|
355
|
+
if route_response_media_type and is_body_allowed_for_status_code(
|
|
356
|
+
route.status_code
|
|
357
|
+
):
|
|
358
|
+
response_schema = {"type": "string"}
|
|
359
|
+
if lenient_issubclass(current_response_class, JSONResponse):
|
|
360
|
+
if route.response_field:
|
|
361
|
+
response_schema = get_schema_from_model_field(
|
|
362
|
+
field=route.response_field,
|
|
363
|
+
model_name_map=model_name_map,
|
|
364
|
+
field_mapping=field_mapping,
|
|
365
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
response_schema = {}
|
|
369
|
+
operation.setdefault("responses", {}).setdefault(
|
|
370
|
+
status_code, {}
|
|
371
|
+
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
|
372
|
+
"schema"
|
|
373
|
+
] = response_schema
|
|
374
|
+
if route.responses:
|
|
375
|
+
operation_responses = operation.setdefault("responses", {})
|
|
376
|
+
for (
|
|
377
|
+
additional_status_code,
|
|
378
|
+
additional_response,
|
|
379
|
+
) in route.responses.items():
|
|
380
|
+
process_response = additional_response.copy()
|
|
381
|
+
process_response.pop("model", None)
|
|
382
|
+
status_code_key = str(additional_status_code).upper()
|
|
383
|
+
if status_code_key == "DEFAULT":
|
|
384
|
+
status_code_key = "default"
|
|
385
|
+
openapi_response = operation_responses.setdefault(
|
|
386
|
+
status_code_key, {}
|
|
387
|
+
)
|
|
388
|
+
assert isinstance(process_response, dict), (
|
|
389
|
+
"An additional response must be a dict"
|
|
390
|
+
)
|
|
391
|
+
field = route.response_fields.get(additional_status_code)
|
|
392
|
+
additional_field_schema: Optional[dict[str, Any]] = None
|
|
393
|
+
if field:
|
|
394
|
+
additional_field_schema = get_schema_from_model_field(
|
|
395
|
+
field=field,
|
|
396
|
+
model_name_map=model_name_map,
|
|
397
|
+
field_mapping=field_mapping,
|
|
398
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
399
|
+
)
|
|
400
|
+
media_type = route_response_media_type or "application/json"
|
|
401
|
+
additional_schema = (
|
|
402
|
+
process_response.setdefault("content", {})
|
|
403
|
+
.setdefault(media_type, {})
|
|
404
|
+
.setdefault("schema", {})
|
|
405
|
+
)
|
|
406
|
+
deep_dict_update(additional_schema, additional_field_schema)
|
|
407
|
+
status_text: Optional[str] = status_code_ranges.get(
|
|
408
|
+
str(additional_status_code).upper()
|
|
409
|
+
) or http.client.responses.get(int(additional_status_code))
|
|
410
|
+
description = (
|
|
411
|
+
process_response.get("description")
|
|
412
|
+
or openapi_response.get("description")
|
|
413
|
+
or status_text
|
|
414
|
+
or "Additional Response"
|
|
415
|
+
)
|
|
416
|
+
deep_dict_update(openapi_response, process_response)
|
|
417
|
+
openapi_response["description"] = description
|
|
418
|
+
http422 = "422"
|
|
419
|
+
all_route_params = get_flat_params(route.dependant)
|
|
420
|
+
if (all_route_params or route.body_field) and not any(
|
|
421
|
+
status in operation["responses"]
|
|
422
|
+
for status in [http422, "4XX", "default"]
|
|
423
|
+
):
|
|
424
|
+
operation["responses"][http422] = {
|
|
425
|
+
"description": "Validation Error",
|
|
426
|
+
"content": {
|
|
427
|
+
"application/json": {
|
|
428
|
+
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
if "ValidationError" not in definitions:
|
|
433
|
+
definitions.update(
|
|
434
|
+
{
|
|
435
|
+
"ValidationError": validation_error_definition,
|
|
436
|
+
"HTTPValidationError": validation_error_response_definition,
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
if route.openapi_extra:
|
|
440
|
+
deep_dict_update(operation, route.openapi_extra)
|
|
441
|
+
path[method.lower()] = operation
|
|
442
|
+
return path, security_schemes, definitions
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_fields_from_routes(
|
|
446
|
+
routes: Sequence[BaseRoute],
|
|
447
|
+
) -> list[ModelField]:
|
|
448
|
+
body_fields_from_routes: list[ModelField] = []
|
|
449
|
+
responses_from_routes: list[ModelField] = []
|
|
450
|
+
request_fields_from_routes: list[ModelField] = []
|
|
451
|
+
callback_flat_models: list[ModelField] = []
|
|
452
|
+
for route in routes:
|
|
453
|
+
if getattr(route, "include_in_schema", None) and isinstance(
|
|
454
|
+
route, routing.APIRoute
|
|
455
|
+
):
|
|
456
|
+
if route.body_field:
|
|
457
|
+
assert isinstance(route.body_field, ModelField), (
|
|
458
|
+
"A request body must be a Pydantic Field"
|
|
459
|
+
)
|
|
460
|
+
body_fields_from_routes.append(route.body_field)
|
|
461
|
+
if route.response_field:
|
|
462
|
+
responses_from_routes.append(route.response_field)
|
|
463
|
+
if route.response_fields:
|
|
464
|
+
responses_from_routes.extend(route.response_fields.values())
|
|
465
|
+
if route.callbacks:
|
|
466
|
+
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
|
467
|
+
params = get_flat_params(route.dependant)
|
|
468
|
+
request_fields_from_routes.extend(params)
|
|
469
|
+
|
|
470
|
+
flat_models = callback_flat_models + list(
|
|
471
|
+
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
|
472
|
+
)
|
|
473
|
+
return flat_models
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_openapi(
|
|
477
|
+
*,
|
|
478
|
+
title: str,
|
|
479
|
+
version: str,
|
|
480
|
+
openapi_version: str = "3.1.0",
|
|
481
|
+
summary: Optional[str] = None,
|
|
482
|
+
description: Optional[str] = None,
|
|
483
|
+
routes: Sequence[BaseRoute],
|
|
484
|
+
webhooks: Optional[Sequence[BaseRoute]] = None,
|
|
485
|
+
tags: Optional[list[dict[str, Any]]] = None,
|
|
486
|
+
servers: Optional[list[dict[str, Union[str, Any]]]] = None,
|
|
487
|
+
terms_of_service: Optional[str] = None,
|
|
488
|
+
contact: Optional[dict[str, Union[str, Any]]] = None,
|
|
489
|
+
license_info: Optional[dict[str, Union[str, Any]]] = None,
|
|
490
|
+
separate_input_output_schemas: bool = True,
|
|
491
|
+
external_docs: Optional[dict[str, Any]] = None,
|
|
492
|
+
) -> dict[str, Any]:
|
|
493
|
+
info: dict[str, Any] = {"title": title, "version": version}
|
|
494
|
+
if summary:
|
|
495
|
+
info["summary"] = summary
|
|
496
|
+
if description:
|
|
497
|
+
info["description"] = description
|
|
498
|
+
if terms_of_service:
|
|
499
|
+
info["termsOfService"] = terms_of_service
|
|
500
|
+
if contact:
|
|
501
|
+
info["contact"] = contact
|
|
502
|
+
if license_info:
|
|
503
|
+
info["license"] = license_info
|
|
504
|
+
output: dict[str, Any] = {"openapi": openapi_version, "info": info}
|
|
505
|
+
if servers:
|
|
506
|
+
output["servers"] = servers
|
|
507
|
+
components: dict[str, dict[str, Any]] = {}
|
|
508
|
+
paths: dict[str, dict[str, Any]] = {}
|
|
509
|
+
webhook_paths: dict[str, dict[str, Any]] = {}
|
|
510
|
+
operation_ids: set[str] = set()
|
|
511
|
+
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
|
|
512
|
+
model_name_map = get_compat_model_name_map(all_fields)
|
|
513
|
+
field_mapping, definitions = get_definitions(
|
|
514
|
+
fields=all_fields,
|
|
515
|
+
model_name_map=model_name_map,
|
|
516
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
517
|
+
)
|
|
518
|
+
for route in routes or []:
|
|
519
|
+
if isinstance(route, routing.APIRoute):
|
|
520
|
+
result = get_openapi_path(
|
|
521
|
+
route=route,
|
|
522
|
+
operation_ids=operation_ids,
|
|
523
|
+
model_name_map=model_name_map,
|
|
524
|
+
field_mapping=field_mapping,
|
|
525
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
526
|
+
)
|
|
527
|
+
if result:
|
|
528
|
+
path, security_schemes, path_definitions = result
|
|
529
|
+
if path:
|
|
530
|
+
paths.setdefault(route.path_format, {}).update(path)
|
|
531
|
+
if security_schemes:
|
|
532
|
+
components.setdefault("securitySchemes", {}).update(
|
|
533
|
+
security_schemes
|
|
534
|
+
)
|
|
535
|
+
if path_definitions:
|
|
536
|
+
definitions.update(path_definitions)
|
|
537
|
+
for webhook in webhooks or []:
|
|
538
|
+
if isinstance(webhook, routing.APIRoute):
|
|
539
|
+
result = get_openapi_path(
|
|
540
|
+
route=webhook,
|
|
541
|
+
operation_ids=operation_ids,
|
|
542
|
+
model_name_map=model_name_map,
|
|
543
|
+
field_mapping=field_mapping,
|
|
544
|
+
separate_input_output_schemas=separate_input_output_schemas,
|
|
545
|
+
)
|
|
546
|
+
if result:
|
|
547
|
+
path, security_schemes, path_definitions = result
|
|
548
|
+
if path:
|
|
549
|
+
webhook_paths.setdefault(webhook.path_format, {}).update(path)
|
|
550
|
+
if security_schemes:
|
|
551
|
+
components.setdefault("securitySchemes", {}).update(
|
|
552
|
+
security_schemes
|
|
553
|
+
)
|
|
554
|
+
if path_definitions:
|
|
555
|
+
definitions.update(path_definitions)
|
|
556
|
+
if definitions:
|
|
557
|
+
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
|
558
|
+
if components:
|
|
559
|
+
output["components"] = components
|
|
560
|
+
output["paths"] = paths
|
|
561
|
+
if webhook_paths:
|
|
562
|
+
output["webhooks"] = webhook_paths
|
|
563
|
+
if tags:
|
|
564
|
+
output["tags"] = tags
|
|
565
|
+
if external_docs:
|
|
566
|
+
output["externalDocs"] = external_docs
|
|
567
|
+
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
|