python-openapi 0.1.10__tar.gz → 0.3.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 (34) hide show
  1. {python_openapi-0.1.10 → python_openapi-0.3.0}/LICENSE +1 -1
  2. python_openapi-0.3.0/MANIFEST.in +2 -0
  3. {python_openapi-0.1.10 → python_openapi-0.3.0}/PKG-INFO +28 -17
  4. {python_openapi-0.1.10 → python_openapi-0.3.0}/README.md +5 -5
  5. python_openapi-0.3.0/pyopenapi/__init__.py +14 -0
  6. python_openapi-0.1.10/pyopenapi/__init__.py → python_openapi-0.3.0/pyopenapi/decorators.py +25 -21
  7. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/generator.py +85 -75
  8. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/metadata.py +14 -4
  9. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/operations.py +48 -33
  10. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/options.py +19 -12
  11. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/proxy.py +20 -10
  12. python_openapi-0.3.0/pyopenapi/specification.py +272 -0
  13. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/utility.py +12 -8
  14. python_openapi-0.3.0/pyproject.toml +71 -0
  15. {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/PKG-INFO +28 -17
  16. {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/SOURCES.txt +5 -2
  17. python_openapi-0.3.0/python_openapi.egg-info/requires.txt +13 -0
  18. python_openapi-0.3.0/setup.cfg +4 -0
  19. python_openapi-0.3.0/setup.py +12 -0
  20. python_openapi-0.3.0/tests/endpoint.md +5 -0
  21. python_openapi-0.3.0/tests/endpoint.py +396 -0
  22. {python_openapi-0.1.10 → python_openapi-0.3.0}/tests/test_openapi.py +9 -10
  23. {python_openapi-0.1.10 → python_openapi-0.3.0}/tests/test_proxy.py +13 -12
  24. python_openapi-0.1.10/pyopenapi/specification.py +0 -253
  25. python_openapi-0.1.10/pyproject.toml +0 -6
  26. python_openapi-0.1.10/python_openapi.egg-info/requires.txt +0 -2
  27. python_openapi-0.1.10/setup.cfg +0 -48
  28. python_openapi-0.1.10/setup.py +0 -4
  29. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/py.typed +0 -0
  30. {python_openapi-0.1.10 → python_openapi-0.3.0}/pyopenapi/template.html +0 -0
  31. {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/dependency_links.txt +0 -0
  32. {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/top_level.txt +0 -0
  33. {python_openapi-0.1.10 → python_openapi-0.3.0}/python_openapi.egg-info/zip-safe +0 -0
  34. /python_openapi-0.1.10/pyopenapi/__main__.py → /python_openapi-0.3.0/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-2025 Levente Hunyadi
3
+ Copyright (c) 2021-2026 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
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ recursive-include tests *.md
@@ -1,30 +1,41 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-openapi
3
- Version: 0.1.10
3
+ Version: 0.3.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
- Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
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.10
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: json_strong_typing>=0.4
28
+ Provides-Extra: proxy
29
+ Requires-Dist: aiohttp>=3.13; extra == "proxy"
30
+ Provides-Extra: dev
31
+ Requires-Dist: build; extra == "dev"
32
+ Requires-Dist: mypy; extra == "dev"
33
+ Requires-Dist: ruff; extra == "dev"
34
+ Requires-Dist: PyYAML; extra == "dev"
35
+ Requires-Dist: Pygments; extra == "dev"
36
+ Requires-Dist: types-PyYAML; extra == "dev"
37
+ Requires-Dist: types-Pygments; extra == "dev"
38
+ Dynamic: license-file
28
39
 
29
40
  # Generate an OpenAPI specification from a Python class
30
41
 
@@ -35,7 +46,7 @@ Requires-Dist: json_strong_typing>=0.3.7
35
46
  * supports standard and asynchronous functions (`async def`)
36
47
  * maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
37
48
  * 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]`)
49
+ * handles generic types (`list[T]`, `dict[K, V]`, `T | None`, `T1 | T2 | T3`)
39
50
  * maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
40
51
  * maps composite types to HTTP request body
41
52
  * supports user-defined routes, request and response samples with decorator `@webmethod`
@@ -88,7 +99,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
88
99
 
89
100
  ```python
90
101
  class JobManagement:
91
- def create_job(self, items: List[URL]) -> uuid.UUID:
102
+ def create_job(self, items: list[URL]) -> uuid.UUID:
92
103
  ...
93
104
 
94
105
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -127,7 +138,7 @@ The custom path must have placeholders for all positional-only parameters in the
127
138
 
128
139
  ### Documenting operations
129
140
 
130
- Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
141
+ Use Python ReST (ReStructured Text) doc-strings to attach documentation to operations:
131
142
 
132
143
  ```python
133
144
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -160,7 +171,7 @@ OpenAPI supports specifying [examples](https://spec.openapis.org/oas/latest.html
160
171
  Teacher("Vacska", "Mati", "Négyszögletű Kerek Erdő"),
161
172
  ],
162
173
  )
163
- def get_member_by_name(self, family: str, given: str, /) -> Union[Student, Teacher]:
174
+ def get_member_by_name(self, family: str, given: str, /) -> Student | Teacher:
164
175
  ...
165
176
  ```
166
177
 
@@ -170,7 +181,7 @@ The Python objects in `request_examples` and `response_examples` are translated
170
181
 
171
182
  ### Mapping function name prefixes to HTTP verbs
172
183
 
173
- The following table identifies which function name prefixes map to which HTTP verbs:
184
+ The following table identifies which function name prefixes map to which HTTP verbs:
174
185
 
175
186
  | Prefix | HTTP verb |
176
187
  | ------ | ----------- |
@@ -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]`, `T | None`, `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:
@@ -132,7 +132,7 @@ OpenAPI supports specifying [examples](https://spec.openapis.org/oas/latest.html
132
132
  Teacher("Vacska", "Mati", "Négyszögletű Kerek Erdő"),
133
133
  ],
134
134
  )
135
- def get_member_by_name(self, family: str, given: str, /) -> Union[Student, Teacher]:
135
+ def get_member_by_name(self, family: str, given: str, /) -> Student | Teacher:
136
136
  ...
137
137
  ```
138
138
 
@@ -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 2021-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
9
+ __version__ = "0.3.0"
10
+ __author__ = "Levente Hunyadi"
11
+ __copyright__ = "Copyright 2021-2026, Levente Hunyadi"
12
+ __license__ = "MIT"
13
+ __maintainer__ = "Levente Hunyadi"
14
+ __status__ = "Production"
@@ -1,22 +1,29 @@
1
- from typing import Any, Callable, List, Optional, TypeVar
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2021-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
9
+ from typing import Any, Callable, 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(
13
- route: Optional[str] = None,
14
- public: Optional[bool] = False,
15
- request_example: Optional[Any] = None,
16
- response_example: Optional[Any] = None,
17
- request_examples: Optional[List[Any]] = None,
18
- response_examples: Optional[List[Any]] = None,
19
- ) -> Callable[[T], T]:
19
+ route: str | None = None,
20
+ public: bool = False,
21
+ deprecated: bool = False,
22
+ request_example: Any | None = None,
23
+ response_example: Any | None = None,
24
+ request_examples: list[Any] | None = None,
25
+ response_examples: list[Any] | None = None,
26
+ ) -> Callable[[F], F]:
20
27
  """
21
28
  Decorator that supplies additional metadata to an endpoint operation function.
22
29
 
@@ -38,16 +45,13 @@ def webmethod(
38
45
  if response_example:
39
46
  response_examples = [response_example]
40
47
 
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
- ),
48
+ def wrap(cls: F) -> F:
49
+ cls.__webmethod__ = WebMethod( # type: ignore[attr-defined]
50
+ route=route,
51
+ public=public or False,
52
+ deprecated=deprecated or False,
53
+ request_examples=request_examples,
54
+ response_examples=response_examples,
51
55
  )
52
56
  return cls
53
57
 
@@ -1,16 +1,26 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2021-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
1
9
  import dataclasses
2
10
  import hashlib
3
11
  import ipaddress
12
+ import operator
4
13
  import typing
5
14
  from dataclasses import dataclass
15
+ from functools import reduce
6
16
  from http import HTTPStatus
7
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
17
+ from typing import Any, Callable
8
18
 
9
- from strong_typing.core import JsonType
19
+ from strong_typing.core import JsonType, Schema
10
20
  from strong_typing.docstring import Docstring, parse_type
11
- from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
21
+ from strong_typing.inspection import is_type_optional, is_type_union, unwrap_optional_type, unwrap_union_types
12
22
  from strong_typing.name import python_type_to_name
13
- from strong_typing.schema import JsonSchemaGenerator, Schema, SchemaOptions, get_schema_identifier, register_schema
23
+ from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
14
24
  from strong_typing.serialization import json_dump_string, object_to_json
15
25
 
16
26
  from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
@@ -28,7 +38,6 @@ from .specification import (
28
38
  RequestBody,
29
39
  Response,
30
40
  ResponseRef,
31
- SchemaOrRef,
32
41
  SchemaRef,
33
42
  Tag,
34
43
  TagGroup,
@@ -66,25 +75,24 @@ register_schema(
66
75
  def http_status_to_string(status_code: HTTPStatusCode) -> str:
67
76
  "Converts an HTTP status code to a string."
68
77
 
69
- if isinstance(status_code, HTTPStatus):
70
- return str(status_code.value)
71
- elif isinstance(status_code, int):
72
- return str(status_code)
73
- elif isinstance(status_code, str):
74
- return status_code
75
- else:
76
- raise TypeError("expected: HTTP status code")
78
+ match status_code:
79
+ case HTTPStatus():
80
+ return str(status_code.value)
81
+ case int():
82
+ return str(status_code)
83
+ case str():
84
+ return status_code
77
85
 
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
85
93
  self.schemas = {}
86
94
 
87
- def classdef_to_schema(self, typ: type) -> Schema:
95
+ def classdef_to_schema(self, typ: type[Any]) -> Schema:
88
96
  """
89
97
  Converts a type to a JSON schema.
90
98
  For nested types found in the type hierarchy, adds the type to the schema registry in the OpenAPI specification section `components`.
@@ -98,12 +106,12 @@ class SchemaBuilder:
98
106
 
99
107
  return type_schema
100
108
 
101
- def classdef_to_named_schema(self, name: str, typ: type) -> Schema:
109
+ def classdef_to_named_schema(self, name: str, typ: type[Any]) -> Schema:
102
110
  schema = self.classdef_to_schema(typ)
103
111
  self._add_ref(name, schema)
104
112
  return schema
105
113
 
106
- def classdef_to_ref(self, typ: type) -> SchemaOrRef:
114
+ def classdef_to_ref(self, typ: type[Any]) -> Schema | SchemaRef:
107
115
  """
108
116
  Converts a type to a JSON schema, and if possible, returns a schema reference.
109
117
  For composite types (such as classes), adds the type to the schema registry in the OpenAPI specification section `components`.
@@ -137,35 +145,35 @@ class SchemaBuilder:
137
145
 
138
146
  class ContentBuilder:
139
147
  schema_builder: SchemaBuilder
140
- schema_transformer: Optional[Callable[[SchemaOrRef], SchemaOrRef]]
141
- sample_transformer: Optional[Callable[[JsonType], JsonType]]
148
+ schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None
149
+ sample_transformer: Callable[[JsonType], JsonType] | None
142
150
 
143
151
  def __init__(
144
152
  self,
145
153
  schema_builder: SchemaBuilder,
146
- schema_transformer: Optional[Callable[[SchemaOrRef], SchemaOrRef]] = None,
147
- sample_transformer: Optional[Callable[[JsonType], JsonType]] = None,
154
+ schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] | None = None,
155
+ sample_transformer: Callable[[JsonType], JsonType] | None = None,
148
156
  ) -> None:
149
157
  self.schema_builder = schema_builder
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[Any], examples: list[Any] | None = None) -> dict[str, MediaType]:
154
162
  "Creates the content subtree for a request or response."
155
163
 
156
- if is_generic_list(payload_type):
164
+ if typing.get_origin(payload_type) is list:
157
165
  media_type = "application/jsonl"
158
- item_type = unwrap_generic_list(payload_type)
166
+ (item_type,) = typing.get_args(payload_type) # unpack single tuple element
159
167
  else:
160
168
  media_type = "application/json"
161
169
  item_type = payload_type
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[Any], examples: list[Any] | None = None) -> MediaType:
166
174
  schema = self.schema_builder.classdef_to_ref(item_type)
167
175
  if self.schema_transformer:
168
- schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
176
+ schema_transformer: Callable[[Schema | SchemaRef], Schema | SchemaRef] = self.schema_transformer
169
177
  schema = schema_transformer(schema)
170
178
 
171
179
  if not examples:
@@ -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, 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, Example | ExampleRef] = {}
188
196
  for example in examples:
189
197
  name, value = builder.get_named(example)
190
198
  results[name] = Example(value=value)
@@ -203,12 +211,12 @@ class ExampleBuilder:
203
211
 
204
212
  def __init__(
205
213
  self,
206
- sample_transformer: Optional[Callable[[JsonType], JsonType]] = None,
214
+ sample_transformer: Callable[[JsonType], JsonType] | None = None,
207
215
  ) -> None:
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
- name: Optional[str] = None
230
+ name: str | None = 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[Any], str]
256
+ examples: list[Any] | None
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[Any]] = dataclasses.field(default_factory=list[type[Any]])
265
+ examples: list[Any] = dataclasses.field(default_factory=list[Any])
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,20 +291,20 @@ 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, 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, 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)
295
- if len(response_types) > 1:
296
- composite_response_type: type = Union[response_types] # type: ignore
303
+ composite_response_type: type[Any] | None
304
+ if len(response_types) > 0:
305
+ composite_response_type = reduce(operator.or_, response_types)
297
306
  else:
298
- (response_type,) = response_types
299
- composite_response_type = response_type
307
+ composite_response_type = None
300
308
 
301
309
  description = " **OR** ".join(
302
310
  filter(
@@ -315,9 +323,9 @@ class ResponseBuilder:
315
323
 
316
324
  def _build_response(
317
325
  self,
318
- response_type: type,
326
+ response_type: type[Any] | None,
319
327
  description: str,
320
- examples: Optional[List[Any]] = None,
328
+ examples: list[Any] | None = None,
321
329
  ) -> Response:
322
330
  "Creates a response subtree."
323
331
 
@@ -330,7 +338,7 @@ class ResponseBuilder:
330
338
  return Response(description=description)
331
339
 
332
340
 
333
- def schema_error_wrapper(schema: SchemaOrRef) -> Schema:
341
+ def schema_error_wrapper(schema: Schema | SchemaRef) -> Schema:
334
342
  "Wraps an error output schema into a top-level error schema."
335
343
 
336
344
  return {
@@ -352,12 +360,12 @@ def sample_error_wrapper(error: JsonType) -> JsonType:
352
360
 
353
361
 
354
362
  class Generator:
355
- endpoint: type
363
+ endpoint: type[Any]
356
364
  options: Options
357
365
  schema_builder: SchemaBuilder
358
- responses: Dict[str, Response]
366
+ responses: dict[str, Response]
359
367
 
360
- def __init__(self, endpoint: type, options: Options) -> None:
368
+ def __init__(self, endpoint: type[Any], options: Options) -> None:
361
369
  self.endpoint = endpoint
362
370
  self.options = options
363
371
  schema_generator = JsonSchemaGenerator(
@@ -372,24 +380,24 @@ class Generator:
372
380
 
373
381
  def _build_type_tag(self, ref: str, schema: Schema) -> Tag:
374
382
  definition = f'<SchemaDefinition schemaRef="#/components/schemas/{ref}" />'
375
- title = typing.cast(str, schema.get("title"))
376
- description = typing.cast(str, schema.get("description"))
383
+ title = typing.cast(str | None, schema.get("title"))
384
+ description = typing.cast(str | None, schema.get("description"))
377
385
  return Tag(
378
386
  name=ref,
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[Any]]]) -> 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)
@@ -418,10 +426,10 @@ class Generator:
418
426
  ]
419
427
 
420
428
  # parameters passed in URL component query string
421
- query_parameters = []
429
+ query_parameters: list[Parameter] = []
422
430
  for param_name, param_type in op.query_params:
423
431
  if is_type_optional(param_type):
424
- inner_type: type = unwrap_optional_type(param_type)
432
+ inner_type: type[Any] = unwrap_optional_type(param_type)
425
433
  required = False
426
434
  else:
427
435
  inner_type = param_type
@@ -454,7 +462,9 @@ 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[Any], Docstring] = {
466
+ typing.cast(type[Any], item): parse_type(item) for item in unwrap_union_types(op.response_type)
467
+ }
458
468
  success_type_descriptions = {
459
469
  item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
460
470
  }
@@ -477,7 +487,7 @@ class Generator:
477
487
 
478
488
  # failure response types
479
489
  if doc_string.raises:
480
- exception_types: Dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
490
+ exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
481
491
  exception_examples = [example for example in response_examples if isinstance(example, Exception)]
482
492
 
483
493
  if self.options.error_wrapper:
@@ -525,12 +535,13 @@ class Generator:
525
535
  requestBody=requestBody,
526
536
  responses=responses,
527
537
  callbacks=callbacks,
538
+ deprecated=op.deprecated,
528
539
  security=[] if op.public else None,
529
540
  )
530
541
 
531
542
  def generate(self) -> Document:
532
- paths: Dict[str, PathItem] = {}
533
- endpoint_classes: Set[type] = set()
543
+ paths: dict[str, PathItem] = {}
544
+ endpoint_classes: set[type[Any]] = set()
534
545
  for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
535
546
  endpoint_classes.add(op.defining_class)
536
547
 
@@ -555,9 +566,9 @@ class Generator:
555
566
  else:
556
567
  paths[route] = pathItem
557
568
 
558
- operation_tags: List[Tag] = []
569
+ operation_tags: list[Tag] = []
559
570
  for cls in endpoint_classes:
560
- doc_string = parse_type(cls)
571
+ doc_string = parse_type(cls) # type: ignore[arg-type]
561
572
  operation_tags.append(
562
573
  Tag(
563
574
  name=cls.__name__,
@@ -570,31 +581,30 @@ class Generator:
570
581
  type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
571
582
 
572
583
  # types that are emitted by events
573
- event_tags: List[Tag] = []
584
+ event_tags: list[Tag] = []
574
585
  events = get_endpoint_events(self.endpoint)
575
586
  for ref, event_type in events.items():
576
587
  event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
577
588
  event_tags.append(self._build_type_tag(ref, event_schema))
578
589
 
579
590
  # types that are explicitly declared
580
- extra_tag_groups: Dict[str, List[Tag]] = {}
591
+ extra_tag_groups: dict[str, list[Tag]] = {}
581
592
  if self.options.extra_types is not None:
582
- if isinstance(self.options.extra_types, list):
583
- extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
584
- elif isinstance(self.options.extra_types, dict):
585
- extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
586
- else:
587
- raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
593
+ match self.options.extra_types:
594
+ case list():
595
+ extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
596
+ case dict():
597
+ extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
588
598
 
589
599
  # list all operations and types
590
- tags: List[Tag] = []
600
+ tags: list[Tag] = []
591
601
  tags.extend(operation_tags)
592
602
  tags.extend(type_tags)
593
603
  tags.extend(event_tags)
594
604
  for extra_tag_group in extra_tag_groups.values():
595
605
  tags.extend(extra_tag_group)
596
606
 
597
- tag_groups = []
607
+ tag_groups: list[TagGroup] = []
598
608
  if operation_tags:
599
609
  tag_groups.append(
600
610
  TagGroup(