python-openapi 0.1.10__py3-none-any.whl → 0.3.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.
- pyopenapi/__init__.py +11 -51
- pyopenapi/decorators.py +58 -0
- pyopenapi/generator.py +85 -75
- pyopenapi/metadata.py +14 -4
- pyopenapi/operations.py +48 -33
- pyopenapi/options.py +19 -12
- pyopenapi/proxy.py +20 -10
- pyopenapi/specification.py +89 -70
- pyopenapi/utility.py +12 -8
- {python_openapi-0.1.10.dist-info → python_openapi-0.3.0.dist-info}/METADATA +28 -17
- python_openapi-0.3.0.dist-info/RECORD +17 -0
- {python_openapi-0.1.10.dist-info → python_openapi-0.3.0.dist-info}/WHEEL +1 -1
- {python_openapi-0.1.10.dist-info → python_openapi-0.3.0.dist-info/licenses}/LICENSE +1 -1
- pyopenapi/__main__.py +0 -0
- python_openapi-0.1.10.dist-info/RECORD +0 -17
- {python_openapi-0.1.10.dist-info → python_openapi-0.3.0.dist-info}/top_level.txt +0 -0
- {python_openapi-0.1.10.dist-info → python_openapi-0.3.0.dist-info}/zip-safe +0 -0
pyopenapi/operations.py
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import collections.abc
|
|
2
10
|
import enum
|
|
3
11
|
import inspect
|
|
4
12
|
import typing
|
|
5
13
|
import uuid
|
|
6
14
|
from dataclasses import dataclass
|
|
7
|
-
from
|
|
15
|
+
from types import NoneType
|
|
16
|
+
from typing import Any, Callable, Iterable, Iterator
|
|
8
17
|
|
|
9
18
|
from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
|
|
10
19
|
|
|
11
20
|
from .metadata import WebMethod
|
|
12
21
|
|
|
13
22
|
|
|
14
|
-
def split_prefix(s: str, sep: str, prefix:
|
|
23
|
+
def split_prefix(s: str, sep: str, prefix: str | Iterable[str]) -> tuple[str | None, str]:
|
|
15
24
|
"""
|
|
16
25
|
Recognizes a prefix at the beginning of a string.
|
|
17
26
|
|
|
@@ -34,11 +43,11 @@ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> Tuple[O
|
|
|
34
43
|
return None, s
|
|
35
44
|
|
|
36
45
|
|
|
37
|
-
def _get_annotation_type(annotation:
|
|
38
|
-
"Maps a
|
|
46
|
+
def _get_annotation_type(annotation: type[Any] | str, callable: Callable[..., Any]) -> type[Any]:
|
|
47
|
+
"Maps a string (forward) reference to a type, as if using `from __future__ import annotations`."
|
|
39
48
|
|
|
40
49
|
if isinstance(annotation, str):
|
|
41
|
-
return eval(annotation, callable.__globals__)
|
|
50
|
+
return typing.cast(type[Any], eval(annotation, callable.__globals__))
|
|
42
51
|
else:
|
|
43
52
|
return annotation
|
|
44
53
|
|
|
@@ -54,7 +63,7 @@ class HTTPMethod(enum.Enum):
|
|
|
54
63
|
PATCH = "PATCH"
|
|
55
64
|
|
|
56
65
|
|
|
57
|
-
OperationParameter =
|
|
66
|
+
OperationParameter = tuple[str, type[Any]]
|
|
58
67
|
|
|
59
68
|
|
|
60
69
|
class ValidationError(TypeError):
|
|
@@ -66,7 +75,7 @@ class EndpointOperation:
|
|
|
66
75
|
"""
|
|
67
76
|
Type information and metadata associated with an endpoint operation.
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
:param defining_class: The most specific class that defines the endpoint operation.
|
|
70
79
|
:param name: The short name of the endpoint operation.
|
|
71
80
|
:param func_name: The name of the function to invoke when the operation is triggered.
|
|
72
81
|
:param func_ref: The callable to invoke when the operation is triggered.
|
|
@@ -78,24 +87,26 @@ class EndpointOperation:
|
|
|
78
87
|
:param response_type: The Python type of the data that is transmitted in the response body.
|
|
79
88
|
:param http_method: The HTTP method used to invoke the endpoint such as POST, GET or PUT.
|
|
80
89
|
:param public: True if the operation can be invoked without prior authentication.
|
|
90
|
+
:param deprecated: True if consumers should refrain from using the operation.
|
|
81
91
|
:param request_examples: Sample requests that the operation might take.
|
|
82
92
|
:param response_examples: Sample responses that the operation might produce.
|
|
83
93
|
"""
|
|
84
94
|
|
|
85
|
-
defining_class: type
|
|
95
|
+
defining_class: type[Any]
|
|
86
96
|
name: str
|
|
87
97
|
func_name: str
|
|
88
98
|
func_ref: Callable[..., Any]
|
|
89
|
-
route:
|
|
90
|
-
path_params:
|
|
91
|
-
query_params:
|
|
92
|
-
request_param:
|
|
93
|
-
event_type:
|
|
94
|
-
response_type: type
|
|
99
|
+
route: str | None
|
|
100
|
+
path_params: list[OperationParameter]
|
|
101
|
+
query_params: list[OperationParameter]
|
|
102
|
+
request_param: OperationParameter | None
|
|
103
|
+
event_type: type[Any] | None
|
|
104
|
+
response_type: type[Any]
|
|
95
105
|
http_method: HTTPMethod
|
|
96
106
|
public: bool
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
deprecated: bool
|
|
108
|
+
request_examples: list[Any] | None = None
|
|
109
|
+
response_examples: list[Any] | None = None
|
|
99
110
|
|
|
100
111
|
def get_route(self) -> str:
|
|
101
112
|
if self.route is not None:
|
|
@@ -108,9 +119,9 @@ class EndpointOperation:
|
|
|
108
119
|
|
|
109
120
|
|
|
110
121
|
class _FormatParameterExtractor:
|
|
111
|
-
"A visitor to
|
|
122
|
+
"A visitor to extract parameters in a format string."
|
|
112
123
|
|
|
113
|
-
keys:
|
|
124
|
+
keys: list[str]
|
|
114
125
|
|
|
115
126
|
def __init__(self) -> None:
|
|
116
127
|
self.keys = []
|
|
@@ -120,13 +131,13 @@ class _FormatParameterExtractor:
|
|
|
120
131
|
return None
|
|
121
132
|
|
|
122
133
|
|
|
123
|
-
def _get_route_parameters(route: str) ->
|
|
134
|
+
def _get_route_parameters(route: str) -> list[str]:
|
|
124
135
|
extractor = _FormatParameterExtractor()
|
|
125
136
|
route.format_map(extractor)
|
|
126
137
|
return extractor.keys
|
|
127
138
|
|
|
128
139
|
|
|
129
|
-
def _get_endpoint_functions(endpoint: type, prefixes:
|
|
140
|
+
def _get_endpoint_functions(endpoint: type[Any], prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
|
|
130
141
|
if not inspect.isclass(endpoint):
|
|
131
142
|
raise ValidationError(f"object is not a class type: {endpoint}")
|
|
132
143
|
|
|
@@ -139,7 +150,7 @@ def _get_endpoint_functions(endpoint: type, prefixes: List[str]) -> Iterator[Tup
|
|
|
139
150
|
yield prefix, operation_name, func_name, func_ref
|
|
140
151
|
|
|
141
152
|
|
|
142
|
-
def _get_defining_class(member_fn: str, derived_cls: type) -> type:
|
|
153
|
+
def _get_defining_class(member_fn: str, derived_cls: type[Any]) -> type[Any]:
|
|
143
154
|
"Find the class in which a member function is first defined in a class inheritance hierarchy."
|
|
144
155
|
|
|
145
156
|
# iterate in reverse member resolution order to find most specific class first
|
|
@@ -151,7 +162,7 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
|
|
|
151
162
|
raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
|
|
152
163
|
|
|
153
164
|
|
|
154
|
-
def get_endpoint_operations(endpoint: type, use_examples: bool = True) ->
|
|
165
|
+
def get_endpoint_operations(endpoint: type[Any], use_examples: bool = True) -> list[EndpointOperation]:
|
|
155
166
|
"""
|
|
156
167
|
Extracts a list of member functions in a class eligible for HTTP interface binding.
|
|
157
168
|
|
|
@@ -171,7 +182,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
171
182
|
:param use_examples: Whether to return examples associated with member functions.
|
|
172
183
|
"""
|
|
173
184
|
|
|
174
|
-
result = []
|
|
185
|
+
result: list[EndpointOperation] = []
|
|
175
186
|
|
|
176
187
|
for prefix, operation_name, func_name, func_ref in _get_endpoint_functions(
|
|
177
188
|
endpoint,
|
|
@@ -188,25 +199,27 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
188
199
|
],
|
|
189
200
|
):
|
|
190
201
|
# extract routing information from function metadata
|
|
191
|
-
webmethod:
|
|
202
|
+
webmethod: WebMethod | None = getattr(func_ref, "__webmethod__", None)
|
|
192
203
|
if webmethod is not None:
|
|
193
204
|
route = webmethod.route
|
|
194
205
|
route_params = _get_route_parameters(route) if route is not None else None
|
|
195
206
|
public = webmethod.public
|
|
207
|
+
deprecated = webmethod.deprecated
|
|
196
208
|
request_examples = webmethod.request_examples
|
|
197
209
|
response_examples = webmethod.response_examples
|
|
198
210
|
else:
|
|
199
211
|
route = None
|
|
200
212
|
route_params = None
|
|
201
213
|
public = False
|
|
214
|
+
deprecated = False
|
|
202
215
|
request_examples = None
|
|
203
216
|
response_examples = None
|
|
204
217
|
|
|
205
218
|
# inspect function signature for path and query parameters, and request/response payload type
|
|
206
219
|
signature = get_signature(func_ref)
|
|
207
220
|
|
|
208
|
-
path_params = []
|
|
209
|
-
query_params = []
|
|
221
|
+
path_params: list[tuple[str, type[Any]]] = []
|
|
222
|
+
query_params: list[tuple[str, type[Any]]] = []
|
|
210
223
|
request_param = None
|
|
211
224
|
|
|
212
225
|
for param_name, parameter in signature.parameters.items():
|
|
@@ -221,7 +234,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
221
234
|
raise ValidationError(f"parameter '{param_name}' in function '{func_name}' has no type annotation")
|
|
222
235
|
|
|
223
236
|
if is_type_optional(param_type):
|
|
224
|
-
inner_type: type = unwrap_optional_type(param_type)
|
|
237
|
+
inner_type: type[Any] = unwrap_optional_type(param_type)
|
|
225
238
|
else:
|
|
226
239
|
inner_type = param_type
|
|
227
240
|
|
|
@@ -247,7 +260,8 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
247
260
|
if request_param is not None:
|
|
248
261
|
param = (param_name, param_type)
|
|
249
262
|
raise ValidationError(
|
|
250
|
-
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}':
|
|
263
|
+
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': "
|
|
264
|
+
f"{request_param} and {param}"
|
|
251
265
|
)
|
|
252
266
|
|
|
253
267
|
# composite types are read from body
|
|
@@ -263,7 +277,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
263
277
|
# where YieldType is the event type, SendType is None, and ReturnType is the immediate response type to the request
|
|
264
278
|
if typing.get_origin(return_type) is collections.abc.Generator:
|
|
265
279
|
event_type, send_type, response_type = typing.get_args(return_type)
|
|
266
|
-
if send_type is not
|
|
280
|
+
if send_type is not NoneType:
|
|
267
281
|
raise ValidationError(
|
|
268
282
|
f"function '{func_name}' has a return type Generator[Y,S,R] and therefore looks like an event but has an explicit send type"
|
|
269
283
|
)
|
|
@@ -299,6 +313,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
299
313
|
response_type=response_type,
|
|
300
314
|
http_method=http_method,
|
|
301
315
|
public=public,
|
|
316
|
+
deprecated=deprecated,
|
|
302
317
|
request_examples=request_examples if use_examples else None,
|
|
303
318
|
response_examples=response_examples if use_examples else None,
|
|
304
319
|
)
|
|
@@ -310,8 +325,8 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
310
325
|
return result
|
|
311
326
|
|
|
312
327
|
|
|
313
|
-
def get_endpoint_events(endpoint: type) ->
|
|
314
|
-
results = {}
|
|
328
|
+
def get_endpoint_events(endpoint: type[Any]) -> dict[str, type[Any]]:
|
|
329
|
+
results: dict[str, type[Any]] = {}
|
|
315
330
|
|
|
316
331
|
for decl in typing.get_type_hints(endpoint).values():
|
|
317
332
|
# check if signature is Callable[...]
|
|
@@ -324,11 +339,11 @@ def get_endpoint_events(endpoint: type) -> Dict[str, type]:
|
|
|
324
339
|
if len(args) != 2:
|
|
325
340
|
continue
|
|
326
341
|
params_type, return_type = args
|
|
327
|
-
if
|
|
342
|
+
if typing.get_origin(params_type) is not list:
|
|
328
343
|
continue
|
|
329
344
|
|
|
330
345
|
# check if signature is Callable[[...], None]
|
|
331
|
-
if not issubclass(return_type,
|
|
346
|
+
if not issubclass(return_type, NoneType):
|
|
332
347
|
continue
|
|
333
348
|
|
|
334
349
|
# check if signature is Callable[[EventType], None]
|
pyopenapi/options.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import dataclasses
|
|
2
10
|
from dataclasses import dataclass
|
|
3
11
|
from http import HTTPStatus
|
|
4
|
-
from typing import Callable, ClassVar
|
|
12
|
+
from typing import Any, Callable, ClassVar
|
|
5
13
|
|
|
6
|
-
from .specification import Info, SecurityScheme
|
|
14
|
+
from .specification import Info, SecurityScheme, Server
|
|
7
15
|
from .specification import SecuritySchemeAPI as SecuritySchemeAPI
|
|
8
16
|
from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
|
|
9
17
|
from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
|
|
10
|
-
from .specification import Server
|
|
11
18
|
|
|
12
|
-
HTTPStatusCode =
|
|
19
|
+
HTTPStatusCode = HTTPStatus | int | str
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
@dataclass
|
|
@@ -30,17 +37,17 @@ class Options:
|
|
|
30
37
|
|
|
31
38
|
server: Server
|
|
32
39
|
info: Info
|
|
33
|
-
version:
|
|
34
|
-
default_security_scheme:
|
|
35
|
-
extra_types:
|
|
40
|
+
version: tuple[int, int, int] = (3, 1, 0)
|
|
41
|
+
default_security_scheme: SecurityScheme | None = None
|
|
42
|
+
extra_types: list[type[Any]] | dict[str, list[type[Any]]] | None = None
|
|
36
43
|
use_examples: bool = True
|
|
37
|
-
success_responses:
|
|
38
|
-
error_responses:
|
|
44
|
+
success_responses: dict[type[Any], HTTPStatusCode] = dataclasses.field(default_factory=dict[type[Any], HTTPStatusCode])
|
|
45
|
+
error_responses: dict[type[Any], HTTPStatusCode] = dataclasses.field(default_factory=dict[type[Any], HTTPStatusCode])
|
|
39
46
|
error_wrapper: bool = False
|
|
40
|
-
property_description_fun:
|
|
41
|
-
captions:
|
|
47
|
+
property_description_fun: Callable[[type, str, str], str] | None = None
|
|
48
|
+
captions: dict[str, str] | None = None
|
|
42
49
|
|
|
43
|
-
default_captions: ClassVar[
|
|
50
|
+
default_captions: ClassVar[dict[str, str]] = {
|
|
44
51
|
"Operations": "Operations",
|
|
45
52
|
"Types": "Types",
|
|
46
53
|
"Events": "Events",
|
pyopenapi/proxy.py
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import json
|
|
2
|
-
|
|
10
|
+
import typing
|
|
11
|
+
from typing import Any, Callable, TypeVar
|
|
3
12
|
|
|
4
13
|
import aiohttp
|
|
14
|
+
from strong_typing.inspection import get_signature
|
|
5
15
|
from strong_typing.serialization import json_to_object, object_to_json
|
|
6
16
|
|
|
7
|
-
from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations
|
|
17
|
+
from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations
|
|
8
18
|
|
|
9
19
|
|
|
10
20
|
async def make_request(
|
|
11
21
|
http_method: HTTPMethod,
|
|
12
22
|
server: str,
|
|
13
23
|
path: str,
|
|
14
|
-
query:
|
|
15
|
-
data:
|
|
16
|
-
) ->
|
|
24
|
+
query: dict[str, str],
|
|
25
|
+
data: str | None,
|
|
26
|
+
) -> tuple[int, str]:
|
|
17
27
|
"Makes an asynchronous HTTP request and returns the response."
|
|
18
28
|
|
|
19
29
|
headers = {"Accept": "application/json"}
|
|
@@ -90,14 +100,14 @@ class OperationProxy:
|
|
|
90
100
|
data = None
|
|
91
101
|
|
|
92
102
|
# make HTTP request
|
|
93
|
-
|
|
103
|
+
_status, response = await make_request(self.op.http_method, endpoint_proxy.base_url, path, query, data)
|
|
94
104
|
|
|
95
105
|
# process HTTP response
|
|
96
106
|
if response:
|
|
97
107
|
try:
|
|
98
108
|
s = json.loads(response)
|
|
99
109
|
except json.JSONDecodeError:
|
|
100
|
-
raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}")
|
|
110
|
+
raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}") from None
|
|
101
111
|
|
|
102
112
|
return json_to_object(self.op.response_type, s)
|
|
103
113
|
else:
|
|
@@ -118,7 +128,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
|
|
|
118
128
|
T = TypeVar("T")
|
|
119
129
|
|
|
120
130
|
|
|
121
|
-
def make_proxy_class(api:
|
|
131
|
+
def make_proxy_class(api: type[T]) -> type[T]:
|
|
122
132
|
"""
|
|
123
133
|
Creates a proxy class for calling an HTTP REST API.
|
|
124
134
|
|
|
@@ -127,5 +137,5 @@ def make_proxy_class(api: Type[T]) -> Type[T]:
|
|
|
127
137
|
|
|
128
138
|
ops = get_endpoint_operations(api)
|
|
129
139
|
properties = {op.func_name: _get_operation_proxy(op) for op in ops}
|
|
130
|
-
proxy = type(f"{api.__name__}Proxy", (api, EndpointProxy), properties)
|
|
131
|
-
return proxy
|
|
140
|
+
proxy = type(f"{api.__name__}Proxy", (api, EndpointProxy), properties) # pyright: ignore[reportGeneralTypeIssues]
|
|
141
|
+
return typing.cast(type[T], proxy)
|
pyopenapi/specification.py
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import dataclasses
|
|
2
10
|
import enum
|
|
3
11
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, ClassVar
|
|
12
|
+
from typing import Any, ClassVar
|
|
5
13
|
|
|
6
|
-
from strong_typing.
|
|
7
|
-
from strong_typing.
|
|
14
|
+
from strong_typing.core import JsonType as JsonType
|
|
15
|
+
from strong_typing.core import Schema, StrictJsonType
|
|
8
16
|
|
|
9
17
|
URL = str
|
|
10
18
|
|
|
@@ -23,9 +31,6 @@ class SchemaRef(Ref):
|
|
|
23
31
|
ref_type: ClassVar[str] = "schemas"
|
|
24
32
|
|
|
25
33
|
|
|
26
|
-
SchemaOrRef = Union[Schema, SchemaRef]
|
|
27
|
-
|
|
28
|
-
|
|
29
34
|
@dataclass
|
|
30
35
|
class ResponseRef(Ref):
|
|
31
36
|
ref_type: ClassVar[str] = "responses"
|
|
@@ -43,45 +48,45 @@ class ExampleRef(Ref):
|
|
|
43
48
|
|
|
44
49
|
@dataclass
|
|
45
50
|
class Contact:
|
|
46
|
-
name:
|
|
47
|
-
url:
|
|
48
|
-
email:
|
|
51
|
+
name: str | None = None
|
|
52
|
+
url: URL | None = None
|
|
53
|
+
email: str | None = None
|
|
49
54
|
|
|
50
55
|
|
|
51
56
|
@dataclass
|
|
52
57
|
class License:
|
|
53
58
|
name: str
|
|
54
|
-
url:
|
|
59
|
+
url: URL | None = None
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
@dataclass
|
|
58
63
|
class Info:
|
|
59
64
|
title: str
|
|
60
65
|
version: str
|
|
61
|
-
description:
|
|
62
|
-
termsOfService:
|
|
63
|
-
contact:
|
|
64
|
-
license:
|
|
66
|
+
description: str | None = None
|
|
67
|
+
termsOfService: str | None = None
|
|
68
|
+
contact: Contact | None = None
|
|
69
|
+
license: License | None = None
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
@dataclass
|
|
68
73
|
class MediaType:
|
|
69
|
-
schema:
|
|
70
|
-
example:
|
|
71
|
-
examples:
|
|
74
|
+
schema: Schema | SchemaRef | None = None
|
|
75
|
+
example: Any | None = None
|
|
76
|
+
examples: dict[str, "Example | ExampleRef"] | None = None
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
@dataclass
|
|
75
80
|
class RequestBody:
|
|
76
|
-
content:
|
|
77
|
-
description:
|
|
78
|
-
required:
|
|
81
|
+
content: dict[str, MediaType]
|
|
82
|
+
description: str | None = None
|
|
83
|
+
required: bool | None = None
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
@dataclass
|
|
82
87
|
class Response:
|
|
83
88
|
description: str
|
|
84
|
-
content:
|
|
89
|
+
content: dict[str, MediaType] | None = None
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
@enum.unique
|
|
@@ -96,37 +101,38 @@ class ParameterLocation(enum.Enum):
|
|
|
96
101
|
class Parameter:
|
|
97
102
|
name: str
|
|
98
103
|
in_: ParameterLocation
|
|
99
|
-
description:
|
|
100
|
-
required:
|
|
101
|
-
schema:
|
|
102
|
-
example:
|
|
104
|
+
description: str | None = None
|
|
105
|
+
required: bool | None = None
|
|
106
|
+
schema: Schema | SchemaRef | None = None
|
|
107
|
+
example: Any | None = None
|
|
103
108
|
|
|
104
109
|
|
|
105
110
|
@dataclass
|
|
106
111
|
class Operation:
|
|
107
|
-
responses:
|
|
108
|
-
tags:
|
|
109
|
-
summary:
|
|
110
|
-
description:
|
|
111
|
-
operationId:
|
|
112
|
-
parameters:
|
|
113
|
-
requestBody:
|
|
114
|
-
callbacks:
|
|
115
|
-
security:
|
|
112
|
+
responses: dict[str, Response | ResponseRef]
|
|
113
|
+
tags: list[str] | None = None
|
|
114
|
+
summary: str | None = None
|
|
115
|
+
description: str | None = None
|
|
116
|
+
operationId: str | None = None
|
|
117
|
+
parameters: list[Parameter] | None = None
|
|
118
|
+
requestBody: RequestBody | None = None
|
|
119
|
+
callbacks: dict[str, "Callback"] | None = None
|
|
120
|
+
security: list["SecurityRequirement"] | None = None
|
|
121
|
+
deprecated: bool | None = None
|
|
116
122
|
|
|
117
123
|
|
|
118
124
|
@dataclass
|
|
119
125
|
class PathItem:
|
|
120
|
-
summary:
|
|
121
|
-
description:
|
|
122
|
-
get:
|
|
123
|
-
put:
|
|
124
|
-
post:
|
|
125
|
-
delete:
|
|
126
|
-
options:
|
|
127
|
-
head:
|
|
128
|
-
patch:
|
|
129
|
-
trace:
|
|
126
|
+
summary: str | None = None
|
|
127
|
+
description: str | None = None
|
|
128
|
+
get: Operation | None = None
|
|
129
|
+
put: Operation | None = None
|
|
130
|
+
post: Operation | None = None
|
|
131
|
+
delete: Operation | None = None
|
|
132
|
+
options: Operation | None = None
|
|
133
|
+
head: Operation | None = None
|
|
134
|
+
patch: Operation | None = None
|
|
135
|
+
trace: Operation | None = None
|
|
130
136
|
|
|
131
137
|
def update(self, other: "PathItem") -> None:
|
|
132
138
|
"Merges another instance of this class into this object."
|
|
@@ -138,21 +144,21 @@ class PathItem:
|
|
|
138
144
|
|
|
139
145
|
|
|
140
146
|
# maps run-time expressions such as "$request.body#/url" to path items
|
|
141
|
-
Callback =
|
|
147
|
+
Callback = dict[str, PathItem]
|
|
142
148
|
|
|
143
149
|
|
|
144
150
|
@dataclass
|
|
145
151
|
class Example:
|
|
146
|
-
summary:
|
|
147
|
-
description:
|
|
148
|
-
value:
|
|
149
|
-
externalValue:
|
|
152
|
+
summary: str | None = None
|
|
153
|
+
description: str | None = None
|
|
154
|
+
value: Any | None = None
|
|
155
|
+
externalValue: URL | None = None
|
|
150
156
|
|
|
151
157
|
|
|
152
158
|
@dataclass
|
|
153
159
|
class Server:
|
|
154
160
|
url: URL
|
|
155
|
-
description:
|
|
161
|
+
description: str | None = None
|
|
156
162
|
|
|
157
163
|
|
|
158
164
|
@enum.unique
|
|
@@ -183,9 +189,9 @@ class SecuritySchemeAPI(SecurityScheme):
|
|
|
183
189
|
@dataclass(init=False)
|
|
184
190
|
class SecuritySchemeHTTP(SecurityScheme):
|
|
185
191
|
scheme: str
|
|
186
|
-
bearerFormat:
|
|
192
|
+
bearerFormat: str | None = None
|
|
187
193
|
|
|
188
|
-
def __init__(self, description: str, scheme: str, bearerFormat:
|
|
194
|
+
def __init__(self, description: str, scheme: str, bearerFormat: str | None = None) -> None:
|
|
189
195
|
super().__init__(SecuritySchemeType.HTTP, description)
|
|
190
196
|
self.scheme = scheme
|
|
191
197
|
self.bearerFormat = bearerFormat
|
|
@@ -202,24 +208,24 @@ class SecuritySchemeOpenIDConnect(SecurityScheme):
|
|
|
202
208
|
|
|
203
209
|
@dataclass
|
|
204
210
|
class Components:
|
|
205
|
-
schemas:
|
|
206
|
-
responses:
|
|
207
|
-
parameters:
|
|
208
|
-
examples:
|
|
209
|
-
requestBodies:
|
|
210
|
-
securitySchemes:
|
|
211
|
-
callbacks:
|
|
211
|
+
schemas: dict[str, Schema] | None = None
|
|
212
|
+
responses: dict[str, Response] | None = None
|
|
213
|
+
parameters: dict[str, Parameter] | None = None
|
|
214
|
+
examples: dict[str, Example] | None = None
|
|
215
|
+
requestBodies: dict[str, RequestBody] | None = None
|
|
216
|
+
securitySchemes: dict[str, SecurityScheme] | None = None
|
|
217
|
+
callbacks: dict[str, Callback] | None = None
|
|
212
218
|
|
|
213
219
|
|
|
214
220
|
SecurityScope = str
|
|
215
|
-
SecurityRequirement =
|
|
221
|
+
SecurityRequirement = dict[str, list[SecurityScope]]
|
|
216
222
|
|
|
217
223
|
|
|
218
224
|
@dataclass
|
|
219
225
|
class Tag:
|
|
220
226
|
name: str
|
|
221
|
-
description:
|
|
222
|
-
displayName:
|
|
227
|
+
description: str | None = None
|
|
228
|
+
displayName: str | None = None
|
|
223
229
|
|
|
224
230
|
|
|
225
231
|
@dataclass
|
|
@@ -228,10 +234,13 @@ class TagGroup:
|
|
|
228
234
|
A ReDoc extension to provide information about groups of tags.
|
|
229
235
|
|
|
230
236
|
Exposed via the vendor-specific property "x-tagGroups" of the top-level object.
|
|
237
|
+
|
|
238
|
+
:param name: Group name.
|
|
239
|
+
:param tags: Tags that are members of this group.
|
|
231
240
|
"""
|
|
232
241
|
|
|
233
242
|
name: str
|
|
234
|
-
tags:
|
|
243
|
+
tags: list[str]
|
|
235
244
|
|
|
236
245
|
|
|
237
246
|
@dataclass
|
|
@@ -240,14 +249,24 @@ class Document:
|
|
|
240
249
|
This class is a Python dataclass adaptation of the OpenAPI Specification.
|
|
241
250
|
|
|
242
251
|
For details, see <https://swagger.io/specification/>
|
|
252
|
+
|
|
253
|
+
:param openapi: Version number of the OpenAPI Specification that the OpenAPI document uses.
|
|
254
|
+
:param info: Provides metadata about the API.
|
|
255
|
+
:param servers: An array of objects that provide connectivity information to a target server.
|
|
256
|
+
:param paths: The available paths and operations for the API.
|
|
257
|
+
:param jsonSchemaDialect: The default value for the `$schema` keyword within schema objects in this document, in the form of a URI.
|
|
258
|
+
:param components: An element to hold various objects for the OpenAPI description.
|
|
259
|
+
:param security: A declaration of which security mechanisms can be used across the API.
|
|
260
|
+
:param tags: A list of tags used by the OpenAPI description with additional metadata.
|
|
261
|
+
:param tagGroups: Provides information about a group of tags. (Extension to OpenAPI.)
|
|
243
262
|
"""
|
|
244
263
|
|
|
245
264
|
openapi: str
|
|
246
265
|
info: Info
|
|
247
|
-
servers:
|
|
248
|
-
paths:
|
|
249
|
-
jsonSchemaDialect:
|
|
250
|
-
components:
|
|
251
|
-
security:
|
|
252
|
-
tags:
|
|
253
|
-
tagGroups:
|
|
266
|
+
servers: list[Server]
|
|
267
|
+
paths: dict[str, PathItem]
|
|
268
|
+
jsonSchemaDialect: str | None = None
|
|
269
|
+
components: Components | None = None
|
|
270
|
+
security: list[SecurityRequirement] | None = None
|
|
271
|
+
tags: list[Tag] | None = None
|
|
272
|
+
tagGroups: list[TagGroup] | None = None
|
pyopenapi/utility.py
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import importlib.resources
|
|
2
10
|
import json
|
|
3
|
-
import sys
|
|
4
11
|
import typing
|
|
5
12
|
from typing import TextIO
|
|
6
13
|
|
|
7
|
-
from strong_typing.
|
|
14
|
+
from strong_typing.core import StrictJsonType
|
|
15
|
+
from strong_typing.serialization import object_to_json
|
|
8
16
|
|
|
9
17
|
from .generator import Generator
|
|
10
18
|
from .options import Options
|
|
@@ -94,12 +102,8 @@ class Specification:
|
|
|
94
102
|
:param pretty_print: Whether to use line indents to beautify the JSON string in the HTML file.
|
|
95
103
|
"""
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
html_template = html_template_file.read()
|
|
100
|
-
else:
|
|
101
|
-
with importlib.resources.open_text(__package__, "template.html", encoding="utf-8", errors="strict") as html_template_file:
|
|
102
|
-
html_template = html_template_file.read()
|
|
105
|
+
with importlib.resources.files(__package__).joinpath("template.html").open(encoding="utf-8", errors="strict") as html_template_file:
|
|
106
|
+
html_template = html_template_file.read()
|
|
103
107
|
|
|
104
108
|
html = html_template.replace(
|
|
105
109
|
"{ /* OPENAPI_SPECIFICATION */ }",
|