python-openapi 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {python_openapi-0.2.0 → python_openapi-0.3.0}/LICENSE +1 -1
- {python_openapi-0.2.0/python_openapi.egg-info → python_openapi-0.3.0}/PKG-INFO +16 -7
- {python_openapi-0.2.0 → python_openapi-0.3.0}/README.md +2 -2
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/__init__.py +3 -3
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/decorators.py +16 -9
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/generator.py +60 -58
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/metadata.py +7 -5
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/operations.py +33 -27
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/options.py +9 -9
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/proxy.py +7 -6
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/specification.py +74 -63
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/utility.py +1 -1
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyproject.toml +17 -4
- {python_openapi-0.2.0 → python_openapi-0.3.0/python_openapi.egg-info}/PKG-INFO +16 -7
- {python_openapi-0.2.0 → python_openapi-0.3.0}/python_openapi.egg-info/SOURCES.txt +0 -1
- python_openapi-0.3.0/python_openapi.egg-info/requires.txt +13 -0
- python_openapi-0.3.0/setup.py +12 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/tests/endpoint.py +10 -3
- {python_openapi-0.2.0 → python_openapi-0.3.0}/tests/test_openapi.py +6 -8
- {python_openapi-0.2.0 → python_openapi-0.3.0}/tests/test_proxy.py +2 -2
- python_openapi-0.2.0/pyopenapi/__main__.py +0 -0
- python_openapi-0.2.0/python_openapi.egg-info/requires.txt +0 -2
- python_openapi-0.2.0/setup.py +0 -4
- {python_openapi-0.2.0 → python_openapi-0.3.0}/MANIFEST.in +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/py.typed +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/pyopenapi/template.html +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/python_openapi.egg-info/dependency_links.txt +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/python_openapi.egg-info/top_level.txt +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/python_openapi.egg-info/zip-safe +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/setup.cfg +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/tests/__init__.py +0 -0
- {python_openapi-0.2.0 → python_openapi-0.3.0}/tests/endpoint.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-openapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Generate an OpenAPI specification from a Python class definition
|
|
5
5
|
Author-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
6
6
|
Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
@@ -12,20 +12,29 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
21
|
Classifier: Topic :: Software Development :: Code Generators
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
|
|
27
|
+
Requires-Dist: json_strong_typing>=0.4
|
|
28
|
+
Provides-Extra: proxy
|
|
29
|
+
Requires-Dist: aiohttp>=3.13; extra == "proxy"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff; extra == "dev"
|
|
34
|
+
Requires-Dist: PyYAML; extra == "dev"
|
|
35
|
+
Requires-Dist: Pygments; extra == "dev"
|
|
36
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
37
|
+
Requires-Dist: types-Pygments; extra == "dev"
|
|
29
38
|
Dynamic: license-file
|
|
30
39
|
|
|
31
40
|
# Generate an OpenAPI specification from a Python class
|
|
@@ -37,7 +46,7 @@ Dynamic: license-file
|
|
|
37
46
|
* supports standard and asynchronous functions (`async def`)
|
|
38
47
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
39
48
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
40
|
-
* handles generic types (`list[T]`, `dict[K, V]`, `
|
|
49
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `T | None`, `T1 | T2 | T3`)
|
|
41
50
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
42
51
|
* maps composite types to HTTP request body
|
|
43
52
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -162,7 +171,7 @@ OpenAPI supports specifying [examples](https://spec.openapis.org/oas/latest.html
|
|
|
162
171
|
Teacher("Vacska", "Mati", "Négyszögletű Kerek Erdő"),
|
|
163
172
|
],
|
|
164
173
|
)
|
|
165
|
-
def get_member_by_name(self, family: str, given: str, /) ->
|
|
174
|
+
def get_member_by_name(self, family: str, given: str, /) -> Student | Teacher:
|
|
166
175
|
...
|
|
167
176
|
```
|
|
168
177
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* supports standard and asynchronous functions (`async def`)
|
|
8
8
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
9
9
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
10
|
-
* handles generic types (`list[T]`, `dict[K, V]`, `
|
|
10
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `T | None`, `T1 | T2 | T3`)
|
|
11
11
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
12
12
|
* maps composite types to HTTP request body
|
|
13
13
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -132,7 +132,7 @@ OpenAPI supports specifying [examples](https://spec.openapis.org/oas/latest.html
|
|
|
132
132
|
Teacher("Vacska", "Mati", "Négyszögletű Kerek Erdő"),
|
|
133
133
|
],
|
|
134
134
|
)
|
|
135
|
-
def get_member_by_name(self, family: str, given: str, /) ->
|
|
135
|
+
def get_member_by_name(self, family: str, given: str, /) -> Student | Teacher:
|
|
136
136
|
...
|
|
137
137
|
```
|
|
138
138
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate an OpenAPI specification from a Python class definition
|
|
3
3
|
|
|
4
|
-
Copyright
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/pyopenapi
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
10
|
__author__ = "Levente Hunyadi"
|
|
11
|
-
__copyright__ = "Copyright
|
|
11
|
+
__copyright__ = "Copyright 2021-2026, Levente Hunyadi"
|
|
12
12
|
__license__ = "MIT"
|
|
13
13
|
__maintainer__ = "Levente Hunyadi"
|
|
14
14
|
__status__ = "Production"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate an OpenAPI specification from a Python class definition
|
|
3
3
|
|
|
4
|
-
Copyright
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/pyopenapi
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Any, Callable,
|
|
9
|
+
from typing import Any, Callable, TypeVar
|
|
10
10
|
|
|
11
11
|
from .metadata import WebMethod
|
|
12
12
|
from .options import * # noqa: F403
|
|
@@ -16,12 +16,13 @@ F = TypeVar("F", bound=Callable[..., Any])
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def webmethod(
|
|
19
|
-
route:
|
|
20
|
-
public:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
25
26
|
) -> Callable[[F], F]:
|
|
26
27
|
"""
|
|
27
28
|
Decorator that supplies additional metadata to an endpoint operation function.
|
|
@@ -45,7 +46,13 @@ def webmethod(
|
|
|
45
46
|
response_examples = [response_example]
|
|
46
47
|
|
|
47
48
|
def wrap(cls: F) -> F:
|
|
48
|
-
cls.__webmethod__ = WebMethod(
|
|
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
|
+
)
|
|
49
56
|
return cls
|
|
50
57
|
|
|
51
58
|
return wrap
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate an OpenAPI specification from a Python class definition
|
|
3
3
|
|
|
4
|
-
Copyright
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/pyopenapi
|
|
7
7
|
"""
|
|
@@ -9,14 +9,16 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
9
9
|
import dataclasses
|
|
10
10
|
import hashlib
|
|
11
11
|
import ipaddress
|
|
12
|
+
import operator
|
|
12
13
|
import typing
|
|
13
14
|
from dataclasses import dataclass
|
|
15
|
+
from functools import reduce
|
|
14
16
|
from http import HTTPStatus
|
|
15
|
-
from typing import Any, Callable
|
|
17
|
+
from typing import Any, Callable
|
|
16
18
|
|
|
17
19
|
from strong_typing.core import JsonType, Schema
|
|
18
20
|
from strong_typing.docstring import Docstring, parse_type
|
|
19
|
-
from strong_typing.inspection import
|
|
21
|
+
from strong_typing.inspection import is_type_optional, is_type_union, unwrap_optional_type, unwrap_union_types
|
|
20
22
|
from strong_typing.name import python_type_to_name
|
|
21
23
|
from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
|
|
22
24
|
from strong_typing.serialization import json_dump_string, object_to_json
|
|
@@ -36,7 +38,6 @@ from .specification import (
|
|
|
36
38
|
RequestBody,
|
|
37
39
|
Response,
|
|
38
40
|
ResponseRef,
|
|
39
|
-
SchemaOrRef,
|
|
40
41
|
SchemaRef,
|
|
41
42
|
Tag,
|
|
42
43
|
TagGroup,
|
|
@@ -74,14 +75,13 @@ register_schema(
|
|
|
74
75
|
def http_status_to_string(status_code: HTTPStatusCode) -> str:
|
|
75
76
|
"Converts an HTTP status code to a string."
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
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
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
class SchemaBuilder:
|
|
@@ -92,7 +92,7 @@ class SchemaBuilder:
|
|
|
92
92
|
self.schema_generator = schema_generator
|
|
93
93
|
self.schemas = {}
|
|
94
94
|
|
|
95
|
-
def classdef_to_schema(self, typ: type) -> Schema:
|
|
95
|
+
def classdef_to_schema(self, typ: type[Any]) -> Schema:
|
|
96
96
|
"""
|
|
97
97
|
Converts a type to a JSON schema.
|
|
98
98
|
For nested types found in the type hierarchy, adds the type to the schema registry in the OpenAPI specification section `components`.
|
|
@@ -106,12 +106,12 @@ class SchemaBuilder:
|
|
|
106
106
|
|
|
107
107
|
return type_schema
|
|
108
108
|
|
|
109
|
-
def classdef_to_named_schema(self, name: str, typ: type) -> Schema:
|
|
109
|
+
def classdef_to_named_schema(self, name: str, typ: type[Any]) -> Schema:
|
|
110
110
|
schema = self.classdef_to_schema(typ)
|
|
111
111
|
self._add_ref(name, schema)
|
|
112
112
|
return schema
|
|
113
113
|
|
|
114
|
-
def classdef_to_ref(self, typ: type) ->
|
|
114
|
+
def classdef_to_ref(self, typ: type[Any]) -> Schema | SchemaRef:
|
|
115
115
|
"""
|
|
116
116
|
Converts a type to a JSON schema, and if possible, returns a schema reference.
|
|
117
117
|
For composite types (such as classes), adds the type to the schema registry in the OpenAPI specification section `components`.
|
|
@@ -145,35 +145,35 @@ class SchemaBuilder:
|
|
|
145
145
|
|
|
146
146
|
class ContentBuilder:
|
|
147
147
|
schema_builder: SchemaBuilder
|
|
148
|
-
schema_transformer:
|
|
149
|
-
sample_transformer:
|
|
148
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None
|
|
149
|
+
sample_transformer: Callable[[JsonType], JsonType] | None
|
|
150
150
|
|
|
151
151
|
def __init__(
|
|
152
152
|
self,
|
|
153
153
|
schema_builder: SchemaBuilder,
|
|
154
|
-
schema_transformer:
|
|
155
|
-
sample_transformer:
|
|
154
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None = None,
|
|
155
|
+
sample_transformer: Callable[[JsonType], JsonType] | None = None,
|
|
156
156
|
) -> None:
|
|
157
157
|
self.schema_builder = schema_builder
|
|
158
158
|
self.schema_transformer = schema_transformer
|
|
159
159
|
self.sample_transformer = sample_transformer
|
|
160
160
|
|
|
161
|
-
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]:
|
|
162
162
|
"Creates the content subtree for a request or response."
|
|
163
163
|
|
|
164
|
-
if
|
|
164
|
+
if typing.get_origin(payload_type) is list:
|
|
165
165
|
media_type = "application/jsonl"
|
|
166
|
-
item_type =
|
|
166
|
+
(item_type,) = typing.get_args(payload_type) # unpack single tuple element
|
|
167
167
|
else:
|
|
168
168
|
media_type = "application/json"
|
|
169
169
|
item_type = payload_type
|
|
170
170
|
|
|
171
171
|
return {media_type: self.build_media_type(item_type, examples)}
|
|
172
172
|
|
|
173
|
-
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:
|
|
174
174
|
schema = self.schema_builder.classdef_to_ref(item_type)
|
|
175
175
|
if self.schema_transformer:
|
|
176
|
-
schema_transformer: Callable[[
|
|
176
|
+
schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] = self.schema_transformer
|
|
177
177
|
schema = schema_transformer(schema)
|
|
178
178
|
|
|
179
179
|
if not examples:
|
|
@@ -187,12 +187,12 @@ class ContentBuilder:
|
|
|
187
187
|
examples=self._build_examples(examples),
|
|
188
188
|
)
|
|
189
189
|
|
|
190
|
-
def _build_examples(self, examples: list[Any]) -> dict[str,
|
|
190
|
+
def _build_examples(self, examples: list[Any]) -> dict[str, Example | ExampleRef]:
|
|
191
191
|
"Creates a set of several examples for a media type."
|
|
192
192
|
|
|
193
193
|
builder = ExampleBuilder(self.sample_transformer)
|
|
194
194
|
|
|
195
|
-
results: dict[str,
|
|
195
|
+
results: dict[str, Example | ExampleRef] = {}
|
|
196
196
|
for example in examples:
|
|
197
197
|
name, value = builder.get_named(example)
|
|
198
198
|
results[name] = Example(value=value)
|
|
@@ -211,7 +211,7 @@ class ExampleBuilder:
|
|
|
211
211
|
|
|
212
212
|
def __init__(
|
|
213
213
|
self,
|
|
214
|
-
sample_transformer:
|
|
214
|
+
sample_transformer: Callable[[JsonType], JsonType] | None = None,
|
|
215
215
|
) -> None:
|
|
216
216
|
if sample_transformer:
|
|
217
217
|
self.sample_transformer = sample_transformer
|
|
@@ -227,7 +227,7 @@ class ExampleBuilder:
|
|
|
227
227
|
def get_named(self, example: Any) -> tuple[str, JsonType]:
|
|
228
228
|
value = self._get_value(example)
|
|
229
229
|
|
|
230
|
-
name:
|
|
230
|
+
name: str | None = None
|
|
231
231
|
|
|
232
232
|
if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
|
|
233
233
|
friendly_name = str(example)
|
|
@@ -252,8 +252,8 @@ class ResponseOptions:
|
|
|
252
252
|
:param default_status_code: HTTP status code assigned to responses that have no mapping.
|
|
253
253
|
"""
|
|
254
254
|
|
|
255
|
-
type_descriptions: dict[type, str]
|
|
256
|
-
examples:
|
|
255
|
+
type_descriptions: dict[type[Any], str]
|
|
256
|
+
examples: list[Any] | None
|
|
257
257
|
status_catalog: dict[type, HTTPStatusCode]
|
|
258
258
|
default_status_code: HTTPStatusCode
|
|
259
259
|
|
|
@@ -261,8 +261,8 @@ class ResponseOptions:
|
|
|
261
261
|
@dataclass
|
|
262
262
|
class StatusResponse:
|
|
263
263
|
status_code: str
|
|
264
|
-
types: list[type] = dataclasses.field(default_factory=list)
|
|
265
|
-
examples: list[Any] = dataclasses.field(default_factory=list)
|
|
264
|
+
types: list[type[Any]] = dataclasses.field(default_factory=list[type[Any]])
|
|
265
|
+
examples: list[Any] = dataclasses.field(default_factory=list[Any])
|
|
266
266
|
|
|
267
267
|
|
|
268
268
|
class ResponseBuilder:
|
|
@@ -291,20 +291,20 @@ class ResponseBuilder:
|
|
|
291
291
|
|
|
292
292
|
return dict(sorted(status_responses.items()))
|
|
293
293
|
|
|
294
|
-
def build_response(self, options: ResponseOptions) -> dict[str,
|
|
294
|
+
def build_response(self, options: ResponseOptions) -> dict[str, Response | ResponseRef]:
|
|
295
295
|
"""
|
|
296
296
|
Groups responses that have the same status code.
|
|
297
297
|
"""
|
|
298
298
|
|
|
299
|
-
responses: dict[str,
|
|
299
|
+
responses: dict[str, Response | ResponseRef] = {}
|
|
300
300
|
status_responses = self._get_status_responses(options)
|
|
301
301
|
for status_code, status_response in status_responses.items():
|
|
302
302
|
response_types = tuple(status_response.types)
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
composite_response_type: type[Any] | None
|
|
304
|
+
if len(response_types) > 0:
|
|
305
|
+
composite_response_type = reduce(operator.or_, response_types)
|
|
305
306
|
else:
|
|
306
|
-
|
|
307
|
-
composite_response_type = response_type
|
|
307
|
+
composite_response_type = None
|
|
308
308
|
|
|
309
309
|
description = " **OR** ".join(
|
|
310
310
|
filter(
|
|
@@ -323,9 +323,9 @@ class ResponseBuilder:
|
|
|
323
323
|
|
|
324
324
|
def _build_response(
|
|
325
325
|
self,
|
|
326
|
-
response_type:
|
|
326
|
+
response_type: type[Any] | None,
|
|
327
327
|
description: str,
|
|
328
|
-
examples:
|
|
328
|
+
examples: list[Any] | None = None,
|
|
329
329
|
) -> Response:
|
|
330
330
|
"Creates a response subtree."
|
|
331
331
|
|
|
@@ -338,7 +338,7 @@ class ResponseBuilder:
|
|
|
338
338
|
return Response(description=description)
|
|
339
339
|
|
|
340
340
|
|
|
341
|
-
def schema_error_wrapper(schema:
|
|
341
|
+
def schema_error_wrapper(schema: Schema | SchemaRef) -> Schema:
|
|
342
342
|
"Wraps an error output schema into a top-level error schema."
|
|
343
343
|
|
|
344
344
|
return {
|
|
@@ -360,12 +360,12 @@ def sample_error_wrapper(error: JsonType) -> JsonType:
|
|
|
360
360
|
|
|
361
361
|
|
|
362
362
|
class Generator:
|
|
363
|
-
endpoint: type
|
|
363
|
+
endpoint: type[Any]
|
|
364
364
|
options: Options
|
|
365
365
|
schema_builder: SchemaBuilder
|
|
366
366
|
responses: dict[str, Response]
|
|
367
367
|
|
|
368
|
-
def __init__(self, endpoint: type, options: Options) -> None:
|
|
368
|
+
def __init__(self, endpoint: type[Any], options: Options) -> None:
|
|
369
369
|
self.endpoint = endpoint
|
|
370
370
|
self.options = options
|
|
371
371
|
schema_generator = JsonSchemaGenerator(
|
|
@@ -380,14 +380,14 @@ class Generator:
|
|
|
380
380
|
|
|
381
381
|
def _build_type_tag(self, ref: str, schema: Schema) -> Tag:
|
|
382
382
|
definition = f'<SchemaDefinition schemaRef="#/components/schemas/{ref}" />'
|
|
383
|
-
title = typing.cast(str, schema.get("title"))
|
|
384
|
-
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"))
|
|
385
385
|
return Tag(
|
|
386
386
|
name=ref,
|
|
387
387
|
description="\n\n".join(s for s in (title, description, definition) if s is not None),
|
|
388
388
|
)
|
|
389
389
|
|
|
390
|
-
def _build_extra_tag_groups(self, extra_types: dict[str, list[type]]) -> dict[str, list[Tag]]:
|
|
390
|
+
def _build_extra_tag_groups(self, extra_types: dict[str, list[type[Any]]]) -> dict[str, list[Tag]]:
|
|
391
391
|
"""
|
|
392
392
|
Creates a dictionary of tag group captions as keys, and tag lists as values.
|
|
393
393
|
|
|
@@ -426,10 +426,10 @@ class Generator:
|
|
|
426
426
|
]
|
|
427
427
|
|
|
428
428
|
# parameters passed in URL component query string
|
|
429
|
-
query_parameters = []
|
|
429
|
+
query_parameters: list[Parameter] = []
|
|
430
430
|
for param_name, param_type in op.query_params:
|
|
431
431
|
if is_type_optional(param_type):
|
|
432
|
-
inner_type: type = unwrap_optional_type(param_type)
|
|
432
|
+
inner_type: type[Any] = unwrap_optional_type(param_type)
|
|
433
433
|
required = False
|
|
434
434
|
else:
|
|
435
435
|
inner_type = param_type
|
|
@@ -462,7 +462,9 @@ class Generator:
|
|
|
462
462
|
# success response types
|
|
463
463
|
if doc_string.returns is None and is_type_union(op.response_type):
|
|
464
464
|
# split union of return types into a list of response types
|
|
465
|
-
success_type_docstring: dict[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
|
+
}
|
|
466
468
|
success_type_descriptions = {
|
|
467
469
|
item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
|
|
468
470
|
}
|
|
@@ -533,12 +535,13 @@ class Generator:
|
|
|
533
535
|
requestBody=requestBody,
|
|
534
536
|
responses=responses,
|
|
535
537
|
callbacks=callbacks,
|
|
538
|
+
deprecated=op.deprecated,
|
|
536
539
|
security=[] if op.public else None,
|
|
537
540
|
)
|
|
538
541
|
|
|
539
542
|
def generate(self) -> Document:
|
|
540
543
|
paths: dict[str, PathItem] = {}
|
|
541
|
-
endpoint_classes: set[type] = set()
|
|
544
|
+
endpoint_classes: set[type[Any]] = set()
|
|
542
545
|
for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
|
|
543
546
|
endpoint_classes.add(op.defining_class)
|
|
544
547
|
|
|
@@ -565,7 +568,7 @@ class Generator:
|
|
|
565
568
|
|
|
566
569
|
operation_tags: list[Tag] = []
|
|
567
570
|
for cls in endpoint_classes:
|
|
568
|
-
doc_string = parse_type(cls)
|
|
571
|
+
doc_string = parse_type(cls) # type: ignore[arg-type]
|
|
569
572
|
operation_tags.append(
|
|
570
573
|
Tag(
|
|
571
574
|
name=cls.__name__,
|
|
@@ -587,12 +590,11 @@ class Generator:
|
|
|
587
590
|
# types that are explicitly declared
|
|
588
591
|
extra_tag_groups: dict[str, list[Tag]] = {}
|
|
589
592
|
if self.options.extra_types is not None:
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
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)
|
|
596
598
|
|
|
597
599
|
# list all operations and types
|
|
598
600
|
tags: list[Tag] = []
|
|
@@ -602,7 +604,7 @@ class Generator:
|
|
|
602
604
|
for extra_tag_group in extra_tag_groups.values():
|
|
603
605
|
tags.extend(extra_tag_group)
|
|
604
606
|
|
|
605
|
-
tag_groups = []
|
|
607
|
+
tag_groups: list[TagGroup] = []
|
|
606
608
|
if operation_tags:
|
|
607
609
|
tag_groups.append(
|
|
608
610
|
TagGroup(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate an OpenAPI specification from a Python class definition
|
|
3
3
|
|
|
4
|
-
Copyright
|
|
4
|
+
Copyright 2021-2026, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/pyopenapi
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -17,11 +17,13 @@ class WebMethod:
|
|
|
17
17
|
|
|
18
18
|
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
19
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.
|
|
20
21
|
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
21
22
|
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
route:
|
|
25
|
+
route: str | None = None
|
|
25
26
|
public: bool = False
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
deprecated: bool = False
|
|
28
|
+
request_examples: list[Any] | None = None
|
|
29
|
+
response_examples: list[Any] | None = None
|