python-openapi 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi/__init__.py +11 -55
- pyopenapi/decorators.py +51 -0
- pyopenapi/generator.py +100 -150
- pyopenapi/metadata.py +11 -3
- pyopenapi/operations.py +34 -52
- pyopenapi/options.py +19 -19
- pyopenapi/proxy.py +22 -28
- pyopenapi/specification.py +37 -28
- pyopenapi/utility.py +14 -16
- {python_openapi-0.1.9.dist-info → python_openapi-0.2.0.dist-info}/METADATA +19 -15
- python_openapi-0.2.0.dist-info/RECORD +18 -0
- {python_openapi-0.1.9.dist-info → python_openapi-0.2.0.dist-info}/WHEEL +1 -1
- {python_openapi-0.1.9.dist-info → python_openapi-0.2.0.dist-info/licenses}/LICENSE +1 -1
- python_openapi-0.1.9.dist-info/RECORD +0 -17
- {python_openapi-0.1.9.dist-info → python_openapi-0.2.0.dist-info}/top_level.txt +0 -0
- {python_openapi-0.1.9.dist-info → python_openapi-0.2.0.dist-info}/zip-safe +0 -0
pyopenapi/__init__.py
CHANGED
|
@@ -1,58 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Generate an OpenAPI specification from a Python class definition
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
from .options import *
|
|
5
|
-
from .utility import Specification
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
:see: https://github.com/hunyadi/pyopenapi
|
|
7
|
+
"""
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
request_example: Optional[Any] = None,
|
|
16
|
-
response_example: Optional[Any] = None,
|
|
17
|
-
request_examples: Optional[List[Any]] = None,
|
|
18
|
-
response_examples: Optional[List[Any]] = None,
|
|
19
|
-
) -> Callable[[T], T]:
|
|
20
|
-
"""
|
|
21
|
-
Decorator that supplies additional metadata to an endpoint operation function.
|
|
22
|
-
|
|
23
|
-
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
24
|
-
:param public: True if the operation can be invoked without prior authentication.
|
|
25
|
-
:param request_example: A sample request that the operation might take.
|
|
26
|
-
:param response_example: A sample response that the operation might produce.
|
|
27
|
-
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
28
|
-
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
if request_example is not None and request_examples is not None:
|
|
32
|
-
raise ValueError(
|
|
33
|
-
"arguments `request_example` and `request_examples` are exclusive"
|
|
34
|
-
)
|
|
35
|
-
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
|
-
)
|
|
39
|
-
|
|
40
|
-
if request_example:
|
|
41
|
-
request_examples = [request_example]
|
|
42
|
-
if response_example:
|
|
43
|
-
response_examples = [response_example]
|
|
44
|
-
|
|
45
|
-
def wrap(cls: T) -> T:
|
|
46
|
-
setattr(
|
|
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
|
-
)
|
|
56
|
-
return cls
|
|
57
|
-
|
|
58
|
-
return wrap
|
|
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"
|
pyopenapi/decorators.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
|
10
|
+
|
|
11
|
+
from .metadata import WebMethod
|
|
12
|
+
from .options import * # noqa: F403
|
|
13
|
+
from .utility import Specification as Specification
|
|
14
|
+
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def webmethod(
|
|
19
|
+
route: Optional[str] = None,
|
|
20
|
+
public: Optional[bool] = False,
|
|
21
|
+
request_example: Optional[Any] = None,
|
|
22
|
+
response_example: Optional[Any] = None,
|
|
23
|
+
request_examples: Optional[list[Any]] = None,
|
|
24
|
+
response_examples: Optional[list[Any]] = None,
|
|
25
|
+
) -> Callable[[F], F]:
|
|
26
|
+
"""
|
|
27
|
+
Decorator that supplies additional metadata to an endpoint operation function.
|
|
28
|
+
|
|
29
|
+
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
|
|
30
|
+
:param public: True if the operation can be invoked without prior authentication.
|
|
31
|
+
:param request_example: A sample request that the operation might take.
|
|
32
|
+
:param response_example: A sample response that the operation might produce.
|
|
33
|
+
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
|
|
34
|
+
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if request_example is not None and request_examples is not None:
|
|
38
|
+
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
|
|
39
|
+
if response_example is not None and response_examples is not None:
|
|
40
|
+
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
|
|
41
|
+
|
|
42
|
+
if request_example:
|
|
43
|
+
request_examples = [request_example]
|
|
44
|
+
if response_example:
|
|
45
|
+
response_examples = [response_example]
|
|
46
|
+
|
|
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]
|
|
49
|
+
return cls
|
|
50
|
+
|
|
51
|
+
return wrap
|
pyopenapi/generator.py
CHANGED
|
@@ -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(
|
pyopenapi/metadata.py
CHANGED
|
@@ -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
|
pyopenapi/operations.py
CHANGED
|
@@ -1,24 +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
|
-
from strong_typing.inspection import
|
|
10
|
-
get_signature,
|
|
11
|
-
is_type_enum,
|
|
12
|
-
is_type_optional,
|
|
13
|
-
unwrap_optional_type,
|
|
14
|
-
)
|
|
17
|
+
from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
|
|
15
18
|
|
|
16
19
|
from .metadata import WebMethod
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
def split_prefix(
|
|
20
|
-
s: str, sep: str, prefix: Union[str, Iterable[str]]
|
|
21
|
-
) -> Tuple[Optional[str], str]:
|
|
22
|
+
def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> tuple[Optional[str], str]:
|
|
22
23
|
"""
|
|
23
24
|
Recognizes a prefix at the beginning of a string.
|
|
24
25
|
|
|
@@ -41,15 +42,16 @@ def split_prefix(
|
|
|
41
42
|
return None, s
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type:
|
|
45
|
-
"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`."
|
|
46
47
|
|
|
47
48
|
if isinstance(annotation, str):
|
|
48
|
-
return eval(annotation, callable.__globals__)
|
|
49
|
+
return typing.cast(type, eval(annotation, callable.__globals__))
|
|
49
50
|
else:
|
|
50
51
|
return annotation
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
@enum.unique
|
|
53
55
|
class HTTPMethod(enum.Enum):
|
|
54
56
|
"HTTP method used to invoke an endpoint operation."
|
|
55
57
|
|
|
@@ -60,7 +62,7 @@ class HTTPMethod(enum.Enum):
|
|
|
60
62
|
PATCH = "PATCH"
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
OperationParameter =
|
|
65
|
+
OperationParameter = tuple[str, type]
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
class ValidationError(TypeError):
|
|
@@ -93,15 +95,15 @@ class EndpointOperation:
|
|
|
93
95
|
func_name: str
|
|
94
96
|
func_ref: Callable[..., Any]
|
|
95
97
|
route: Optional[str]
|
|
96
|
-
path_params:
|
|
97
|
-
query_params:
|
|
98
|
+
path_params: list[OperationParameter]
|
|
99
|
+
query_params: list[OperationParameter]
|
|
98
100
|
request_param: Optional[OperationParameter]
|
|
99
101
|
event_type: Optional[type]
|
|
100
102
|
response_type: type
|
|
101
103
|
http_method: HTTPMethod
|
|
102
104
|
public: bool
|
|
103
|
-
request_examples: Optional[
|
|
104
|
-
response_examples: Optional[
|
|
105
|
+
request_examples: Optional[list[Any]] = None
|
|
106
|
+
response_examples: Optional[list[Any]] = None
|
|
105
107
|
|
|
106
108
|
def get_route(self) -> str:
|
|
107
109
|
if self.route is not None:
|
|
@@ -114,9 +116,9 @@ class EndpointOperation:
|
|
|
114
116
|
|
|
115
117
|
|
|
116
118
|
class _FormatParameterExtractor:
|
|
117
|
-
"A visitor to
|
|
119
|
+
"A visitor to extract parameters in a format string."
|
|
118
120
|
|
|
119
|
-
keys:
|
|
121
|
+
keys: list[str]
|
|
120
122
|
|
|
121
123
|
def __init__(self) -> None:
|
|
122
124
|
self.keys = []
|
|
@@ -126,15 +128,13 @@ class _FormatParameterExtractor:
|
|
|
126
128
|
return None
|
|
127
129
|
|
|
128
130
|
|
|
129
|
-
def _get_route_parameters(route: str) ->
|
|
131
|
+
def _get_route_parameters(route: str) -> list[str]:
|
|
130
132
|
extractor = _FormatParameterExtractor()
|
|
131
133
|
route.format_map(extractor)
|
|
132
134
|
return extractor.keys
|
|
133
135
|
|
|
134
136
|
|
|
135
|
-
def _get_endpoint_functions(
|
|
136
|
-
endpoint: type, prefixes: List[str]
|
|
137
|
-
) -> Iterator[Tuple[str, str, str, Callable]]:
|
|
137
|
+
def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
|
|
138
138
|
if not inspect.isclass(endpoint):
|
|
139
139
|
raise ValidationError(f"object is not a class type: {endpoint}")
|
|
140
140
|
|
|
@@ -156,14 +156,10 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
|
|
|
156
156
|
if name == member_fn:
|
|
157
157
|
return cls
|
|
158
158
|
|
|
159
|
-
raise ValidationError(
|
|
160
|
-
f"cannot find defining class for {member_fn} in {derived_cls}"
|
|
161
|
-
)
|
|
159
|
+
raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
|
|
162
160
|
|
|
163
161
|
|
|
164
|
-
def get_endpoint_operations(
|
|
165
|
-
endpoint: type, use_examples: bool = True
|
|
166
|
-
) -> List[EndpointOperation]:
|
|
162
|
+
def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> list[EndpointOperation]:
|
|
167
163
|
"""
|
|
168
164
|
Extracts a list of member functions in a class eligible for HTTP interface binding.
|
|
169
165
|
|
|
@@ -230,36 +226,23 @@ def get_endpoint_operations(
|
|
|
230
226
|
|
|
231
227
|
# check if all parameters have explicit type
|
|
232
228
|
if parameter.annotation is inspect.Parameter.empty:
|
|
233
|
-
raise ValidationError(
|
|
234
|
-
f"parameter '{param_name}' in function '{func_name}' has no type annotation"
|
|
235
|
-
)
|
|
229
|
+
raise ValidationError(f"parameter '{param_name}' in function '{func_name}' has no type annotation")
|
|
236
230
|
|
|
237
231
|
if is_type_optional(param_type):
|
|
238
232
|
inner_type: type = unwrap_optional_type(param_type)
|
|
239
233
|
else:
|
|
240
234
|
inner_type = param_type
|
|
241
235
|
|
|
242
|
-
if (
|
|
243
|
-
inner_type is bool
|
|
244
|
-
or inner_type is int
|
|
245
|
-
or inner_type is float
|
|
246
|
-
or inner_type is str
|
|
247
|
-
or inner_type is uuid.UUID
|
|
248
|
-
or is_type_enum(inner_type)
|
|
249
|
-
):
|
|
236
|
+
if inner_type is bool or inner_type is int or inner_type is float or inner_type is str or inner_type is uuid.UUID or is_type_enum(inner_type):
|
|
250
237
|
if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
251
238
|
if route_params is not None and param_name not in route_params:
|
|
252
|
-
raise ValidationError(
|
|
253
|
-
f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'"
|
|
254
|
-
)
|
|
239
|
+
raise ValidationError(f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'")
|
|
255
240
|
|
|
256
241
|
# simple type maps to route path element, e.g. /study/{uuid}/{version}
|
|
257
242
|
path_params.append((param_name, param_type))
|
|
258
243
|
else:
|
|
259
244
|
if route_params is not None and param_name in route_params:
|
|
260
|
-
raise ValidationError(
|
|
261
|
-
f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'"
|
|
262
|
-
)
|
|
245
|
+
raise ValidationError(f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'")
|
|
263
246
|
|
|
264
247
|
# simple type maps to key=value pair in query string
|
|
265
248
|
query_params.append((param_name, param_type))
|
|
@@ -272,7 +255,8 @@ def get_endpoint_operations(
|
|
|
272
255
|
if request_param is not None:
|
|
273
256
|
param = (param_name, param_type)
|
|
274
257
|
raise ValidationError(
|
|
275
|
-
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}"
|
|
276
260
|
)
|
|
277
261
|
|
|
278
262
|
# composite types are read from body
|
|
@@ -280,9 +264,7 @@ def get_endpoint_operations(
|
|
|
280
264
|
|
|
281
265
|
# check if function has explicit return type
|
|
282
266
|
if signature.return_annotation is inspect.Signature.empty:
|
|
283
|
-
raise ValidationError(
|
|
284
|
-
f"function '{func_name}' has no return type annotation"
|
|
285
|
-
)
|
|
267
|
+
raise ValidationError(f"function '{func_name}' has no return type annotation")
|
|
286
268
|
|
|
287
269
|
return_type = _get_annotation_type(signature.return_annotation, func_ref)
|
|
288
270
|
|
|
@@ -337,7 +319,7 @@ def get_endpoint_operations(
|
|
|
337
319
|
return result
|
|
338
320
|
|
|
339
321
|
|
|
340
|
-
def get_endpoint_events(endpoint: type) ->
|
|
322
|
+
def get_endpoint_events(endpoint: type) -> dict[str, type]:
|
|
341
323
|
results = {}
|
|
342
324
|
|
|
343
325
|
for decl in typing.get_type_hints(endpoint).values():
|
pyopenapi/options.py
CHANGED
|
@@ -1,16 +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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SecuritySchemeHTTP,
|
|
11
|
-
SecuritySchemeOpenIDConnect,
|
|
12
|
-
Server,
|
|
13
|
-
)
|
|
14
|
+
from .specification import Info, SecurityScheme, Server
|
|
15
|
+
from .specification import SecuritySchemeAPI as SecuritySchemeAPI
|
|
16
|
+
from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
|
|
17
|
+
from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
|
|
14
18
|
|
|
15
19
|
HTTPStatusCode = Union[HTTPStatus, int, str]
|
|
16
20
|
|
|
@@ -33,21 +37,17 @@ class Options:
|
|
|
33
37
|
|
|
34
38
|
server: Server
|
|
35
39
|
info: Info
|
|
36
|
-
version:
|
|
40
|
+
version: tuple[int, int, int] = (3, 1, 0)
|
|
37
41
|
default_security_scheme: Optional[SecurityScheme] = None
|
|
38
|
-
extra_types: Union[
|
|
42
|
+
extra_types: Union[list[type], dict[str, list[type]], None] = None
|
|
39
43
|
use_examples: bool = True
|
|
40
|
-
success_responses:
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
error_responses: Dict[type, HTTPStatusCode] = dataclasses.field(
|
|
44
|
-
default_factory=dict
|
|
45
|
-
)
|
|
44
|
+
success_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
45
|
+
error_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
46
46
|
error_wrapper: bool = False
|
|
47
47
|
property_description_fun: Optional[Callable[[type, str, str], str]] = None
|
|
48
|
-
captions: Optional[
|
|
48
|
+
captions: Optional[dict[str, str]] = None
|
|
49
49
|
|
|
50
|
-
default_captions: ClassVar[
|
|
50
|
+
default_captions: ClassVar[dict[str, str]] = {
|
|
51
51
|
"Operations": "Operations",
|
|
52
52
|
"Types": "Types",
|
|
53
53
|
"Events": "Events",
|
pyopenapi/proxy.py
CHANGED
|
@@ -1,24 +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
|
+
|
|
1
9
|
import json
|
|
2
|
-
from typing import Any, Callable,
|
|
10
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
3
11
|
|
|
4
12
|
import aiohttp
|
|
13
|
+
from strong_typing.inspection import get_signature
|
|
5
14
|
from strong_typing.serialization import json_to_object, object_to_json
|
|
6
15
|
|
|
7
|
-
from .operations import
|
|
8
|
-
EndpointOperation,
|
|
9
|
-
HTTPMethod,
|
|
10
|
-
get_endpoint_operations,
|
|
11
|
-
get_signature,
|
|
12
|
-
)
|
|
16
|
+
from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
async def make_request(
|
|
16
20
|
http_method: HTTPMethod,
|
|
17
21
|
server: str,
|
|
18
22
|
path: str,
|
|
19
|
-
query:
|
|
23
|
+
query: dict[str, str],
|
|
20
24
|
data: Optional[str],
|
|
21
|
-
) ->
|
|
25
|
+
) -> tuple[int, str]:
|
|
22
26
|
"Makes an asynchronous HTTP request and returns the response."
|
|
23
27
|
|
|
24
28
|
headers = {"Accept": "application/json"}
|
|
@@ -51,9 +55,9 @@ class ProxyInvokeError(RuntimeError):
|
|
|
51
55
|
class EndpointProxy:
|
|
52
56
|
"The HTTP REST proxy class for an endpoint."
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
base_url: str
|
|
55
59
|
|
|
56
|
-
def __init__(self, base_url: str):
|
|
60
|
+
def __init__(self, base_url: str) -> None:
|
|
57
61
|
self.base_url = base_url
|
|
58
62
|
|
|
59
63
|
|
|
@@ -65,22 +69,18 @@ class OperationProxy:
|
|
|
65
69
|
payload, builds an HTTP request, and processes the HTTP response.
|
|
66
70
|
"""
|
|
67
71
|
|
|
68
|
-
def __init__(self, op: EndpointOperation):
|
|
72
|
+
def __init__(self, op: EndpointOperation) -> None:
|
|
69
73
|
self.op = op
|
|
70
74
|
self.sig = get_signature(op.func_ref)
|
|
71
75
|
|
|
72
|
-
async def __call__(
|
|
73
|
-
self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any
|
|
74
|
-
) -> Any:
|
|
76
|
+
async def __call__(self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
|
|
75
77
|
"Invokes an API operation via HTTP REST."
|
|
76
78
|
|
|
77
79
|
ba = self.sig.bind(self, *args, **kwargs)
|
|
78
80
|
|
|
79
81
|
# substitute parameters in URL path
|
|
80
82
|
route = self.op.get_route()
|
|
81
|
-
path = route.format_map(
|
|
82
|
-
{name: ba.arguments[name] for name, _type in self.op.path_params}
|
|
83
|
-
)
|
|
83
|
+
path = route.format_map({name: ba.arguments[name] for name, _type in self.op.path_params})
|
|
84
84
|
|
|
85
85
|
# gather URL query parameters
|
|
86
86
|
query = {name: str(ba.arguments[name]) for name, _type in self.op.query_params}
|
|
@@ -99,18 +99,14 @@ class OperationProxy:
|
|
|
99
99
|
data = None
|
|
100
100
|
|
|
101
101
|
# make HTTP request
|
|
102
|
-
status, response = await make_request(
|
|
103
|
-
self.op.http_method, endpoint_proxy.base_url, path, query, data
|
|
104
|
-
)
|
|
102
|
+
status, response = await make_request(self.op.http_method, endpoint_proxy.base_url, path, query, data)
|
|
105
103
|
|
|
106
104
|
# process HTTP response
|
|
107
105
|
if response:
|
|
108
106
|
try:
|
|
109
107
|
s = json.loads(response)
|
|
110
108
|
except json.JSONDecodeError:
|
|
111
|
-
raise ProxyInvokeError(
|
|
112
|
-
f"response body is not well-formed JSON:\n{response}"
|
|
113
|
-
)
|
|
109
|
+
raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}") from None
|
|
114
110
|
|
|
115
111
|
return json_to_object(self.op.response_type, s)
|
|
116
112
|
else:
|
|
@@ -122,9 +118,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
|
|
|
122
118
|
|
|
123
119
|
operation_proxy = OperationProxy(op)
|
|
124
120
|
|
|
125
|
-
async def _operation_proxy_fn(
|
|
126
|
-
self: EndpointProxy, *args: Any, **kwargs: Any
|
|
127
|
-
) -> Any:
|
|
121
|
+
async def _operation_proxy_fn(self: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
|
|
128
122
|
return await operation_proxy(self, *args, **kwargs)
|
|
129
123
|
|
|
130
124
|
return _operation_proxy_fn
|
|
@@ -133,7 +127,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
|
|
|
133
127
|
T = TypeVar("T")
|
|
134
128
|
|
|
135
129
|
|
|
136
|
-
def make_proxy_class(api:
|
|
130
|
+
def make_proxy_class(api: type[T]) -> type[T]:
|
|
137
131
|
"""
|
|
138
132
|
Creates a proxy class for calling an HTTP REST API.
|
|
139
133
|
|
pyopenapi/specification.py
CHANGED
|
@@ -1,9 +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
|
import dataclasses
|
|
2
10
|
import enum
|
|
3
11
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, ClassVar,
|
|
12
|
+
from typing import Any, ClassVar, Optional, Union
|
|
5
13
|
|
|
6
|
-
from strong_typing.
|
|
14
|
+
from strong_typing.core import JsonType as JsonType
|
|
15
|
+
from strong_typing.core import Schema, StrictJsonType
|
|
7
16
|
|
|
8
17
|
URL = str
|
|
9
18
|
|
|
@@ -67,12 +76,12 @@ class Info:
|
|
|
67
76
|
class MediaType:
|
|
68
77
|
schema: Optional[SchemaOrRef] = None
|
|
69
78
|
example: Optional[Any] = None
|
|
70
|
-
examples: Optional[
|
|
79
|
+
examples: Optional[dict[str, Union["Example", ExampleRef]]] = None
|
|
71
80
|
|
|
72
81
|
|
|
73
82
|
@dataclass
|
|
74
83
|
class RequestBody:
|
|
75
|
-
content:
|
|
84
|
+
content: dict[str, MediaType]
|
|
76
85
|
description: Optional[str] = None
|
|
77
86
|
required: Optional[bool] = None
|
|
78
87
|
|
|
@@ -80,9 +89,10 @@ class RequestBody:
|
|
|
80
89
|
@dataclass
|
|
81
90
|
class Response:
|
|
82
91
|
description: str
|
|
83
|
-
content: Optional[
|
|
92
|
+
content: Optional[dict[str, MediaType]] = None
|
|
84
93
|
|
|
85
94
|
|
|
95
|
+
@enum.unique
|
|
86
96
|
class ParameterLocation(enum.Enum):
|
|
87
97
|
Query = "query"
|
|
88
98
|
Header = "header"
|
|
@@ -102,15 +112,15 @@ class Parameter:
|
|
|
102
112
|
|
|
103
113
|
@dataclass
|
|
104
114
|
class Operation:
|
|
105
|
-
responses:
|
|
106
|
-
tags: Optional[
|
|
115
|
+
responses: dict[str, Union[Response, ResponseRef]]
|
|
116
|
+
tags: Optional[list[str]] = None
|
|
107
117
|
summary: Optional[str] = None
|
|
108
118
|
description: Optional[str] = None
|
|
109
119
|
operationId: Optional[str] = None
|
|
110
|
-
parameters: Optional[
|
|
120
|
+
parameters: Optional[list[Parameter]] = None
|
|
111
121
|
requestBody: Optional[RequestBody] = None
|
|
112
|
-
callbacks: Optional[
|
|
113
|
-
security: Optional[
|
|
122
|
+
callbacks: Optional[dict[str, "Callback"]] = None
|
|
123
|
+
security: Optional[list["SecurityRequirement"]] = None
|
|
114
124
|
|
|
115
125
|
|
|
116
126
|
@dataclass
|
|
@@ -136,7 +146,7 @@ class PathItem:
|
|
|
136
146
|
|
|
137
147
|
|
|
138
148
|
# maps run-time expressions such as "$request.body#/url" to path items
|
|
139
|
-
Callback =
|
|
149
|
+
Callback = dict[str, PathItem]
|
|
140
150
|
|
|
141
151
|
|
|
142
152
|
@dataclass
|
|
@@ -153,6 +163,7 @@ class Server:
|
|
|
153
163
|
description: Optional[str] = None
|
|
154
164
|
|
|
155
165
|
|
|
166
|
+
@enum.unique
|
|
156
167
|
class SecuritySchemeType(enum.Enum):
|
|
157
168
|
ApiKey = "apiKey"
|
|
158
169
|
HTTP = "http"
|
|
@@ -182,9 +193,7 @@ class SecuritySchemeHTTP(SecurityScheme):
|
|
|
182
193
|
scheme: str
|
|
183
194
|
bearerFormat: Optional[str] = None
|
|
184
195
|
|
|
185
|
-
def __init__(
|
|
186
|
-
self, description: str, scheme: str, bearerFormat: Optional[str] = None
|
|
187
|
-
) -> None:
|
|
196
|
+
def __init__(self, description: str, scheme: str, bearerFormat: Optional[str] = None) -> None:
|
|
188
197
|
super().__init__(SecuritySchemeType.HTTP, description)
|
|
189
198
|
self.scheme = scheme
|
|
190
199
|
self.bearerFormat = bearerFormat
|
|
@@ -201,17 +210,17 @@ class SecuritySchemeOpenIDConnect(SecurityScheme):
|
|
|
201
210
|
|
|
202
211
|
@dataclass
|
|
203
212
|
class Components:
|
|
204
|
-
schemas: Optional[
|
|
205
|
-
responses: Optional[
|
|
206
|
-
parameters: Optional[
|
|
207
|
-
examples: Optional[
|
|
208
|
-
requestBodies: Optional[
|
|
209
|
-
securitySchemes: Optional[
|
|
210
|
-
callbacks: Optional[
|
|
213
|
+
schemas: Optional[dict[str, Schema]] = None
|
|
214
|
+
responses: Optional[dict[str, Response]] = None
|
|
215
|
+
parameters: Optional[dict[str, Parameter]] = None
|
|
216
|
+
examples: Optional[dict[str, Example]] = None
|
|
217
|
+
requestBodies: Optional[dict[str, RequestBody]] = None
|
|
218
|
+
securitySchemes: Optional[dict[str, SecurityScheme]] = None
|
|
219
|
+
callbacks: Optional[dict[str, Callback]] = None
|
|
211
220
|
|
|
212
221
|
|
|
213
222
|
SecurityScope = str
|
|
214
|
-
SecurityRequirement =
|
|
223
|
+
SecurityRequirement = dict[str, list[SecurityScope]]
|
|
215
224
|
|
|
216
225
|
|
|
217
226
|
@dataclass
|
|
@@ -230,7 +239,7 @@ class TagGroup:
|
|
|
230
239
|
"""
|
|
231
240
|
|
|
232
241
|
name: str
|
|
233
|
-
tags:
|
|
242
|
+
tags: list[str]
|
|
234
243
|
|
|
235
244
|
|
|
236
245
|
@dataclass
|
|
@@ -243,10 +252,10 @@ class Document:
|
|
|
243
252
|
|
|
244
253
|
openapi: str
|
|
245
254
|
info: Info
|
|
246
|
-
servers:
|
|
247
|
-
paths:
|
|
255
|
+
servers: list[Server]
|
|
256
|
+
paths: dict[str, PathItem]
|
|
248
257
|
jsonSchemaDialect: Optional[str] = None
|
|
249
258
|
components: Optional[Components] = None
|
|
250
|
-
security: Optional[
|
|
251
|
-
tags: Optional[
|
|
252
|
-
tagGroups: Optional[
|
|
259
|
+
security: Optional[list[SecurityRequirement]] = None
|
|
260
|
+
tags: Optional[list[Tag]] = None
|
|
261
|
+
tagGroups: Optional[list[TagGroup]] = None
|
pyopenapi/utility.py
CHANGED
|
@@ -1,10 +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
|
import importlib.resources
|
|
2
10
|
import json
|
|
3
|
-
import sys
|
|
4
11
|
import typing
|
|
5
12
|
from typing import TextIO
|
|
6
13
|
|
|
7
|
-
from strong_typing.
|
|
14
|
+
from strong_typing.core import StrictJsonType
|
|
15
|
+
from strong_typing.serialization import object_to_json
|
|
8
16
|
|
|
9
17
|
from .generator import Generator
|
|
10
18
|
from .options import Options
|
|
@@ -14,7 +22,7 @@ from .specification import Document
|
|
|
14
22
|
class Specification:
|
|
15
23
|
document: Document
|
|
16
24
|
|
|
17
|
-
def __init__(self, endpoint: type, options: Options):
|
|
25
|
+
def __init__(self, endpoint: type, options: Options) -> None:
|
|
18
26
|
generator = Generator(endpoint, options)
|
|
19
27
|
self.document = generator.generate()
|
|
20
28
|
|
|
@@ -53,9 +61,7 @@ class Specification:
|
|
|
53
61
|
|
|
54
62
|
json_doc = self.get_json()
|
|
55
63
|
if pretty_print:
|
|
56
|
-
return json.dumps(
|
|
57
|
-
json_doc, check_circular=False, ensure_ascii=False, indent=4
|
|
58
|
-
)
|
|
64
|
+
return json.dumps(json_doc, check_circular=False, ensure_ascii=False, indent=4)
|
|
59
65
|
else:
|
|
60
66
|
return json.dumps(
|
|
61
67
|
json_doc,
|
|
@@ -96,16 +102,8 @@ class Specification:
|
|
|
96
102
|
:param pretty_print: Whether to use line indents to beautify the JSON string in the HTML file.
|
|
97
103
|
"""
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
encoding="utf-8", errors="strict"
|
|
102
|
-
) as html_template_file:
|
|
103
|
-
html_template = html_template_file.read()
|
|
104
|
-
else:
|
|
105
|
-
with importlib.resources.open_text(
|
|
106
|
-
__package__, "template.html", encoding="utf-8", errors="strict"
|
|
107
|
-
) as html_template_file:
|
|
108
|
-
html_template = html_template_file.read()
|
|
105
|
+
with importlib.resources.files(__package__).joinpath("template.html").open(encoding="utf-8", errors="strict") as html_template_file:
|
|
106
|
+
html_template = html_template_file.read()
|
|
109
107
|
|
|
110
108
|
html = html_template.replace(
|
|
111
109
|
"{ /* OPENAPI_SPECIFICATION */ }",
|
|
@@ -1,28 +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
|
|
24
|
-
Requires-Dist: aiohttp
|
|
25
|
-
Requires-Dist:
|
|
27
|
+
Requires-Dist: aiohttp>=3.12
|
|
28
|
+
Requires-Dist: json_strong_typing>=0.3.9
|
|
29
|
+
Dynamic: license-file
|
|
26
30
|
|
|
27
31
|
# Generate an OpenAPI specification from a Python class
|
|
28
32
|
|
|
@@ -33,7 +37,7 @@ Requires-Dist: json-strong-typing (>=0.2.7)
|
|
|
33
37
|
* supports standard and asynchronous functions (`async def`)
|
|
34
38
|
* maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
|
|
35
39
|
* handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
|
|
36
|
-
* handles generic types (`
|
|
40
|
+
* handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
|
|
37
41
|
* maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
|
|
38
42
|
* maps composite types to HTTP request body
|
|
39
43
|
* supports user-defined routes, request and response samples with decorator `@webmethod`
|
|
@@ -86,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
|
|
|
86
90
|
|
|
87
91
|
```python
|
|
88
92
|
class JobManagement:
|
|
89
|
-
def create_job(self, items:
|
|
93
|
+
def create_job(self, items: list[URL]) -> uuid.UUID:
|
|
90
94
|
...
|
|
91
95
|
|
|
92
96
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -125,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
|
|
|
125
129
|
|
|
126
130
|
### Documenting operations
|
|
127
131
|
|
|
128
|
-
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:
|
|
129
133
|
|
|
130
134
|
```python
|
|
131
135
|
def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
|
|
@@ -168,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
|
|
|
168
172
|
|
|
169
173
|
### Mapping function name prefixes to HTTP verbs
|
|
170
174
|
|
|
171
|
-
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:
|
|
172
176
|
|
|
173
177
|
| Prefix | HTTP verb |
|
|
174
178
|
| ------ | ----------- |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pyopenapi/__init__.py,sha256=6XdaFzQkGdBLTQj0lv-7FyuMN8gFpLKctsA1nIW4pSU,345
|
|
2
|
+
pyopenapi/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pyopenapi/decorators.py,sha256=wYZNlZ7diaT5tnr8BsdS7aYpgFTndhfknqbhrU3jR4g,2065
|
|
4
|
+
pyopenapi/generator.py,sha256=HclcfyADHymH3be5w3Xa39MHe0xLsU28kWJhCTLmrG4,23971
|
|
5
|
+
pyopenapi/metadata.py,sha256=-w5C5qiphvnuOkyh9QEUURg7OT-5UffRowjAwmZJkc8,916
|
|
6
|
+
pyopenapi/operations.py,sha256=oprIalF_1XD1-7AjfmJQQeC3xRLdB8oeLlMEhjO2wGc,13529
|
|
7
|
+
pyopenapi/options.py,sha256=S6RWhpZfWrrZBqXzIsPwlVKOkvaI7eyDDUBqGICTlZo,2906
|
|
8
|
+
pyopenapi/proxy.py,sha256=ExDvESQdULTqfnunmfRKtkdjGbcLt5v9yaHd2h9HHlo,4279
|
|
9
|
+
pyopenapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pyopenapi/specification.py,sha256=ZKQbhl0mhf5ceHan-2ymzt-tba3MhMH-nGsFQ_07XBE,6114
|
|
11
|
+
pyopenapi/template.html,sha256=hmZsSnN3awQcwlN6u-j06qrw_qYzWqYBLC8uPt_x17A,1234
|
|
12
|
+
pyopenapi/utility.py,sha256=Cdub8VUrklyUvZ9cmahDmIYKq4d6X7IT--rMP_ogd_4,3553
|
|
13
|
+
python_openapi-0.2.0.dist-info/licenses/LICENSE,sha256=sNQ9jvMoMB8FfhB7JbkJYLr8SWSO6jrYvcS-mRL485w,1118
|
|
14
|
+
python_openapi-0.2.0.dist-info/METADATA,sha256=eAVEMvh6P1YF-HWODkAXv7p0N5mOFQJ_6mlUP5IRWSg,10499
|
|
15
|
+
python_openapi-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
python_openapi-0.2.0.dist-info/top_level.txt,sha256=3M9FA79QfjyZyTipZP5fDiMcrRdcbSl7lW-iaq5RIRQ,10
|
|
17
|
+
python_openapi-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
18
|
+
python_openapi-0.2.0.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
pyopenapi/__init__.py,sha256=X8PJxd3kz3KOgekDQahA8iqrM1s232maTDsTu4g-Dmk,2026
|
|
2
|
-
pyopenapi/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
pyopenapi/generator.py,sha256=Abr7kzW2E1a7CowVQeWUvo-wc1FyilhWj83CIHaWaqo,24134
|
|
4
|
-
pyopenapi/metadata.py,sha256=RnAfH79SJVE7-3HWJmdXAm_Kc5L5-4gutyh6xOZc8Fs,766
|
|
5
|
-
pyopenapi/operations.py,sha256=jJqmQOtKDGrQ678TbjwOH_3TuCdFXa5VF8EHr57UUK0,13646
|
|
6
|
-
pyopenapi/options.py,sha256=Su7UIjqxLgenmsj5QDbT8x5fkJyL9K-Q_dhyxUnMJDM,2674
|
|
7
|
-
pyopenapi/proxy.py,sha256=EciA8pl3pNBLAfwuzXfaUelW3UMKfp95ftqTTYvjvz0,4206
|
|
8
|
-
pyopenapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
pyopenapi/specification.py,sha256=wizvkOzma_ygFIe8uxwB2Iu0MyTiUPwMZxYO6lzUqMM,5918
|
|
10
|
-
pyopenapi/template.html,sha256=hmZsSnN3awQcwlN6u-j06qrw_qYzWqYBLC8uPt_x17A,1234
|
|
11
|
-
pyopenapi/utility.py,sha256=ivNI7rUcQdnmQ9Cd7Bae5S2m-pMIfB1ePTxA87g12L8,3707
|
|
12
|
-
python_openapi-0.1.9.dist-info/LICENSE,sha256=YR7xSxwfVEogZT0M3cQpVXTfAm5MT5dQ6WWeDesiTxo,1118
|
|
13
|
-
python_openapi-0.1.9.dist-info/METADATA,sha256=Hfra860obzYkBYQbTBh1jMC_hyD6sLnCeNyj6rU7Q6g,10207
|
|
14
|
-
python_openapi-0.1.9.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
15
|
-
python_openapi-0.1.9.dist-info/top_level.txt,sha256=3M9FA79QfjyZyTipZP5fDiMcrRdcbSl7lW-iaq5RIRQ,10
|
|
16
|
-
python_openapi-0.1.9.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
17
|
-
python_openapi-0.1.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|