flask-openapi 4.3.1__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.
- flask_openapi/__init__.py +44 -0
- flask_openapi/__version__.py +5 -0
- flask_openapi/blueprint.py +201 -0
- flask_openapi/commands.py +38 -0
- flask_openapi/models/__init__.py +91 -0
- flask_openapi/models/callback.py +18 -0
- flask_openapi/models/components.py +37 -0
- flask_openapi/models/contact.py +17 -0
- flask_openapi/models/data_type.py +18 -0
- flask_openapi/models/discriminator.py +16 -0
- flask_openapi/models/encoding.py +27 -0
- flask_openapi/models/example.py +19 -0
- flask_openapi/models/external_documentation.py +16 -0
- flask_openapi/models/file.py +27 -0
- flask_openapi/models/header.py +17 -0
- flask_openapi/models/info.py +24 -0
- flask_openapi/models/license.py +17 -0
- flask_openapi/models/link.py +23 -0
- flask_openapi/models/media_type.py +24 -0
- flask_openapi/models/oauth_flow.py +18 -0
- flask_openapi/models/oauth_flows.py +20 -0
- flask_openapi/models/operation.py +36 -0
- flask_openapi/models/parameter.py +34 -0
- flask_openapi/models/parameter_in_type.py +13 -0
- flask_openapi/models/path_item.py +36 -0
- flask_openapi/models/paths.py +9 -0
- flask_openapi/models/reference.py +14 -0
- flask_openapi/models/request_body.py +19 -0
- flask_openapi/models/response.py +23 -0
- flask_openapi/models/responses.py +11 -0
- flask_openapi/models/schema.py +59 -0
- flask_openapi/models/security_requirement.py +8 -0
- flask_openapi/models/security_scheme.py +25 -0
- flask_openapi/models/security_scheme_in_type.py +12 -0
- flask_openapi/models/server.py +19 -0
- flask_openapi/models/server_variable.py +17 -0
- flask_openapi/models/style_values.py +14 -0
- flask_openapi/models/tag.py +15 -0
- flask_openapi/models/validation_error.py +24 -0
- flask_openapi/models/xml.py +19 -0
- flask_openapi/openapi.py +449 -0
- flask_openapi/plugins.py +17 -0
- flask_openapi/py.typed +0 -0
- flask_openapi/request.py +250 -0
- flask_openapi/scaffold.py +547 -0
- flask_openapi/templates.py +113 -0
- flask_openapi/types.py +27 -0
- flask_openapi/utils.py +633 -0
- flask_openapi/view.py +230 -0
- flask_openapi-4.3.1.dist-info/METADATA +258 -0
- flask_openapi-4.3.1.dist-info/RECORD +53 -0
- flask_openapi-4.3.1.dist-info/WHEEL +4 -0
- flask_openapi-4.3.1.dist-info/licenses/LICENSE.rst +21 -0
flask_openapi/utils.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Author : llc
|
|
3
|
+
# @Time : 2021/5/1 21:34
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from http import HTTPStatus
|
|
10
|
+
from typing import Any, Callable, DefaultDict, Type, get_type_hints
|
|
11
|
+
|
|
12
|
+
from flask import current_app, make_response
|
|
13
|
+
from flask.wrappers import Response as FlaskResponse
|
|
14
|
+
from pydantic import BaseModel, ValidationError
|
|
15
|
+
from pydantic.json_schema import JsonSchemaMode
|
|
16
|
+
|
|
17
|
+
from .models import (
|
|
18
|
+
OPENAPI3_REF_PREFIX,
|
|
19
|
+
OPENAPI3_REF_TEMPLATE,
|
|
20
|
+
Encoding,
|
|
21
|
+
MediaType,
|
|
22
|
+
Operation,
|
|
23
|
+
Parameter,
|
|
24
|
+
ParameterInType,
|
|
25
|
+
PathItem,
|
|
26
|
+
RawModel,
|
|
27
|
+
RequestBody,
|
|
28
|
+
Response,
|
|
29
|
+
Schema,
|
|
30
|
+
Tag,
|
|
31
|
+
)
|
|
32
|
+
from .models.data_type import DataType
|
|
33
|
+
from .types import ParametersTuple, ResponseDict, ResponseStrKeyDict
|
|
34
|
+
|
|
35
|
+
HTTP_STATUS = {str(status.value): status.phrase for status in HTTPStatus}
|
|
36
|
+
|
|
37
|
+
if sys.version_info < (3, 11): # pragma: no cover
|
|
38
|
+
|
|
39
|
+
class HTTPMethod(str, Enum):
|
|
40
|
+
GET = "GET"
|
|
41
|
+
POST = "POST"
|
|
42
|
+
PUT = "PUT"
|
|
43
|
+
DELETE = "DELETE"
|
|
44
|
+
PATCH = "PATCH"
|
|
45
|
+
HEAD = "HEAD"
|
|
46
|
+
OPTIONS = "OPTIONS"
|
|
47
|
+
TRACE = "TRACE"
|
|
48
|
+
CONNECT = "CONNECT"
|
|
49
|
+
else:
|
|
50
|
+
from http import HTTPMethod
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_operation(
|
|
54
|
+
func: Callable,
|
|
55
|
+
*,
|
|
56
|
+
summary: str | None = None,
|
|
57
|
+
description: str | None = None,
|
|
58
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
59
|
+
) -> Operation:
|
|
60
|
+
"""
|
|
61
|
+
Return an Operation object with the specified summary and description.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
func: The function or method for which the operation is being defined.
|
|
65
|
+
summary: A short summary of what the operation does.
|
|
66
|
+
description: A verbose explanation of the operation behavior.
|
|
67
|
+
openapi_extensions: Additional extensions to the OpenAPI Schema.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
An Operation object representing the operation.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
# Get the docstring of the function
|
|
74
|
+
doc = inspect.getdoc(func) or ""
|
|
75
|
+
doc = doc.strip()
|
|
76
|
+
lines = doc.split("\n")
|
|
77
|
+
doc_summary = lines[0]
|
|
78
|
+
|
|
79
|
+
# Determine the summary and description based on provided arguments or docstring
|
|
80
|
+
if summary is None:
|
|
81
|
+
doc_description = lines[0] if len(lines) == 0 else "<br/>".join(lines[1:])
|
|
82
|
+
else:
|
|
83
|
+
doc_description = "<br/>".join(lines)
|
|
84
|
+
|
|
85
|
+
summary = summary or doc_summary
|
|
86
|
+
description = description or doc_description
|
|
87
|
+
|
|
88
|
+
# Create the operation dictionary with summary and description
|
|
89
|
+
operation_dict = {}
|
|
90
|
+
|
|
91
|
+
if summary:
|
|
92
|
+
operation_dict["summary"] = summary # type: ignore
|
|
93
|
+
|
|
94
|
+
if description:
|
|
95
|
+
operation_dict["description"] = description # type: ignore
|
|
96
|
+
|
|
97
|
+
# Add any additional openapi_extensions to the operation dictionary
|
|
98
|
+
operation_dict.update(openapi_extensions or {})
|
|
99
|
+
|
|
100
|
+
# Create and return the Operation object
|
|
101
|
+
operation = Operation(**operation_dict)
|
|
102
|
+
|
|
103
|
+
return operation
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_operation_id_for_path(*, bp_name: str = "", name: str = "", path: str = "", method: str = "") -> str:
|
|
107
|
+
"""
|
|
108
|
+
Generate a unique operation ID based on the name, path, and method.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: The name or identifier for the operation.
|
|
112
|
+
path: The URL path for the operation.
|
|
113
|
+
method: The HTTP method for the operation.
|
|
114
|
+
bp_name: The Blueprint name
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A unique operation ID generated based on the provided name, path, and method.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
if bp_name:
|
|
121
|
+
name = bp_name + "_" + name
|
|
122
|
+
return re.sub(r"\W", "_", name + path) + "_" + method.lower()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_model_schema(model: Type[BaseModel], mode: JsonSchemaMode = "validation") -> dict:
|
|
126
|
+
"""Converts a Pydantic model to an OpenAPI schema."""
|
|
127
|
+
|
|
128
|
+
assert inspect.isclass(model) and issubclass(model, BaseModel), f"{model} is invalid `pydantic.BaseModel`"
|
|
129
|
+
|
|
130
|
+
model_config = model.model_config
|
|
131
|
+
by_alias = bool(model_config.get("by_alias", True))
|
|
132
|
+
|
|
133
|
+
return model.model_json_schema(by_alias=by_alias, ref_template=OPENAPI3_REF_TEMPLATE, mode=mode)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_header(header: Type[BaseModel]) -> tuple[list[Parameter], dict]:
|
|
137
|
+
"""Parses a header model and returns a list of parameters and component schemas."""
|
|
138
|
+
schema = get_model_schema(header)
|
|
139
|
+
parameters = []
|
|
140
|
+
components_schemas: dict = dict()
|
|
141
|
+
properties = schema.get("properties", {})
|
|
142
|
+
|
|
143
|
+
for name, value in properties.items():
|
|
144
|
+
data = {
|
|
145
|
+
"name": name,
|
|
146
|
+
"in": ParameterInType.HEADER,
|
|
147
|
+
"required": name in schema.get("required", []),
|
|
148
|
+
"schema": Schema(**value),
|
|
149
|
+
}
|
|
150
|
+
# Parse extra values
|
|
151
|
+
if "description" in value.keys():
|
|
152
|
+
data["description"] = value.get("description")
|
|
153
|
+
if "deprecated" in value.keys():
|
|
154
|
+
data["deprecated"] = value.get("deprecated")
|
|
155
|
+
if "example" in value.keys():
|
|
156
|
+
data["example"] = value.get("example")
|
|
157
|
+
if "examples" in value.keys():
|
|
158
|
+
data["examples"] = value.get("examples")
|
|
159
|
+
parameters.append(Parameter(**data))
|
|
160
|
+
|
|
161
|
+
# Parse definitions
|
|
162
|
+
definitions = schema.get("$defs", {})
|
|
163
|
+
for name, value in definitions.items():
|
|
164
|
+
components_schemas[name] = Schema(**value)
|
|
165
|
+
|
|
166
|
+
return parameters, components_schemas
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_cookie(cookie: Type[BaseModel]) -> tuple[list[Parameter], dict]:
|
|
170
|
+
"""Parses a cookie model and returns a list of parameters and component schemas."""
|
|
171
|
+
schema = get_model_schema(cookie)
|
|
172
|
+
parameters = []
|
|
173
|
+
components_schemas: dict = dict()
|
|
174
|
+
properties = schema.get("properties", {})
|
|
175
|
+
|
|
176
|
+
for name, value in properties.items():
|
|
177
|
+
data = {
|
|
178
|
+
"name": name,
|
|
179
|
+
"in": ParameterInType.COOKIE,
|
|
180
|
+
"required": name in schema.get("required", []),
|
|
181
|
+
"schema": Schema(**value),
|
|
182
|
+
}
|
|
183
|
+
# Parse extra values
|
|
184
|
+
if "description" in value.keys():
|
|
185
|
+
data["description"] = value.get("description")
|
|
186
|
+
if "deprecated" in value.keys():
|
|
187
|
+
data["deprecated"] = value.get("deprecated")
|
|
188
|
+
if "example" in value.keys():
|
|
189
|
+
data["example"] = value.get("example")
|
|
190
|
+
if "examples" in value.keys():
|
|
191
|
+
data["examples"] = value.get("examples")
|
|
192
|
+
parameters.append(Parameter(**data))
|
|
193
|
+
|
|
194
|
+
# Parse definitions
|
|
195
|
+
definitions = schema.get("$defs", {})
|
|
196
|
+
for name, value in definitions.items():
|
|
197
|
+
components_schemas[name] = Schema(**value)
|
|
198
|
+
|
|
199
|
+
return parameters, components_schemas
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parse_path(path: Type[BaseModel]) -> tuple[list[Parameter], dict]:
|
|
203
|
+
"""Parses a path model and returns a list of parameters and component schemas."""
|
|
204
|
+
schema = get_model_schema(path)
|
|
205
|
+
parameters = []
|
|
206
|
+
components_schemas: dict = dict()
|
|
207
|
+
properties = schema.get("properties", {})
|
|
208
|
+
|
|
209
|
+
for name, value in properties.items():
|
|
210
|
+
data = {"name": name, "in": ParameterInType.PATH, "required": True, "schema": Schema(**value)}
|
|
211
|
+
# Parse extra values
|
|
212
|
+
if "description" in value.keys():
|
|
213
|
+
data["description"] = value.get("description")
|
|
214
|
+
if "deprecated" in value.keys():
|
|
215
|
+
data["deprecated"] = value.get("deprecated")
|
|
216
|
+
if "example" in value.keys():
|
|
217
|
+
data["example"] = value.get("example")
|
|
218
|
+
if "examples" in value.keys():
|
|
219
|
+
data["examples"] = value.get("examples")
|
|
220
|
+
parameters.append(Parameter(**data))
|
|
221
|
+
|
|
222
|
+
# Parse definitions
|
|
223
|
+
definitions = schema.get("$defs", {})
|
|
224
|
+
for name, value in definitions.items():
|
|
225
|
+
components_schemas[name] = Schema(**value)
|
|
226
|
+
|
|
227
|
+
return parameters, components_schemas
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def parse_query(query: Type[BaseModel]) -> tuple[list[Parameter], dict]:
|
|
231
|
+
"""Parses a query model and returns a list of parameters and component schemas."""
|
|
232
|
+
schema = get_model_schema(query)
|
|
233
|
+
parameters = []
|
|
234
|
+
components_schemas: dict = dict()
|
|
235
|
+
properties = schema.get("properties", {})
|
|
236
|
+
|
|
237
|
+
for name, value in properties.items():
|
|
238
|
+
data = {
|
|
239
|
+
"name": name,
|
|
240
|
+
"in": ParameterInType.QUERY,
|
|
241
|
+
"required": name in schema.get("required", []),
|
|
242
|
+
"schema": Schema(**value),
|
|
243
|
+
}
|
|
244
|
+
# Parse extra values
|
|
245
|
+
if "description" in value.keys():
|
|
246
|
+
data["description"] = value.get("description")
|
|
247
|
+
if "deprecated" in value.keys():
|
|
248
|
+
data["deprecated"] = value.get("deprecated")
|
|
249
|
+
if "example" in value.keys():
|
|
250
|
+
data["example"] = value.get("example")
|
|
251
|
+
if "examples" in value.keys():
|
|
252
|
+
data["examples"] = value.get("examples")
|
|
253
|
+
parameters.append(Parameter(**data))
|
|
254
|
+
|
|
255
|
+
# Parse definitions
|
|
256
|
+
definitions = schema.get("$defs", {})
|
|
257
|
+
for name, value in definitions.items():
|
|
258
|
+
components_schemas[name] = Schema(**value)
|
|
259
|
+
|
|
260
|
+
return parameters, components_schemas
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def parse_form(
|
|
264
|
+
form: Type[BaseModel],
|
|
265
|
+
) -> tuple[dict[str, MediaType], dict]:
|
|
266
|
+
"""Parses a form model and returns a list of parameters and component schemas."""
|
|
267
|
+
schema = get_model_schema(form)
|
|
268
|
+
components_schemas = dict()
|
|
269
|
+
properties = schema.get("properties", {})
|
|
270
|
+
|
|
271
|
+
assert properties, f"{form.__name__}'s properties cannot be empty."
|
|
272
|
+
|
|
273
|
+
original_title = schema.get("title") or form.__name__
|
|
274
|
+
title = normalize_name(original_title)
|
|
275
|
+
components_schemas[title] = Schema(**schema)
|
|
276
|
+
encoding = {}
|
|
277
|
+
for k, v in properties.items():
|
|
278
|
+
if v.get("type") == "array":
|
|
279
|
+
encoding[k] = Encoding(style="form", explode=True)
|
|
280
|
+
content = {
|
|
281
|
+
"multipart/form-data": MediaType(
|
|
282
|
+
schema=Schema(**{"$ref": f"{OPENAPI3_REF_PREFIX}/{title}"}),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
if encoding:
|
|
286
|
+
content["multipart/form-data"].encoding = encoding
|
|
287
|
+
|
|
288
|
+
# Parse definitions
|
|
289
|
+
definitions = schema.get("$defs", {})
|
|
290
|
+
for name, value in definitions.items():
|
|
291
|
+
components_schemas[name] = Schema(**value)
|
|
292
|
+
|
|
293
|
+
return content, components_schemas
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def parse_body(
|
|
297
|
+
body: Type[BaseModel],
|
|
298
|
+
) -> tuple[dict[str, MediaType], dict]:
|
|
299
|
+
"""Parses a body model and returns a list of parameters and component schemas."""
|
|
300
|
+
schema = get_model_schema(body)
|
|
301
|
+
components_schemas = dict()
|
|
302
|
+
|
|
303
|
+
original_title = schema.get("title") or body.__name__
|
|
304
|
+
title = normalize_name(original_title)
|
|
305
|
+
components_schemas[title] = Schema(**schema)
|
|
306
|
+
content = {"application/json": MediaType(schema=Schema(**{"$ref": f"{OPENAPI3_REF_PREFIX}/{title}"}))}
|
|
307
|
+
|
|
308
|
+
# Parse definitions
|
|
309
|
+
definitions = schema.get("$defs", {})
|
|
310
|
+
for name, value in definitions.items():
|
|
311
|
+
components_schemas[name] = Schema(**value)
|
|
312
|
+
|
|
313
|
+
return content, components_schemas
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_responses(responses: ResponseStrKeyDict, components_schemas: dict, operation: Operation) -> None:
|
|
317
|
+
_responses = {}
|
|
318
|
+
_schemas = {}
|
|
319
|
+
|
|
320
|
+
for key, response in responses.items():
|
|
321
|
+
if response is None:
|
|
322
|
+
# If the response is None, it means HTTP status code "204" (No Content)
|
|
323
|
+
_responses[key] = Response(description=HTTP_STATUS.get(key, ""))
|
|
324
|
+
elif isinstance(response, dict):
|
|
325
|
+
response["description"] = response.get("description", HTTP_STATUS.get(key, ""))
|
|
326
|
+
_responses[key] = Response(**response)
|
|
327
|
+
else:
|
|
328
|
+
# OpenAPI 3 support ^[a-zA-Z0-9\.\-_]+$ so we should normalize __name__
|
|
329
|
+
schema = get_model_schema(response, mode="serialization")
|
|
330
|
+
original_title = schema.get("title") or response.__name__
|
|
331
|
+
name = normalize_name(original_title)
|
|
332
|
+
_responses[key] = Response(
|
|
333
|
+
description=HTTP_STATUS.get(key, ""),
|
|
334
|
+
content={"application/json": MediaType(schema=Schema(**{"$ref": f"{OPENAPI3_REF_PREFIX}/{name}"}))},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
model_config: DefaultDict[str, Any] = response.model_config # type: ignore
|
|
338
|
+
openapi_extra = model_config.get("openapi_extra", {})
|
|
339
|
+
if openapi_extra:
|
|
340
|
+
openapi_extra_keys = openapi_extra.keys()
|
|
341
|
+
# Add additional information from model_config to the response
|
|
342
|
+
if "description" in openapi_extra_keys:
|
|
343
|
+
_responses[key].description = openapi_extra.get("description")
|
|
344
|
+
if "headers" in openapi_extra_keys:
|
|
345
|
+
_responses[key].headers = openapi_extra.get("headers")
|
|
346
|
+
if "links" in openapi_extra_keys:
|
|
347
|
+
_responses[key].links = openapi_extra.get("links")
|
|
348
|
+
_content = _responses[key].content
|
|
349
|
+
if "example" in openapi_extra_keys:
|
|
350
|
+
_content["application/json"].example = openapi_extra.get("example") # type: ignore
|
|
351
|
+
if "examples" in openapi_extra_keys:
|
|
352
|
+
_content["application/json"].examples = openapi_extra.get("examples") # type: ignore
|
|
353
|
+
if "encoding" in openapi_extra_keys:
|
|
354
|
+
_content["application/json"].encoding = openapi_extra.get("encoding") # type: ignore
|
|
355
|
+
_content.update(openapi_extra.get("content", {})) # type: ignore
|
|
356
|
+
|
|
357
|
+
_schemas[name] = Schema(**schema)
|
|
358
|
+
definitions = schema.get("$defs")
|
|
359
|
+
if definitions:
|
|
360
|
+
# Add schema definitions to _schemas
|
|
361
|
+
for name, value in definitions.items():
|
|
362
|
+
_schemas[normalize_name(name)] = Schema(**value)
|
|
363
|
+
|
|
364
|
+
components_schemas.update(**_schemas)
|
|
365
|
+
operation.responses = _responses
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def parse_and_store_tags(
|
|
369
|
+
new_tags: list[Tag], old_tags: list[Tag], old_tag_names: list[str], operation: Operation
|
|
370
|
+
) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Parses new tags, stores them in an old_tags list if they are not already present,
|
|
373
|
+
and updates the tags attribute of the operation object.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
new_tags: A list of new Tag objects to be parsed and stored.
|
|
377
|
+
old_tags: The list of existing Tag objects.
|
|
378
|
+
old_tag_names: The list that names of existing tags.
|
|
379
|
+
operation: The operation object whose tag attribute needs to be updated.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
None
|
|
383
|
+
"""
|
|
384
|
+
# Iterate over each tag in new_tags
|
|
385
|
+
for tag in new_tags:
|
|
386
|
+
if tag.name not in old_tag_names:
|
|
387
|
+
old_tag_names.append(tag.name)
|
|
388
|
+
old_tags.append(tag)
|
|
389
|
+
|
|
390
|
+
# Set the tags attribute of the operation object to a list of unique tag names from new_tags
|
|
391
|
+
# If the resulting list is empty, set it to ["default"]
|
|
392
|
+
operation.tags = list(set([tag.name for tag in new_tags])) or ["default"]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def parse_parameters(
|
|
396
|
+
func: Callable,
|
|
397
|
+
*,
|
|
398
|
+
components_schemas: dict | None = None,
|
|
399
|
+
operation: Operation | None = None,
|
|
400
|
+
doc_ui: bool = True,
|
|
401
|
+
) -> ParametersTuple:
|
|
402
|
+
"""
|
|
403
|
+
Parses the parameters of a given function and returns the types for header, cookie, path,
|
|
404
|
+
query, form, and body parameters. Also populates the Operation object with the parsed parameters.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
func: The function to parse the parameters from.
|
|
408
|
+
components_schemas: Dictionary to store the parsed components schemas (default: None).
|
|
409
|
+
operation: Operation object to populate with parsed parameters (default: None).
|
|
410
|
+
doc_ui: Flag indicating whether to return types for documentation UI (default: True).
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
tuple[Type[BaseModel], Type[BaseModel], Type[BaseModel], Type[BaseModel], Type[BaseModel], Type[BaseModel]]:
|
|
414
|
+
The types for header, cookie, path, query, form, and body parameters respectively.
|
|
415
|
+
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
# If components_schemas is None, initialize it as an empty dictionary
|
|
419
|
+
if components_schemas is None:
|
|
420
|
+
components_schemas = dict()
|
|
421
|
+
|
|
422
|
+
# If operation is None, initialize it as an Operation object
|
|
423
|
+
if operation is None:
|
|
424
|
+
operation = Operation()
|
|
425
|
+
|
|
426
|
+
# Get the type hints from the function
|
|
427
|
+
annotations = get_type_hints(func)
|
|
428
|
+
|
|
429
|
+
# Get the types for header, cookie, path, query, form, and body parameters
|
|
430
|
+
header: Type[BaseModel] | None = annotations.get("header")
|
|
431
|
+
cookie: Type[BaseModel] | None = annotations.get("cookie")
|
|
432
|
+
path: Type[BaseModel] | None = annotations.get("path")
|
|
433
|
+
query: Type[BaseModel] | None = annotations.get("query")
|
|
434
|
+
form: Type[BaseModel] | None = annotations.get("form")
|
|
435
|
+
body: Type[BaseModel] | None = annotations.get("body")
|
|
436
|
+
raw: Type[RawModel] | None = annotations.get("raw")
|
|
437
|
+
|
|
438
|
+
# If doc_ui is False, return the types without further processing
|
|
439
|
+
if doc_ui is False:
|
|
440
|
+
return header, cookie, path, query, form, body, raw
|
|
441
|
+
|
|
442
|
+
parameters = []
|
|
443
|
+
|
|
444
|
+
if header:
|
|
445
|
+
_parameters, _components_schemas = parse_header(header)
|
|
446
|
+
parameters.extend(_parameters)
|
|
447
|
+
components_schemas.update(**_components_schemas)
|
|
448
|
+
|
|
449
|
+
if cookie:
|
|
450
|
+
_parameters, _components_schemas = parse_cookie(cookie)
|
|
451
|
+
parameters.extend(_parameters)
|
|
452
|
+
components_schemas.update(**_components_schemas)
|
|
453
|
+
|
|
454
|
+
if path:
|
|
455
|
+
_parameters, _components_schemas = parse_path(path)
|
|
456
|
+
parameters.extend(_parameters)
|
|
457
|
+
components_schemas.update(**_components_schemas)
|
|
458
|
+
|
|
459
|
+
if query:
|
|
460
|
+
_parameters, _components_schemas = parse_query(query)
|
|
461
|
+
parameters.extend(_parameters)
|
|
462
|
+
components_schemas.update(**_components_schemas)
|
|
463
|
+
|
|
464
|
+
if form:
|
|
465
|
+
_content, _components_schemas = parse_form(form)
|
|
466
|
+
components_schemas.update(**_components_schemas)
|
|
467
|
+
request_body = RequestBody(content=_content, required=True)
|
|
468
|
+
model_config: DefaultDict[str, Any] = form.model_config # type: ignore
|
|
469
|
+
openapi_extra = model_config.get("openapi_extra", {})
|
|
470
|
+
if openapi_extra:
|
|
471
|
+
openapi_extra_keys = openapi_extra.keys()
|
|
472
|
+
if "description" in openapi_extra_keys:
|
|
473
|
+
request_body.description = openapi_extra.get("description")
|
|
474
|
+
if "example" in openapi_extra_keys:
|
|
475
|
+
request_body.content["multipart/form-data"].example = openapi_extra.get("example")
|
|
476
|
+
if "examples" in openapi_extra_keys:
|
|
477
|
+
request_body.content["multipart/form-data"].examples = openapi_extra.get("examples")
|
|
478
|
+
if "encoding" in openapi_extra_keys:
|
|
479
|
+
request_body.content["multipart/form-data"].encoding = openapi_extra.get("encoding")
|
|
480
|
+
operation.requestBody = request_body
|
|
481
|
+
|
|
482
|
+
if body:
|
|
483
|
+
_content, _components_schemas = parse_body(body)
|
|
484
|
+
components_schemas.update(**_components_schemas)
|
|
485
|
+
request_body = RequestBody(content=_content, required=True)
|
|
486
|
+
model_config: DefaultDict[str, Any] = body.model_config # type: ignore
|
|
487
|
+
openapi_extra = model_config.get("openapi_extra", {})
|
|
488
|
+
if openapi_extra:
|
|
489
|
+
openapi_extra_keys = openapi_extra.keys()
|
|
490
|
+
if "description" in openapi_extra_keys:
|
|
491
|
+
request_body.description = openapi_extra.get("description")
|
|
492
|
+
request_body.required = openapi_extra.get("required", True)
|
|
493
|
+
if "example" in openapi_extra_keys:
|
|
494
|
+
request_body.content["application/json"].example = openapi_extra.get("example")
|
|
495
|
+
if "examples" in openapi_extra_keys:
|
|
496
|
+
request_body.content["application/json"].examples = openapi_extra.get("examples")
|
|
497
|
+
if "encoding" in openapi_extra_keys:
|
|
498
|
+
request_body.content["application/json"].encoding = openapi_extra.get("encoding")
|
|
499
|
+
operation.requestBody = request_body
|
|
500
|
+
|
|
501
|
+
if raw:
|
|
502
|
+
_content = {}
|
|
503
|
+
for mimetype in raw.mimetypes:
|
|
504
|
+
if mimetype.startswith("application/json"):
|
|
505
|
+
_content[mimetype] = MediaType(schema=Schema(type=DataType.OBJECT))
|
|
506
|
+
else:
|
|
507
|
+
_content[mimetype] = MediaType(schema=Schema(type=DataType.STRING))
|
|
508
|
+
request_body = RequestBody(content=_content)
|
|
509
|
+
operation.requestBody = request_body
|
|
510
|
+
|
|
511
|
+
if parameters:
|
|
512
|
+
# Set the parsed parameters in the operation object
|
|
513
|
+
operation.parameters = parameters
|
|
514
|
+
|
|
515
|
+
return header, cookie, path, query, form, body, raw
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def parse_method(uri: str, method: str, paths: dict, operation: Operation) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Parses the HTTP method and updates the corresponding PathItem object in the paths' dictionary.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
uri: The URI of the API endpoint.
|
|
524
|
+
method: The HTTP method for the API endpoint.
|
|
525
|
+
paths: A dictionary containing the API paths and their corresponding PathItem objects.
|
|
526
|
+
operation: The Operation object to assign to the PathItem.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
None
|
|
530
|
+
"""
|
|
531
|
+
# Check the HTTP method and update the PathItem object in the path dictionary
|
|
532
|
+
if method == HTTPMethod.GET:
|
|
533
|
+
if not paths.get(uri):
|
|
534
|
+
paths[uri] = PathItem(get=operation)
|
|
535
|
+
else:
|
|
536
|
+
paths[uri].get = operation
|
|
537
|
+
elif method == HTTPMethod.POST:
|
|
538
|
+
if not paths.get(uri):
|
|
539
|
+
paths[uri] = PathItem(post=operation)
|
|
540
|
+
else:
|
|
541
|
+
paths[uri].post = operation
|
|
542
|
+
elif method == HTTPMethod.PUT:
|
|
543
|
+
if not paths.get(uri):
|
|
544
|
+
paths[uri] = PathItem(put=operation)
|
|
545
|
+
else:
|
|
546
|
+
paths[uri].put = operation
|
|
547
|
+
elif method == HTTPMethod.PATCH:
|
|
548
|
+
if not paths.get(uri):
|
|
549
|
+
paths[uri] = PathItem(patch=operation)
|
|
550
|
+
else:
|
|
551
|
+
paths[uri].patch = operation
|
|
552
|
+
elif method == HTTPMethod.DELETE:
|
|
553
|
+
if not paths.get(uri):
|
|
554
|
+
paths[uri] = PathItem(delete=operation)
|
|
555
|
+
else:
|
|
556
|
+
paths[uri].delete = operation
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def make_validation_error_response(e: ValidationError) -> FlaskResponse:
|
|
560
|
+
"""
|
|
561
|
+
Create a Flask response for a validation error.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
e: The ValidationError object containing the details of the error.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
FlaskResponse: A Flask Response object with the JSON representation of the error.
|
|
568
|
+
"""
|
|
569
|
+
response = make_response(e.json())
|
|
570
|
+
response.headers["Content-Type"] = "application/json"
|
|
571
|
+
response.status_code = getattr(current_app, "validation_error_status", 422)
|
|
572
|
+
return response
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def run_validate_response(response: Any, responses: ResponseDict | None = None) -> Any:
|
|
576
|
+
"""Validate response"""
|
|
577
|
+
if responses is None:
|
|
578
|
+
return response
|
|
579
|
+
|
|
580
|
+
if isinstance(response, tuple): # noqa
|
|
581
|
+
_resp, status_code = response[:2]
|
|
582
|
+
elif isinstance(response, FlaskResponse):
|
|
583
|
+
if response.mimetype != "application/json":
|
|
584
|
+
# only application/json
|
|
585
|
+
return response
|
|
586
|
+
_resp, status_code = response.json, response.status_code # noqa
|
|
587
|
+
else:
|
|
588
|
+
_resp, status_code = response, 200
|
|
589
|
+
|
|
590
|
+
# status_code is http.HTTPStatus
|
|
591
|
+
if isinstance(status_code, HTTPStatus):
|
|
592
|
+
status_code = status_code.value
|
|
593
|
+
|
|
594
|
+
resp_model = responses.get(status_code)
|
|
595
|
+
|
|
596
|
+
if resp_model is None:
|
|
597
|
+
return response
|
|
598
|
+
|
|
599
|
+
assert inspect.isclass(resp_model) and issubclass(resp_model, BaseModel), (
|
|
600
|
+
f"{resp_model} is invalid `pydantic.BaseModel`"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if isinstance(_resp, str):
|
|
604
|
+
resp_model.model_validate_json(_resp)
|
|
605
|
+
else:
|
|
606
|
+
resp_model.model_validate(_resp)
|
|
607
|
+
|
|
608
|
+
return response
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def parse_rule(rule: str, url_prefix=None) -> str:
|
|
612
|
+
trail_slash = rule.endswith("/")
|
|
613
|
+
|
|
614
|
+
# Merge url_prefix and uri
|
|
615
|
+
uri = url_prefix.rstrip("/") + "/" + rule.lstrip("/") if url_prefix else rule
|
|
616
|
+
|
|
617
|
+
if not trail_slash:
|
|
618
|
+
uri = uri.rstrip("/")
|
|
619
|
+
|
|
620
|
+
# Convert a route parameter format from /pet/<petId> to /pet/{petId}
|
|
621
|
+
uri = re.sub(r"<([^<:]+:)?", "{", uri).replace(">", "}")
|
|
622
|
+
|
|
623
|
+
return uri
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def convert_responses_key_to_string(responses: ResponseDict) -> ResponseStrKeyDict:
|
|
627
|
+
"""Convert key to string"""
|
|
628
|
+
|
|
629
|
+
return {str(key.value if isinstance(key, HTTPStatus) else key): value for key, value in responses.items()}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def normalize_name(name: str) -> str:
|
|
633
|
+
return re.sub(r"[^\w.\-]", "_", name)
|