python-openapi 0.1.9__tar.gz → 0.1.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python-openapi-0.1.9 → python_openapi-0.1.10}/LICENSE +1 -1
- {python-openapi-0.1.9 → python_openapi-0.1.10}/PKG-INFO +5 -1
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/__init__.py +6 -10
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/generator.py +71 -129
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/operations.py +11 -38
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/options.py +7 -14
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/proxy.py +9 -24
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/specification.py +5 -4
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/utility.py +4 -10
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyproject.toml +3 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/PKG-INFO +5 -1
- python_openapi-0.1.10/python_openapi.egg-info/requires.txt +2 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/setup.cfg +4 -2
- {python-openapi-0.1.9 → python_openapi-0.1.10}/tests/test_openapi.py +4 -12
- {python-openapi-0.1.9 → python_openapi-0.1.10}/tests/test_proxy.py +5 -7
- python-openapi-0.1.9/python_openapi.egg-info/requires.txt +0 -2
- {python-openapi-0.1.9 → python_openapi-0.1.10}/README.md +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/__main__.py +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/metadata.py +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/py.typed +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/template.html +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/SOURCES.txt +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/dependency_links.txt +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/top_level.txt +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/zip-safe +0 -0
- {python-openapi-0.1.9 → python_openapi-0.1.10}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-openapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Generate an OpenAPI specification from a Python class definition
|
|
5
5
|
Home-page: https://github.com/hunyadi/pyopenapi
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -15,12 +15,16 @@ 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
|
|
18
20
|
Classifier: Topic :: Software Development :: Code Generators
|
|
19
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Python: >=3.8
|
|
22
24
|
Description-Content-Type: text/markdown
|
|
23
25
|
License-File: LICENSE
|
|
26
|
+
Requires-Dist: aiohttp>=3.11
|
|
27
|
+
Requires-Dist: json_strong_typing>=0.3.7
|
|
24
28
|
|
|
25
29
|
# Generate an OpenAPI specification from a Python class
|
|
26
30
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Any, Callable, Optional, TypeVar
|
|
1
|
+
from typing import Any, Callable, List, Optional, TypeVar
|
|
2
2
|
|
|
3
3
|
from .metadata import WebMethod
|
|
4
|
-
from .options import *
|
|
5
|
-
from .utility import Specification
|
|
4
|
+
from .options import * # noqa: F403
|
|
5
|
+
from .utility import Specification as Specification
|
|
6
6
|
|
|
7
|
-
__version__ = "0.1.
|
|
7
|
+
__version__ = "0.1.10"
|
|
8
8
|
|
|
9
9
|
T = TypeVar("T")
|
|
10
10
|
|
|
@@ -29,13 +29,9 @@ def webmethod(
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
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
|
-
)
|
|
32
|
+
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
|
|
35
33
|
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
|
-
)
|
|
34
|
+
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
|
|
39
35
|
|
|
40
36
|
if request_example:
|
|
41
37
|
request_examples = [request_example]
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import hashlib
|
|
2
3
|
import ipaddress
|
|
3
4
|
import typing
|
|
4
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
5
8
|
|
|
6
9
|
from strong_typing.core import JsonType
|
|
7
10
|
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
|
-
)
|
|
11
|
+
from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
|
|
16
12
|
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
|
-
)
|
|
13
|
+
from strong_typing.schema import JsonSchemaGenerator, Schema, SchemaOptions, get_schema_identifier, register_schema
|
|
24
14
|
from strong_typing.serialization import json_dump_string, object_to_json
|
|
25
15
|
|
|
26
|
-
from .operations import
|
|
27
|
-
|
|
28
|
-
HTTPMethod,
|
|
29
|
-
get_endpoint_events,
|
|
30
|
-
get_endpoint_operations,
|
|
31
|
-
)
|
|
32
|
-
from .options import *
|
|
16
|
+
from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
|
|
17
|
+
from .options import HTTPStatusCode, Options
|
|
33
18
|
from .specification import (
|
|
34
19
|
Components,
|
|
35
20
|
Document,
|
|
@@ -165,9 +150,7 @@ class ContentBuilder:
|
|
|
165
150
|
self.schema_transformer = schema_transformer
|
|
166
151
|
self.sample_transformer = sample_transformer
|
|
167
152
|
|
|
168
|
-
def build_content(
|
|
169
|
-
self, payload_type: type, examples: Optional[List[Any]] = None
|
|
170
|
-
) -> Dict[str, MediaType]:
|
|
153
|
+
def build_content(self, payload_type: type, examples: Optional[List[Any]] = None) -> Dict[str, MediaType]:
|
|
171
154
|
"Creates the content subtree for a request or response."
|
|
172
155
|
|
|
173
156
|
if is_generic_list(payload_type):
|
|
@@ -179,12 +162,10 @@ class ContentBuilder:
|
|
|
179
162
|
|
|
180
163
|
return {media_type: self.build_media_type(item_type, examples)}
|
|
181
164
|
|
|
182
|
-
def build_media_type(
|
|
183
|
-
self, item_type: type, examples: Optional[List[Any]] = None
|
|
184
|
-
) -> MediaType:
|
|
165
|
+
def build_media_type(self, item_type: type, examples: Optional[List[Any]] = None) -> MediaType:
|
|
185
166
|
schema = self.schema_builder.classdef_to_ref(item_type)
|
|
186
167
|
if self.schema_transformer:
|
|
187
|
-
schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
|
|
168
|
+
schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
|
|
188
169
|
schema = schema_transformer(schema)
|
|
189
170
|
|
|
190
171
|
if not examples:
|
|
@@ -198,25 +179,14 @@ class ContentBuilder:
|
|
|
198
179
|
examples=self._build_examples(examples),
|
|
199
180
|
)
|
|
200
181
|
|
|
201
|
-
def _build_examples(
|
|
202
|
-
self, examples: List[Any]
|
|
203
|
-
) -> Dict[str, Union[Example, ExampleRef]]:
|
|
182
|
+
def _build_examples(self, examples: List[Any]) -> Dict[str, Union[Example, ExampleRef]]:
|
|
204
183
|
"Creates a set of several examples for a media type."
|
|
205
184
|
|
|
206
|
-
|
|
207
|
-
sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore
|
|
208
|
-
else:
|
|
209
|
-
sample_transformer = lambda sample: sample
|
|
185
|
+
builder = ExampleBuilder(self.sample_transformer)
|
|
210
186
|
|
|
211
187
|
results: Dict[str, Union[Example, ExampleRef]] = {}
|
|
212
188
|
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
|
-
|
|
189
|
+
name, value = builder.get_named(example)
|
|
220
190
|
results[name] = Example(value=value)
|
|
221
191
|
|
|
222
192
|
return results
|
|
@@ -224,12 +194,43 @@ class ContentBuilder:
|
|
|
224
194
|
def _build_example(self, example: Any) -> Any:
|
|
225
195
|
"Creates a single example for a media type."
|
|
226
196
|
|
|
227
|
-
|
|
228
|
-
|
|
197
|
+
builder = ExampleBuilder(self.sample_transformer)
|
|
198
|
+
return builder.get_anonymous(example)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ExampleBuilder:
|
|
202
|
+
sample_transformer: Callable[[JsonType], JsonType]
|
|
203
|
+
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
sample_transformer: Optional[Callable[[JsonType], JsonType]] = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
if sample_transformer:
|
|
209
|
+
self.sample_transformer = sample_transformer
|
|
229
210
|
else:
|
|
230
|
-
sample_transformer = lambda sample: sample
|
|
211
|
+
self.sample_transformer = lambda sample: sample # noqa: E731
|
|
212
|
+
|
|
213
|
+
def _get_value(self, example: Any) -> JsonType:
|
|
214
|
+
return self.sample_transformer(object_to_json(example))
|
|
215
|
+
|
|
216
|
+
def get_anonymous(self, example: Any) -> JsonType:
|
|
217
|
+
return self._get_value(example)
|
|
218
|
+
|
|
219
|
+
def get_named(self, example: Any) -> Tuple[str, JsonType]:
|
|
220
|
+
value = self._get_value(example)
|
|
221
|
+
|
|
222
|
+
name: Optional[str] = None
|
|
231
223
|
|
|
232
|
-
|
|
224
|
+
if type(example).__str__ is not object.__str__:
|
|
225
|
+
friendly_name = str(example)
|
|
226
|
+
if friendly_name.isprintable():
|
|
227
|
+
name = friendly_name
|
|
228
|
+
|
|
229
|
+
if name is None:
|
|
230
|
+
hash_string = hashlib.md5(json_dump_string(value).encode("utf-8")).digest().hex()
|
|
231
|
+
name = f"ex-{hash_string}"
|
|
232
|
+
|
|
233
|
+
return name, value
|
|
233
234
|
|
|
234
235
|
|
|
235
236
|
@dataclass
|
|
@@ -262,15 +263,11 @@ class ResponseBuilder:
|
|
|
262
263
|
def __init__(self, content_builder: ContentBuilder) -> None:
|
|
263
264
|
self.content_builder = content_builder
|
|
264
265
|
|
|
265
|
-
def _get_status_responses(
|
|
266
|
-
self, options: ResponseOptions
|
|
267
|
-
) -> Dict[str, StatusResponse]:
|
|
266
|
+
def _get_status_responses(self, options: ResponseOptions) -> Dict[str, StatusResponse]:
|
|
268
267
|
status_responses: Dict[str, StatusResponse] = {}
|
|
269
268
|
|
|
270
269
|
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
|
-
)
|
|
270
|
+
status_code = http_status_to_string(options.status_catalog.get(response_type, options.default_status_code))
|
|
274
271
|
|
|
275
272
|
# look up response for status code
|
|
276
273
|
if status_code not in status_responses:
|
|
@@ -282,17 +279,11 @@ class ResponseBuilder:
|
|
|
282
279
|
|
|
283
280
|
# append examples that have the matching response type
|
|
284
281
|
if options.examples:
|
|
285
|
-
status_response.examples.extend(
|
|
286
|
-
example
|
|
287
|
-
for example in options.examples
|
|
288
|
-
if isinstance(example, response_type)
|
|
289
|
-
)
|
|
282
|
+
status_response.examples.extend(example for example in options.examples if isinstance(example, response_type))
|
|
290
283
|
|
|
291
284
|
return dict(sorted(status_responses.items()))
|
|
292
285
|
|
|
293
|
-
def build_response(
|
|
294
|
-
self, options: ResponseOptions
|
|
295
|
-
) -> Dict[str, Union[Response, ResponseRef]]:
|
|
286
|
+
def build_response(self, options: ResponseOptions) -> Dict[str, Union[Response, ResponseRef]]:
|
|
296
287
|
"""
|
|
297
288
|
Groups responses that have the same status code.
|
|
298
289
|
"""
|
|
@@ -310,10 +301,7 @@ class ResponseBuilder:
|
|
|
310
301
|
description = " **OR** ".join(
|
|
311
302
|
filter(
|
|
312
303
|
None,
|
|
313
|
-
(
|
|
314
|
-
options.type_descriptions[response_type]
|
|
315
|
-
for response_type in response_types
|
|
316
|
-
),
|
|
304
|
+
(options.type_descriptions[response_type] for response_type in response_types),
|
|
317
305
|
)
|
|
318
306
|
)
|
|
319
307
|
|
|
@@ -388,14 +376,10 @@ class Generator:
|
|
|
388
376
|
description = typing.cast(str, schema.get("description"))
|
|
389
377
|
return Tag(
|
|
390
378
|
name=ref,
|
|
391
|
-
description="\n\n".join(
|
|
392
|
-
s for s in (title, description, definition) if s is not None
|
|
393
|
-
),
|
|
379
|
+
description="\n\n".join(s for s in (title, description, definition) if s is not None),
|
|
394
380
|
)
|
|
395
381
|
|
|
396
|
-
def _build_extra_tag_groups(
|
|
397
|
-
self, extra_types: Dict[str, List[type]]
|
|
398
|
-
) -> Dict[str, List[Tag]]:
|
|
382
|
+
def _build_extra_tag_groups(self, extra_types: Dict[str, List[type]]) -> Dict[str, List[Tag]]:
|
|
399
383
|
"""
|
|
400
384
|
Creates a dictionary of tag group captions as keys, and tag lists as values.
|
|
401
385
|
|
|
@@ -419,9 +403,7 @@ class Generator:
|
|
|
419
403
|
|
|
420
404
|
def _build_operation(self, op: EndpointOperation) -> Operation:
|
|
421
405
|
doc_string = parse_type(op.func_ref)
|
|
422
|
-
doc_params = dict(
|
|
423
|
-
(param.name, param.description) for param in doc_string.params.values()
|
|
424
|
-
)
|
|
406
|
+
doc_params = dict((param.name, param.description) for param in doc_string.params.values())
|
|
425
407
|
|
|
426
408
|
# parameters passed in URL component path
|
|
427
409
|
path_parameters = [
|
|
@@ -462,11 +444,7 @@ class Generator:
|
|
|
462
444
|
builder = ContentBuilder(self.schema_builder)
|
|
463
445
|
request_name, request_type = op.request_param
|
|
464
446
|
requestBody = RequestBody(
|
|
465
|
-
content={
|
|
466
|
-
"application/json": builder.build_media_type(
|
|
467
|
-
request_type, op.request_examples
|
|
468
|
-
)
|
|
469
|
-
},
|
|
447
|
+
content={"application/json": builder.build_media_type(request_type, op.request_examples)},
|
|
470
448
|
description=doc_params.get(request_name),
|
|
471
449
|
required=True,
|
|
472
450
|
)
|
|
@@ -476,29 +454,16 @@ class Generator:
|
|
|
476
454
|
# success response types
|
|
477
455
|
if doc_string.returns is None and is_type_union(op.response_type):
|
|
478
456
|
# split union of return types into a list of response types
|
|
479
|
-
success_type_docstring: Dict[type, Docstring] = {
|
|
480
|
-
typing.cast(type, item): parse_type(item)
|
|
481
|
-
for item in unwrap_union_types(op.response_type)
|
|
482
|
-
}
|
|
457
|
+
success_type_docstring: Dict[type, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
|
|
483
458
|
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
|
|
459
|
+
item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
|
|
487
460
|
}
|
|
488
461
|
else:
|
|
489
462
|
# 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
|
-
}
|
|
463
|
+
success_type_descriptions = {op.response_type: (doc_string.returns.description if doc_string.returns else "OK")}
|
|
495
464
|
|
|
496
465
|
response_examples = op.response_examples or []
|
|
497
|
-
success_examples = [
|
|
498
|
-
example
|
|
499
|
-
for example in response_examples
|
|
500
|
-
if not isinstance(example, Exception)
|
|
501
|
-
]
|
|
466
|
+
success_examples = [example for example in response_examples if not isinstance(example, Exception)]
|
|
502
467
|
|
|
503
468
|
content_builder = ContentBuilder(self.schema_builder)
|
|
504
469
|
response_builder = ResponseBuilder(content_builder)
|
|
@@ -512,14 +477,8 @@ class Generator:
|
|
|
512
477
|
|
|
513
478
|
# failure response types
|
|
514
479
|
if doc_string.raises:
|
|
515
|
-
exception_types: Dict[type, str] = {
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
exception_examples = [
|
|
519
|
-
example
|
|
520
|
-
for example in response_examples
|
|
521
|
-
if isinstance(example, Exception)
|
|
522
|
-
]
|
|
480
|
+
exception_types: Dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
|
|
481
|
+
exception_examples = [example for example in response_examples if isinstance(example, Exception)]
|
|
523
482
|
|
|
524
483
|
if self.options.error_wrapper:
|
|
525
484
|
schema_transformer = schema_error_wrapper
|
|
@@ -548,9 +507,7 @@ class Generator:
|
|
|
548
507
|
f"{op.func_name}_callback": {
|
|
549
508
|
"{$request.query.callback}": PathItem(
|
|
550
509
|
post=Operation(
|
|
551
|
-
requestBody=RequestBody(
|
|
552
|
-
content=builder.build_content(op.event_type)
|
|
553
|
-
),
|
|
510
|
+
requestBody=RequestBody(content=builder.build_content(op.event_type)),
|
|
554
511
|
responses={"200": Response(description="OK")},
|
|
555
512
|
)
|
|
556
513
|
)
|
|
@@ -574,9 +531,7 @@ class Generator:
|
|
|
574
531
|
def generate(self) -> Document:
|
|
575
532
|
paths: Dict[str, PathItem] = {}
|
|
576
533
|
endpoint_classes: Set[type] = set()
|
|
577
|
-
for op in get_endpoint_operations(
|
|
578
|
-
self.endpoint, use_examples=self.options.use_examples
|
|
579
|
-
):
|
|
534
|
+
for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
|
|
580
535
|
endpoint_classes.add(op.defining_class)
|
|
581
536
|
|
|
582
537
|
operation = self._build_operation(op)
|
|
@@ -612,10 +567,7 @@ class Generator:
|
|
|
612
567
|
)
|
|
613
568
|
|
|
614
569
|
# 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
|
-
]
|
|
570
|
+
type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
|
|
619
571
|
|
|
620
572
|
# types that are emitted by events
|
|
621
573
|
event_tags: List[Tag] = []
|
|
@@ -628,17 +580,11 @@ class Generator:
|
|
|
628
580
|
extra_tag_groups: Dict[str, List[Tag]] = {}
|
|
629
581
|
if self.options.extra_types is not None:
|
|
630
582
|
if isinstance(self.options.extra_types, list):
|
|
631
|
-
extra_tag_groups = self._build_extra_tag_groups(
|
|
632
|
-
{"AdditionalTypes": self.options.extra_types}
|
|
633
|
-
)
|
|
583
|
+
extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
|
|
634
584
|
elif isinstance(self.options.extra_types, dict):
|
|
635
|
-
extra_tag_groups = self._build_extra_tag_groups(
|
|
636
|
-
self.options.extra_types
|
|
637
|
-
)
|
|
585
|
+
extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
|
|
638
586
|
else:
|
|
639
|
-
raise TypeError(
|
|
640
|
-
f"type mismatch for collection of extra types: {type(self.options.extra_types)}"
|
|
641
|
-
)
|
|
587
|
+
raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
|
|
642
588
|
|
|
643
589
|
# list all operations and types
|
|
644
590
|
tags: List[Tag] = []
|
|
@@ -686,11 +632,7 @@ class Generator:
|
|
|
686
632
|
return Document(
|
|
687
633
|
openapi=".".join(str(item) for item in self.options.version),
|
|
688
634
|
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
|
-
),
|
|
635
|
+
jsonSchemaDialect=("https://json-schema.org/draft/2020-12/schema" if self.options.version >= (3, 1, 0) else None),
|
|
694
636
|
servers=[self.options.server],
|
|
695
637
|
paths=paths,
|
|
696
638
|
components=Components(
|
|
@@ -6,19 +6,12 @@ import uuid
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
8
8
|
|
|
9
|
-
from strong_typing.inspection import
|
|
10
|
-
get_signature,
|
|
11
|
-
is_type_enum,
|
|
12
|
-
is_type_optional,
|
|
13
|
-
unwrap_optional_type,
|
|
14
|
-
)
|
|
9
|
+
from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
|
|
15
10
|
|
|
16
11
|
from .metadata import WebMethod
|
|
17
12
|
|
|
18
13
|
|
|
19
|
-
def split_prefix(
|
|
20
|
-
s: str, sep: str, prefix: Union[str, Iterable[str]]
|
|
21
|
-
) -> Tuple[Optional[str], str]:
|
|
14
|
+
def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> Tuple[Optional[str], str]:
|
|
22
15
|
"""
|
|
23
16
|
Recognizes a prefix at the beginning of a string.
|
|
24
17
|
|
|
@@ -50,6 +43,7 @@ def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> ty
|
|
|
50
43
|
return annotation
|
|
51
44
|
|
|
52
45
|
|
|
46
|
+
@enum.unique
|
|
53
47
|
class HTTPMethod(enum.Enum):
|
|
54
48
|
"HTTP method used to invoke an endpoint operation."
|
|
55
49
|
|
|
@@ -132,9 +126,7 @@ def _get_route_parameters(route: str) -> List[str]:
|
|
|
132
126
|
return extractor.keys
|
|
133
127
|
|
|
134
128
|
|
|
135
|
-
def _get_endpoint_functions(
|
|
136
|
-
endpoint: type, prefixes: List[str]
|
|
137
|
-
) -> Iterator[Tuple[str, str, str, Callable]]:
|
|
129
|
+
def _get_endpoint_functions(endpoint: type, prefixes: List[str]) -> Iterator[Tuple[str, str, str, Callable]]:
|
|
138
130
|
if not inspect.isclass(endpoint):
|
|
139
131
|
raise ValidationError(f"object is not a class type: {endpoint}")
|
|
140
132
|
|
|
@@ -156,14 +148,10 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
|
|
|
156
148
|
if name == member_fn:
|
|
157
149
|
return cls
|
|
158
150
|
|
|
159
|
-
raise ValidationError(
|
|
160
|
-
f"cannot find defining class for {member_fn} in {derived_cls}"
|
|
161
|
-
)
|
|
151
|
+
raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
|
|
162
152
|
|
|
163
153
|
|
|
164
|
-
def get_endpoint_operations(
|
|
165
|
-
endpoint: type, use_examples: bool = True
|
|
166
|
-
) -> List[EndpointOperation]:
|
|
154
|
+
def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[EndpointOperation]:
|
|
167
155
|
"""
|
|
168
156
|
Extracts a list of member functions in a class eligible for HTTP interface binding.
|
|
169
157
|
|
|
@@ -230,36 +218,23 @@ def get_endpoint_operations(
|
|
|
230
218
|
|
|
231
219
|
# check if all parameters have explicit type
|
|
232
220
|
if parameter.annotation is inspect.Parameter.empty:
|
|
233
|
-
raise ValidationError(
|
|
234
|
-
f"parameter '{param_name}' in function '{func_name}' has no type annotation"
|
|
235
|
-
)
|
|
221
|
+
raise ValidationError(f"parameter '{param_name}' in function '{func_name}' has no type annotation")
|
|
236
222
|
|
|
237
223
|
if is_type_optional(param_type):
|
|
238
224
|
inner_type: type = unwrap_optional_type(param_type)
|
|
239
225
|
else:
|
|
240
226
|
inner_type = param_type
|
|
241
227
|
|
|
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
|
-
):
|
|
228
|
+
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
229
|
if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
251
230
|
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
|
-
)
|
|
231
|
+
raise ValidationError(f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'")
|
|
255
232
|
|
|
256
233
|
# simple type maps to route path element, e.g. /study/{uuid}/{version}
|
|
257
234
|
path_params.append((param_name, param_type))
|
|
258
235
|
else:
|
|
259
236
|
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
|
-
)
|
|
237
|
+
raise ValidationError(f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'")
|
|
263
238
|
|
|
264
239
|
# simple type maps to key=value pair in query string
|
|
265
240
|
query_params.append((param_name, param_type))
|
|
@@ -280,9 +255,7 @@ def get_endpoint_operations(
|
|
|
280
255
|
|
|
281
256
|
# check if function has explicit return type
|
|
282
257
|
if signature.return_annotation is inspect.Signature.empty:
|
|
283
|
-
raise ValidationError(
|
|
284
|
-
f"function '{func_name}' has no return type annotation"
|
|
285
|
-
)
|
|
258
|
+
raise ValidationError(f"function '{func_name}' has no return type annotation")
|
|
286
259
|
|
|
287
260
|
return_type = _get_annotation_type(signature.return_annotation, func_ref)
|
|
288
261
|
|
|
@@ -3,14 +3,11 @@ from dataclasses import dataclass
|
|
|
3
3
|
from http import HTTPStatus
|
|
4
4
|
from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
|
-
from .specification import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
SecuritySchemeOpenIDConnect,
|
|
12
|
-
Server,
|
|
13
|
-
)
|
|
6
|
+
from .specification import Info, SecurityScheme
|
|
7
|
+
from .specification import SecuritySchemeAPI as SecuritySchemeAPI
|
|
8
|
+
from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
|
|
9
|
+
from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
|
|
10
|
+
from .specification import Server
|
|
14
11
|
|
|
15
12
|
HTTPStatusCode = Union[HTTPStatus, int, str]
|
|
16
13
|
|
|
@@ -37,12 +34,8 @@ class Options:
|
|
|
37
34
|
default_security_scheme: Optional[SecurityScheme] = None
|
|
38
35
|
extra_types: Union[List[type], Dict[str, List[type]], None] = None
|
|
39
36
|
use_examples: bool = True
|
|
40
|
-
success_responses: Dict[type, HTTPStatusCode] = dataclasses.field(
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
error_responses: Dict[type, HTTPStatusCode] = dataclasses.field(
|
|
44
|
-
default_factory=dict
|
|
45
|
-
)
|
|
37
|
+
success_responses: Dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
38
|
+
error_responses: Dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
|
|
46
39
|
error_wrapper: bool = False
|
|
47
40
|
property_description_fun: Optional[Callable[[type, str, str], str]] = None
|
|
48
41
|
captions: Optional[Dict[str, str]] = None
|
|
@@ -4,12 +4,7 @@ from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar
|
|
|
4
4
|
import aiohttp
|
|
5
5
|
from strong_typing.serialization import json_to_object, object_to_json
|
|
6
6
|
|
|
7
|
-
from .operations import
|
|
8
|
-
EndpointOperation,
|
|
9
|
-
HTTPMethod,
|
|
10
|
-
get_endpoint_operations,
|
|
11
|
-
get_signature,
|
|
12
|
-
)
|
|
7
|
+
from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations, get_signature
|
|
13
8
|
|
|
14
9
|
|
|
15
10
|
async def make_request(
|
|
@@ -51,9 +46,9 @@ class ProxyInvokeError(RuntimeError):
|
|
|
51
46
|
class EndpointProxy:
|
|
52
47
|
"The HTTP REST proxy class for an endpoint."
|
|
53
48
|
|
|
54
|
-
|
|
49
|
+
base_url: str
|
|
55
50
|
|
|
56
|
-
def __init__(self, base_url: str):
|
|
51
|
+
def __init__(self, base_url: str) -> None:
|
|
57
52
|
self.base_url = base_url
|
|
58
53
|
|
|
59
54
|
|
|
@@ -65,22 +60,18 @@ class OperationProxy:
|
|
|
65
60
|
payload, builds an HTTP request, and processes the HTTP response.
|
|
66
61
|
"""
|
|
67
62
|
|
|
68
|
-
def __init__(self, op: EndpointOperation):
|
|
63
|
+
def __init__(self, op: EndpointOperation) -> None:
|
|
69
64
|
self.op = op
|
|
70
65
|
self.sig = get_signature(op.func_ref)
|
|
71
66
|
|
|
72
|
-
async def __call__(
|
|
73
|
-
self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any
|
|
74
|
-
) -> Any:
|
|
67
|
+
async def __call__(self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
|
|
75
68
|
"Invokes an API operation via HTTP REST."
|
|
76
69
|
|
|
77
70
|
ba = self.sig.bind(self, *args, **kwargs)
|
|
78
71
|
|
|
79
72
|
# substitute parameters in URL path
|
|
80
73
|
route = self.op.get_route()
|
|
81
|
-
path = route.format_map(
|
|
82
|
-
{name: ba.arguments[name] for name, _type in self.op.path_params}
|
|
83
|
-
)
|
|
74
|
+
path = route.format_map({name: ba.arguments[name] for name, _type in self.op.path_params})
|
|
84
75
|
|
|
85
76
|
# gather URL query parameters
|
|
86
77
|
query = {name: str(ba.arguments[name]) for name, _type in self.op.query_params}
|
|
@@ -99,18 +90,14 @@ class OperationProxy:
|
|
|
99
90
|
data = None
|
|
100
91
|
|
|
101
92
|
# make HTTP request
|
|
102
|
-
status, response = await make_request(
|
|
103
|
-
self.op.http_method, endpoint_proxy.base_url, path, query, data
|
|
104
|
-
)
|
|
93
|
+
status, response = await make_request(self.op.http_method, endpoint_proxy.base_url, path, query, data)
|
|
105
94
|
|
|
106
95
|
# process HTTP response
|
|
107
96
|
if response:
|
|
108
97
|
try:
|
|
109
98
|
s = json.loads(response)
|
|
110
99
|
except json.JSONDecodeError:
|
|
111
|
-
raise ProxyInvokeError(
|
|
112
|
-
f"response body is not well-formed JSON:\n{response}"
|
|
113
|
-
)
|
|
100
|
+
raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}")
|
|
114
101
|
|
|
115
102
|
return json_to_object(self.op.response_type, s)
|
|
116
103
|
else:
|
|
@@ -122,9 +109,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
|
|
|
122
109
|
|
|
123
110
|
operation_proxy = OperationProxy(op)
|
|
124
111
|
|
|
125
|
-
async def _operation_proxy_fn(
|
|
126
|
-
self: EndpointProxy, *args: Any, **kwargs: Any
|
|
127
|
-
) -> Any:
|
|
112
|
+
async def _operation_proxy_fn(self: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
|
|
128
113
|
return await operation_proxy(self, *args, **kwargs)
|
|
129
114
|
|
|
130
115
|
return _operation_proxy_fn
|
|
@@ -3,7 +3,8 @@ import enum
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, ClassVar, Dict, List, Optional, Union
|
|
5
5
|
|
|
6
|
-
from strong_typing.schema import JsonType
|
|
6
|
+
from strong_typing.schema import JsonType as JsonType
|
|
7
|
+
from strong_typing.schema import Schema, StrictJsonType
|
|
7
8
|
|
|
8
9
|
URL = str
|
|
9
10
|
|
|
@@ -83,6 +84,7 @@ class Response:
|
|
|
83
84
|
content: Optional[Dict[str, MediaType]] = None
|
|
84
85
|
|
|
85
86
|
|
|
87
|
+
@enum.unique
|
|
86
88
|
class ParameterLocation(enum.Enum):
|
|
87
89
|
Query = "query"
|
|
88
90
|
Header = "header"
|
|
@@ -153,6 +155,7 @@ class Server:
|
|
|
153
155
|
description: Optional[str] = None
|
|
154
156
|
|
|
155
157
|
|
|
158
|
+
@enum.unique
|
|
156
159
|
class SecuritySchemeType(enum.Enum):
|
|
157
160
|
ApiKey = "apiKey"
|
|
158
161
|
HTTP = "http"
|
|
@@ -182,9 +185,7 @@ class SecuritySchemeHTTP(SecurityScheme):
|
|
|
182
185
|
scheme: str
|
|
183
186
|
bearerFormat: Optional[str] = None
|
|
184
187
|
|
|
185
|
-
def __init__(
|
|
186
|
-
self, description: str, scheme: str, bearerFormat: Optional[str] = None
|
|
187
|
-
) -> None:
|
|
188
|
+
def __init__(self, description: str, scheme: str, bearerFormat: Optional[str] = None) -> None:
|
|
188
189
|
super().__init__(SecuritySchemeType.HTTP, description)
|
|
189
190
|
self.scheme = scheme
|
|
190
191
|
self.bearerFormat = bearerFormat
|
|
@@ -14,7 +14,7 @@ from .specification import Document
|
|
|
14
14
|
class Specification:
|
|
15
15
|
document: Document
|
|
16
16
|
|
|
17
|
-
def __init__(self, endpoint: type, options: Options):
|
|
17
|
+
def __init__(self, endpoint: type, options: Options) -> None:
|
|
18
18
|
generator = Generator(endpoint, options)
|
|
19
19
|
self.document = generator.generate()
|
|
20
20
|
|
|
@@ -53,9 +53,7 @@ class Specification:
|
|
|
53
53
|
|
|
54
54
|
json_doc = self.get_json()
|
|
55
55
|
if pretty_print:
|
|
56
|
-
return json.dumps(
|
|
57
|
-
json_doc, check_circular=False, ensure_ascii=False, indent=4
|
|
58
|
-
)
|
|
56
|
+
return json.dumps(json_doc, check_circular=False, ensure_ascii=False, indent=4)
|
|
59
57
|
else:
|
|
60
58
|
return json.dumps(
|
|
61
59
|
json_doc,
|
|
@@ -97,14 +95,10 @@ class Specification:
|
|
|
97
95
|
"""
|
|
98
96
|
|
|
99
97
|
if sys.version_info >= (3, 9):
|
|
100
|
-
with importlib.resources.files(__package__).joinpath("template.html").open(
|
|
101
|
-
encoding="utf-8", errors="strict"
|
|
102
|
-
) as html_template_file:
|
|
98
|
+
with importlib.resources.files(__package__).joinpath("template.html").open(encoding="utf-8", errors="strict") as html_template_file:
|
|
103
99
|
html_template = html_template_file.read()
|
|
104
100
|
else:
|
|
105
|
-
with importlib.resources.open_text(
|
|
106
|
-
__package__, "template.html", encoding="utf-8", errors="strict"
|
|
107
|
-
) as html_template_file:
|
|
101
|
+
with importlib.resources.open_text(__package__, "template.html", encoding="utf-8", errors="strict") as html_template_file:
|
|
108
102
|
html_template = html_template_file.read()
|
|
109
103
|
|
|
110
104
|
html = html_template.replace(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-openapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Generate an OpenAPI specification from a Python class definition
|
|
5
5
|
Home-page: https://github.com/hunyadi/pyopenapi
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -15,12 +15,16 @@ 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
|
|
18
20
|
Classifier: Topic :: Software Development :: Code Generators
|
|
19
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Python: >=3.8
|
|
22
24
|
Description-Content-Type: text/markdown
|
|
23
25
|
License-File: LICENSE
|
|
26
|
+
Requires-Dist: aiohttp>=3.11
|
|
27
|
+
Requires-Dist: json_strong_typing>=0.3.7
|
|
24
28
|
|
|
25
29
|
# Generate an OpenAPI specification from a Python class
|
|
26
30
|
|
|
@@ -18,6 +18,8 @@ classifiers =
|
|
|
18
18
|
Programming Language :: Python :: 3.9
|
|
19
19
|
Programming Language :: Python :: 3.10
|
|
20
20
|
Programming Language :: Python :: 3.11
|
|
21
|
+
Programming Language :: Python :: 3.12
|
|
22
|
+
Programming Language :: Python :: 3.13
|
|
21
23
|
Topic :: Software Development :: Code Generators
|
|
22
24
|
Topic :: Software Development :: Libraries :: Python Modules
|
|
23
25
|
Typing :: Typed
|
|
@@ -28,8 +30,8 @@ include_package_data = True
|
|
|
28
30
|
packages = find:
|
|
29
31
|
python_requires = >=3.8
|
|
30
32
|
install_requires =
|
|
31
|
-
aiohttp >= 3.
|
|
32
|
-
json_strong_typing >= 0.
|
|
33
|
+
aiohttp >= 3.11
|
|
34
|
+
json_strong_typing >= 0.3.7
|
|
33
35
|
|
|
34
36
|
[options.packages.find]
|
|
35
37
|
exclude =
|
|
@@ -6,28 +6,20 @@ from http import HTTPStatus
|
|
|
6
6
|
from typing import TextIO
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
|
-
from endpoint import
|
|
10
|
-
AuthenticationError,
|
|
11
|
-
BadRequestError,
|
|
12
|
-
Endpoint,
|
|
13
|
-
InternalServerError,
|
|
14
|
-
NotFoundError,
|
|
15
|
-
Student,
|
|
16
|
-
Teacher,
|
|
17
|
-
ValidationError,
|
|
18
|
-
)
|
|
9
|
+
from endpoint import AuthenticationError, BadRequestError, Endpoint, InternalServerError, NotFoundError, Student, Teacher, ValidationError
|
|
19
10
|
|
|
20
11
|
from pyopenapi import Info, Options, Server, Specification
|
|
21
12
|
from pyopenapi.specification import SecuritySchemeHTTP
|
|
22
13
|
|
|
23
14
|
try:
|
|
24
15
|
from pygments import highlight
|
|
16
|
+
from pygments.formatter import Formatter
|
|
25
17
|
from pygments.formatters import HtmlFormatter
|
|
26
18
|
from pygments.lexers import get_lexer_by_name
|
|
27
19
|
|
|
28
20
|
def save_with_highlight(f: TextIO, code: str, format: str) -> None:
|
|
29
21
|
lexer = get_lexer_by_name(format)
|
|
30
|
-
formatter = HtmlFormatter()
|
|
22
|
+
formatter: Formatter = HtmlFormatter()
|
|
31
23
|
style = formatter.get_style_defs(".highlight")
|
|
32
24
|
f.writelines(
|
|
33
25
|
[
|
|
@@ -82,7 +74,7 @@ class TestOpenAPI(unittest.TestCase):
|
|
|
82
74
|
with open(os.path.join(os.path.dirname(__file__), "endpoint.md"), "r") as f:
|
|
83
75
|
description = f.read()
|
|
84
76
|
|
|
85
|
-
self.root = os.path.join(os.path.dirname(__file__), "..", "examples")
|
|
77
|
+
self.root = os.path.join(os.path.dirname(__file__), "..", "website", "examples")
|
|
86
78
|
os.makedirs(self.root, exist_ok=True)
|
|
87
79
|
self.specification = Specification(
|
|
88
80
|
Endpoint,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import unittest
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import Dict, Optional, Protocol
|
|
@@ -32,12 +33,10 @@ class HTTPBinPostResponse(HTTPBinResponse):
|
|
|
32
33
|
|
|
33
34
|
class API(Protocol):
|
|
34
35
|
@webmethod(route="/get")
|
|
35
|
-
async def get_method(self, /, id: str) -> HTTPBinResponse:
|
|
36
|
-
...
|
|
36
|
+
async def get_method(self, /, id: str) -> HTTPBinResponse: ...
|
|
37
37
|
|
|
38
38
|
@webmethod(route="/put")
|
|
39
|
-
async def set_method(self, /, id: str, doc: Document) -> HTTPBinPostResponse:
|
|
40
|
-
...
|
|
39
|
+
async def set_method(self, /, id: str, doc: Document) -> HTTPBinPostResponse: ...
|
|
41
40
|
|
|
42
41
|
|
|
43
42
|
class TestOpenAPI(unittest.IsolatedAsyncioTestCase):
|
|
@@ -55,6 +54,7 @@ class TestOpenAPI(unittest.IsolatedAsyncioTestCase):
|
|
|
55
54
|
if headers:
|
|
56
55
|
self.assertDictSubset(headers, response.headers)
|
|
57
56
|
|
|
57
|
+
@unittest.skipUnless(sys.version_info >= (3, 9), "requires Python 3.9 or later")
|
|
58
58
|
async def test_http(self) -> None:
|
|
59
59
|
Proxy = make_proxy_class(API) # type: ignore
|
|
60
60
|
proxy = Proxy("http://httpbin.org") # type: ignore
|
|
@@ -63,9 +63,7 @@ class TestOpenAPI(unittest.IsolatedAsyncioTestCase):
|
|
|
63
63
|
self.assertResponse(response, params={"id": "abc"})
|
|
64
64
|
|
|
65
65
|
response = await proxy.set_method("abc", Document("title", "text"))
|
|
66
|
-
self.assertResponse(
|
|
67
|
-
response, params={"id": "abc"}, headers={"Content-Type": "application/json"}
|
|
68
|
-
)
|
|
66
|
+
self.assertResponse(response, params={"id": "abc"}, headers={"Content-Type": "application/json"})
|
|
69
67
|
|
|
70
68
|
|
|
71
69
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|