python-openapi 0.1.9__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.1.9 → python_openapi-0.2.0}/LICENSE +1 -1
  2. python_openapi-0.2.0/MANIFEST.in +2 -0
  3. {python-openapi-0.1.9 → python_openapi-0.2.0}/PKG-INFO +19 -13
  4. {python-openapi-0.1.9 → python_openapi-0.2.0}/README.md +4 -4
  5. python_openapi-0.2.0/pyopenapi/__init__.py +14 -0
  6. python-openapi-0.1.9/pyopenapi/__init__.py → python_openapi-0.2.0/pyopenapi/decorators.py +18 -25
  7. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/generator.py +100 -150
  8. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/metadata.py +11 -3
  9. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/operations.py +34 -52
  10. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/options.py +19 -19
  11. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/proxy.py +22 -28
  12. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/specification.py +37 -28
  13. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/utility.py +14 -16
  14. python_openapi-0.2.0/pyproject.toml +58 -0
  15. {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/PKG-INFO +19 -13
  16. {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/SOURCES.txt +5 -1
  17. python_openapi-0.2.0/python_openapi.egg-info/requires.txt +2 -0
  18. python_openapi-0.2.0/setup.cfg +4 -0
  19. python_openapi-0.2.0/tests/__init__.py +0 -0
  20. python_openapi-0.2.0/tests/endpoint.md +5 -0
  21. python_openapi-0.2.0/tests/endpoint.py +389 -0
  22. {python-openapi-0.1.9 → python_openapi-0.2.0}/tests/test_openapi.py +8 -15
  23. {python-openapi-0.1.9 → python_openapi-0.2.0}/tests/test_proxy.py +16 -17
  24. python-openapi-0.1.9/pyproject.toml +0 -3
  25. python-openapi-0.1.9/python_openapi.egg-info/requires.txt +0 -2
  26. python-openapi-0.1.9/setup.cfg +0 -46
  27. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/__main__.py +0 -0
  28. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/py.typed +0 -0
  29. {python-openapi-0.1.9 → python_openapi-0.2.0}/pyopenapi/template.html +0 -0
  30. {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/dependency_links.txt +0 -0
  31. {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/top_level.txt +0 -0
  32. {python-openapi-0.1.9 → python_openapi-0.2.0}/python_openapi.egg-info/zip-safe +0 -0
  33. {python-openapi-0.1.9 → python_openapi-0.2.0}/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
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ recursive-include tests *.md
@@ -1,26 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-openapi
3
- Version: 0.1.9
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
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3 :: Only
18
21
  Classifier: Topic :: Software Development :: Code Generators
19
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
23
  Classifier: Typing :: Typed
21
- Requires-Python: >=3.8
24
+ Requires-Python: >=3.9
22
25
  Description-Content-Type: text/markdown
23
26
  License-File: LICENSE
27
+ Requires-Dist: aiohttp>=3.12
28
+ Requires-Dist: json_strong_typing>=0.3.9
29
+ Dynamic: license-file
24
30
 
25
31
  # Generate an OpenAPI specification from a Python class
26
32
 
@@ -31,7 +37,7 @@ License-File: LICENSE
31
37
  * supports standard and asynchronous functions (`async def`)
32
38
  * maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
33
39
  * handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
34
- * 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]`)
35
41
  * maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
36
42
  * maps composite types to HTTP request body
37
43
  * supports user-defined routes, request and response samples with decorator `@webmethod`
@@ -84,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
84
90
 
85
91
  ```python
86
92
  class JobManagement:
87
- def create_job(self, items: List[URL]) -> uuid.UUID:
93
+ def create_job(self, items: list[URL]) -> uuid.UUID:
88
94
  ...
89
95
 
90
96
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -123,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
123
129
 
124
130
  ### Documenting operations
125
131
 
126
- 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:
127
133
 
128
134
  ```python
129
135
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -166,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
166
172
 
167
173
  ### Mapping function name prefixes to HTTP verbs
168
174
 
169
- 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:
170
176
 
171
177
  | Prefix | HTTP verb |
172
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
+ """
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 typing import Any, Callable, Optional, TypeVar
2
10
 
3
11
  from .metadata import WebMethod
4
- from .options import *
5
- from .utility import Specification
6
-
7
- __version__ = "0.1.9"
12
+ from .options import * # noqa: F403
13
+ from .utility import Specification as Specification
8
14
 
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
 
@@ -29,30 +35,17 @@ def webmethod(
29
35
  """
30
36
 
31
37
  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
- )
38
+ raise ValueError("arguments `request_example` and `request_examples` are exclusive")
35
39
  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
- )
40
+ raise ValueError("arguments `response_example` and `response_examples` are exclusive")
39
41
 
40
42
  if request_example:
41
43
  request_examples = [request_example]
42
44
  if response_example:
43
45
  response_examples = [response_example]
44
46
 
45
- def wrap(cls: T) -> T:
46
- setattr(
47
- cls,
48
- "__webmethod__",
49
- WebMethod(
50
- route=route,
51
- public=public or False,
52
- request_examples=request_examples,
53
- response_examples=response_examples,
54
- ),
55
- )
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]
56
49
  return cls
57
50
 
58
51
  return wrap
@@ -1,35 +1,28 @@
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
+
9
+ import dataclasses
1
10
  import hashlib
2
11
  import ipaddress
3
12
  import typing
4
- from typing import Any, Dict, Set, Union
13
+ from dataclasses import dataclass
14
+ from http import HTTPStatus
15
+ from typing import Any, Callable, Optional, Union
5
16
 
6
- from strong_typing.core import JsonType
17
+ from strong_typing.core import JsonType, Schema
7
18
  from strong_typing.docstring import Docstring, parse_type
8
- from strong_typing.inspection import (
9
- is_generic_list,
10
- is_type_optional,
11
- is_type_union,
12
- unwrap_generic_list,
13
- unwrap_optional_type,
14
- unwrap_union_types,
15
- )
19
+ from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
16
20
  from strong_typing.name import python_type_to_name
17
- from strong_typing.schema import (
18
- JsonSchemaGenerator,
19
- Schema,
20
- SchemaOptions,
21
- get_schema_identifier,
22
- register_schema,
23
- )
21
+ from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
24
22
  from strong_typing.serialization import json_dump_string, object_to_json
25
23
 
26
- from .operations import (
27
- EndpointOperation,
28
- HTTPMethod,
29
- get_endpoint_events,
30
- get_endpoint_operations,
31
- )
32
- from .options import *
24
+ from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
25
+ from .options import HTTPStatusCode, Options
33
26
  from .specification import (
34
27
  Components,
35
28
  Document,
@@ -93,7 +86,7 @@ def http_status_to_string(status_code: HTTPStatusCode) -> str:
93
86
 
94
87
  class SchemaBuilder:
95
88
  schema_generator: JsonSchemaGenerator
96
- schemas: Dict[str, Schema]
89
+ schemas: dict[str, Schema]
97
90
 
98
91
  def __init__(self, schema_generator: JsonSchemaGenerator) -> None:
99
92
  self.schema_generator = schema_generator
@@ -165,9 +158,7 @@ class ContentBuilder:
165
158
  self.schema_transformer = schema_transformer
166
159
  self.sample_transformer = sample_transformer
167
160
 
168
- def build_content(
169
- self, payload_type: type, examples: Optional[List[Any]] = None
170
- ) -> Dict[str, MediaType]:
161
+ def build_content(self, payload_type: type, examples: Optional[list[Any]] = None) -> dict[str, MediaType]:
171
162
  "Creates the content subtree for a request or response."
172
163
 
173
164
  if is_generic_list(payload_type):
@@ -179,12 +170,10 @@ class ContentBuilder:
179
170
 
180
171
  return {media_type: self.build_media_type(item_type, examples)}
181
172
 
182
- def build_media_type(
183
- self, item_type: type, examples: Optional[List[Any]] = None
184
- ) -> MediaType:
173
+ def build_media_type(self, item_type: type, examples: Optional[list[Any]] = None) -> MediaType:
185
174
  schema = self.schema_builder.classdef_to_ref(item_type)
186
175
  if self.schema_transformer:
187
- schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer # type: ignore
176
+ schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer
188
177
  schema = schema_transformer(schema)
189
178
 
190
179
  if not examples:
@@ -198,25 +187,14 @@ class ContentBuilder:
198
187
  examples=self._build_examples(examples),
199
188
  )
200
189
 
201
- def _build_examples(
202
- self, examples: List[Any]
203
- ) -> Dict[str, Union[Example, ExampleRef]]:
190
+ def _build_examples(self, examples: list[Any]) -> dict[str, Union[Example, ExampleRef]]:
204
191
  "Creates a set of several examples for a media type."
205
192
 
206
- if self.sample_transformer:
207
- sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore
208
- else:
209
- sample_transformer = lambda sample: sample
193
+ builder = ExampleBuilder(self.sample_transformer)
210
194
 
211
- results: Dict[str, Union[Example, ExampleRef]] = {}
195
+ results: dict[str, Union[Example, ExampleRef]] = {}
212
196
  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
-
197
+ name, value = builder.get_named(example)
220
198
  results[name] = Example(value=value)
221
199
 
222
200
  return results
@@ -224,12 +202,43 @@ class ContentBuilder:
224
202
  def _build_example(self, example: Any) -> Any:
225
203
  "Creates a single example for a media type."
226
204
 
227
- if self.sample_transformer:
228
- sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore
205
+ builder = ExampleBuilder(self.sample_transformer)
206
+ return builder.get_anonymous(example)
207
+
208
+
209
+ class ExampleBuilder:
210
+ sample_transformer: Callable[[JsonType], JsonType]
211
+
212
+ def __init__(
213
+ self,
214
+ sample_transformer: Optional[Callable[[JsonType], JsonType]] = None,
215
+ ) -> None:
216
+ if sample_transformer:
217
+ self.sample_transformer = sample_transformer
229
218
  else:
230
- sample_transformer = lambda sample: sample
219
+ self.sample_transformer = lambda sample: sample
220
+
221
+ def _get_value(self, example: Any) -> JsonType:
222
+ return self.sample_transformer(object_to_json(example))
223
+
224
+ def get_anonymous(self, example: Any) -> JsonType:
225
+ return self._get_value(example)
226
+
227
+ def get_named(self, example: Any) -> tuple[str, JsonType]:
228
+ value = self._get_value(example)
231
229
 
232
- return sample_transformer(object_to_json(example))
230
+ name: Optional[str] = None
231
+
232
+ if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
233
+ friendly_name = str(example)
234
+ if friendly_name.isprintable():
235
+ name = friendly_name
236
+
237
+ if name is None:
238
+ hash_string = hashlib.md5(json_dump_string(value).encode("utf-8")).digest().hex()
239
+ name = f"ex-{hash_string}"
240
+
241
+ return name, value
233
242
 
234
243
 
235
244
  @dataclass
@@ -243,17 +252,17 @@ class ResponseOptions:
243
252
  :param default_status_code: HTTP status code assigned to responses that have no mapping.
244
253
  """
245
254
 
246
- type_descriptions: Dict[type, str]
247
- examples: Optional[List[Any]]
248
- status_catalog: Dict[type, HTTPStatusCode]
255
+ type_descriptions: dict[type, str]
256
+ examples: Optional[list[Any]]
257
+ status_catalog: dict[type, HTTPStatusCode]
249
258
  default_status_code: HTTPStatusCode
250
259
 
251
260
 
252
261
  @dataclass
253
262
  class StatusResponse:
254
263
  status_code: str
255
- types: List[type] = dataclasses.field(default_factory=list)
256
- 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)
257
266
 
258
267
 
259
268
  class ResponseBuilder:
@@ -262,15 +271,11 @@ class ResponseBuilder:
262
271
  def __init__(self, content_builder: ContentBuilder) -> None:
263
272
  self.content_builder = content_builder
264
273
 
265
- def _get_status_responses(
266
- self, options: ResponseOptions
267
- ) -> Dict[str, StatusResponse]:
268
- status_responses: Dict[str, StatusResponse] = {}
274
+ def _get_status_responses(self, options: ResponseOptions) -> dict[str, StatusResponse]:
275
+ status_responses: dict[str, StatusResponse] = {}
269
276
 
270
277
  for response_type in options.type_descriptions.keys():
271
- status_code = http_status_to_string(
272
- options.status_catalog.get(response_type, options.default_status_code)
273
- )
278
+ status_code = http_status_to_string(options.status_catalog.get(response_type, options.default_status_code))
274
279
 
275
280
  # look up response for status code
276
281
  if status_code not in status_responses:
@@ -282,22 +287,16 @@ class ResponseBuilder:
282
287
 
283
288
  # append examples that have the matching response type
284
289
  if options.examples:
285
- status_response.examples.extend(
286
- example
287
- for example in options.examples
288
- if isinstance(example, response_type)
289
- )
290
+ status_response.examples.extend(example for example in options.examples if isinstance(example, response_type))
290
291
 
291
292
  return dict(sorted(status_responses.items()))
292
293
 
293
- def build_response(
294
- self, options: ResponseOptions
295
- ) -> Dict[str, Union[Response, ResponseRef]]:
294
+ def build_response(self, options: ResponseOptions) -> dict[str, Union[Response, ResponseRef]]:
296
295
  """
297
296
  Groups responses that have the same status code.
298
297
  """
299
298
 
300
- responses: Dict[str, Union[Response, ResponseRef]] = {}
299
+ responses: dict[str, Union[Response, ResponseRef]] = {}
301
300
  status_responses = self._get_status_responses(options)
302
301
  for status_code, status_response in status_responses.items():
303
302
  response_types = tuple(status_response.types)
@@ -310,10 +309,7 @@ class ResponseBuilder:
310
309
  description = " **OR** ".join(
311
310
  filter(
312
311
  None,
313
- (
314
- options.type_descriptions[response_type]
315
- for response_type in response_types
316
- ),
312
+ (options.type_descriptions[response_type] for response_type in response_types),
317
313
  )
318
314
  )
319
315
 
@@ -327,9 +323,9 @@ class ResponseBuilder:
327
323
 
328
324
  def _build_response(
329
325
  self,
330
- response_type: type,
326
+ response_type: Optional[type],
331
327
  description: str,
332
- examples: Optional[List[Any]] = None,
328
+ examples: Optional[list[Any]] = None,
333
329
  ) -> Response:
334
330
  "Creates a response subtree."
335
331
 
@@ -367,7 +363,7 @@ class Generator:
367
363
  endpoint: type
368
364
  options: Options
369
365
  schema_builder: SchemaBuilder
370
- responses: Dict[str, Response]
366
+ responses: dict[str, Response]
371
367
 
372
368
  def __init__(self, endpoint: type, options: Options) -> None:
373
369
  self.endpoint = endpoint
@@ -388,24 +384,20 @@ class Generator:
388
384
  description = typing.cast(str, schema.get("description"))
389
385
  return Tag(
390
386
  name=ref,
391
- description="\n\n".join(
392
- s for s in (title, description, definition) if s is not None
393
- ),
387
+ description="\n\n".join(s for s in (title, description, definition) if s is not None),
394
388
  )
395
389
 
396
- def _build_extra_tag_groups(
397
- self, extra_types: Dict[str, List[type]]
398
- ) -> Dict[str, List[Tag]]:
390
+ def _build_extra_tag_groups(self, extra_types: dict[str, list[type]]) -> dict[str, list[Tag]]:
399
391
  """
400
392
  Creates a dictionary of tag group captions as keys, and tag lists as values.
401
393
 
402
394
  :param extra_types: A dictionary of type categories and list of types in that category.
403
395
  """
404
396
 
405
- extra_tags: Dict[str, List[Tag]] = {}
397
+ extra_tags: dict[str, list[Tag]] = {}
406
398
 
407
399
  for category_name, category_items in extra_types.items():
408
- tag_list: List[Tag] = []
400
+ tag_list: list[Tag] = []
409
401
 
410
402
  for extra_type in category_items:
411
403
  name = python_type_to_name(extra_type)
@@ -419,9 +411,7 @@ class Generator:
419
411
 
420
412
  def _build_operation(self, op: EndpointOperation) -> Operation:
421
413
  doc_string = parse_type(op.func_ref)
422
- doc_params = dict(
423
- (param.name, param.description) for param in doc_string.params.values()
424
- )
414
+ doc_params = dict((param.name, param.description) for param in doc_string.params.values())
425
415
 
426
416
  # parameters passed in URL component path
427
417
  path_parameters = [
@@ -462,11 +452,7 @@ class Generator:
462
452
  builder = ContentBuilder(self.schema_builder)
463
453
  request_name, request_type = op.request_param
464
454
  requestBody = RequestBody(
465
- content={
466
- "application/json": builder.build_media_type(
467
- request_type, op.request_examples
468
- )
469
- },
455
+ content={"application/json": builder.build_media_type(request_type, op.request_examples)},
470
456
  description=doc_params.get(request_name),
471
457
  required=True,
472
458
  )
@@ -476,29 +462,16 @@ class Generator:
476
462
  # success response types
477
463
  if doc_string.returns is None and is_type_union(op.response_type):
478
464
  # split union of return types into a list of response types
479
- success_type_docstring: Dict[type, Docstring] = {
480
- typing.cast(type, item): parse_type(item)
481
- for item in unwrap_union_types(op.response_type)
482
- }
465
+ success_type_docstring: dict[type, Docstring] = {typing.cast(type, item): parse_type(item) for item in unwrap_union_types(op.response_type)}
483
466
  success_type_descriptions = {
484
- item: doc_string.short_description
485
- for item, doc_string in success_type_docstring.items()
486
- if doc_string.short_description
467
+ item: doc_string.short_description for item, doc_string in success_type_docstring.items() if doc_string.short_description
487
468
  }
488
469
  else:
489
470
  # use return type as a single response type
490
- success_type_descriptions = {
491
- op.response_type: (
492
- doc_string.returns.description if doc_string.returns else "OK"
493
- )
494
- }
471
+ success_type_descriptions = {op.response_type: (doc_string.returns.description if doc_string.returns else "OK")}
495
472
 
496
473
  response_examples = op.response_examples or []
497
- success_examples = [
498
- example
499
- for example in response_examples
500
- if not isinstance(example, Exception)
501
- ]
474
+ success_examples = [example for example in response_examples if not isinstance(example, Exception)]
502
475
 
503
476
  content_builder = ContentBuilder(self.schema_builder)
504
477
  response_builder = ResponseBuilder(content_builder)
@@ -512,14 +485,8 @@ class Generator:
512
485
 
513
486
  # failure response types
514
487
  if doc_string.raises:
515
- exception_types: 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
- ]
488
+ exception_types: dict[type, str] = {item.raise_type: item.description for item in doc_string.raises.values()}
489
+ exception_examples = [example for example in response_examples if isinstance(example, Exception)]
523
490
 
524
491
  if self.options.error_wrapper:
525
492
  schema_transformer = schema_error_wrapper
@@ -548,9 +515,7 @@ class Generator:
548
515
  f"{op.func_name}_callback": {
549
516
  "{$request.query.callback}": PathItem(
550
517
  post=Operation(
551
- requestBody=RequestBody(
552
- content=builder.build_content(op.event_type)
553
- ),
518
+ requestBody=RequestBody(content=builder.build_content(op.event_type)),
554
519
  responses={"200": Response(description="OK")},
555
520
  )
556
521
  )
@@ -572,11 +537,9 @@ class Generator:
572
537
  )
573
538
 
574
539
  def generate(self) -> Document:
575
- paths: Dict[str, PathItem] = {}
576
- endpoint_classes: Set[type] = set()
577
- for op in get_endpoint_operations(
578
- self.endpoint, use_examples=self.options.use_examples
579
- ):
540
+ paths: dict[str, PathItem] = {}
541
+ endpoint_classes: set[type] = set()
542
+ for op in get_endpoint_operations(self.endpoint, use_examples=self.options.use_examples):
580
543
  endpoint_classes.add(op.defining_class)
581
544
 
582
545
  operation = self._build_operation(op)
@@ -600,7 +563,7 @@ class Generator:
600
563
  else:
601
564
  paths[route] = pathItem
602
565
 
603
- operation_tags: List[Tag] = []
566
+ operation_tags: list[Tag] = []
604
567
  for cls in endpoint_classes:
605
568
  doc_string = parse_type(cls)
606
569
  operation_tags.append(
@@ -612,36 +575,27 @@ class Generator:
612
575
  )
613
576
 
614
577
  # types that are produced/consumed by operations
615
- type_tags = [
616
- self._build_type_tag(ref, schema)
617
- for ref, schema in self.schema_builder.schemas.items()
618
- ]
578
+ type_tags = [self._build_type_tag(ref, schema) for ref, schema in self.schema_builder.schemas.items()]
619
579
 
620
580
  # types that are emitted by events
621
- event_tags: List[Tag] = []
581
+ event_tags: list[Tag] = []
622
582
  events = get_endpoint_events(self.endpoint)
623
583
  for ref, event_type in events.items():
624
584
  event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type)
625
585
  event_tags.append(self._build_type_tag(ref, event_schema))
626
586
 
627
587
  # types that are explicitly declared
628
- extra_tag_groups: Dict[str, List[Tag]] = {}
588
+ extra_tag_groups: dict[str, list[Tag]] = {}
629
589
  if self.options.extra_types is not None:
630
590
  if isinstance(self.options.extra_types, list):
631
- extra_tag_groups = self._build_extra_tag_groups(
632
- {"AdditionalTypes": self.options.extra_types}
633
- )
591
+ extra_tag_groups = self._build_extra_tag_groups({"AdditionalTypes": self.options.extra_types})
634
592
  elif isinstance(self.options.extra_types, dict):
635
- extra_tag_groups = self._build_extra_tag_groups(
636
- self.options.extra_types
637
- )
593
+ extra_tag_groups = self._build_extra_tag_groups(self.options.extra_types)
638
594
  else:
639
- raise TypeError(
640
- f"type mismatch for collection of extra types: {type(self.options.extra_types)}"
641
- )
595
+ raise TypeError(f"type mismatch for collection of extra types: {type(self.options.extra_types)}")
642
596
 
643
597
  # list all operations and types
644
- tags: List[Tag] = []
598
+ tags: list[Tag] = []
645
599
  tags.extend(operation_tags)
646
600
  tags.extend(type_tags)
647
601
  tags.extend(event_tags)
@@ -686,11 +640,7 @@ class Generator:
686
640
  return Document(
687
641
  openapi=".".join(str(item) for item in self.options.version),
688
642
  info=self.options.info,
689
- jsonSchemaDialect=(
690
- "https://json-schema.org/draft/2020-12/schema"
691
- if self.options.version >= (3, 1, 0)
692
- else None
693
- ),
643
+ jsonSchemaDialect=("https://json-schema.org/draft/2020-12/schema" if self.options.version >= (3, 1, 0) else None),
694
644
  servers=[self.options.server],
695
645
  paths=paths,
696
646
  components=Components(