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.
Files changed (26) hide show
  1. {python-openapi-0.1.9 → python_openapi-0.1.10}/LICENSE +1 -1
  2. {python-openapi-0.1.9 → python_openapi-0.1.10}/PKG-INFO +5 -1
  3. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/__init__.py +6 -10
  4. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/generator.py +71 -129
  5. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/operations.py +11 -38
  6. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/options.py +7 -14
  7. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/proxy.py +9 -24
  8. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/specification.py +5 -4
  9. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/utility.py +4 -10
  10. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyproject.toml +3 -0
  11. {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/PKG-INFO +5 -1
  12. python_openapi-0.1.10/python_openapi.egg-info/requires.txt +2 -0
  13. {python-openapi-0.1.9 → python_openapi-0.1.10}/setup.cfg +4 -2
  14. {python-openapi-0.1.9 → python_openapi-0.1.10}/tests/test_openapi.py +4 -12
  15. {python-openapi-0.1.9 → python_openapi-0.1.10}/tests/test_proxy.py +5 -7
  16. python-openapi-0.1.9/python_openapi.egg-info/requires.txt +0 -2
  17. {python-openapi-0.1.9 → python_openapi-0.1.10}/README.md +0 -0
  18. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/__main__.py +0 -0
  19. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/metadata.py +0 -0
  20. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/py.typed +0 -0
  21. {python-openapi-0.1.9 → python_openapi-0.1.10}/pyopenapi/template.html +0 -0
  22. {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/SOURCES.txt +0 -0
  23. {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/dependency_links.txt +0 -0
  24. {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/top_level.txt +0 -0
  25. {python-openapi-0.1.9 → python_openapi-0.1.10}/python_openapi.egg-info/zip-safe +0 -0
  26. {python-openapi-0.1.9 → python_openapi-0.1.10}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-2022 Levente Hunyadi
3
+ Copyright (c) 2021-2025 Levente Hunyadi
4
4
  Copyright (c) 2021-2022 Instructure Inc.
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-openapi
3
- Version: 0.1.9
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.9"
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 typing import Any, Dict, Set, Union
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
- EndpointOperation,
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 # type: ignore
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
- if self.sample_transformer:
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 = sample_transformer(object_to_json(example))
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
- if self.sample_transformer:
228
- sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore
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
- return sample_transformer(object_to_json(example))
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
- item.raise_type: item.description for item in doc_string.raises.values()
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
- Info,
8
- SecurityScheme,
9
- SecuritySchemeAPI,
10
- SecuritySchemeHTTP,
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
- default_factory=dict
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
- url: str
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, Schema, StrictJsonType
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,3 +1,6 @@
1
1
  [build-system]
2
2
  requires = ["setuptools>=42", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
+
5
+ [tool.ruff]
6
+ line-length = 160
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-openapi
3
- Version: 0.1.9
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
 
@@ -0,0 +1,2 @@
1
+ aiohttp>=3.11
2
+ json_strong_typing>=0.3.7
@@ -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.8
32
- json_strong_typing >= 0.2.7
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__":
@@ -1,2 +0,0 @@
1
- aiohttp>=3.8
2
- json_strong_typing>=0.2.7
File without changes