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/__init__.py
CHANGED
|
@@ -1,54 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
from .options import * # noqa: F403
|
|
5
|
-
from .utility import Specification as Specification
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
request_example: Optional[Any] = None,
|
|
16
|
-
response_example: Optional[Any] = None,
|
|
17
|
-
request_examples: Optional[List[Any]] = None,
|
|
18
|
-
response_examples: Optional[List[Any]] = None,
|
|
19
|
-
) -> Callable[[T], T]:
|
|
20
|
-
"""
|
|
21
|
-
Decorator that supplies additional metadata to an endpoint operation function.
|
|
22
|
-
|
|
23
|
-
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
24
|
-
:param public: True if the operation can be invoked without prior authentication.
|
|
25
|
-
:param request_example: A sample request that the operation might take.
|
|
26
|
-
:param response_example: A sample response that the operation might produce.
|
|
27
|
-
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
28
|
-
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
if request_example is not None and request_examples is not None:
|
|
32
|
-
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
|
|
33
|
-
if response_example is not None and response_examples is not None:
|
|
34
|
-
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
|
|
35
|
-
|
|
36
|
-
if request_example:
|
|
37
|
-
request_examples = [request_example]
|
|
38
|
-
if response_example:
|
|
39
|
-
response_examples = [response_example]
|
|
40
|
-
|
|
41
|
-
def wrap(cls: T) -> T:
|
|
42
|
-
setattr(
|
|
43
|
-
cls,
|
|
44
|
-
"__webmethod__",
|
|
45
|
-
WebMethod(
|
|
46
|
-
route=route,
|
|
47
|
-
public=public or False,
|
|
48
|
-
request_examples=request_examples,
|
|
49
|
-
response_examples=response_examples,
|
|
50
|
-
),
|
|
51
|
-
)
|
|
52
|
-
return cls
|
|
53
|
-
|
|
54
|
-
return wrap
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
|
+
__author__ = "Levente Hunyadi"
|
|
11
|
+
__copyright__ = "Copyright 2021-2026, Levente Hunyadi"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
__maintainer__ = "Levente Hunyadi"
|
|
14
|
+
__status__ = "Production"
|
pyopenapi/decorators.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
|
|
9
|
+
from typing import Any, Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
from .metadata import WebMethod
|
|
12
|
+
from .options import * # noqa: F403
|
|
13
|
+
from .utility import Specification as Specification
|
|
14
|
+
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def webmethod(
|
|
19
|
+
route: str | None = None,
|
|
20
|
+
public: bool = False,
|
|
21
|
+
deprecated: bool = False,
|
|
22
|
+
request_example: Any | None = None,
|
|
23
|
+
response_example: Any | None = None,
|
|
24
|
+
request_examples: list[Any] | None = None,
|
|
25
|
+
response_examples: list[Any] | None = None,
|
|
26
|
+
) -> Callable[[F], F]:
|
|
27
|
+
"""
|
|
28
|
+
Decorator that supplies additional metadata to an endpoint operation function.
|
|
29
|
+
|
|
30
|
+
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
31
|
+
:param public: True if the operation can be invoked without prior authentication.
|
|
32
|
+
:param request_example: A sample request that the operation might take.
|
|
33
|
+
:param response_example: A sample response that the operation might produce.
|
|
34
|
+
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
35
|
+
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if request_example is not None and request_examples is not None:
|
|
39
|
+
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
|
|
40
|
+
if response_example is not None and response_examples is not None:
|
|
41
|
+
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
|
|
42
|
+
|
|
43
|
+
if request_example:
|
|
44
|
+
request_examples = [request_example]
|
|
45
|
+
if response_example:
|
|
46
|
+
response_examples = [response_example]
|
|
47
|
+
|
|
48
|
+
def wrap(cls: F) -> F:
|
|
49
|
+
cls.__webmethod__ = WebMethod( # type: ignore[attr-defined]
|
|
50
|
+
route=route,
|
|
51
|
+
public=public or False,
|
|
52
|
+
deprecated=deprecated or False,
|
|
53
|
+
request_examples=request_examples,
|
|
54
|
+
response_examples=response_examples,
|
|
55
|
+
)
|
|
56
|
+
return cls
|
|
57
|
+
|
|
58
|
+
return wrap
|
pyopenapi/generator.py
CHANGED
|
@@ -1,16 +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 dataclasses
|
|
2
10
|
import hashlib
|
|
3
11
|
import ipaddress
|
|
12
|
+
import operator
|
|
4
13
|
import typing
|
|
5
14
|
from dataclasses import dataclass
|
|
15
|
+
from functools import reduce
|
|
6
16
|
from http import HTTPStatus
|
|
7
|
-
from typing import Any, Callable
|
|
17
|
+
from typing import Any, Callable
|
|
8
18
|
|
|
9
|
-
from strong_typing.core import JsonType
|
|
19
|
+
from strong_typing.core import JsonType, Schema
|
|
10
20
|
from strong_typing.docstring import Docstring, parse_type
|
|
11
|
-
from strong_typing.inspection import
|
|
21
|
+
from strong_typing.inspection import is_type_optional, is_type_union, unwrap_optional_type, unwrap_union_types
|
|
12
22
|
from strong_typing.name import python_type_to_name
|
|
13
|
-
from strong_typing.schema import JsonSchemaGenerator,
|
|
23
|
+
from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
|
|
14
24
|
from strong_typing.serialization import json_dump_string, object_to_json
|
|
15
25
|
|
|
16
26
|
from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
|
|
@@ -28,7 +38,6 @@ from .specification import (
|
|
|
28
38
|
RequestBody,
|
|
29
39
|
Response,
|
|
30
40
|
ResponseRef,
|
|
31
|
-
SchemaOrRef,
|
|
32
41
|
SchemaRef,
|
|
33
42
|
Tag,
|
|
34
43
|
TagGroup,
|
|
@@ -66,25 +75,24 @@ register_schema(
|
|
|
66
75
|
def http_status_to_string(status_code: HTTPStatusCode) -> str:
|
|
67
76
|
"Converts an HTTP status code to a string."
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
raise TypeError("expected: HTTP status code")
|
|
78
|
+
match status_code:
|
|
79
|
+
case HTTPStatus():
|
|
80
|
+
return str(status_code.value)
|
|
81
|
+
case int():
|
|
82
|
+
return str(status_code)
|
|
83
|
+
case str():
|
|
84
|
+
return status_code
|
|
77
85
|
|
|
78
86
|
|
|
79
87
|
class SchemaBuilder:
|
|
80
88
|
schema_generator: JsonSchemaGenerator
|
|
81
|
-
schemas:
|
|
89
|
+
schemas: dict[str, Schema]
|
|
82
90
|
|
|
83
91
|
def __init__(self, schema_generator: JsonSchemaGenerator) -> None:
|
|
84
92
|
self.schema_generator = schema_generator
|
|
85
93
|
self.schemas = {}
|
|
86
94
|
|
|
87
|
-
def classdef_to_schema(self, typ: type) -> Schema:
|
|
95
|
+
def classdef_to_schema(self, typ: type[Any]) -> Schema:
|
|
88
96
|
"""
|
|
89
97
|
Converts a type to a JSON schema.
|
|
90
98
|
For nested types found in the type hierarchy, adds the type to the schema registry in the OpenAPI specification section `components`.
|
|
@@ -98,12 +106,12 @@ class SchemaBuilder:
|
|
|
98
106
|
|
|
99
107
|
return type_schema
|
|
100
108
|
|
|
101
|
-
def classdef_to_named_schema(self, name: str, typ: type) -> Schema:
|
|
109
|
+
def classdef_to_named_schema(self, name: str, typ: type[Any]) -> Schema:
|
|
102
110
|
schema = self.classdef_to_schema(typ)
|
|
103
111
|
self._add_ref(name, schema)
|
|
104
112
|
return schema
|
|
105
113
|
|
|
106
|
-
def classdef_to_ref(self, typ: type) ->
|
|
114
|
+
def classdef_to_ref(self, typ: type[Any]) -> Schema | SchemaRef:
|
|
107
115
|
"""
|
|
108
116
|
Converts a type to a JSON schema, and if possible, returns a schema reference.
|
|
109
117
|
For composite types (such as classes), adds the type to the schema registry in the OpenAPI specification section `components`.
|
|
@@ -137,35 +145,35 @@ class SchemaBuilder:
|
|
|
137
145
|
|
|
138
146
|
class ContentBuilder:
|
|
139
147
|
schema_builder: SchemaBuilder
|
|
140
|
-
schema_transformer:
|
|
141
|
-
sample_transformer:
|
|
148
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None
|
|
149
|
+
sample_transformer: Callable[[JsonType], JsonType] | None
|
|
142
150
|
|
|
143
151
|
def __init__(
|
|
144
152
|
self,
|
|
145
153
|
schema_builder: SchemaBuilder,
|
|
146
|
-
schema_transformer:
|
|
147
|
-
sample_transformer:
|
|
154
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None = None,
|
|
155
|
+
sample_transformer: Callable[[JsonType], JsonType] | None = None,
|
|
148
156
|
) -> None:
|
|
149
157
|
self.schema_builder = schema_builder
|
|
150
158
|
self.schema_transformer = schema_transformer
|
|
151
159
|
self.sample_transformer = sample_transformer
|
|
152
160
|
|
|
153
|
-
def build_content(self, payload_type: type, examples:
|
|
161
|
+
def build_content(self, payload_type: type[Any], examples: list[Any] | None = None) -> dict[str, MediaType]:
|
|
154
162
|
"Creates the content subtree for a request or response."
|
|
155
163
|
|
|
156
|
-
if
|
|
164
|
+
if typing.get_origin(payload_type) is list:
|
|
157
165
|
media_type = "application/jsonl"
|
|
158
|
-
item_type =
|
|
166
|
+
(item_type,) = typing.get_args(payload_type) # unpack single tuple element
|
|
159
167
|
else:
|
|
160
168
|
media_type = "application/json"
|
|
161
169
|
item_type = payload_type
|
|
162
170
|
|
|
163
171
|
return {media_type: self.build_media_type(item_type, examples)}
|
|
164
172
|
|
|
165
|
-
def build_media_type(self, item_type: type, examples:
|
|
173
|
+
def build_media_type(self, item_type: type[Any], examples: list[Any] | None = None) -> MediaType:
|
|
166
174
|
schema = self.schema_builder.classdef_to_ref(item_type)
|
|
167
175
|
if self.schema_transformer:
|
|
168
|
-
schema_transformer: Callable[[
|
|
176
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] = self.schema_transformer
|
|
169
177
|
schema = schema_transformer(schema)
|
|
170
178
|
|
|
171
179
|
if not examples:
|
|
@@ -179,12 +187,12 @@ class ContentBuilder:
|
|
|
179
187
|
examples=self._build_examples(examples),
|
|
180
188
|
)
|
|
181
189
|
|
|
182
|
-
def _build_examples(self, examples:
|
|
190
|
+
def _build_examples(self, examples: list[Any]) -> dict[str, Example | ExampleRef]:
|
|
183
191
|
"Creates a set of several examples for a media type."
|
|
184
192
|
|
|
185
193
|
builder = ExampleBuilder(self.sample_transformer)
|
|
186
194
|
|
|
187
|
-
results:
|
|
195
|
+
results: dict[str, Example | ExampleRef] = {}
|
|
188
196
|
for example in examples:
|
|
189
197
|
name, value = builder.get_named(example)
|
|
190
198
|
results[name] = Example(value=value)
|
|
@@ -203,12 +211,12 @@ class ExampleBuilder:
|
|
|
203
211
|
|
|
204
212
|
def __init__(
|
|
205
213
|
self,
|
|
206
|
-
sample_transformer:
|
|
214
|
+
sample_transformer: Callable[[JsonType], JsonType] | None = None,
|
|
207
215
|
) -> None:
|
|
208
216
|
if sample_transformer:
|
|
209
217
|
self.sample_transformer = sample_transformer
|
|
210
218
|
else:
|
|
211
|
-
self.sample_transformer = lambda sample: sample
|
|
219
|
+
self.sample_transformer = lambda sample: sample
|
|
212
220
|
|
|
213
221
|
def _get_value(self, example: Any) -> JsonType:
|
|
214
222
|
return self.sample_transformer(object_to_json(example))
|
|
@@ -216,12 +224,12 @@ class ExampleBuilder:
|
|
|
216
224
|
def get_anonymous(self, example: Any) -> JsonType:
|
|
217
225
|
return self._get_value(example)
|
|
218
226
|
|
|
219
|
-
def get_named(self, example: Any) ->
|
|
227
|
+
def get_named(self, example: Any) -> tuple[str, JsonType]:
|
|
220
228
|
value = self._get_value(example)
|
|
221
229
|
|
|
222
|
-
name:
|
|
230
|
+
name: str | None = None
|
|
223
231
|
|
|
224
|
-
if type(example).__str__ is not object.__str__:
|
|
232
|
+
if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
|
|
225
233
|
friendly_name = str(example)
|
|
226
234
|
if friendly_name.isprintable():
|
|
227
235
|
name = friendly_name
|
|
@@ -244,17 +252,17 @@ class ResponseOptions:
|
|
|
244
252
|
:param default_status_code: HTTP status code assigned to responses that have no mapping.
|
|
245
253
|
"""
|
|
246
254
|
|
|
247
|
-
type_descriptions:
|
|
248
|
-
examples:
|
|
249
|
-
status_catalog:
|
|
255
|
+
type_descriptions: dict[type[Any], str]
|
|
256
|
+
examples: list[Any] | None
|
|
257
|
+
status_catalog: dict[type, HTTPStatusCode]
|
|
250
258
|
default_status_code: HTTPStatusCode
|
|
251
259
|
|
|
252
260
|
|
|
253
261
|
@dataclass
|
|
254
262
|
class StatusResponse:
|
|
255
263
|
status_code: str
|
|
256
|
-
types:
|
|
257
|
-
examples:
|
|
264
|
+
types: list[type[Any]] = dataclasses.field(default_factory=list[type[Any]])
|
|
265
|
+
examples: list[Any] = dataclasses.field(default_factory=list[Any])
|
|
258
266
|
|
|
259
267
|
|
|
260
268
|
class ResponseBuilder:
|
|
@@ -263,8 +271,8 @@ class ResponseBuilder:
|
|
|
263
271
|
def __init__(self, content_builder: ContentBuilder) -> None:
|
|
264
272
|
self.content_builder = content_builder
|
|
265
273
|
|
|
266
|
-
def _get_status_responses(self, options: ResponseOptions) ->
|
|
267
|
-
status_responses:
|
|
274
|
+
def _get_status_responses(self, options: ResponseOptions) -> dict[str, StatusResponse]:
|
|
275
|
+
status_responses: dict[str, StatusResponse] = {}
|
|
268
276
|
|
|
269
277
|
for response_type in options.type_descriptions.keys():
|
|
270
278
|
status_code = http_status_to_string(options.status_catalog.get(response_type, options.default_status_code))
|
|
@@ -283,20 +291,20 @@ class ResponseBuilder:
|
|
|
283
291
|
|
|
284
292
|
return dict(sorted(status_responses.items()))
|
|
285
293
|
|
|
286
|
-
def build_response(self, options: ResponseOptions) ->
|
|
294
|
+
def build_response(self, options: ResponseOptions) -> dict[str, Response | ResponseRef]:
|
|
287
295
|
"""
|
|
288
296
|
Groups responses that have the same status code.
|
|
289
297
|
"""
|
|
290
298
|
|
|
291
|
-
responses:
|
|
299
|
+
responses: dict[str, Response | ResponseRef] = {}
|
|
292
300
|
status_responses = self._get_status_responses(options)
|
|
293
301
|
for status_code, status_response in status_responses.items():
|
|
294
302
|
response_types = tuple(status_response.types)
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
composite_response_type: type[Any] | None
|
|
304
|
+
if len(response_types) > 0:
|
|
305
|
+
composite_response_type = reduce(operator.or_, response_types)
|
|
297
306
|
else:
|
|
298
|
-
|
|
299
|
-
composite_response_type = response_type
|
|
307
|
+
composite_response_type = None
|
|
300
308
|
|
|
301
309
|
description = " **OR** ".join(
|
|
302
310
|
filter(
|
|
@@ -315,9 +323,9 @@ class ResponseBuilder:
|
|
|
315
323
|
|
|
316
324
|
def _build_response(
|
|
317
325
|
self,
|
|
318
|
-
response_type: type,
|
|
326
|
+
response_type: type[Any] | None,
|
|
319
327
|
description: str,
|
|
320
|
-
examples:
|
|
328
|
+
examples: list[Any] | None = None,
|
|
321
329
|
) -> Response:
|
|
322
330
|
"Creates a response subtree."
|
|
323
331
|
|
|
@@ -330,7 +338,7 @@ class ResponseBuilder:
|
|
|
330
338
|
return Response(description=description)
|
|
331
339
|
|
|
332
340
|
|
|
333
|
-
def schema_error_wrapper(schema:
|
|
341
|
+
def schema_error_wrapper(schema: Schema | SchemaRef) -> Schema:
|
|
334
342
|
"Wraps an error output schema into a top-level error schema."
|
|
335
343
|
|
|
336
344
|
return {
|
|
@@ -352,12 +360,12 @@ def sample_error_wrapper(error: JsonType) -> JsonType:
|
|
|
352
360
|
|
|
353
361
|
|
|
354
362
|
class Generator:
|
|
355
|
-
endpoint: type
|
|
363
|
+
endpoint: type[Any]
|
|
356
364
|
options: Options
|
|
357
365
|
schema_builder: SchemaBuilder
|
|
358
|
-
responses:
|
|
366
|
+
responses: dict[str, Response]
|
|
359
367
|
|
|
360
|
-
def __init__(self, endpoint: type, options: Options) -> None:
|
|
368
|
+
def __init__(self, endpoint: type[Any], options: Options) -> None:
|
|
361
369
|
self.endpoint = endpoint
|
|
362
370
|
self.options = options
|
|
363
371
|
schema_generator = JsonSchemaGenerator(
|
|
@@ -372,24 +380,24 @@ class Generator:
|
|
|
372
380
|
|
|
373
381
|
def _build_type_tag(self, ref: str, schema: Schema) -> Tag:
|
|
374
382
|
definition = f'<SchemaDefinition schemaRef="#/components/schemas/{ref}" />'
|
|
375
|
-
title = typing.cast(str, schema.get("title"))
|
|
376
|
-
description = typing.cast(str, schema.get("description"))
|
|
383
|
+
title = typing.cast(str | None, schema.get("title"))
|
|
384
|
+
description = typing.cast(str | None, schema.get("description"))
|
|
377
385
|
return Tag(
|
|
378
386
|
name=ref,
|
|
379
387
|
description="\n\n".join(s for s in (title, description, definition) if s is not None),
|
|
380
388
|
)
|
|
381
389
|
|
|
382
|
-
def _build_extra_tag_groups(self, extra_types:
|
|
390
|
+
def _build_extra_tag_groups(self, extra_types: dict[str, list[type[Any]]]) -> dict[str, list[Tag]]:
|
|
383
391
|
"""
|
|
384
392
|
Creates a dictionary of tag group captions as keys, and tag lists as values.
|
|
385
393
|
|
|
386
394
|
:param extra_types: A dictionary of type categories and list of types in that category.
|
|
387
395
|
"""
|
|
388
396
|
|
|
389
|
-
extra_tags:
|
|
397
|
+
extra_tags: dict[str, list[Tag]] = {}
|
|
390
398
|
|
|
391
399
|
for category_name, category_items in extra_types.items():
|
|
392
|
-
tag_list:
|
|
400
|
+
tag_list: list[Tag] = []
|
|
393
401
|
|
|
394
402
|
for extra_type in category_items:
|
|
395
403
|
name = python_type_to_name(extra_type)
|
|
@@ -418,10 +426,10 @@ class Generator:
|
|
|
418
426
|
]
|
|
419
427
|
|
|
420
428
|
# parameters passed in URL component query string
|
|
421
|
-
query_parameters = []
|
|
429
|
+
query_parameters: list[Parameter] = []
|
|
422
430
|
for param_name, param_type in op.query_params:
|
|
423
431
|
if is_type_optional(param_type):
|
|
424
|
-
inner_type: type = unwrap_optional_type(param_type)
|
|
432
|
+
inner_type: type[Any] = unwrap_optional_type(param_type)
|
|
425
433
|
required = False
|
|
426
434
|
else:
|
|
427
435
|
inner_type = param_type
|
|
@@ -454,7 +462,9 @@ class Generator:
|
|
|
454
462
|
# success response types
|
|
455
463
|
if doc_string.returns is None and is_type_union(op.response_type):
|
|
456
464
|
# split union of return types into a list of response types
|
|
457
|
-
success_type_docstring:
|
|
465
|
+
success_type_docstring: dict[type[Any], Docstring] = {
|
|
466
|
+
typing.cast(type[Any], item): parse_type(item) for item in unwrap_union_types(op.response_type)
|
|
467
|
+
}
|
|
458
468
|
success_type_descriptions = {
|
|
459
469
|
item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
|
|
460
470
|
}
|
|
@@ -477,7 +487,7 @@ class Generator:
|
|
|
477
487
|
|
|
478
488
|
# failure response types
|
|
479
489
|
if doc_string.raises:
|
|
480
|
-
exception_types:
|
|
490
|
+
exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
|
|
481
491
|
exception_examples = [example for example in response_examples if isinstance(example, Exception)]
|
|
482
492
|
|
|
483
493
|
if self.options.error_wrapper:
|
|
@@ -525,12 +535,13 @@ class Generator:
|
|
|
525
535
|
requestBody=requestBody,
|
|
526
536
|
responses=responses,
|
|
527
537
|
callbacks=callbacks,
|
|
538
|
+
deprecated=op.deprecated,
|
|
528
539
|
security=[] if op.public else None,
|
|
529
540
|
)
|
|
530
541
|
|
|
531
542
|
def generate(self) -> Document:
|
|
532
|
-
paths:
|
|
533
|
-
endpoint_classes:
|
|
543
|
+
paths: dict[str, PathItem] = {}
|
|
544
|
+
endpoint_classes: set[type[Any]] = set()
|
|
534
545
|
for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
|
|
535
546
|
endpoint_classes.add(op.defining_class)
|
|
536
547
|
|
|
@@ -555,9 +566,9 @@ class Generator:
|
|
|
555
566
|
else:
|
|
556
567
|
paths[route] = pathItem
|
|
557
568
|
|
|
558
|
-
operation_tags:
|
|
569
|
+
operation_tags: list[Tag] = []
|
|
559
570
|
for cls in endpoint_classes:
|
|
560
|
-
doc_string = parse_type(cls)
|
|
571
|
+
doc_string = parse_type(cls) # type: ignore[arg-type]
|
|
561
572
|
operation_tags.append(
|
|
562
573
|
Tag(
|
|
563
574
|
name=cls.__name__,
|
|
@@ -570,31 +581,30 @@ class Generator:
|
|
|
570
581
|
type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
|
|
571
582
|
|
|
572
583
|
# types that are emitted by events
|
|
573
|
-
event_tags:
|
|
584
|
+
event_tags: list[Tag] = []
|
|
574
585
|
events = get_endpoint_events(self.endpoint)
|
|
575
586
|
for ref, event_type in events.items():
|
|
576
587
|
event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
|
|
577
588
|
event_tags.append(self._build_type_tag(ref, event_schema))
|
|
578
589
|
|
|
579
590
|
# types that are explicitly declared
|
|
580
|
-
extra_tag_groups:
|
|
591
|
+
extra_tag_groups: dict[str, list[Tag]] = {}
|
|
581
592
|
if self.options.extra_types is not None:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
|
|
593
|
+
match self.options.extra_types:
|
|
594
|
+
case list():
|
|
595
|
+
extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
|
|
596
|
+
case dict():
|
|
597
|
+
extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
|
|
588
598
|
|
|
589
599
|
# list all operations and types
|
|
590
|
-
tags:
|
|
600
|
+
tags: list[Tag] = []
|
|
591
601
|
tags.extend(operation_tags)
|
|
592
602
|
tags.extend(type_tags)
|
|
593
603
|
tags.extend(event_tags)
|
|
594
604
|
for extra_tag_group in extra_tag_groups.values():
|
|
595
605
|
tags.extend(extra_tag_group)
|
|
596
606
|
|
|
597
|
-
tag_groups = []
|
|
607
|
+
tag_groups: list[TagGroup] = []
|
|
598
608
|
if operation_tags:
|
|
599
609
|
tag_groups.append(
|
|
600
610
|
TagGroup(
|
pyopenapi/metadata.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
3
11
|
|
|
4
12
|
|
|
5
13
|
@dataclass
|
|
@@ -9,11 +17,13 @@ class WebMethod:
|
|
|
9
17
|
|
|
10
18
|
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
11
19
|
:param public: True if the operation can be invoked without prior authentication.
|
|
20
|
+
:param deprecated: True if consumers should refrain from using the operation.
|
|
12
21
|
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
13
22
|
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
14
23
|
"""
|
|
15
24
|
|
|
16
|
-
route:
|
|
25
|
+
route: str | None = None
|
|
17
26
|
public: bool = False
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
deprecated: bool = False
|
|
28
|
+
request_examples: list[Any] | None = None
|
|
29
|
+
response_examples: list[Any] | None = None
|