python-openapi 0.1.9__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,58 +1,14 @@
1
- from typing import Any, Callable, Optional, TypeVar
1
+ """
2
+ Generate an OpenAPI specification from a Python class definition
2
3
 
3
- from .metadata import WebMethod
4
- from .options import *
5
- from .utility import Specification
4
+ Copyright 2022-2025, Levente Hunyadi
6
5
 
7
- __version__ = "0.1.9"
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(
33
- "arguments `request_example` and `request_examples` are exclusive"
34
- )
35
- 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
- )
39
-
40
- if request_example:
41
- request_examples = [request_example]
42
- if response_example:
43
- response_examples = [response_example]
44
-
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
- )
56
- return cls
57
-
58
- 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,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(
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,24 +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
- from strong_typing.inspection import (
10
- get_signature,
11
- is_type_enum,
12
- is_type_optional,
13
- unwrap_optional_type,
14
- )
17
+ from strong_typing.inspection import get_signature, is_type_enum, is_type_optional, unwrap_optional_type
15
18
 
16
19
  from .metadata import WebMethod
17
20
 
18
21
 
19
- def split_prefix(
20
- s: str, sep: str, prefix: Union[str, Iterable[str]]
21
- ) -> Tuple[Optional[str], str]:
22
+ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> tuple[Optional[str], str]:
22
23
  """
23
24
  Recognizes a prefix at the beginning of a string.
24
25
 
@@ -41,15 +42,16 @@ def split_prefix(
41
42
  return None, s
42
43
 
43
44
 
44
- def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type:
45
- "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`."
46
47
 
47
48
  if isinstance(annotation, str):
48
- return eval(annotation, callable.__globals__)
49
+ return typing.cast(type, eval(annotation, callable.__globals__))
49
50
  else:
50
51
  return annotation
51
52
 
52
53
 
54
+ @enum.unique
53
55
  class HTTPMethod(enum.Enum):
54
56
  "HTTP method used to invoke an endpoint operation."
55
57
 
@@ -60,7 +62,7 @@ class HTTPMethod(enum.Enum):
60
62
  PATCH = "PATCH"
61
63
 
62
64
 
63
- OperationParameter = Tuple[str, type]
65
+ OperationParameter = tuple[str, type]
64
66
 
65
67
 
66
68
  class ValidationError(TypeError):
@@ -93,15 +95,15 @@ class EndpointOperation:
93
95
  func_name: str
94
96
  func_ref: Callable[..., Any]
95
97
  route: Optional[str]
96
- path_params: List[OperationParameter]
97
- query_params: List[OperationParameter]
98
+ path_params: list[OperationParameter]
99
+ query_params: list[OperationParameter]
98
100
  request_param: Optional[OperationParameter]
99
101
  event_type: Optional[type]
100
102
  response_type: type
101
103
  http_method: HTTPMethod
102
104
  public: bool
103
- request_examples: Optional[List[Any]] = None
104
- response_examples: Optional[List[Any]] = None
105
+ request_examples: Optional[list[Any]] = None
106
+ response_examples: Optional[list[Any]] = None
105
107
 
106
108
  def get_route(self) -> str:
107
109
  if self.route is not None:
@@ -114,9 +116,9 @@ class EndpointOperation:
114
116
 
115
117
 
116
118
  class _FormatParameterExtractor:
117
- "A visitor to exract parameters in a format string."
119
+ "A visitor to extract parameters in a format string."
118
120
 
119
- keys: List[str]
121
+ keys: list[str]
120
122
 
121
123
  def __init__(self) -> None:
122
124
  self.keys = []
@@ -126,15 +128,13 @@ class _FormatParameterExtractor:
126
128
  return None
127
129
 
128
130
 
129
- def _get_route_parameters(route: str) -> List[str]:
131
+ def _get_route_parameters(route: str) -> list[str]:
130
132
  extractor = _FormatParameterExtractor()
131
133
  route.format_map(extractor)
132
134
  return extractor.keys
133
135
 
134
136
 
135
- def _get_endpoint_functions(
136
- endpoint: type, prefixes: List[str]
137
- ) -> Iterator[Tuple[str, str, str, Callable]]:
137
+ def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
138
138
  if not inspect.isclass(endpoint):
139
139
  raise ValidationError(f"object is not a class type: {endpoint}")
140
140
 
@@ -156,14 +156,10 @@ def _get_defining_class(member_fn: str, derived_cls: type) -> type:
156
156
  if name == member_fn:
157
157
  return cls
158
158
 
159
- raise ValidationError(
160
- f"cannot find defining class for {member_fn} in {derived_cls}"
161
- )
159
+ raise ValidationError(f"cannot find defining class for {member_fn} in {derived_cls}")
162
160
 
163
161
 
164
- def get_endpoint_operations(
165
- endpoint: type, use_examples: bool = True
166
- ) -> List[EndpointOperation]:
162
+ def get_endpoint_operations(endpoint: type, use_examples: bool = True) -> list[EndpointOperation]:
167
163
  """
168
164
  Extracts a list of member functions in a class eligible for HTTP interface binding.
169
165
 
@@ -230,36 +226,23 @@ def get_endpoint_operations(
230
226
 
231
227
  # check if all parameters have explicit type
232
228
  if parameter.annotation is inspect.Parameter.empty:
233
- raise ValidationError(
234
- f"parameter '{param_name}' in function '{func_name}' has no type annotation"
235
- )
229
+ raise ValidationError(f"parameter '{param_name}' in function '{func_name}' has no type annotation")
236
230
 
237
231
  if is_type_optional(param_type):
238
232
  inner_type: type = unwrap_optional_type(param_type)
239
233
  else:
240
234
  inner_type = param_type
241
235
 
242
- if (
243
- inner_type is bool
244
- or inner_type is int
245
- or inner_type is float
246
- or inner_type is str
247
- or inner_type is uuid.UUID
248
- or is_type_enum(inner_type)
249
- ):
236
+ if inner_type is bool or inner_type is int or inner_type is float or inner_type is str or inner_type is uuid.UUID or is_type_enum(inner_type):
250
237
  if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
251
238
  if route_params is not None and param_name not in route_params:
252
- raise ValidationError(
253
- f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'"
254
- )
239
+ raise ValidationError(f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'")
255
240
 
256
241
  # simple type maps to route path element, e.g. /study/{uuid}/{version}
257
242
  path_params.append((param_name, param_type))
258
243
  else:
259
244
  if route_params is not None and param_name in route_params:
260
- raise ValidationError(
261
- f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'"
262
- )
245
+ raise ValidationError(f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'")
263
246
 
264
247
  # simple type maps to key=value pair in query string
265
248
  query_params.append((param_name, param_type))
@@ -272,7 +255,8 @@ def get_endpoint_operations(
272
255
  if request_param is not None:
273
256
  param = (param_name, param_type)
274
257
  raise ValidationError(
275
- 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}"
276
260
  )
277
261
 
278
262
  # composite types are read from body
@@ -280,9 +264,7 @@ def get_endpoint_operations(
280
264
 
281
265
  # check if function has explicit return type
282
266
  if signature.return_annotation is inspect.Signature.empty:
283
- raise ValidationError(
284
- f"function '{func_name}' has no return type annotation"
285
- )
267
+ raise ValidationError(f"function '{func_name}' has no return type annotation")
286
268
 
287
269
  return_type = _get_annotation_type(signature.return_annotation, func_ref)
288
270
 
@@ -337,7 +319,7 @@ def get_endpoint_operations(
337
319
  return result
338
320
 
339
321
 
340
- def get_endpoint_events(endpoint: type) -> Dict[str, type]:
322
+ def get_endpoint_events(endpoint: type) -> dict[str, type]:
341
323
  results = {}
342
324
 
343
325
  for decl in typing.get_type_hints(endpoint).values():
pyopenapi/options.py CHANGED
@@ -1,16 +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 (
7
- Info,
8
- SecurityScheme,
9
- SecuritySchemeAPI,
10
- SecuritySchemeHTTP,
11
- SecuritySchemeOpenIDConnect,
12
- Server,
13
- )
14
+ from .specification import Info, SecurityScheme, Server
15
+ from .specification import SecuritySchemeAPI as SecuritySchemeAPI
16
+ from .specification import SecuritySchemeHTTP as SecuritySchemeHTTP
17
+ from .specification import SecuritySchemeOpenIDConnect as SecuritySchemeOpenIDConnect
14
18
 
15
19
  HTTPStatusCode = Union[HTTPStatus, int, str]
16
20
 
@@ -33,21 +37,17 @@ class Options:
33
37
 
34
38
  server: Server
35
39
  info: Info
36
- version: Tuple[int, int, int] = (3, 1, 0)
40
+ version: tuple[int, int, int] = (3, 1, 0)
37
41
  default_security_scheme: Optional[SecurityScheme] = None
38
- extra_types: Union[List[type], Dict[str, List[type]], None] = None
42
+ extra_types: Union[list[type], dict[str, list[type]], None] = None
39
43
  use_examples: bool = True
40
- success_responses: Dict[type, HTTPStatusCode] = dataclasses.field(
41
- default_factory=dict
42
- )
43
- error_responses: Dict[type, HTTPStatusCode] = dataclasses.field(
44
- default_factory=dict
45
- )
44
+ success_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
45
+ error_responses: dict[type, HTTPStatusCode] = dataclasses.field(default_factory=dict)
46
46
  error_wrapper: bool = False
47
47
  property_description_fun: Optional[Callable[[type, str, str], str]] = None
48
- captions: Optional[Dict[str, str]] = None
48
+ captions: Optional[dict[str, str]] = None
49
49
 
50
- default_captions: ClassVar[Dict[str, str]] = {
50
+ default_captions: ClassVar[dict[str, str]] = {
51
51
  "Operations": "Operations",
52
52
  "Types": "Types",
53
53
  "Events": "Events",
pyopenapi/proxy.py CHANGED
@@ -1,24 +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 (
8
- EndpointOperation,
9
- HTTPMethod,
10
- get_endpoint_operations,
11
- get_signature,
12
- )
16
+ from .operations import EndpointOperation, HTTPMethod, get_endpoint_operations
13
17
 
14
18
 
15
19
  async def make_request(
16
20
  http_method: HTTPMethod,
17
21
  server: str,
18
22
  path: str,
19
- query: Dict[str, str],
23
+ query: dict[str, str],
20
24
  data: Optional[str],
21
- ) -> Tuple[int, str]:
25
+ ) -> tuple[int, str]:
22
26
  "Makes an asynchronous HTTP request and returns the response."
23
27
 
24
28
  headers = {"Accept": "application/json"}
@@ -51,9 +55,9 @@ class ProxyInvokeError(RuntimeError):
51
55
  class EndpointProxy:
52
56
  "The HTTP REST proxy class for an endpoint."
53
57
 
54
- url: str
58
+ base_url: str
55
59
 
56
- def __init__(self, base_url: str):
60
+ def __init__(self, base_url: str) -> None:
57
61
  self.base_url = base_url
58
62
 
59
63
 
@@ -65,22 +69,18 @@ class OperationProxy:
65
69
  payload, builds an HTTP request, and processes the HTTP response.
66
70
  """
67
71
 
68
- def __init__(self, op: EndpointOperation):
72
+ def __init__(self, op: EndpointOperation) -> None:
69
73
  self.op = op
70
74
  self.sig = get_signature(op.func_ref)
71
75
 
72
- async def __call__(
73
- self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any
74
- ) -> Any:
76
+ async def __call__(self, endpoint_proxy: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
75
77
  "Invokes an API operation via HTTP REST."
76
78
 
77
79
  ba = self.sig.bind(self, *args, **kwargs)
78
80
 
79
81
  # substitute parameters in URL path
80
82
  route = self.op.get_route()
81
- path = route.format_map(
82
- {name: ba.arguments[name] for name, _type in self.op.path_params}
83
- )
83
+ path = route.format_map({name: ba.arguments[name] for name, _type in self.op.path_params})
84
84
 
85
85
  # gather URL query parameters
86
86
  query = {name: str(ba.arguments[name]) for name, _type in self.op.query_params}
@@ -99,18 +99,14 @@ class OperationProxy:
99
99
  data = None
100
100
 
101
101
  # make HTTP request
102
- status, response = await make_request(
103
- self.op.http_method, endpoint_proxy.base_url, path, query, data
104
- )
102
+ status, response = await make_request(self.op.http_method, endpoint_proxy.base_url, path, query, data)
105
103
 
106
104
  # process HTTP response
107
105
  if response:
108
106
  try:
109
107
  s = json.loads(response)
110
108
  except json.JSONDecodeError:
111
- raise ProxyInvokeError(
112
- f"response body is not well-formed JSON:\n{response}"
113
- )
109
+ raise ProxyInvokeError(f"response body is not well-formed JSON:\n{response}") from None
114
110
 
115
111
  return json_to_object(self.op.response_type, s)
116
112
  else:
@@ -122,9 +118,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
122
118
 
123
119
  operation_proxy = OperationProxy(op)
124
120
 
125
- async def _operation_proxy_fn(
126
- self: EndpointProxy, *args: Any, **kwargs: Any
127
- ) -> Any:
121
+ async def _operation_proxy_fn(self: EndpointProxy, *args: Any, **kwargs: Any) -> Any:
128
122
  return await operation_proxy(self, *args, **kwargs)
129
123
 
130
124
  return _operation_proxy_fn
@@ -133,7 +127,7 @@ def _get_operation_proxy(op: EndpointOperation) -> Callable[..., Any]:
133
127
  T = TypeVar("T")
134
128
 
135
129
 
136
- def make_proxy_class(api: Type[T]) -> Type[T]:
130
+ def make_proxy_class(api: type[T]) -> type[T]:
137
131
  """
138
132
  Creates a proxy class for calling an HTTP REST API.
139
133
 
@@ -1,9 +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, Schema, StrictJsonType
14
+ from strong_typing.core import JsonType as JsonType
15
+ from strong_typing.core import Schema, StrictJsonType
7
16
 
8
17
  URL = str
9
18
 
@@ -67,12 +76,12 @@ class Info:
67
76
  class MediaType:
68
77
  schema: Optional[SchemaOrRef] = None
69
78
  example: Optional[Any] = None
70
- examples: Optional[Dict[str, Union["Example", ExampleRef]]] = None
79
+ examples: Optional[dict[str, Union["Example", ExampleRef]]] = None
71
80
 
72
81
 
73
82
  @dataclass
74
83
  class RequestBody:
75
- content: Dict[str, MediaType]
84
+ content: dict[str, MediaType]
76
85
  description: Optional[str] = None
77
86
  required: Optional[bool] = None
78
87
 
@@ -80,9 +89,10 @@ class RequestBody:
80
89
  @dataclass
81
90
  class Response:
82
91
  description: str
83
- content: Optional[Dict[str, MediaType]] = None
92
+ content: Optional[dict[str, MediaType]] = None
84
93
 
85
94
 
95
+ @enum.unique
86
96
  class ParameterLocation(enum.Enum):
87
97
  Query = "query"
88
98
  Header = "header"
@@ -102,15 +112,15 @@ class Parameter:
102
112
 
103
113
  @dataclass
104
114
  class Operation:
105
- responses: Dict[str, Union[Response, ResponseRef]]
106
- tags: Optional[List[str]] = None
115
+ responses: dict[str, Union[Response, ResponseRef]]
116
+ tags: Optional[list[str]] = None
107
117
  summary: Optional[str] = None
108
118
  description: Optional[str] = None
109
119
  operationId: Optional[str] = None
110
- parameters: Optional[List[Parameter]] = None
120
+ parameters: Optional[list[Parameter]] = None
111
121
  requestBody: Optional[RequestBody] = None
112
- callbacks: Optional[Dict[str, "Callback"]] = None
113
- security: Optional[List["SecurityRequirement"]] = None
122
+ callbacks: Optional[dict[str, "Callback"]] = None
123
+ security: Optional[list["SecurityRequirement"]] = None
114
124
 
115
125
 
116
126
  @dataclass
@@ -136,7 +146,7 @@ class PathItem:
136
146
 
137
147
 
138
148
  # maps run-time expressions such as "$request.body#/url" to path items
139
- Callback = Dict[str, PathItem]
149
+ Callback = dict[str, PathItem]
140
150
 
141
151
 
142
152
  @dataclass
@@ -153,6 +163,7 @@ class Server:
153
163
  description: Optional[str] = None
154
164
 
155
165
 
166
+ @enum.unique
156
167
  class SecuritySchemeType(enum.Enum):
157
168
  ApiKey = "apiKey"
158
169
  HTTP = "http"
@@ -182,9 +193,7 @@ class SecuritySchemeHTTP(SecurityScheme):
182
193
  scheme: str
183
194
  bearerFormat: Optional[str] = None
184
195
 
185
- def __init__(
186
- self, description: str, scheme: str, bearerFormat: Optional[str] = None
187
- ) -> None:
196
+ def __init__(self, description: str, scheme: str, bearerFormat: Optional[str] = None) -> None:
188
197
  super().__init__(SecuritySchemeType.HTTP, description)
189
198
  self.scheme = scheme
190
199
  self.bearerFormat = bearerFormat
@@ -201,17 +210,17 @@ class SecuritySchemeOpenIDConnect(SecurityScheme):
201
210
 
202
211
  @dataclass
203
212
  class Components:
204
- schemas: Optional[Dict[str, Schema]] = None
205
- responses: Optional[Dict[str, Response]] = None
206
- parameters: Optional[Dict[str, Parameter]] = None
207
- examples: Optional[Dict[str, Example]] = None
208
- requestBodies: Optional[Dict[str, RequestBody]] = None
209
- securitySchemes: Optional[Dict[str, SecurityScheme]] = None
210
- 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
211
220
 
212
221
 
213
222
  SecurityScope = str
214
- SecurityRequirement = Dict[str, List[SecurityScope]]
223
+ SecurityRequirement = dict[str, list[SecurityScope]]
215
224
 
216
225
 
217
226
  @dataclass
@@ -230,7 +239,7 @@ class TagGroup:
230
239
  """
231
240
 
232
241
  name: str
233
- tags: List[str]
242
+ tags: list[str]
234
243
 
235
244
 
236
245
  @dataclass
@@ -243,10 +252,10 @@ class Document:
243
252
 
244
253
  openapi: str
245
254
  info: Info
246
- servers: List[Server]
247
- paths: Dict[str, PathItem]
255
+ servers: list[Server]
256
+ paths: dict[str, PathItem]
248
257
  jsonSchemaDialect: Optional[str] = None
249
258
  components: Optional[Components] = None
250
- security: Optional[List[SecurityRequirement]] = None
251
- tags: Optional[List[Tag]] = None
252
- 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
@@ -14,7 +22,7 @@ from .specification import Document
14
22
  class Specification:
15
23
  document: Document
16
24
 
17
- def __init__(self, endpoint: type, options: Options):
25
+ def __init__(self, endpoint: type, options: Options) -> None:
18
26
  generator = Generator(endpoint, options)
19
27
  self.document = generator.generate()
20
28
 
@@ -53,9 +61,7 @@ class Specification:
53
61
 
54
62
  json_doc = self.get_json()
55
63
  if pretty_print:
56
- return json.dumps(
57
- json_doc, check_circular=False, ensure_ascii=False, indent=4
58
- )
64
+ return json.dumps(json_doc, check_circular=False, ensure_ascii=False, indent=4)
59
65
  else:
60
66
  return json.dumps(
61
67
  json_doc,
@@ -96,16 +102,8 @@ class Specification:
96
102
  :param pretty_print: Whether to use line indents to beautify the JSON string in the HTML file.
97
103
  """
98
104
 
99
- if sys.version_info >= (3, 9):
100
- with importlib.resources.files(__package__).joinpath("template.html").open(
101
- encoding="utf-8", errors="strict"
102
- ) as html_template_file:
103
- html_template = html_template_file.read()
104
- else:
105
- with importlib.resources.open_text(
106
- __package__, "template.html", encoding="utf-8", errors="strict"
107
- ) as html_template_file:
108
- 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()
109
107
 
110
108
  html = html_template.replace(
111
109
  "{ /* OPENAPI_SPECIFICATION */ }",
@@ -1,28 +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
24
- Requires-Dist: aiohttp (>=3.8)
25
- Requires-Dist: json-strong-typing (>=0.2.7)
27
+ Requires-Dist: aiohttp>=3.12
28
+ Requires-Dist: json_strong_typing>=0.3.9
29
+ Dynamic: license-file
26
30
 
27
31
  # Generate an OpenAPI specification from a Python class
28
32
 
@@ -33,7 +37,7 @@ Requires-Dist: json-strong-typing (>=0.2.7)
33
37
  * supports standard and asynchronous functions (`async def`)
34
38
  * maps function name prefixes such as `get_` or `create_` to HTTP GET, POST, PUT, DELETE, PATCH
35
39
  * handles both simple and composite types (`int`, `str`, `Enum`, `@dataclass`)
36
- * 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]`)
37
41
  * maps Python positional-only and keyword-only arguments (of simple types) to path and query parameters, respectively
38
42
  * maps composite types to HTTP request body
39
43
  * supports user-defined routes, request and response samples with decorator `@webmethod`
@@ -86,7 +90,7 @@ Let's take a look at the definition of a simple endpoint called `JobManagement`:
86
90
 
87
91
  ```python
88
92
  class JobManagement:
89
- def create_job(self, items: List[URL]) -> uuid.UUID:
93
+ def create_job(self, items: list[URL]) -> uuid.UUID:
90
94
  ...
91
95
 
92
96
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -125,7 +129,7 @@ The custom path must have placeholders for all positional-only parameters in the
125
129
 
126
130
  ### Documenting operations
127
131
 
128
- 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:
129
133
 
130
134
  ```python
131
135
  def get_job(self, job_id: uuid.UUID, /, format: Format) -> Job:
@@ -168,7 +172,7 @@ The Python objects in `request_examples` and `response_examples` are translated
168
172
 
169
173
  ### Mapping function name prefixes to HTTP verbs
170
174
 
171
- 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:
172
176
 
173
177
  | Prefix | HTTP verb |
174
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: bdist_wheel (0.40.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-2022 Levente Hunyadi
3
+ Copyright (c) 2021-2025 Levente Hunyadi
4
4
  Copyright (c) 2021-2022 Instructure Inc.
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,17 +0,0 @@
1
- pyopenapi/__init__.py,sha256=X8PJxd3kz3KOgekDQahA8iqrM1s232maTDsTu4g-Dmk,2026
2
- pyopenapi/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- pyopenapi/generator.py,sha256=Abr7kzW2E1a7CowVQeWUvo-wc1FyilhWj83CIHaWaqo,24134
4
- pyopenapi/metadata.py,sha256=RnAfH79SJVE7-3HWJmdXAm_Kc5L5-4gutyh6xOZc8Fs,766
5
- pyopenapi/operations.py,sha256=jJqmQOtKDGrQ678TbjwOH_3TuCdFXa5VF8EHr57UUK0,13646
6
- pyopenapi/options.py,sha256=Su7UIjqxLgenmsj5QDbT8x5fkJyL9K-Q_dhyxUnMJDM,2674
7
- pyopenapi/proxy.py,sha256=EciA8pl3pNBLAfwuzXfaUelW3UMKfp95ftqTTYvjvz0,4206
8
- pyopenapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- pyopenapi/specification.py,sha256=wizvkOzma_ygFIe8uxwB2Iu0MyTiUPwMZxYO6lzUqMM,5918
10
- pyopenapi/template.html,sha256=hmZsSnN3awQcwlN6u-j06qrw_qYzWqYBLC8uPt_x17A,1234
11
- pyopenapi/utility.py,sha256=ivNI7rUcQdnmQ9Cd7Bae5S2m-pMIfB1ePTxA87g12L8,3707
12
- python_openapi-0.1.9.dist-info/LICENSE,sha256=YR7xSxwfVEogZT0M3cQpVXTfAm5MT5dQ6WWeDesiTxo,1118
13
- python_openapi-0.1.9.dist-info/METADATA,sha256=Hfra860obzYkBYQbTBh1jMC_hyD6sLnCeNyj6rU7Q6g,10207
14
- python_openapi-0.1.9.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
- python_openapi-0.1.9.dist-info/top_level.txt,sha256=3M9FA79QfjyZyTipZP5fDiMcrRdcbSl7lW-iaq5RIRQ,10
16
- python_openapi-0.1.9.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
17
- python_openapi-0.1.9.dist-info/RECORD,,