python-openapi 0.1.10__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.1.10 → python_openapi-0.3.0}/LICENSE +1 -1
- python_openapi-0.3.0/MANIFEST.in +2 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/PKG-INFO +28 -17
- {python_openapi-0.1.10 → python_openapi-0.3.0}/README.md +5 -5
- python_openapi-0.3.0/pyopenapi/__init__.py +14 -0
- python_openapi-0.1.10/pyopenapi/__init__.py → python_openapi-0.3.0/pyopenapi/decorators.py +25 -21
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/generator.py +85 -75
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/metadata.py +14 -4
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/operations.py +48 -33
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/options.py +19 -12
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/proxy.py +20 -10
- python_openapi-0.3.0/pyopenapi/specification.py +272 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/utility.py +12 -8
- python_openapi-0.3.0/pyproject.toml +71 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/PKG-INFO +28 -17
- {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/SOURCES.txt +5 -2
- python_openapi-0.3.0/python_openapi.egg-info/requires.txt +13 -0
- python_openapi-0.3.0/setup.cfg +4 -0
- python_openapi-0.3.0/setup.py +12 -0
- python_openapi-0.3.0/tests/endpoint.md +5 -0
- python_openapi-0.3.0/tests/endpoint.py +396 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/tests/test_openapi.py +9 -10
- {python_openapi-0.1.10 → python_openapi-0.3.0}/tests/test_proxy.py +13 -12
- python_openapi-0.1.10/pyopenapi/specification.py +0 -253
- python_openapi-0.1.10/pyproject.toml +0 -6
- python_openapi-0.1.10/python_openapi.egg-info/requires.txt +0 -2
- python_openapi-0.1.10/setup.cfg +0 -48
- python_openapi-0.1.10/setup.py +0 -4
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/py.typed +0 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/template.html +0 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/dependency_links.txt +0 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/top_level.txt +0 -0
- {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/zip-safe +0 -0
- /python_openapi-0.1.10/pyopenapi/__main__.py → /python_openapi-0.3.0/tests/__init__.py +0 -0
|
@@ -1,30 +1,41 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
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
|
-
|
|
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
|
-
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
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
21
|
Classifier: Topic :: Software Development :: Code Generators
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
|
|
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"
|
|
38
|
+
Dynamic: license-file
|
|
28
39
|
|
|
29
40
|
# Generate an OpenAPI specification from a Python class
|
|
30
41
|
|
|
@@ -35,7 +46,7 @@ Requires-Dist: json_strong_typing>=0.3.7
|
|
|
35
46
|
* supports standard and asynchronous functions (`async def`)
|
|
36
47
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
37
48
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
38
|
-
* handles generic types (`
|
|
49
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `T | None`, `T1 | T2 | T3`)
|
|
39
50
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
40
51
|
* maps composite types to HTTP request body
|
|
41
52
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -88,7 +99,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
|
|
|
88
99
|
|
|
89
100
|
```python
|
|
90
101
|
class JobManagement:
|
|
91
|
-
def create_job(self, items:
|
|
102
|
+
def create_job(self, items: list[URL]) -> uuid.UUID:
|
|
92
103
|
...
|
|
93
104
|
|
|
94
105
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -127,7 +138,7 @@ The custom path must have placeholders for all positional-only parameters in the
|
|
|
127
138
|
|
|
128
139
|
### Documenting operations
|
|
129
140
|
|
|
130
|
-
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
141
|
+
Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
|
|
131
142
|
|
|
132
143
|
```python
|
|
133
144
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -160,7 +171,7 @@ OpenAPI supports specifying [examples](https://spec.openapis.org/oas/latest.html
|
|
|
160
171
|
Teacher("Vacska", "Mati", "Négyszögletű Kerek Erdő"),
|
|
161
172
|
],
|
|
162
173
|
)
|
|
163
|
-
def get_member_by_name(self, family: str, given: str, /) ->
|
|
174
|
+
def get_member_by_name(self, family: str, given: str, /) -> Student | Teacher:
|
|
164
175
|
...
|
|
165
176
|
```
|
|
166
177
|
|
|
@@ -170,7 +181,7 @@ The Python objects in `request_examples` and `response_examples` are translated
|
|
|
170
181
|
|
|
171
182
|
### Mapping function name prefixes to HTTP verbs
|
|
172
183
|
|
|
173
|
-
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
184
|
+
The following table identifies which function name prefixes map to which HTTP verbs:
|
|
174
185
|
|
|
175
186
|
| Prefix | HTTP verb |
|
|
176
187
|
| ------ | ----------- |
|
|
@@ -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]`, `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`
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -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 2021-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
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"
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
|
|
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
|
|
2
10
|
|
|
3
11
|
from .metadata import WebMethod
|
|
4
12
|
from .options import * # noqa: F403
|
|
5
13
|
from .utility import Specification as Specification
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
T = TypeVar("T")
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
def webmethod(
|
|
13
|
-
route:
|
|
14
|
-
public:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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]:
|
|
20
27
|
"""
|
|
21
28
|
Decorator that supplies additional metadata to an endpoint operation function.
|
|
22
29
|
|
|
@@ -38,16 +45,13 @@ def webmethod(
|
|
|
38
45
|
if response_example:
|
|
39
46
|
response_examples = [response_example]
|
|
40
47
|
|
|
41
|
-
def wrap(cls:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
request_examples=request_examples,
|
|
49
|
-
response_examples=response_examples,
|
|
50
|
-
),
|
|
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,
|
|
51
55
|
)
|
|
52
56
|
return cls
|
|
53
57
|
|
|
@@ -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(
|