python-openapi 0.1.10__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.2.0/MANIFEST.in +2 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/PKG-INFO +17 -15
- {python_openapi-0.1.10 → python_openapi-0.2.0}/README.md +4 -4
- python_openapi-0.2.0/pyopenapi/__init__.py +14 -0
- python_openapi-0.1.10/pyopenapi/__init__.py → python_openapi-0.2.0/pyopenapi/decorators.py +15 -18
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/generator.py +42 -34
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/metadata.py +11 -3
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/operations.py +26 -17
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/options.py +16 -9
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/proxy.py +15 -6
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/specification.py +34 -26
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/utility.py +12 -8
- python_openapi-0.2.0/pyproject.toml +58 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/PKG-INFO +17 -15
- {python_openapi-0.1.10 → 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.10 → python_openapi-0.2.0}/tests/test_openapi.py +4 -3
- {python_openapi-0.1.10 → python_openapi-0.2.0}/tests/test_proxy.py +13 -12
- 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 → python_openapi-0.2.0}/LICENSE +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/__main__.py +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/py.typed +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/template.html +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/dependency_links.txt +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/top_level.txt +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/zip-safe +0 -0
- {python_openapi-0.1.10 → python_openapi-0.2.0}/setup.py +0 -0
|
@@ -1,30 +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
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
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.9
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: aiohttp>=3.
|
|
27
|
-
Requires-Dist: json_strong_typing>=0.3.
|
|
27
|
+
Requires-Dist: aiohttp>=3.12
|
|
28
|
+
Requires-Dist: json_strong_typing>=0.3.9
|
|
29
|
+
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# Generate an OpenAPI specification from a Python class
|
|
30
32
|
|
|
@@ -35,7 +37,7 @@ Requires-Dist: json_strong_typing>=0.3.7
|
|
|
35
37
|
* supports standard and asynchronous functions (`async def`)
|
|
36
38
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
37
39
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
38
|
-
* handles generic types (`
|
|
40
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
|
|
39
41
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
40
42
|
* maps composite types to HTTP request body
|
|
41
43
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -88,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
|
|
|
88
90
|
|
|
89
91
|
```python
|
|
90
92
|
class JobManagement:
|
|
91
|
-
def create_job(self, items:
|
|
93
|
+
def create_job(self, items: list[URL]) -> uuid.UUID:
|
|
92
94
|
...
|
|
93
95
|
|
|
94
96
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -127,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
|
|
|
127
129
|
|
|
128
130
|
### Documenting operations
|
|
129
131
|
|
|
130
|
-
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:
|
|
131
133
|
|
|
132
134
|
```python
|
|
133
135
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -170,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
|
|
|
170
172
|
|
|
171
173
|
### Mapping function name prefixes to HTTP verbs
|
|
172
174
|
|
|
173
|
-
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:
|
|
174
176
|
|
|
175
177
|
| Prefix | HTTP verb |
|
|
176
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
|
-
|
|
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
|
+
from typing import Any, Callable, Optional, 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(
|
|
@@ -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
|
|
|
@@ -38,17 +44,8 @@ def webmethod(
|
|
|
38
44
|
if response_example:
|
|
39
45
|
response_examples = [response_example]
|
|
40
46
|
|
|
41
|
-
def wrap(cls:
|
|
42
|
-
|
|
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
|
-
)
|
|
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]
|
|
52
49
|
return cls
|
|
53
50
|
|
|
54
51
|
return wrap
|
|
@@ -1,16 +1,24 @@
|
|
|
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
|
import dataclasses
|
|
2
10
|
import hashlib
|
|
3
11
|
import ipaddress
|
|
4
12
|
import typing
|
|
5
13
|
from dataclasses import dataclass
|
|
6
14
|
from http import HTTPStatus
|
|
7
|
-
from typing import Any, Callable,
|
|
15
|
+
from typing import Any, Callable, Optional, Union
|
|
8
16
|
|
|
9
|
-
from strong_typing.core import JsonType
|
|
17
|
+
from strong_typing.core import JsonType, Schema
|
|
10
18
|
from strong_typing.docstring import Docstring, parse_type
|
|
11
19
|
from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
|
|
12
20
|
from strong_typing.name import python_type_to_name
|
|
13
|
-
from strong_typing.schema import JsonSchemaGenerator,
|
|
21
|
+
from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
|
|
14
22
|
from strong_typing.serialization import json_dump_string, object_to_json
|
|
15
23
|
|
|
16
24
|
from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
|
|
@@ -78,7 +86,7 @@ def http_status_to_string(status_code: HTTPStatusCode) -> str:
|
|
|
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
|
|
@@ -150,7 +158,7 @@ class ContentBuilder:
|
|
|
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: Optional[
|
|
161
|
+
def build_content(self, payload_type: type, examples: Optional[list[Any]] = None) -> dict[str, MediaType]:
|
|
154
162
|
"Creates the content subtree for a request or response."
|
|
155
163
|
|
|
156
164
|
if is_generic_list(payload_type):
|
|
@@ -162,7 +170,7 @@ class ContentBuilder:
|
|
|
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: Optional[
|
|
173
|
+
def build_media_type(self, item_type: type, examples: Optional[list[Any]] = None) -> MediaType:
|
|
166
174
|
schema = self.schema_builder.classdef_to_ref(item_type)
|
|
167
175
|
if self.schema_transformer:
|
|
168
176
|
schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
|
|
@@ -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, Union[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, Union[Example, ExampleRef]] = {}
|
|
188
196
|
for example in examples:
|
|
189
197
|
name, value = builder.get_named(example)
|
|
190
198
|
results[name] = Example(value=value)
|
|
@@ -208,7 +216,7 @@ class ExampleBuilder:
|
|
|
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
230
|
name: Optional[str] = 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: Optional[
|
|
249
|
-
status_catalog:
|
|
255
|
+
type_descriptions: dict[type, str]
|
|
256
|
+
examples: Optional[list[Any]]
|
|
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] = dataclasses.field(default_factory=list)
|
|
265
|
+
examples: list[Any] = dataclasses.field(default_factory=list)
|
|
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,12 +291,12 @@ 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, Union[Response, ResponseRef]]:
|
|
287
295
|
"""
|
|
288
296
|
Groups responses that have the same status code.
|
|
289
297
|
"""
|
|
290
298
|
|
|
291
|
-
responses:
|
|
299
|
+
responses: dict[str, Union[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)
|
|
@@ -315,9 +323,9 @@ class ResponseBuilder:
|
|
|
315
323
|
|
|
316
324
|
def _build_response(
|
|
317
325
|
self,
|
|
318
|
-
response_type: type,
|
|
326
|
+
response_type: Optional[type],
|
|
319
327
|
description: str,
|
|
320
|
-
examples: Optional[
|
|
328
|
+
examples: Optional[list[Any]] = None,
|
|
321
329
|
) -> Response:
|
|
322
330
|
"Creates a response subtree."
|
|
323
331
|
|
|
@@ -355,7 +363,7 @@ class Generator:
|
|
|
355
363
|
endpoint: type
|
|
356
364
|
options: Options
|
|
357
365
|
schema_builder: SchemaBuilder
|
|
358
|
-
responses:
|
|
366
|
+
responses: dict[str, Response]
|
|
359
367
|
|
|
360
368
|
def __init__(self, endpoint: type, options: Options) -> None:
|
|
361
369
|
self.endpoint = endpoint
|
|
@@ -379,17 +387,17 @@ class Generator:
|
|
|
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]]) -> 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)
|
|
@@ -454,7 +462,7 @@ 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, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
|
|
458
466
|
success_type_descriptions = {
|
|
459
467
|
item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
|
|
460
468
|
}
|
|
@@ -477,7 +485,7 @@ class Generator:
|
|
|
477
485
|
|
|
478
486
|
# failure response types
|
|
479
487
|
if doc_string.raises:
|
|
480
|
-
exception_types:
|
|
488
|
+
exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
|
|
481
489
|
exception_examples = [example for example in response_examples if isinstance(example, Exception)]
|
|
482
490
|
|
|
483
491
|
if self.options.error_wrapper:
|
|
@@ -529,8 +537,8 @@ class Generator:
|
|
|
529
537
|
)
|
|
530
538
|
|
|
531
539
|
def generate(self) -> Document:
|
|
532
|
-
paths:
|
|
533
|
-
endpoint_classes:
|
|
540
|
+
paths: dict[str, PathItem] = {}
|
|
541
|
+
endpoint_classes: set[type] = set()
|
|
534
542
|
for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
|
|
535
543
|
endpoint_classes.add(op.defining_class)
|
|
536
544
|
|
|
@@ -555,7 +563,7 @@ class Generator:
|
|
|
555
563
|
else:
|
|
556
564
|
paths[route] = pathItem
|
|
557
565
|
|
|
558
|
-
operation_tags:
|
|
566
|
+
operation_tags: list[Tag] = []
|
|
559
567
|
for cls in endpoint_classes:
|
|
560
568
|
doc_string = parse_type(cls)
|
|
561
569
|
operation_tags.append(
|
|
@@ -570,14 +578,14 @@ class Generator:
|
|
|
570
578
|
type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
|
|
571
579
|
|
|
572
580
|
# types that are emitted by events
|
|
573
|
-
event_tags:
|
|
581
|
+
event_tags: list[Tag] = []
|
|
574
582
|
events = get_endpoint_events(self.endpoint)
|
|
575
583
|
for ref, event_type in events.items():
|
|
576
584
|
event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
|
|
577
585
|
event_tags.append(self._build_type_tag(ref, event_schema))
|
|
578
586
|
|
|
579
587
|
# types that are explicitly declared
|
|
580
|
-
extra_tag_groups:
|
|
588
|
+
extra_tag_groups: dict[str, list[Tag]] = {}
|
|
581
589
|
if self.options.extra_types is not None:
|
|
582
590
|
if isinstance(self.options.extra_types, list):
|
|
583
591
|
extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
|
|
@@ -587,7 +595,7 @@ class Generator:
|
|
|
587
595
|
raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
|
|
588
596
|
|
|
589
597
|
# list all operations and types
|
|
590
|
-
tags:
|
|
598
|
+
tags: list[Tag] = []
|
|
591
599
|
tags.extend(operation_tags)
|
|
592
600
|
tags.extend(type_tags)
|
|
593
601
|
tags.extend(event_tags)
|
|
@@ -1,5 +1,13 @@
|
|
|
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 dataclasses import dataclass
|
|
2
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Optional
|
|
3
11
|
|
|
4
12
|
|
|
5
13
|
@dataclass
|
|
@@ -15,5 +23,5 @@ class WebMethod:
|
|
|
15
23
|
|
|
16
24
|
route: Optional[str] = None
|
|
17
25
|
public: bool = False
|
|
18
|
-
request_examples: Optional[
|
|
19
|
-
response_examples: Optional[
|
|
26
|
+
request_examples: Optional[list[Any]] = None
|
|
27
|
+
response_examples: Optional[list[Any]] = None
|
|
@@ -1,17 +1,25 @@
|
|
|
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
|
import collections.abc
|
|
2
10
|
import enum
|
|
3
11
|
import inspect
|
|
4
12
|
import typing
|
|
5
13
|
import uuid
|
|
6
14
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, Callable,
|
|
15
|
+
from typing import Any, Callable, Iterable, Iterator, Optional, Union
|
|
8
16
|
|
|
9
17
|
from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
|
|
10
18
|
|
|
11
19
|
from .metadata import WebMethod
|
|
12
20
|
|
|
13
21
|
|
|
14
|
-
def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) ->
|
|
22
|
+
def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> tuple[Optional[str], str]:
|
|
15
23
|
"""
|
|
16
24
|
Recognizes a prefix at the beginning of a string.
|
|
17
25
|
|
|
@@ -34,11 +42,11 @@ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> Tuple[O
|
|
|
34
42
|
return None, s
|
|
35
43
|
|
|
36
44
|
|
|
37
|
-
def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type:
|
|
38
|
-
"Maps a
|
|
45
|
+
def _get_annotation_type(annotation: Union[type, str], callable: Callable[..., Any]) -> type:
|
|
46
|
+
"Maps a string (forward) reference to a type, as if using `from __future__ import annotations`."
|
|
39
47
|
|
|
40
48
|
if isinstance(annotation, str):
|
|
41
|
-
return eval(annotation, callable.__globals__)
|
|
49
|
+
return typing.cast(type, eval(annotation, callable.__globals__))
|
|
42
50
|
else:
|
|
43
51
|
return annotation
|
|
44
52
|
|
|
@@ -54,7 +62,7 @@ class HTTPMethod(enum.Enum):
|
|
|
54
62
|
PATCH = "PATCH"
|
|
55
63
|
|
|
56
64
|
|
|
57
|
-
OperationParameter =
|
|
65
|
+
OperationParameter = tuple[str, type]
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
class ValidationError(TypeError):
|
|
@@ -87,15 +95,15 @@ class EndpointOperation:
|
|
|
87
95
|
func_name: str
|
|
88
96
|
func_ref: Callable[..., Any]
|
|
89
97
|
route: Optional[str]
|
|
90
|
-
path_params:
|
|
91
|
-
query_params:
|
|
98
|
+
path_params: list[OperationParameter]
|
|
99
|
+
query_params: list[OperationParameter]
|
|
92
100
|
request_param: Optional[OperationParameter]
|
|
93
101
|
event_type: Optional[type]
|
|
94
102
|
response_type: type
|
|
95
103
|
http_method: HTTPMethod
|
|
96
104
|
public: bool
|
|
97
|
-
request_examples: Optional[
|
|
98
|
-
response_examples: Optional[
|
|
105
|
+
request_examples: Optional[list[Any]] = None
|
|
106
|
+
response_examples: Optional[list[Any]] = None
|
|
99
107
|
|
|
100
108
|
def get_route(self) -> str:
|
|
101
109
|
if self.route is not None:
|
|
@@ -108,9 +116,9 @@ class EndpointOperation:
|
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
class _FormatParameterExtractor:
|
|
111
|
-
"A visitor to
|
|
119
|
+
"A visitor to extract parameters in a format string."
|
|
112
120
|
|
|
113
|
-
keys:
|
|
121
|
+
keys: list[str]
|
|
114
122
|
|
|
115
123
|
def __init__(self) -> None:
|
|
116
124
|
self.keys = []
|
|
@@ -120,13 +128,13 @@ class _FormatParameterExtractor:
|
|
|
120
128
|
return None
|
|
121
129
|
|
|
122
130
|
|
|
123
|
-
def _get_route_parameters(route: str) ->
|
|
131
|
+
def _get_route_parameters(route: str) -> list[str]:
|
|
124
132
|
extractor = _FormatParameterExtractor()
|
|
125
133
|
route.format_map(extractor)
|
|
126
134
|
return extractor.keys
|
|
127
135
|
|
|
128
136
|
|
|
129
|
-
def _get_endpoint_functions(endpoint: type, prefixes:
|
|
137
|
+
def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
|
|
130
138
|
if not inspect.isclass(endpoint):
|
|
131
139
|
raise ValidationError(f"object is not a class type: {endpoint}")
|
|
132
140
|
|
|
@@ -151,7 +159,7 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
|
|
|
151
159
|
raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
|
|
152
160
|
|
|
153
161
|
|
|
154
|
-
def get_endpoint_operations(endpoint: type, use_examples: bool = True) ->
|
|
162
|
+
def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> list[EndpointOperation]:
|
|
155
163
|
"""
|
|
156
164
|
Extracts a list of member functions in a class eligible for HTTP interface binding.
|
|
157
165
|
|
|
@@ -247,7 +255,8 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
247
255
|
if request_param is not None:
|
|
248
256
|
param = (param_name, param_type)
|
|
249
257
|
raise ValidationError(
|
|
250
|
-
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}':
|
|
258
|
+
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': "
|
|
259
|
+
f"{request_param} and {param}"
|
|
251
260
|
)
|
|
252
261
|
|
|
253
262
|
# composite types are read from body
|
|
@@ -310,7 +319,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
|
|
|
310
319
|
return result
|
|
311
320
|
|
|
312
321
|
|
|
313
|
-
def get_endpoint_events(endpoint: type) ->
|
|
322
|
+
def get_endpoint_events(endpoint: type) -> dict[str, type]:
|
|
314
323
|
results = {}
|
|
315
324
|
|
|
316
325
|
for decl in typing.get_type_hints(endpoint).values():
|
|
@@ -1,13 +1,20 @@
|
|
|
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
|
import dataclasses
|
|
2
10
|
from dataclasses import dataclass
|
|
3
11
|
from http import HTTPStatus
|
|
4
|
-
from typing import Callable, ClassVar,
|
|
12
|
+
from typing import Callable, ClassVar, Optional, Union
|
|
5
13
|
|
|
6
|
-
from .specification import Info, SecurityScheme
|
|
14
|
+
from .specification import Info, SecurityScheme, Server
|
|
7
15
|
from .specification import SecuritySchemeAPI as SecuritySchemeAPI
|
|
8
16
|
from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
|
|
9
17
|
from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
|
|
10
|
-
from .specification import Server
|
|
11
18
|
|
|
12
19
|
HTTPStatusCode = Union[HTTPStatus, int, str]
|
|
13
20
|
|
|
@@ -30,17 +37,17 @@ class Options:
|
|
|
30
37
|
|
|
31
38
|
server: Server
|
|
32
39
|
info: Info
|
|
33
|
-
version:
|
|
40
|
+
version: tuple[int, int, int] = (3, 1, 0)
|
|
34
41
|
default_security_scheme: Optional[SecurityScheme] = None
|
|
35
|
-
extra_types: Union[
|
|
42
|
+
extra_types: Union[list[type], dict[str, list[type]], None] = None
|
|
36
43
|
use_examples: bool = True
|
|
37
|
-
success_responses:
|
|
38
|
-
error_responses:
|
|
44
|
+
success_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
45
|
+
error_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
39
46
|
error_wrapper: bool = False
|
|
40
47
|
property_description_fun: Optional[Callable[[type, str, str], str]] = None
|
|
41
|
-
captions: Optional[
|
|
48
|
+
captions: Optional[dict[str, str]] = None
|
|
42
49
|
|
|
43
|
-
default_captions: ClassVar[
|
|
50
|
+
default_captions: ClassVar[dict[str, str]] = {
|
|
44
51
|
"Operations": "Operations",
|
|
45
52
|
"Types": "Types",
|
|
46
53
|
"Events": "Events",
|