python-openapi 0.1.10__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
pyopenapi/__init__.py CHANGED
@@ -1,54 +1,14 @@
1
- from typing import Any, Callable, List, Optional, TypeVar
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
2
3
 
3
- from .metadata import WebMethod
4
- from .options import * # noqa: F403
5
- from .utility import Specification as Specification
4
+ Copyright 2022-2025, Levente Hunyadi
6
5
 
7
- __version__ = "0.1.10"
6
+ :see: https://github.com/hunyadi/pyopenapi
7
+ """
8
8
 
9
- T = TypeVar("T")
10
-
11
-
12
- 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]:
20
- """
21
- Decorator that supplies additional metadata to an endpoint operation function.
22
-
23
- :param route: The URL path pattern associated with this operation which path parameters are substituted into.
24
- :param public: True if the operation can be invoked without prior authentication.
25
- :param request_example: A sample request that the operation might take.
26
- :param response_example: A sample response that the operation might produce.
27
- :param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
28
- :param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
29
- """
30
-
31
- if request_example is not None and request_examples is not None:
32
- raise ValueError("arguments `request_example` and `request_examples` are exclusive")
33
- if response_example is not None and response_examples is not None:
34
- raise ValueError("arguments `response_example` and `response_examples` are exclusive")
35
-
36
- if request_example:
37
- request_examples = [request_example]
38
- if response_example:
39
- response_examples = [response_example]
40
-
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
- )
52
- return cls
53
-
54
- return wrap
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"
@@ -0,0 +1,51 @@
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
10
+
11
+ from .metadata import WebMethod
12
+ from .options import * # noqa: F403
13
+ from .utility import Specification as Specification
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+
18
+ def webmethod(
19
+ route: Optional[str] = None,
20
+ public: Optional[bool] = False,
21
+ request_example: Optional[Any] = None,
22
+ response_example: Optional[Any] = None,
23
+ request_examples: Optional[list[Any]] = None,
24
+ response_examples: Optional[list[Any]] = None,
25
+ ) -> Callable[[F], F]:
26
+ """
27
+ Decorator that supplies additional metadata to an endpoint operation function.
28
+
29
+ :param route: The URL path pattern associated with this operation which path parameters are substituted into.
30
+ :param public: True if the operation can be invoked without prior authentication.
31
+ :param request_example: A sample request that the operation might take.
32
+ :param response_example: A sample response that the operation might produce.
33
+ :param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
34
+ :param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
35
+ """
36
+
37
+ if request_example is not None and request_examples is not None:
38
+ raise ValueError("arguments `request_example` and `request_examples` are exclusive")
39
+ if response_example is not None and response_examples is not None:
40
+ raise ValueError("arguments `response_example` and `response_examples` are exclusive")
41
+
42
+ if request_example:
43
+ request_examples = [request_example]
44
+ if response_example:
45
+ response_examples = [response_example]
46
+
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]
49
+ return cls
50
+
51
+ return wrap
pyopenapi/generator.py CHANGED
@@ -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)
pyopenapi/metadata.py CHANGED
@@ -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
pyopenapi/operations.py CHANGED
@@ -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():
pyopenapi/options.py CHANGED
@@ -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",
pyopenapi/proxy.py CHANGED
@@ -1,19 +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
+
1
9
  import json
2
- from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar
10
+ from typing import Any, Callable, Optional, TypeVar
3
11
 
4
12
  import aiohttp
13
+ from strong_typing.inspection import get_signature
5
14
  from strong_typing.serialization import json_to_object, object_to_json
6
15
 
7
- from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations, get_signature
16
+ from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations
8
17
 
9
18
 
10
19
  async def make_request(
11
20
  http_method: HTTPMethod,
12
21
  server: str,
13
22
  path: str,
14
- query: Dict[str, str],
23
+ query: dict[str, str],
15
24
  data: Optional[str],
16
- ) -> Tuple[int, str]:
25
+ ) -> tuple[int, str]:
17
26
  "Makes an asynchronous HTTP request and returns the response."
18
27
 
19
28
  headers = {"Accept": "application/json"}
@@ -97,7 +106,7 @@ class OperationProxy:
97
106
  try:
98
107
  s = json.loads(response)
99
108
  except json.JSONDecodeError:
100
- raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}")
109
+ raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}") from None
101
110
 
102
111
  return json_to_object(self.op.response_type, s)
103
112
  else:
@@ -118,7 +127,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
118
127
  T = TypeVar("T")
119
128
 
120
129
 
121
- def make_proxy_class(api: Type[T]) -> Type[T]:
130
+ def make_proxy_class(api: type[T]) -> type[T]:
122
131
  """
123
132
  Creates a proxy class for calling an HTTP REST API.
124
133
 
@@ -1,10 +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
  import dataclasses
2
10
  import enum
3
11
  from dataclasses import dataclass
4
- from typing import Any, ClassVar, Dict, List, Optional, Union
12
+ from typing import Any, ClassVar, Optional, Union
5
13
 
6
- from strong_typing.schema import JsonType as JsonType
7
- from strong_typing.schema import Schema, StrictJsonType
14
+ from strong_typing.core import JsonType as JsonType
15
+ from strong_typing.core import Schema, StrictJsonType
8
16
 
9
17
  URL = str
10
18
 
@@ -68,12 +76,12 @@ class Info:
68
76
  class MediaType:
69
77
  schema: Optional[SchemaOrRef] = None
70
78
  example: Optional[Any] = None
71
- examples: Optional[Dict[str, Union["Example", ExampleRef]]] = None
79
+ examples: Optional[dict[str, Union["Example", ExampleRef]]] = None
72
80
 
73
81
 
74
82
  @dataclass
75
83
  class RequestBody:
76
- content: Dict[str, MediaType]
84
+ content: dict[str, MediaType]
77
85
  description: Optional[str] = None
78
86
  required: Optional[bool] = None
79
87
 
@@ -81,7 +89,7 @@ class RequestBody:
81
89
  @dataclass
82
90
  class Response:
83
91
  description: str
84
- content: Optional[Dict[str, MediaType]] = None
92
+ content: Optional[dict[str, MediaType]] = None
85
93
 
86
94
 
87
95
  @enum.unique
@@ -104,15 +112,15 @@ class Parameter:
104
112
 
105
113
  @dataclass
106
114
  class Operation:
107
- responses: Dict[str, Union[Response, ResponseRef]]
108
- tags: Optional[List[str]] = None
115
+ responses: dict[str, Union[Response, ResponseRef]]
116
+ tags: Optional[list[str]] = None
109
117
  summary: Optional[str] = None
110
118
  description: Optional[str] = None
111
119
  operationId: Optional[str] = None
112
- parameters: Optional[List[Parameter]] = None
120
+ parameters: Optional[list[Parameter]] = None
113
121
  requestBody: Optional[RequestBody] = None
114
- callbacks: Optional[Dict[str, "Callback"]] = None
115
- security: Optional[List["SecurityRequirement"]] = None
122
+ callbacks: Optional[dict[str, "Callback"]] = None
123
+ security: Optional[list["SecurityRequirement"]] = None
116
124
 
117
125
 
118
126
  @dataclass
@@ -138,7 +146,7 @@ class PathItem:
138
146
 
139
147
 
140
148
  # maps run-time expressions such as "$request.body#/url" to path items
141
- Callback = Dict[str, PathItem]
149
+ Callback = dict[str, PathItem]
142
150
 
143
151
 
144
152
  @dataclass
@@ -202,17 +210,17 @@ class SecuritySchemeOpenIDConnect(SecurityScheme):
202
210
 
203
211
  @dataclass
204
212
  class Components:
205
- schemas: Optional[Dict[str, Schema]] = None
206
- responses: Optional[Dict[str, Response]] = None
207
- parameters: Optional[Dict[str, Parameter]] = None
208
- examples: Optional[Dict[str, Example]] = None
209
- requestBodies: Optional[Dict[str, RequestBody]] = None
210
- securitySchemes: Optional[Dict[str, SecurityScheme]] = None
211
- callbacks: Optional[Dict[str, Callback]] = None
213
+ schemas: Optional[dict[str, Schema]] = None
214
+ responses: Optional[dict[str, Response]] = None
215
+ parameters: Optional[dict[str, Parameter]] = None
216
+ examples: Optional[dict[str, Example]] = None
217
+ requestBodies: Optional[dict[str, RequestBody]] = None
218
+ securitySchemes: Optional[dict[str, SecurityScheme]] = None
219
+ callbacks: Optional[dict[str, Callback]] = None
212
220
 
213
221
 
214
222
  SecurityScope = str
215
- SecurityRequirement = Dict[str, List[SecurityScope]]
223
+ SecurityRequirement = dict[str, list[SecurityScope]]
216
224
 
217
225
 
218
226
  @dataclass
@@ -231,7 +239,7 @@ class TagGroup:
231
239
  """
232
240
 
233
241
  name: str
234
- tags: List[str]
242
+ tags: list[str]
235
243
 
236
244
 
237
245
  @dataclass
@@ -244,10 +252,10 @@ class Document:
244
252
 
245
253
  openapi: str
246
254
  info: Info
247
- servers: List[Server]
248
- paths: Dict[str, PathItem]
255
+ servers: list[Server]
256
+ paths: dict[str, PathItem]
249
257
  jsonSchemaDialect: Optional[str] = None
250
258
  components: Optional[Components] = None
251
- security: Optional[List[SecurityRequirement]] = None
252
- tags: Optional[List[Tag]] = None
253
- tagGroups: Optional[List[TagGroup]] = None
259
+ security: Optional[list[SecurityRequirement]] = None
260
+ tags: Optional[list[Tag]] = None
261
+ tagGroups: Optional[list[TagGroup]] = None
pyopenapi/utility.py CHANGED
@@ -1,10 +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
  import importlib.resources
2
10
  import json
3
- import sys
4
11
  import typing
5
12
  from typing import TextIO
6
13
 
7
- from strong_typing.schema import StrictJsonType, object_to_json
14
+ from strong_typing.core import StrictJsonType
15
+ from strong_typing.serialization import object_to_json
8
16
 
9
17
  from .generator import Generator
10
18
  from .options import Options
@@ -94,12 +102,8 @@ class Specification:
94
102
  :param pretty_print: Whether to use line indents to beautify the JSON string in the HTML file.
95
103
  """
96
104
 
97
- if sys.version_info >= (3, 9):
98
- with importlib.resources.files(__package__).joinpath("template.html").open(encoding="utf-8", errors="strict") as html_template_file:
99
- html_template = html_template_file.read()
100
- else:
101
- with importlib.resources.open_text(__package__, "template.html", encoding="utf-8", errors="strict") as html_template_file:
102
- html_template = html_template_file.read()
105
+ with importlib.resources.files(__package__).joinpath("template.html").open(encoding="utf-8", errors="strict") as html_template_file:
106
+ html_template = html_template_file.read()
103
107
 
104
108
  html = html_template.replace(
105
109
  "{ /* OPENAPI_SPECIFICATION */ }",
@@ -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
  | ------ | ----------- |
@@ -0,0 +1,18 @@
1
+ pyopenapi/__init__.py,sha256=6XdaFzQkGdBLTQj0lv-7FyuMN8gFpLKctsA1nIW4pSU,345
2
+ pyopenapi/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pyopenapi/decorators.py,sha256=wYZNlZ7diaT5tnr8BsdS7aYpgFTndhfknqbhrU3jR4g,2065
4
+ pyopenapi/generator.py,sha256=HclcfyADHymH3be5w3Xa39MHe0xLsU28kWJhCTLmrG4,23971
5
+ pyopenapi/metadata.py,sha256=-w5C5qiphvnuOkyh9QEUURg7OT-5UffRowjAwmZJkc8,916
6
+ pyopenapi/operations.py,sha256=oprIalF_1XD1-7AjfmJQQeC3xRLdB8oeLlMEhjO2wGc,13529
7
+ pyopenapi/options.py,sha256=S6RWhpZfWrrZBqXzIsPwlVKOkvaI7eyDDUBqGICTlZo,2906
8
+ pyopenapi/proxy.py,sha256=ExDvESQdULTqfnunmfRKtkdjGbcLt5v9yaHd2h9HHlo,4279
9
+ pyopenapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ pyopenapi/specification.py,sha256=ZKQbhl0mhf5ceHan-2ymzt-tba3MhMH-nGsFQ_07XBE,6114
11
+ pyopenapi/template.html,sha256=hmZsSnN3awQcwlN6u-j06qrw_qYzWqYBLC8uPt_x17A,1234
12
+ pyopenapi/utility.py,sha256=Cdub8VUrklyUvZ9cmahDmIYKq4d6X7IT--rMP_ogd_4,3553
13
+ python_openapi-0.2.0.dist-info/licenses/LICENSE,sha256=sNQ9jvMoMB8FfhB7JbkJYLr8SWSO6jrYvcS-mRL485w,1118
14
+ python_openapi-0.2.0.dist-info/METADATA,sha256=eAVEMvh6P1YF-HWODkAXv7p0N5mOFQJ_6mlUP5IRWSg,10499
15
+ python_openapi-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ python_openapi-0.2.0.dist-info/top_level.txt,sha256=3M9FA79QfjyZyTipZP5fDiMcrRdcbSl7lW-iaq5RIRQ,10
17
+ python_openapi-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
18
+ python_openapi-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,17 +0,0 @@
1
- pyopenapi/__init__.py,sha256=4HS5YmQwTzehGw1lUPkZmlKqnL05kAx7aKRq3Q1Lt_4,2020
2
- pyopenapi/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- pyopenapi/generator.py,sha256=DVR4n06TmSghR14_9tLPX6yCX1_Arfbh2pU7UvBD7C8,23807
4
- pyopenapi/metadata.py,sha256=RnAfH79SJVE7-3HWJmdXAm_Kc5L5-4gutyh6xOZc8Fs,766
5
- pyopenapi/operations.py,sha256=b2wiW93chm9b3pOOXrk4wip1DdjLsB7tgzszyBg2RcU,13318
6
- pyopenapi/options.py,sha256=uc-nnyGJeaI3qHWvtDzDz-JBInhETEySF-SplyOr9_w,2795
7
- pyopenapi/proxy.py,sha256=xEO541yqA6ed8jCq01b339_JW1pDSrMN_vxKYk_ZhBg,4096
8
- pyopenapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- pyopenapi/specification.py,sha256=qr9K9DyDIW6B-MkrAS26_op1Qj3eosNyD92XimtLRn4,5974
10
- pyopenapi/template.html,sha256=hmZsSnN3awQcwlN6u-j06qrw_qYzWqYBLC8uPt_x17A,1234
11
- pyopenapi/utility.py,sha256=ZLe1Ss0MGBRD7fL0uSu7nWqUlMIIfAK2supDbN5-uCU,3625
12
- python_openapi-0.1.10.dist-info/LICENSE,sha256=sNQ9jvMoMB8FfhB7JbkJYLr8SWSO6jrYvcS-mRL485w,1118
13
- python_openapi-0.1.10.dist-info/METADATA,sha256=_g7APIZO9ZbYMkYghCx0RoRzlIzhkWt2CU1KD5Gmcr8,10305
14
- python_openapi-0.1.10.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
15
- python_openapi-0.1.10.dist-info/top_level.txt,sha256=3M9FA79QfjyZyTipZP5fDiMcrRdcbSl7lW-iaq5RIRQ,10
16
- python_openapi-0.1.10.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
17
- python_openapi-0.1.10.dist-info/RECORD,,