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.
Files changed (53) hide show
  1. flask_openapi/__init__.py +44 -0
  2. flask_openapi/__version__.py +5 -0
  3. flask_openapi/blueprint.py +201 -0
  4. flask_openapi/commands.py +38 -0
  5. flask_openapi/models/__init__.py +91 -0
  6. flask_openapi/models/callback.py +18 -0
  7. flask_openapi/models/components.py +37 -0
  8. flask_openapi/models/contact.py +17 -0
  9. flask_openapi/models/data_type.py +18 -0
  10. flask_openapi/models/discriminator.py +16 -0
  11. flask_openapi/models/encoding.py +27 -0
  12. flask_openapi/models/example.py +19 -0
  13. flask_openapi/models/external_documentation.py +16 -0
  14. flask_openapi/models/file.py +27 -0
  15. flask_openapi/models/header.py +17 -0
  16. flask_openapi/models/info.py +24 -0
  17. flask_openapi/models/license.py +17 -0
  18. flask_openapi/models/link.py +23 -0
  19. flask_openapi/models/media_type.py +24 -0
  20. flask_openapi/models/oauth_flow.py +18 -0
  21. flask_openapi/models/oauth_flows.py +20 -0
  22. flask_openapi/models/operation.py +36 -0
  23. flask_openapi/models/parameter.py +34 -0
  24. flask_openapi/models/parameter_in_type.py +13 -0
  25. flask_openapi/models/path_item.py +36 -0
  26. flask_openapi/models/paths.py +9 -0
  27. flask_openapi/models/reference.py +14 -0
  28. flask_openapi/models/request_body.py +19 -0
  29. flask_openapi/models/response.py +23 -0
  30. flask_openapi/models/responses.py +11 -0
  31. flask_openapi/models/schema.py +59 -0
  32. flask_openapi/models/security_requirement.py +8 -0
  33. flask_openapi/models/security_scheme.py +25 -0
  34. flask_openapi/models/security_scheme_in_type.py +12 -0
  35. flask_openapi/models/server.py +19 -0
  36. flask_openapi/models/server_variable.py +17 -0
  37. flask_openapi/models/style_values.py +14 -0
  38. flask_openapi/models/tag.py +15 -0
  39. flask_openapi/models/validation_error.py +24 -0
  40. flask_openapi/models/xml.py +19 -0
  41. flask_openapi/openapi.py +449 -0
  42. flask_openapi/plugins.py +17 -0
  43. flask_openapi/py.typed +0 -0
  44. flask_openapi/request.py +250 -0
  45. flask_openapi/scaffold.py +547 -0
  46. flask_openapi/templates.py +113 -0
  47. flask_openapi/types.py +27 -0
  48. flask_openapi/utils.py +633 -0
  49. flask_openapi/view.py +230 -0
  50. flask_openapi-4.3.1.dist-info/METADATA +258 -0
  51. flask_openapi-4.3.1.dist-info/RECORD +53 -0
  52. flask_openapi-4.3.1.dist-info/WHEEL +4 -0
  53. 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)