python-openapi 0.1.9__tar.gz → 0.2.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.1.9 → python_openapi-0.2.0}/LICENSE +1 -1
- python_openapi-0.2.0/MANIFEST.in +2 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/PKG-INFO +19 -13
- {python-openapi-0.1.9 → python_openapi-0.2.0}/README.md +4 -4
- python_openapi-0.2.0/pyopenapi/__init__.py +14 -0
- python-openapi-0.1.9/pyopenapi/__init__.py → python_openapi-0.2.0/pyopenapi/decorators.py +18 -25
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/generator.py +100 -150
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/metadata.py +11 -3
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/operations.py +34 -52
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/options.py +19 -19
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/proxy.py +22 -28
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/specification.py +37 -28
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/utility.py +14 -16
- python_openapi-0.2.0/pyproject.toml +58 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/PKG-INFO +19 -13
- {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/SOURCES.txt +5 -1
- python_openapi-0.2.0/python_openapi.egg-info/requires.txt +2 -0
- python_openapi-0.2.0/setup.cfg +4 -0
- python_openapi-0.2.0/tests/__init__.py +0 -0
- python_openapi-0.2.0/tests/endpoint.md +5 -0
- python_openapi-0.2.0/tests/endpoint.py +389 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/tests/test_openapi.py +8 -15
- {python-openapi-0.1.9 → python_openapi-0.2.0}/tests/test_proxy.py +16 -17
- python-openapi-0.1.9/pyproject.toml +0 -3
- python-openapi-0.1.9/python_openapi.egg-info/requires.txt +0 -2
- python-openapi-0.1.9/setup.cfg +0 -46
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/__main__.py +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/py.typed +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/template.html +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/dependency_links.txt +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/top_level.txt +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/zip-safe +0 -0
- {python-openapi-0.1.9 → python_openapi-0.2.0}/setup.py +0 -0
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-openapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Generate an OpenAPI specification from a Python class definition
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Author-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
6
|
+
Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/hunyadi/pyopenapi
|
|
9
|
+
Project-URL: Source, https://github.com/hunyadi/pyopenapi
|
|
10
|
+
Keywords: openapi3,openapi,redoc,swagger,json-schema-generator,dataclasses,type-inspection
|
|
9
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
12
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
13
14
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
21
|
Classifier: Topic :: Software Development :: Code Generators
|
|
19
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
23
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
22
25
|
Description-Content-Type: text/markdown
|
|
23
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: aiohttp>=3.12
|
|
28
|
+
Requires-Dist: json_strong_typing>=0.3.9
|
|
29
|
+
Dynamic: license-file
|
|
24
30
|
|
|
25
31
|
# Generate an OpenAPI specification from a Python class
|
|
26
32
|
|
|
@@ -31,7 +37,7 @@ License-File: LICENSE
|
|
|
31
37
|
* supports standard and asynchronous functions (`async def`)
|
|
32
38
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
33
39
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
34
|
-
* handles generic types (`
|
|
40
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
|
|
35
41
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
36
42
|
* maps composite types to HTTP request body
|
|
37
43
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -84,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
|
|
|
84
90
|
|
|
85
91
|
```python
|
|
86
92
|
class JobManagement:
|
|
87
|
-
def create_job(self, items:
|
|
93
|
+
def create_job(self, items: list[URL]) -> uuid.UUID:
|
|
88
94
|
...
|
|
89
95
|
|
|
90
96
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -123,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
|
|
|
123
129
|
|
|
124
130
|
### Documenting operations
|
|
125
131
|
|
|
126
|
-
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
132
|
+
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
127
133
|
|
|
128
134
|
```python
|
|
129
135
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -166,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
|
|
|
166
172
|
|
|
167
173
|
### Mapping function name prefixes to HTTP verbs
|
|
168
174
|
|
|
169
|
-
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
175
|
+
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
170
176
|
|
|
171
177
|
| Prefix | HTTP verb |
|
|
172
178
|
| ------ | ----------- |
|
|
@@ -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 (`
|
|
10
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[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`
|
|
@@ -60,7 +60,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
|
|
|
60
60
|
|
|
61
61
|
```python
|
|
62
62
|
class JobManagement:
|
|
63
|
-
def create_job(self, items:
|
|
63
|
+
def create_job(self, items: list[URL]) -> uuid.UUID:
|
|
64
64
|
...
|
|
65
65
|
|
|
66
66
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -99,7 +99,7 @@ The custom path must have placeholders for all positional-only parameters in the
|
|
|
99
99
|
|
|
100
100
|
### Documenting operations
|
|
101
101
|
|
|
102
|
-
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
102
|
+
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
103
103
|
|
|
104
104
|
```python
|
|
105
105
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -142,7 +142,7 @@ The Python objects in `request_examples` and `response_examples` are translated
|
|
|
142
142
|
|
|
143
143
|
### Mapping function name prefixes to HTTP verbs
|
|
144
144
|
|
|
145
|
-
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
145
|
+
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
146
146
|
|
|
147
147
|
| Prefix | HTTP verb |
|
|
148
148
|
| ------ | ----------- |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
__author__ = "Levente Hunyadi"
|
|
11
|
+
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
__maintainer__ = "Levente Hunyadi"
|
|
14
|
+
__status__ = "Production"
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
from typing import Any, Callable, Optional, TypeVar
|
|
2
10
|
|
|
3
11
|
from .metadata import WebMethod
|
|
4
|
-
from .options import *
|
|
5
|
-
from .utility import Specification
|
|
6
|
-
|
|
7
|
-
__version__ = "0.1.9"
|
|
12
|
+
from .options import * # noqa: F403
|
|
13
|
+
from .utility import Specification as Specification
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
def webmethod(
|
|
@@ -14,9 +20,9 @@ def webmethod(
|
|
|
14
20
|
public: Optional[bool] = False,
|
|
15
21
|
request_example: Optional[Any] = None,
|
|
16
22
|
response_example: Optional[Any] = None,
|
|
17
|
-
request_examples: Optional[
|
|
18
|
-
response_examples: Optional[
|
|
19
|
-
) -> Callable[[
|
|
23
|
+
request_examples: Optional[list[Any]] = None,
|
|
24
|
+
response_examples: Optional[list[Any]] = None,
|
|
25
|
+
) -> Callable[[F], F]:
|
|
20
26
|
"""
|
|
21
27
|
Decorator that supplies additional metadata to an endpoint operation function.
|
|
22
28
|
|
|
@@ -29,30 +35,17 @@ def webmethod(
|
|
|
29
35
|
"""
|
|
30
36
|
|
|
31
37
|
if request_example is not None and request_examples is not None:
|
|
32
|
-
raise ValueError(
|
|
33
|
-
"arguments `request_example` and `request_examples` are exclusive"
|
|
34
|
-
)
|
|
38
|
+
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
|
|
35
39
|
if response_example is not None and response_examples is not None:
|
|
36
|
-
raise ValueError(
|
|
37
|
-
"arguments `response_example` and `response_examples` are exclusive"
|
|
38
|
-
)
|
|
40
|
+
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
|
|
39
41
|
|
|
40
42
|
if request_example:
|
|
41
43
|
request_examples = [request_example]
|
|
42
44
|
if response_example:
|
|
43
45
|
response_examples = [response_example]
|
|
44
46
|
|
|
45
|
-
def wrap(cls:
|
|
46
|
-
|
|
47
|
-
cls,
|
|
48
|
-
"__webmethod__",
|
|
49
|
-
WebMethod(
|
|
50
|
-
route=route,
|
|
51
|
-
public=public or False,
|
|
52
|
-
request_examples=request_examples,
|
|
53
|
-
response_examples=response_examples,
|
|
54
|
-
),
|
|
55
|
-
)
|
|
47
|
+
def wrap(cls: F) -> F:
|
|
48
|
+
cls.__webmethod__ = WebMethod(route=route, public=public or False, request_examples=request_examples, response_examples=response_examples) # type: ignore[attr-defined]
|
|
56
49
|
return cls
|
|
57
50
|
|
|
58
51
|
return wrap
|
|
@@ -1,35 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
1
10
|
import hashlib
|
|
2
11
|
import ipaddress
|
|
3
12
|
import typing
|
|
4
|
-
from
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from http import HTTPStatus
|
|
15
|
+
from typing import Any, Callable, Optional, Union
|
|
5
16
|
|
|
6
|
-
from strong_typing.core import JsonType
|
|
17
|
+
from strong_typing.core import JsonType, Schema
|
|
7
18
|
from strong_typing.docstring import Docstring, parse_type
|
|
8
|
-
from strong_typing.inspection import
|
|
9
|
-
is_generic_list,
|
|
10
|
-
is_type_optional,
|
|
11
|
-
is_type_union,
|
|
12
|
-
unwrap_generic_list,
|
|
13
|
-
unwrap_optional_type,
|
|
14
|
-
unwrap_union_types,
|
|
15
|
-
)
|
|
19
|
+
from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
|
|
16
20
|
from strong_typing.name import python_type_to_name
|
|
17
|
-
from strong_typing.schema import
|
|
18
|
-
JsonSchemaGenerator,
|
|
19
|
-
Schema,
|
|
20
|
-
SchemaOptions,
|
|
21
|
-
get_schema_identifier,
|
|
22
|
-
register_schema,
|
|
23
|
-
)
|
|
21
|
+
from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
|
|
24
22
|
from strong_typing.serialization import json_dump_string, object_to_json
|
|
25
23
|
|
|
26
|
-
from .operations import
|
|
27
|
-
|
|
28
|
-
HTTPMethod,
|
|
29
|
-
get_endpoint_events,
|
|
30
|
-
get_endpoint_operations,
|
|
31
|
-
)
|
|
32
|
-
from .options import *
|
|
24
|
+
from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
|
|
25
|
+
from .options import HTTPStatusCode, Options
|
|
33
26
|
from .specification import (
|
|
34
27
|
Components,
|
|
35
28
|
Document,
|
|
@@ -93,7 +86,7 @@ def http_status_to_string(status_code: HTTPStatusCode) -> str:
|
|
|
93
86
|
|
|
94
87
|
class SchemaBuilder:
|
|
95
88
|
schema_generator: JsonSchemaGenerator
|
|
96
|
-
schemas:
|
|
89
|
+
schemas: dict[str, Schema]
|
|
97
90
|
|
|
98
91
|
def __init__(self, schema_generator: JsonSchemaGenerator) -> None:
|
|
99
92
|
self.schema_generator = schema_generator
|
|
@@ -165,9 +158,7 @@ class ContentBuilder:
|
|
|
165
158
|
self.schema_transformer = schema_transformer
|
|
166
159
|
self.sample_transformer = sample_transformer
|
|
167
160
|
|
|
168
|
-
def build_content(
|
|
169
|
-
self, payload_type: type, examples: Optional[List[Any]] = None
|
|
170
|
-
) -> Dict[str, MediaType]:
|
|
161
|
+
def build_content(self, payload_type: type, examples: Optional[list[Any]] = None) -> dict[str, MediaType]:
|
|
171
162
|
"Creates the content subtree for a request or response."
|
|
172
163
|
|
|
173
164
|
if is_generic_list(payload_type):
|
|
@@ -179,12 +170,10 @@ class ContentBuilder:
|
|
|
179
170
|
|
|
180
171
|
return {media_type: self.build_media_type(item_type, examples)}
|
|
181
172
|
|
|
182
|
-
def build_media_type(
|
|
183
|
-
self, item_type: type, examples: Optional[List[Any]] = None
|
|
184
|
-
) -> MediaType:
|
|
173
|
+
def build_media_type(self, item_type: type, examples: Optional[list[Any]] = None) -> MediaType:
|
|
185
174
|
schema = self.schema_builder.classdef_to_ref(item_type)
|
|
186
175
|
if self.schema_transformer:
|
|
187
|
-
schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
|
|
176
|
+
schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
|
|
188
177
|
schema = schema_transformer(schema)
|
|
189
178
|
|
|
190
179
|
if not examples:
|
|
@@ -198,25 +187,14 @@ class ContentBuilder:
|
|
|
198
187
|
examples=self._build_examples(examples),
|
|
199
188
|
)
|
|
200
189
|
|
|
201
|
-
def _build_examples(
|
|
202
|
-
self, examples: List[Any]
|
|
203
|
-
) -> Dict[str, Union[Example, ExampleRef]]:
|
|
190
|
+
def _build_examples(self, examples: list[Any]) -> dict[str, Union[Example, ExampleRef]]:
|
|
204
191
|
"Creates a set of several examples for a media type."
|
|
205
192
|
|
|
206
|
-
|
|
207
|
-
sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore
|
|
208
|
-
else:
|
|
209
|
-
sample_transformer = lambda sample: sample
|
|
193
|
+
builder = ExampleBuilder(self.sample_transformer)
|
|
210
194
|
|
|
211
|
-
results:
|
|
195
|
+
results: dict[str, Union[Example, ExampleRef]] = {}
|
|
212
196
|
for example in examples:
|
|
213
|
-
value =
|
|
214
|
-
|
|
215
|
-
hash_string = (
|
|
216
|
-
hashlib.md5(json_dump_string(value).encode("utf-8")).digest().hex()
|
|
217
|
-
)
|
|
218
|
-
name = f"ex-{hash_string}"
|
|
219
|
-
|
|
197
|
+
name, value = builder.get_named(example)
|
|
220
198
|
results[name] = Example(value=value)
|
|
221
199
|
|
|
222
200
|
return results
|
|
@@ -224,12 +202,43 @@ class ContentBuilder:
|
|
|
224
202
|
def _build_example(self, example: Any) -> Any:
|
|
225
203
|
"Creates a single example for a media type."
|
|
226
204
|
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
builder = ExampleBuilder(self.sample_transformer)
|
|
206
|
+
return builder.get_anonymous(example)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ExampleBuilder:
|
|
210
|
+
sample_transformer: Callable[[JsonType], JsonType]
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
sample_transformer: Optional[Callable[[JsonType], JsonType]] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
if sample_transformer:
|
|
217
|
+
self.sample_transformer = sample_transformer
|
|
229
218
|
else:
|
|
230
|
-
sample_transformer = lambda sample: sample
|
|
219
|
+
self.sample_transformer = lambda sample: sample
|
|
220
|
+
|
|
221
|
+
def _get_value(self, example: Any) -> JsonType:
|
|
222
|
+
return self.sample_transformer(object_to_json(example))
|
|
223
|
+
|
|
224
|
+
def get_anonymous(self, example: Any) -> JsonType:
|
|
225
|
+
return self._get_value(example)
|
|
226
|
+
|
|
227
|
+
def get_named(self, example: Any) -> tuple[str, JsonType]:
|
|
228
|
+
value = self._get_value(example)
|
|
231
229
|
|
|
232
|
-
|
|
230
|
+
name: Optional[str] = None
|
|
231
|
+
|
|
232
|
+
if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
|
|
233
|
+
friendly_name = str(example)
|
|
234
|
+
if friendly_name.isprintable():
|
|
235
|
+
name = friendly_name
|
|
236
|
+
|
|
237
|
+
if name is None:
|
|
238
|
+
hash_string = hashlib.md5(json_dump_string(value).encode("utf-8")).digest().hex()
|
|
239
|
+
name = f"ex-{hash_string}"
|
|
240
|
+
|
|
241
|
+
return name, value
|
|
233
242
|
|
|
234
243
|
|
|
235
244
|
@dataclass
|
|
@@ -243,17 +252,17 @@ class ResponseOptions:
|
|
|
243
252
|
:param default_status_code: HTTP status code assigned to responses that have no mapping.
|
|
244
253
|
"""
|
|
245
254
|
|
|
246
|
-
type_descriptions:
|
|
247
|
-
examples: Optional[
|
|
248
|
-
status_catalog:
|
|
255
|
+
type_descriptions: dict[type, str]
|
|
256
|
+
examples: Optional[list[Any]]
|
|
257
|
+
status_catalog: dict[type, HTTPStatusCode]
|
|
249
258
|
default_status_code: HTTPStatusCode
|
|
250
259
|
|
|
251
260
|
|
|
252
261
|
@dataclass
|
|
253
262
|
class StatusResponse:
|
|
254
263
|
status_code: str
|
|
255
|
-
types:
|
|
256
|
-
examples:
|
|
264
|
+
types: list[type] = dataclasses.field(default_factory=list)
|
|
265
|
+
examples: list[Any] = dataclasses.field(default_factory=list)
|
|
257
266
|
|
|
258
267
|
|
|
259
268
|
class ResponseBuilder:
|
|
@@ -262,15 +271,11 @@ class ResponseBuilder:
|
|
|
262
271
|
def __init__(self, content_builder: ContentBuilder) -> None:
|
|
263
272
|
self.content_builder = content_builder
|
|
264
273
|
|
|
265
|
-
def _get_status_responses(
|
|
266
|
-
|
|
267
|
-
) -> Dict[str, StatusResponse]:
|
|
268
|
-
status_responses: Dict[str, StatusResponse] = {}
|
|
274
|
+
def _get_status_responses(self, options: ResponseOptions) -> dict[str, StatusResponse]:
|
|
275
|
+
status_responses: dict[str, StatusResponse] = {}
|
|
269
276
|
|
|
270
277
|
for response_type in options.type_descriptions.keys():
|
|
271
|
-
status_code = http_status_to_string(
|
|
272
|
-
options.status_catalog.get(response_type, options.default_status_code)
|
|
273
|
-
)
|
|
278
|
+
status_code = http_status_to_string(options.status_catalog.get(response_type, options.default_status_code))
|
|
274
279
|
|
|
275
280
|
# look up response for status code
|
|
276
281
|
if status_code not in status_responses:
|
|
@@ -282,22 +287,16 @@ class ResponseBuilder:
|
|
|
282
287
|
|
|
283
288
|
# append examples that have the matching response type
|
|
284
289
|
if options.examples:
|
|
285
|
-
status_response.examples.extend(
|
|
286
|
-
example
|
|
287
|
-
for example in options.examples
|
|
288
|
-
if isinstance(example, response_type)
|
|
289
|
-
)
|
|
290
|
+
status_response.examples.extend(example for example in options.examples if isinstance(example, response_type))
|
|
290
291
|
|
|
291
292
|
return dict(sorted(status_responses.items()))
|
|
292
293
|
|
|
293
|
-
def build_response(
|
|
294
|
-
self, options: ResponseOptions
|
|
295
|
-
) -> Dict[str, Union[Response, ResponseRef]]:
|
|
294
|
+
def build_response(self, options: ResponseOptions) -> dict[str, Union[Response, ResponseRef]]:
|
|
296
295
|
"""
|
|
297
296
|
Groups responses that have the same status code.
|
|
298
297
|
"""
|
|
299
298
|
|
|
300
|
-
responses:
|
|
299
|
+
responses: dict[str, Union[Response, ResponseRef]] = {}
|
|
301
300
|
status_responses = self._get_status_responses(options)
|
|
302
301
|
for status_code, status_response in status_responses.items():
|
|
303
302
|
response_types = tuple(status_response.types)
|
|
@@ -310,10 +309,7 @@ class ResponseBuilder:
|
|
|
310
309
|
description = " **OR** ".join(
|
|
311
310
|
filter(
|
|
312
311
|
None,
|
|
313
|
-
(
|
|
314
|
-
options.type_descriptions[response_type]
|
|
315
|
-
for response_type in response_types
|
|
316
|
-
),
|
|
312
|
+
(options.type_descriptions[response_type] for response_type in response_types),
|
|
317
313
|
)
|
|
318
314
|
)
|
|
319
315
|
|
|
@@ -327,9 +323,9 @@ class ResponseBuilder:
|
|
|
327
323
|
|
|
328
324
|
def _build_response(
|
|
329
325
|
self,
|
|
330
|
-
response_type: type,
|
|
326
|
+
response_type: Optional[type],
|
|
331
327
|
description: str,
|
|
332
|
-
examples: Optional[
|
|
328
|
+
examples: Optional[list[Any]] = None,
|
|
333
329
|
) -> Response:
|
|
334
330
|
"Creates a response subtree."
|
|
335
331
|
|
|
@@ -367,7 +363,7 @@ class Generator:
|
|
|
367
363
|
endpoint: type
|
|
368
364
|
options: Options
|
|
369
365
|
schema_builder: SchemaBuilder
|
|
370
|
-
responses:
|
|
366
|
+
responses: dict[str, Response]
|
|
371
367
|
|
|
372
368
|
def __init__(self, endpoint: type, options: Options) -> None:
|
|
373
369
|
self.endpoint = endpoint
|
|
@@ -388,24 +384,20 @@ class Generator:
|
|
|
388
384
|
description = typing.cast(str, schema.get("description"))
|
|
389
385
|
return Tag(
|
|
390
386
|
name=ref,
|
|
391
|
-
description="\n\n".join(
|
|
392
|
-
s for s in (title, description, definition) if s is not None
|
|
393
|
-
),
|
|
387
|
+
description="\n\n".join(s for s in (title, description, definition) if s is not None),
|
|
394
388
|
)
|
|
395
389
|
|
|
396
|
-
def _build_extra_tag_groups(
|
|
397
|
-
self, extra_types: Dict[str, List[type]]
|
|
398
|
-
) -> Dict[str, List[Tag]]:
|
|
390
|
+
def _build_extra_tag_groups(self, extra_types: dict[str, list[type]]) -> dict[str, list[Tag]]:
|
|
399
391
|
"""
|
|
400
392
|
Creates a dictionary of tag group captions as keys, and tag lists as values.
|
|
401
393
|
|
|
402
394
|
:param extra_types: A dictionary of type categories and list of types in that category.
|
|
403
395
|
"""
|
|
404
396
|
|
|
405
|
-
extra_tags:
|
|
397
|
+
extra_tags: dict[str, list[Tag]] = {}
|
|
406
398
|
|
|
407
399
|
for category_name, category_items in extra_types.items():
|
|
408
|
-
tag_list:
|
|
400
|
+
tag_list: list[Tag] = []
|
|
409
401
|
|
|
410
402
|
for extra_type in category_items:
|
|
411
403
|
name = python_type_to_name(extra_type)
|
|
@@ -419,9 +411,7 @@ class Generator:
|
|
|
419
411
|
|
|
420
412
|
def _build_operation(self, op: EndpointOperation) -> Operation:
|
|
421
413
|
doc_string = parse_type(op.func_ref)
|
|
422
|
-
doc_params = dict(
|
|
423
|
-
(param.name, param.description) for param in doc_string.params.values()
|
|
424
|
-
)
|
|
414
|
+
doc_params = dict((param.name, param.description) for param in doc_string.params.values())
|
|
425
415
|
|
|
426
416
|
# parameters passed in URL component path
|
|
427
417
|
path_parameters = [
|
|
@@ -462,11 +452,7 @@ class Generator:
|
|
|
462
452
|
builder = ContentBuilder(self.schema_builder)
|
|
463
453
|
request_name, request_type = op.request_param
|
|
464
454
|
requestBody = RequestBody(
|
|
465
|
-
content={
|
|
466
|
-
"application/json": builder.build_media_type(
|
|
467
|
-
request_type, op.request_examples
|
|
468
|
-
)
|
|
469
|
-
},
|
|
455
|
+
content={"application/json": builder.build_media_type(request_type, op.request_examples)},
|
|
470
456
|
description=doc_params.get(request_name),
|
|
471
457
|
required=True,
|
|
472
458
|
)
|
|
@@ -476,29 +462,16 @@ class Generator:
|
|
|
476
462
|
# success response types
|
|
477
463
|
if doc_string.returns is None and is_type_union(op.response_type):
|
|
478
464
|
# split union of return types into a list of response types
|
|
479
|
-
success_type_docstring:
|
|
480
|
-
typing.cast(type, item): parse_type(item)
|
|
481
|
-
for item in unwrap_union_types(op.response_type)
|
|
482
|
-
}
|
|
465
|
+
success_type_docstring: dict[type, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
|
|
483
466
|
success_type_descriptions = {
|
|
484
|
-
item: doc_string.short_description
|
|
485
|
-
for item, doc_string in success_type_docstring.items()
|
|
486
|
-
if doc_string.short_description
|
|
467
|
+
item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
|
|
487
468
|
}
|
|
488
469
|
else:
|
|
489
470
|
# use return type as a single response type
|
|
490
|
-
success_type_descriptions = {
|
|
491
|
-
op.response_type: (
|
|
492
|
-
doc_string.returns.description if doc_string.returns else "OK"
|
|
493
|
-
)
|
|
494
|
-
}
|
|
471
|
+
success_type_descriptions = {op.response_type: (doc_string.returns.description if doc_string.returns else "OK")}
|
|
495
472
|
|
|
496
473
|
response_examples = op.response_examples or []
|
|
497
|
-
success_examples = [
|
|
498
|
-
example
|
|
499
|
-
for example in response_examples
|
|
500
|
-
if not isinstance(example, Exception)
|
|
501
|
-
]
|
|
474
|
+
success_examples = [example for example in response_examples if not isinstance(example, Exception)]
|
|
502
475
|
|
|
503
476
|
content_builder = ContentBuilder(self.schema_builder)
|
|
504
477
|
response_builder = ResponseBuilder(content_builder)
|
|
@@ -512,14 +485,8 @@ class Generator:
|
|
|
512
485
|
|
|
513
486
|
# failure response types
|
|
514
487
|
if doc_string.raises:
|
|
515
|
-
exception_types:
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
exception_examples = [
|
|
519
|
-
example
|
|
520
|
-
for example in response_examples
|
|
521
|
-
if isinstance(example, Exception)
|
|
522
|
-
]
|
|
488
|
+
exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
|
|
489
|
+
exception_examples = [example for example in response_examples if isinstance(example, Exception)]
|
|
523
490
|
|
|
524
491
|
if self.options.error_wrapper:
|
|
525
492
|
schema_transformer = schema_error_wrapper
|
|
@@ -548,9 +515,7 @@ class Generator:
|
|
|
548
515
|
f"{op.func_name}_callback": {
|
|
549
516
|
"{$request.query.callback}": PathItem(
|
|
550
517
|
post=Operation(
|
|
551
|
-
requestBody=RequestBody(
|
|
552
|
-
content=builder.build_content(op.event_type)
|
|
553
|
-
),
|
|
518
|
+
requestBody=RequestBody(content=builder.build_content(op.event_type)),
|
|
554
519
|
responses={"200": Response(description="OK")},
|
|
555
520
|
)
|
|
556
521
|
)
|
|
@@ -572,11 +537,9 @@ class Generator:
|
|
|
572
537
|
)
|
|
573
538
|
|
|
574
539
|
def generate(self) -> Document:
|
|
575
|
-
paths:
|
|
576
|
-
endpoint_classes:
|
|
577
|
-
for op in get_endpoint_operations(
|
|
578
|
-
self.endpoint, use_examples=self.options.use_examples
|
|
579
|
-
):
|
|
540
|
+
paths: dict[str, PathItem] = {}
|
|
541
|
+
endpoint_classes: set[type] = set()
|
|
542
|
+
for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
|
|
580
543
|
endpoint_classes.add(op.defining_class)
|
|
581
544
|
|
|
582
545
|
operation = self._build_operation(op)
|
|
@@ -600,7 +563,7 @@ class Generator:
|
|
|
600
563
|
else:
|
|
601
564
|
paths[route] = pathItem
|
|
602
565
|
|
|
603
|
-
operation_tags:
|
|
566
|
+
operation_tags: list[Tag] = []
|
|
604
567
|
for cls in endpoint_classes:
|
|
605
568
|
doc_string = parse_type(cls)
|
|
606
569
|
operation_tags.append(
|
|
@@ -612,36 +575,27 @@ class Generator:
|
|
|
612
575
|
)
|
|
613
576
|
|
|
614
577
|
# types that are produced/consumed by operations
|
|
615
|
-
type_tags = [
|
|
616
|
-
self._build_type_tag(ref, schema)
|
|
617
|
-
for ref, schema in self.schema_builder.schemas.items()
|
|
618
|
-
]
|
|
578
|
+
type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
|
|
619
579
|
|
|
620
580
|
# types that are emitted by events
|
|
621
|
-
event_tags:
|
|
581
|
+
event_tags: list[Tag] = []
|
|
622
582
|
events = get_endpoint_events(self.endpoint)
|
|
623
583
|
for ref, event_type in events.items():
|
|
624
584
|
event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
|
|
625
585
|
event_tags.append(self._build_type_tag(ref, event_schema))
|
|
626
586
|
|
|
627
587
|
# types that are explicitly declared
|
|
628
|
-
extra_tag_groups:
|
|
588
|
+
extra_tag_groups: dict[str, list[Tag]] = {}
|
|
629
589
|
if self.options.extra_types is not None:
|
|
630
590
|
if isinstance(self.options.extra_types, list):
|
|
631
|
-
extra_tag_groups = self._build_extra_tag_groups(
|
|
632
|
-
{"AdditionalTypes": self.options.extra_types}
|
|
633
|
-
)
|
|
591
|
+
extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
|
|
634
592
|
elif isinstance(self.options.extra_types, dict):
|
|
635
|
-
extra_tag_groups = self._build_extra_tag_groups(
|
|
636
|
-
self.options.extra_types
|
|
637
|
-
)
|
|
593
|
+
extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
|
|
638
594
|
else:
|
|
639
|
-
raise TypeError(
|
|
640
|
-
f"type mismatch for collection of extra types: {type(self.options.extra_types)}"
|
|
641
|
-
)
|
|
595
|
+
raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
|
|
642
596
|
|
|
643
597
|
# list all operations and types
|
|
644
|
-
tags:
|
|
598
|
+
tags: list[Tag] = []
|
|
645
599
|
tags.extend(operation_tags)
|
|
646
600
|
tags.extend(type_tags)
|
|
647
601
|
tags.extend(event_tags)
|
|
@@ -686,11 +640,7 @@ class Generator:
|
|
|
686
640
|
return Document(
|
|
687
641
|
openapi=".".join(str(item) for item in self.options.version),
|
|
688
642
|
info=self.options.info,
|
|
689
|
-
jsonSchemaDialect=(
|
|
690
|
-
"https://json-schema.org/draft/2020-12/schema"
|
|
691
|
-
if self.options.version >= (3, 1, 0)
|
|
692
|
-
else None
|
|
693
|
-
),
|
|
643
|
+
jsonSchemaDialect=("https://json-schema.org/draft/2020-12/schema" if self.options.version >= (3, 1, 0) else None),
|
|
694
644
|
servers=[self.options.server],
|
|
695
645
|
paths=paths,
|
|
696
646
|
components=Components(
|