python-openapi 0.1.10__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. python_openapi-0.2.0/MANIFEST.in +2 -0
  2. {python_openapi-0.1.10 → python_openapi-0.2.0}/PKG-INFO +17 -15
  3. {python_openapi-0.1.10 → python_openapi-0.2.0}/README.md +4 -4
  4. python_openapi-0.2.0/pyopenapi/__init__.py +14 -0
  5. python_openapi-0.1.10/pyopenapi/__init__.py → python_openapi-0.2.0/pyopenapi/decorators.py +15 -18
  6. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/generator.py +42 -34
  7. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/metadata.py +11 -3
  8. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/operations.py +26 -17
  9. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/options.py +16 -9
  10. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/proxy.py +15 -6
  11. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/specification.py +34 -26
  12. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/utility.py +12 -8
  13. python_openapi-0.2.0/pyproject.toml +58 -0
  14. {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/PKG-INFO +17 -15
  15. {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/SOURCES.txt +5 -1
  16. python_openapi-0.2.0/python_openapi.egg-info/requires.txt +2 -0
  17. python_openapi-0.2.0/setup.cfg +4 -0
  18. python_openapi-0.2.0/tests/__init__.py +0 -0
  19. python_openapi-0.2.0/tests/endpoint.md +5 -0
  20. python_openapi-0.2.0/tests/endpoint.py +389 -0
  21. {python_openapi-0.1.10 → python_openapi-0.2.0}/tests/test_openapi.py +4 -3
  22. {python_openapi-0.1.10 → python_openapi-0.2.0}/tests/test_proxy.py +13 -12
  23. python_openapi-0.1.10/pyproject.toml +0 -6
  24. python_openapi-0.1.10/python_openapi.egg-info/requires.txt +0 -2
  25. python_openapi-0.1.10/setup.cfg +0 -48
  26. {python_openapi-0.1.10 → python_openapi-0.2.0}/LICENSE +0 -0
  27. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/__main__.py +0 -0
  28. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/py.typed +0 -0
  29. {python_openapi-0.1.10 → python_openapi-0.2.0}/pyopenapi/template.html +0 -0
  30. {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/dependency_links.txt +0 -0
  31. {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/top_level.txt +0 -0
  32. {python_openapi-0.1.10 → python_openapi-0.2.0}/python_openapi.egg-info/zip-safe +0 -0
  33. {python_openapi-0.1.10 → python_openapi-0.2.0}/setup.py +0 -0
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ recursive-include tests *.md
@@ -1,30 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-openapi
3
- Version: 0.1.10
3
+ Version: 0.2.0
4
4
  Summary: Generate an OpenAPI specification from a Python class definition
5
- Home-page: https://github.com/hunyadi/pyopenapi
6
- Author: Levente Hunyadi
7
- Author-email: hunyadi@gmail.com
8
- License: MIT
5
+ Author-email: Levente Hunyadi <hunyadi@gmail.com>
6
+ Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/hunyadi/pyopenapi
9
+ Project-URL: Source, https://github.com/hunyadi/pyopenapi
10
+ Keywords: openapi3,openapi,redoc,swagger,json-schema-generator,dataclasses,type-inspection
9
11
  Classifier: Development Status :: 5 - Production/Stable
10
12
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
13
  Classifier: Operating System :: OS Independent
13
14
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3 :: Only
20
21
  Classifier: Topic :: Software Development :: Code Generators
21
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
23
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.8
24
+ Requires-Python: >=3.9
24
25
  Description-Content-Type: text/markdown
25
26
  License-File: LICENSE
26
- Requires-Dist: aiohttp>=3.11
27
- Requires-Dist: json_strong_typing>=0.3.7
27
+ Requires-Dist: aiohttp>=3.12
28
+ Requires-Dist: json_strong_typing>=0.3.9
29
+ Dynamic: license-file
28
30
 
29
31
  # Generate an OpenAPI specification from a Python class
30
32
 
@@ -35,7 +37,7 @@ Requires-Dist: json_strong_typing>=0.3.7
35
37
  * supports standard and asynchronous functions (`async def`)
36
38
  * maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
37
39
  * handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
38
- * handles generic types (`List[T]`, `Dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
40
+ * handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
39
41
  * maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
40
42
  * maps composite types to HTTP request body
41
43
  * supports user-defined routes, request and response samples with decorator `@webmethod`
@@ -88,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
88
90
 
89
91
  ```python
90
92
  class JobManagement:
91
- def create_job(self, items: List[URL]) -> uuid.UUID:
93
+ def create_job(self, items: list[URL]) -> uuid.UUID:
92
94
  ...
93
95
 
94
96
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -127,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
127
129
 
128
130
  ### Documenting operations
129
131
 
130
- Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
132
+ Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
131
133
 
132
134
  ```python
133
135
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -170,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
170
172
 
171
173
  ### Mapping function name prefixes to HTTP verbs
172
174
 
173
- The following table identifies which function name prefixes map to which HTTP verbs:
175
+ The following table identifies which function name prefixes map to which HTTP verbs:
174
176
 
175
177
  | Prefix | HTTP verb |
176
178
  | ------ | ----------- |
@@ -7,7 +7,7 @@
7
7
  * supports standard and asynchronous functions (`async def`)
8
8
  * maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
9
9
  * handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
10
- * handles generic types (`List[T]`, `Dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
10
+ * handles generic types (`list[T]`, `dict[K, V]`, `Optional[T]`, `Union[T1, T2, T3]`)
11
11
  * maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
12
12
  * maps composite types to HTTP request body
13
13
  * supports user-defined routes, request and response samples with decorator `@webmethod`
@@ -60,7 +60,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
60
60
 
61
61
  ```python
62
62
  class JobManagement:
63
- def create_job(self, items: List[URL]) -> uuid.UUID:
63
+ def create_job(self, items: list[URL]) -> uuid.UUID:
64
64
  ...
65
65
 
66
66
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -99,7 +99,7 @@ The custom path must have placeholders for all positional-only parameters in the
99
99
 
100
100
  ### Documenting operations
101
101
 
102
- Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
102
+ Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
103
103
 
104
104
  ```python
105
105
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -142,7 +142,7 @@ The Python objects in `request_examples` and `response_examples` are translated
142
142
 
143
143
  ### Mapping function name prefixes to HTTP verbs
144
144
 
145
- The following table identifies which function name prefixes map to which HTTP verbs:
145
+ The following table identifies which function name prefixes map to which HTTP verbs:
146
146
 
147
147
  | Prefix | HTTP verb |
148
148
  | ------ | ----------- |
@@ -0,0 +1,14 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
9
+ __version__ = "0.2.0"
10
+ __author__ = "Levente Hunyadi"
11
+ __copyright__ = "Copyright 2022-2025, Levente Hunyadi"
12
+ __license__ = "MIT"
13
+ __maintainer__ = "Levente Hunyadi"
14
+ __status__ = "Production"
@@ -1,12 +1,18 @@
1
- from typing import Any, Callable, List, Optional, TypeVar
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
9
+ from typing import Any, Callable, Optional, TypeVar
2
10
 
3
11
  from .metadata import WebMethod
4
12
  from .options import * # noqa: F403
5
13
  from .utility import Specification as Specification
6
14
 
7
- __version__ = "0.1.10"
8
-
9
- T = TypeVar("T")
15
+ F = TypeVar("F", bound=Callable[..., Any])
10
16
 
11
17
 
12
18
  def webmethod(
@@ -14,9 +20,9 @@ def webmethod(
14
20
  public: Optional[bool] = False,
15
21
  request_example: Optional[Any] = None,
16
22
  response_example: Optional[Any] = None,
17
- request_examples: Optional[List[Any]] = None,
18
- response_examples: Optional[List[Any]] = None,
19
- ) -> Callable[[T], T]:
23
+ request_examples: Optional[list[Any]] = None,
24
+ response_examples: Optional[list[Any]] = None,
25
+ ) -> Callable[[F], F]:
20
26
  """
21
27
  Decorator that supplies additional metadata to an endpoint operation function.
22
28
 
@@ -38,17 +44,8 @@ def webmethod(
38
44
  if response_example:
39
45
  response_examples = [response_example]
40
46
 
41
- def wrap(cls: T) -> T:
42
- setattr(
43
- cls,
44
- "__webmethod__",
45
- WebMethod(
46
- route=route,
47
- public=public or False,
48
- request_examples=request_examples,
49
- response_examples=response_examples,
50
- ),
51
- )
47
+ def wrap(cls: F) -> F:
48
+ cls.__webmethod__ = WebMethod(route=route, public=public or False, request_examples=request_examples, response_examples=response_examples) # type: ignore[attr-defined]
52
49
  return cls
53
50
 
54
51
  return wrap
@@ -1,16 +1,24 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
1
9
  import dataclasses
2
10
  import hashlib
3
11
  import ipaddress
4
12
  import typing
5
13
  from dataclasses import dataclass
6
14
  from http import HTTPStatus
7
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
15
+ from typing import Any, Callable, Optional, Union
8
16
 
9
- from strong_typing.core import JsonType
17
+ from strong_typing.core import JsonType, Schema
10
18
  from strong_typing.docstring import Docstring, parse_type
11
19
  from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
12
20
  from strong_typing.name import python_type_to_name
13
- from strong_typing.schema import JsonSchemaGenerator, Schema, SchemaOptions, get_schema_identifier, register_schema
21
+ from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
14
22
  from strong_typing.serialization import json_dump_string, object_to_json
15
23
 
16
24
  from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
@@ -78,7 +86,7 @@ def http_status_to_string(status_code: HTTPStatusCode) -> str:
78
86
 
79
87
  class SchemaBuilder:
80
88
  schema_generator: JsonSchemaGenerator
81
- schemas: Dict[str, Schema]
89
+ schemas: dict[str, Schema]
82
90
 
83
91
  def __init__(self, schema_generator: JsonSchemaGenerator) -> None:
84
92
  self.schema_generator = schema_generator
@@ -150,7 +158,7 @@ class ContentBuilder:
150
158
  self.schema_transformer = schema_transformer
151
159
  self.sample_transformer = sample_transformer
152
160
 
153
- def build_content(self, payload_type: type, examples: Optional[List[Any]] = None) -> Dict[str, MediaType]:
161
+ def build_content(self, payload_type: type, examples: Optional[list[Any]] = None) -> dict[str, MediaType]:
154
162
  "Creates the content subtree for a request or response."
155
163
 
156
164
  if is_generic_list(payload_type):
@@ -162,7 +170,7 @@ class ContentBuilder:
162
170
 
163
171
  return {media_type: self.build_media_type(item_type, examples)}
164
172
 
165
- def build_media_type(self, item_type: type, examples: Optional[List[Any]] = None) -> MediaType:
173
+ def build_media_type(self, item_type: type, examples: Optional[list[Any]] = None) -> MediaType:
166
174
  schema = self.schema_builder.classdef_to_ref(item_type)
167
175
  if self.schema_transformer:
168
176
  schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
@@ -179,12 +187,12 @@ class ContentBuilder:
179
187
  examples=self._build_examples(examples),
180
188
  )
181
189
 
182
- def _build_examples(self, examples: List[Any]) -> Dict[str, Union[Example, ExampleRef]]:
190
+ def _build_examples(self, examples: list[Any]) -> dict[str, Union[Example, ExampleRef]]:
183
191
  "Creates a set of several examples for a media type."
184
192
 
185
193
  builder = ExampleBuilder(self.sample_transformer)
186
194
 
187
- results: Dict[str, Union[Example, ExampleRef]] = {}
195
+ results: dict[str, Union[Example, ExampleRef]] = {}
188
196
  for example in examples:
189
197
  name, value = builder.get_named(example)
190
198
  results[name] = Example(value=value)
@@ -208,7 +216,7 @@ class ExampleBuilder:
208
216
  if sample_transformer:
209
217
  self.sample_transformer = sample_transformer
210
218
  else:
211
- self.sample_transformer = lambda sample: sample # noqa: E731
219
+ self.sample_transformer = lambda sample: sample
212
220
 
213
221
  def _get_value(self, example: Any) -> JsonType:
214
222
  return self.sample_transformer(object_to_json(example))
@@ -216,12 +224,12 @@ class ExampleBuilder:
216
224
  def get_anonymous(self, example: Any) -> JsonType:
217
225
  return self._get_value(example)
218
226
 
219
- def get_named(self, example: Any) -> Tuple[str, JsonType]:
227
+ def get_named(self, example: Any) -> tuple[str, JsonType]:
220
228
  value = self._get_value(example)
221
229
 
222
230
  name: Optional[str] = None
223
231
 
224
- if type(example).__str__ is not object.__str__:
232
+ if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
225
233
  friendly_name = str(example)
226
234
  if friendly_name.isprintable():
227
235
  name = friendly_name
@@ -244,17 +252,17 @@ class ResponseOptions:
244
252
  :param default_status_code: HTTP status code assigned to responses that have no mapping.
245
253
  """
246
254
 
247
- type_descriptions: Dict[type, str]
248
- examples: Optional[List[Any]]
249
- status_catalog: Dict[type, HTTPStatusCode]
255
+ type_descriptions: dict[type, str]
256
+ examples: Optional[list[Any]]
257
+ status_catalog: dict[type, HTTPStatusCode]
250
258
  default_status_code: HTTPStatusCode
251
259
 
252
260
 
253
261
  @dataclass
254
262
  class StatusResponse:
255
263
  status_code: str
256
- types: List[type] = dataclasses.field(default_factory=list)
257
- examples: List[Any] = dataclasses.field(default_factory=list)
264
+ types: list[type] = dataclasses.field(default_factory=list)
265
+ examples: list[Any] = dataclasses.field(default_factory=list)
258
266
 
259
267
 
260
268
  class ResponseBuilder:
@@ -263,8 +271,8 @@ class ResponseBuilder:
263
271
  def __init__(self, content_builder: ContentBuilder) -> None:
264
272
  self.content_builder = content_builder
265
273
 
266
- def _get_status_responses(self, options: ResponseOptions) -> Dict[str, StatusResponse]:
267
- status_responses: Dict[str, StatusResponse] = {}
274
+ def _get_status_responses(self, options: ResponseOptions) -> dict[str, StatusResponse]:
275
+ status_responses: dict[str, StatusResponse] = {}
268
276
 
269
277
  for response_type in options.type_descriptions.keys():
270
278
  status_code = http_status_to_string(options.status_catalog.get(response_type, options.default_status_code))
@@ -283,12 +291,12 @@ class ResponseBuilder:
283
291
 
284
292
  return dict(sorted(status_responses.items()))
285
293
 
286
- def build_response(self, options: ResponseOptions) -> Dict[str, Union[Response, ResponseRef]]:
294
+ def build_response(self, options: ResponseOptions) -> dict[str, Union[Response, ResponseRef]]:
287
295
  """
288
296
  Groups responses that have the same status code.
289
297
  """
290
298
 
291
- responses: Dict[str, Union[Response, ResponseRef]] = {}
299
+ responses: dict[str, Union[Response, ResponseRef]] = {}
292
300
  status_responses = self._get_status_responses(options)
293
301
  for status_code, status_response in status_responses.items():
294
302
  response_types = tuple(status_response.types)
@@ -315,9 +323,9 @@ class ResponseBuilder:
315
323
 
316
324
  def _build_response(
317
325
  self,
318
- response_type: type,
326
+ response_type: Optional[type],
319
327
  description: str,
320
- examples: Optional[List[Any]] = None,
328
+ examples: Optional[list[Any]] = None,
321
329
  ) -> Response:
322
330
  "Creates a response subtree."
323
331
 
@@ -355,7 +363,7 @@ class Generator:
355
363
  endpoint: type
356
364
  options: Options
357
365
  schema_builder: SchemaBuilder
358
- responses: Dict[str, Response]
366
+ responses: dict[str, Response]
359
367
 
360
368
  def __init__(self, endpoint: type, options: Options) -> None:
361
369
  self.endpoint = endpoint
@@ -379,17 +387,17 @@ class Generator:
379
387
  description="\n\n".join(s for s in (title, description, definition) if s is not None),
380
388
  )
381
389
 
382
- def _build_extra_tag_groups(self, extra_types: Dict[str, List[type]]) -> Dict[str, List[Tag]]:
390
+ def _build_extra_tag_groups(self, extra_types: dict[str, list[type]]) -> dict[str, list[Tag]]:
383
391
  """
384
392
  Creates a dictionary of tag group captions as keys, and tag lists as values.
385
393
 
386
394
  :param extra_types: A dictionary of type categories and list of types in that category.
387
395
  """
388
396
 
389
- extra_tags: Dict[str, List[Tag]] = {}
397
+ extra_tags: dict[str, list[Tag]] = {}
390
398
 
391
399
  for category_name, category_items in extra_types.items():
392
- tag_list: List[Tag] = []
400
+ tag_list: list[Tag] = []
393
401
 
394
402
  for extra_type in category_items:
395
403
  name = python_type_to_name(extra_type)
@@ -454,7 +462,7 @@ class Generator:
454
462
  # success response types
455
463
  if doc_string.returns is None and is_type_union(op.response_type):
456
464
  # split union of return types into a list of response types
457
- success_type_docstring: Dict[type, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
465
+ success_type_docstring: dict[type, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
458
466
  success_type_descriptions = {
459
467
  item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
460
468
  }
@@ -477,7 +485,7 @@ class Generator:
477
485
 
478
486
  # failure response types
479
487
  if doc_string.raises:
480
- exception_types: Dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
488
+ exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
481
489
  exception_examples = [example for example in response_examples if isinstance(example, Exception)]
482
490
 
483
491
  if self.options.error_wrapper:
@@ -529,8 +537,8 @@ class Generator:
529
537
  )
530
538
 
531
539
  def generate(self) -> Document:
532
- paths: Dict[str, PathItem] = {}
533
- endpoint_classes: Set[type] = set()
540
+ paths: dict[str, PathItem] = {}
541
+ endpoint_classes: set[type] = set()
534
542
  for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
535
543
  endpoint_classes.add(op.defining_class)
536
544
 
@@ -555,7 +563,7 @@ class Generator:
555
563
  else:
556
564
  paths[route] = pathItem
557
565
 
558
- operation_tags: List[Tag] = []
566
+ operation_tags: list[Tag] = []
559
567
  for cls in endpoint_classes:
560
568
  doc_string = parse_type(cls)
561
569
  operation_tags.append(
@@ -570,14 +578,14 @@ class Generator:
570
578
  type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
571
579
 
572
580
  # types that are emitted by events
573
- event_tags: List[Tag] = []
581
+ event_tags: list[Tag] = []
574
582
  events = get_endpoint_events(self.endpoint)
575
583
  for ref, event_type in events.items():
576
584
  event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
577
585
  event_tags.append(self._build_type_tag(ref, event_schema))
578
586
 
579
587
  # types that are explicitly declared
580
- extra_tag_groups: Dict[str, List[Tag]] = {}
588
+ extra_tag_groups: dict[str, list[Tag]] = {}
581
589
  if self.options.extra_types is not None:
582
590
  if isinstance(self.options.extra_types, list):
583
591
  extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
@@ -587,7 +595,7 @@ class Generator:
587
595
  raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
588
596
 
589
597
  # list all operations and types
590
- tags: List[Tag] = []
598
+ tags: list[Tag] = []
591
599
  tags.extend(operation_tags)
592
600
  tags.extend(type_tags)
593
601
  tags.extend(event_tags)
@@ -1,5 +1,13 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
1
9
  from dataclasses import dataclass
2
- from typing import Any, List, Optional
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[List[Any]] = None
19
- response_examples: Optional[List[Any]] = None
26
+ request_examples: Optional[list[Any]] = None
27
+ response_examples: Optional[list[Any]] = None
@@ -1,17 +1,25 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
1
9
  import collections.abc
2
10
  import enum
3
11
  import inspect
4
12
  import typing
5
13
  import uuid
6
14
  from dataclasses import dataclass
7
- from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
15
+ from typing import Any, Callable, Iterable, Iterator, Optional, Union
8
16
 
9
17
  from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
10
18
 
11
19
  from .metadata import WebMethod
12
20
 
13
21
 
14
- def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> Tuple[Optional[str], str]:
22
+ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> tuple[Optional[str], str]:
15
23
  """
16
24
  Recognizes a prefix at the beginning of a string.
17
25
 
@@ -34,11 +42,11 @@ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> Tuple[O
34
42
  return None, s
35
43
 
36
44
 
37
- def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type:
38
- "Maps a stringized reference to a type, as if using `from __future__ import annotations`."
45
+ def _get_annotation_type(annotation: Union[type, str], callable: Callable[..., Any]) -> type:
46
+ "Maps a string (forward) reference to a type, as if using `from __future__ import annotations`."
39
47
 
40
48
  if isinstance(annotation, str):
41
- return eval(annotation, callable.__globals__)
49
+ return typing.cast(type, eval(annotation, callable.__globals__))
42
50
  else:
43
51
  return annotation
44
52
 
@@ -54,7 +62,7 @@ class HTTPMethod(enum.Enum):
54
62
  PATCH = "PATCH"
55
63
 
56
64
 
57
- OperationParameter = Tuple[str, type]
65
+ OperationParameter = tuple[str, type]
58
66
 
59
67
 
60
68
  class ValidationError(TypeError):
@@ -87,15 +95,15 @@ class EndpointOperation:
87
95
  func_name: str
88
96
  func_ref: Callable[..., Any]
89
97
  route: Optional[str]
90
- path_params: List[OperationParameter]
91
- query_params: List[OperationParameter]
98
+ path_params: list[OperationParameter]
99
+ query_params: list[OperationParameter]
92
100
  request_param: Optional[OperationParameter]
93
101
  event_type: Optional[type]
94
102
  response_type: type
95
103
  http_method: HTTPMethod
96
104
  public: bool
97
- request_examples: Optional[List[Any]] = None
98
- response_examples: Optional[List[Any]] = None
105
+ request_examples: Optional[list[Any]] = None
106
+ response_examples: Optional[list[Any]] = None
99
107
 
100
108
  def get_route(self) -> str:
101
109
  if self.route is not None:
@@ -108,9 +116,9 @@ class EndpointOperation:
108
116
 
109
117
 
110
118
  class _FormatParameterExtractor:
111
- "A visitor to exract parameters in a format string."
119
+ "A visitor to extract parameters in a format string."
112
120
 
113
- keys: List[str]
121
+ keys: list[str]
114
122
 
115
123
  def __init__(self) -> None:
116
124
  self.keys = []
@@ -120,13 +128,13 @@ class _FormatParameterExtractor:
120
128
  return None
121
129
 
122
130
 
123
- def _get_route_parameters(route: str) -> List[str]:
131
+ def _get_route_parameters(route: str) -> list[str]:
124
132
  extractor = _FormatParameterExtractor()
125
133
  route.format_map(extractor)
126
134
  return extractor.keys
127
135
 
128
136
 
129
- def _get_endpoint_functions(endpoint: type, prefixes: List[str]) -> Iterator[Tuple[str, str, str, Callable]]:
137
+ def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
130
138
  if not inspect.isclass(endpoint):
131
139
  raise ValidationError(f"object is not a class type: {endpoint}")
132
140
 
@@ -151,7 +159,7 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
151
159
  raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
152
160
 
153
161
 
154
- def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[EndpointOperation]:
162
+ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> list[EndpointOperation]:
155
163
  """
156
164
  Extracts a list of member functions in a class eligible for HTTP interface binding.
157
165
 
@@ -247,7 +255,8 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
247
255
  if request_param is not None:
248
256
  param = (param_name, param_type)
249
257
  raise ValidationError(
250
- f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': {request_param} and {param}"
258
+ f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': "
259
+ f"{request_param} and {param}"
251
260
  )
252
261
 
253
262
  # composite types are read from body
@@ -310,7 +319,7 @@ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> List[E
310
319
  return result
311
320
 
312
321
 
313
- def get_endpoint_events(endpoint: type) -> Dict[str, type]:
322
+ def get_endpoint_events(endpoint: type) -> dict[str, type]:
314
323
  results = {}
315
324
 
316
325
  for decl in typing.get_type_hints(endpoint).values():
@@ -1,13 +1,20 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
1
9
  import dataclasses
2
10
  from dataclasses import dataclass
3
11
  from http import HTTPStatus
4
- from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Union
12
+ from typing import Callable, ClassVar, Optional, Union
5
13
 
6
- from .specification import Info, SecurityScheme
14
+ from .specification import Info, SecurityScheme, Server
7
15
  from .specification import SecuritySchemeAPI as SecuritySchemeAPI
8
16
  from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
9
17
  from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
10
- from .specification import Server
11
18
 
12
19
  HTTPStatusCode = Union[HTTPStatus, int, str]
13
20
 
@@ -30,17 +37,17 @@ class Options:
30
37
 
31
38
  server: Server
32
39
  info: Info
33
- version: Tuple[int, int, int] = (3, 1, 0)
40
+ version: tuple[int, int, int] = (3, 1, 0)
34
41
  default_security_scheme: Optional[SecurityScheme] = None
35
- extra_types: Union[List[type], Dict[str, List[type]], None] = None
42
+ extra_types: Union[list[type], dict[str, list[type]], None] = None
36
43
  use_examples: bool = True
37
- success_responses: Dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
38
- error_responses: Dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
44
+ success_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
45
+ error_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
39
46
  error_wrapper: bool = False
40
47
  property_description_fun: Optional[Callable[[type, str, str], str]] = None
41
- captions: Optional[Dict[str, str]] = None
48
+ captions: Optional[dict[str, str]] = None
42
49
 
43
- default_captions: ClassVar[Dict[str, str]] = {
50
+ default_captions: ClassVar[dict[str, str]] = {
44
51
  "Operations": "Operations",
45
52
  "Types": "Types",
46
53
  "Events": "Events",