essentials-openapi 1.2.1__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/CHANGELOG.md +30 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/PKG-INFO +31 -3
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/README.md +28 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/__init__.py +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/commands/docs.py +15 -2
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/common.py +20 -10
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/logs.py +0 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/common.py +26 -9
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/contents.py +11 -9
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/generate.py +10 -3
- essentials_openapi-1.4.0/openapidocs/mk/jinja.py +197 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/md.py +4 -3
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/__init__.py +170 -3
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/examples.py +13 -6
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-responses.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/content-examples.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/external-docs.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/info.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/path-items.html +2 -2
- essentials_openapi-1.4.0/openapidocs/mk/v3/views_markdown/partial/request-parameters.html +9 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-responses.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/schema-repr.html +2 -4
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/type.html +4 -4
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-responses.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/external-docs.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/info.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/path-items.html +2 -2
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-parameters.html +2 -2
- essentials_openapi-1.4.0/openapidocs/mk/v3/views_mkdocs/partial/request-responses.html +42 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/schema-repr.html +2 -4
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/type.html +3 -1
- {essentials_openapi-1.2.1/openapidocs/mk/v3/views_plantuml_schemas → essentials_openapi-1.4.0/openapidocs/mk/v3/views_plantuml_api}/partial/schema-repr.html +1 -1
- {essentials_openapi-1.2.1/openapidocs/mk/v3/views_plantuml_api → essentials_openapi-1.4.0/openapidocs/mk/v3/views_plantuml_schemas}/partial/schema-repr.html +1 -1
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/utils/source.py +9 -4
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/utils/web.py +1 -1
- essentials_openapi-1.4.0/openapidocs/v2.py +362 -0
- essentials_openapi-1.4.0/openapidocs/v3.py +981 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/pyproject.toml +2 -2
- essentials_openapi-1.2.1/openapidocs/mk/jinja.py +0 -112
- essentials_openapi-1.2.1/openapidocs/mk/v3/views_markdown/partial/request-parameters.html +0 -9
- essentials_openapi-1.2.1/openapidocs/mk/v3/views_mkdocs/partial/request-responses.html +0 -63
- essentials_openapi-1.2.1/openapidocs/v2.py +0 -361
- essentials_openapi-1.2.1/openapidocs/v3.py +0 -947
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/.gitignore +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/LICENSE +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/commands/__init__.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/main.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/__init__.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/texts.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/README.md +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/__init__.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/layout.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-parameters.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-schemas.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-security-schemes.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-auth.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-body.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/servers.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/tags.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/README.md +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/__init__.py +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/layout.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-parameters.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-schemas.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-security-schemes.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/content-examples.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-auth.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-body.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/servers.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/tags.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_api/README.md +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_api/layout.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_schemas/README.md +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_schemas/layout.html +0 -0
- {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/utils/__init__.py +0 -0
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.0] - 2026-03-07 :copilot:
|
|
9
|
+
|
|
10
|
+
- Add OAS 3.1 support, cross-version warnings, and fix nullable spacing, by @dcode.
|
|
11
|
+
- Fix MARKDOWN style table separators to use minimum 3 hyphens (issue #39), reported by @michael-nok.
|
|
12
|
+
- Fix [#45](https://github.com/Neoteroi/essentials-openapi/issues/45): add support for displaying descriptions of schema properties, reported by @Maia-Everett.
|
|
13
|
+
- Fix [#30](https://github.com/Neoteroi/essentials-openapi/issues/30): raise an error
|
|
14
|
+
when trying to generate output from an older Swagger v2 specification file (these were
|
|
15
|
+
never supported as there was never a `/mk/v2/` namespace, intentionally).
|
|
16
|
+
- Fix [#35](https://github.com/Neoteroi/essentials-openapi/issues/35): group response
|
|
17
|
+
codes in a tab group on MkDocs output, reported by @Andre601.
|
|
18
|
+
- Fix [#47](https://github.com/Neoteroi/essentials-openapi/issues/47): remove `wordwrap`
|
|
19
|
+
filters from all templates as they break links and mermaid chart code blocks in
|
|
20
|
+
descriptions, reported by @ElementalWarrior.
|
|
21
|
+
- Fix [#49](https://github.com/Neoteroi/essentials-openapi/issues/49): support `$ref`
|
|
22
|
+
values of the form `file.yaml#/fragment/path` (external file with JSON Pointer
|
|
23
|
+
fragment), reported by @mbklein.
|
|
24
|
+
- Fix [#55](https://github.com/Neoteroi/essentials-openapi/issues/55): `jsonSchemaDialect`
|
|
25
|
+
is not required and should not have a default value.
|
|
26
|
+
- Fix [#60](https://github.com/Neoteroi/essentials-openapi/issues/60): resolve `$ref`
|
|
27
|
+
values in response headers pointing to `#/components/headers/...` to avoid
|
|
28
|
+
`UndefinedError` when rendering response tables, reported by @copiousfreetime.
|
|
29
|
+
- Fix [#64](https://github.com/Neoteroi/essentials-openapi/issues/64): use `examples`
|
|
30
|
+
array (JSON Schema draft 6+) as a fallback for auto-generated response examples;
|
|
31
|
+
also use `enum` values as examples for all scalar types (integer, number, boolean),
|
|
32
|
+
reported by @jan-ldwg.
|
|
33
|
+
|
|
34
|
+
## [1.3.0] - 2025-11-19
|
|
35
|
+
|
|
36
|
+
- Add support for passing custom Jinja2 templates as an argument, by @sindrehan.
|
|
37
|
+
|
|
8
38
|
## [1.2.1] - 2025-07-30
|
|
9
39
|
|
|
10
40
|
- Added support for using the current working directory (CWD) as an option when
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: essentials-openapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Classes to generate OpenAPI Documentation v3 and v2, in JSON and YAML.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Neoteroi/essentials-openapi
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/Neoteroi/essentials-openapi/issues
|
|
@@ -11,12 +11,12 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: essentials>=1.1.5
|
|
21
21
|
Requires-Dist: markupsafe>=3.0.0
|
|
22
22
|
Requires-Dist: pyyaml>=6
|
|
@@ -102,6 +102,34 @@ oad gen-docs -s source-openapi.json -d schemas.wsd --style "PLANTUML_API"
|
|
|
102
102
|
|
|
103
103
|
_Example of PlantUML diagram generated from path items._
|
|
104
104
|
|
|
105
|
+
#### Using custom templates
|
|
106
|
+
|
|
107
|
+
You can override the default templates by providing a custom templates directory:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
oad gen-docs -s source-openapi.json -d output.md -T ./my-templates/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The custom templates directory should contain template files with the same names as the built-in templates. Any template file found in the custom directory will override the corresponding default template, while non-overridden templates will use the defaults. This follows the same pattern as [MkDocs template customization](https://www.mkdocs.org/user-guide/customizing-your-theme/#overriding-template-blocks).
|
|
114
|
+
|
|
115
|
+
**Important:** The custom templates directory must match the output style being rendered. Each style (MKDOCS, MARKDOWN, PLANTUML_SCHEMAS, PLANTUML_API) has its own template structure. You need to provide templates appropriate for the `--style` parameter you're using.
|
|
116
|
+
|
|
117
|
+
**Template structure:**
|
|
118
|
+
- `layout.html` - Main layout template
|
|
119
|
+
- `partial/` - Directory containing reusable template components
|
|
120
|
+
|
|
121
|
+
**Example custom template directory structure:**
|
|
122
|
+
```
|
|
123
|
+
my-templates/
|
|
124
|
+
├── layout.html # Overrides main layout
|
|
125
|
+
└── partial/
|
|
126
|
+
├── info.html # Overrides info section
|
|
127
|
+
└── path-items.html # Overrides path items section
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
All templates use [Jinja2](https://jinja.palletsprojects.com/) syntax and have access to the same filters, functions, and context variables as the built-in templates.
|
|
131
|
+
|
|
132
|
+
|
|
105
133
|
### Goals
|
|
106
134
|
|
|
107
135
|
* Provide an API to generate OpenAPI Documentation files.
|
|
@@ -73,6 +73,34 @@ oad gen-docs -s source-openapi.json -d schemas.wsd --style "PLANTUML_API"
|
|
|
73
73
|
|
|
74
74
|
_Example of PlantUML diagram generated from path items._
|
|
75
75
|
|
|
76
|
+
#### Using custom templates
|
|
77
|
+
|
|
78
|
+
You can override the default templates by providing a custom templates directory:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
oad gen-docs -s source-openapi.json -d output.md -T ./my-templates/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The custom templates directory should contain template files with the same names as the built-in templates. Any template file found in the custom directory will override the corresponding default template, while non-overridden templates will use the defaults. This follows the same pattern as [MkDocs template customization](https://www.mkdocs.org/user-guide/customizing-your-theme/#overriding-template-blocks).
|
|
85
|
+
|
|
86
|
+
**Important:** The custom templates directory must match the output style being rendered. Each style (MKDOCS, MARKDOWN, PLANTUML_SCHEMAS, PLANTUML_API) has its own template structure. You need to provide templates appropriate for the `--style` parameter you're using.
|
|
87
|
+
|
|
88
|
+
**Template structure:**
|
|
89
|
+
- `layout.html` - Main layout template
|
|
90
|
+
- `partial/` - Directory containing reusable template components
|
|
91
|
+
|
|
92
|
+
**Example custom template directory structure:**
|
|
93
|
+
```
|
|
94
|
+
my-templates/
|
|
95
|
+
├── layout.html # Overrides main layout
|
|
96
|
+
└── partial/
|
|
97
|
+
├── info.html # Overrides info section
|
|
98
|
+
└── path-items.html # Overrides path items section
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
All templates use [Jinja2](https://jinja.palletsprojects.com/) syntax and have access to the same filters, functions, and context variables as the built-in templates.
|
|
102
|
+
|
|
103
|
+
|
|
76
104
|
### Goals
|
|
77
105
|
|
|
78
106
|
* Provide an API to generate OpenAPI Documentation files.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.4.0"
|
|
2
2
|
VERSION = __version__
|
|
@@ -31,7 +31,20 @@ from openapidocs.mk.jinja import OutputStyle
|
|
|
31
31
|
default="MKDOCS",
|
|
32
32
|
show_default=True,
|
|
33
33
|
)
|
|
34
|
-
|
|
34
|
+
@click.option(
|
|
35
|
+
"-T",
|
|
36
|
+
"--templates",
|
|
37
|
+
help=(
|
|
38
|
+
"Path to a custom templates directory. "
|
|
39
|
+
"Templates in this directory will override default templates with matching names. "
|
|
40
|
+
"Unspecified templates will use defaults."
|
|
41
|
+
),
|
|
42
|
+
required=False,
|
|
43
|
+
default=None,
|
|
44
|
+
)
|
|
45
|
+
def generate_documents_command(
|
|
46
|
+
source: str, destination: str, style: Union[int, str], templates: Union[str, None]
|
|
47
|
+
):
|
|
35
48
|
"""
|
|
36
49
|
Generates other kinds of documents from source OpenAPI Documentation files.
|
|
37
50
|
|
|
@@ -48,7 +61,7 @@ def generate_documents_command(source: str, destination: str, style: Union[int,
|
|
|
48
61
|
https://github.com/Neoteroi/essentials-openapi
|
|
49
62
|
"""
|
|
50
63
|
try:
|
|
51
|
-
generate_document(source, destination, style)
|
|
64
|
+
generate_document(source, destination, style, templates)
|
|
52
65
|
except KeyboardInterrupt: # pragma: nocover
|
|
53
66
|
logger.info("User interrupted")
|
|
54
67
|
exit(1)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import copy
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from dataclasses import asdict, fields, is_dataclass
|
|
4
|
+
from dataclasses import asdict, dataclass, fields, is_dataclass
|
|
5
5
|
from datetime import date, datetime, time
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, Callable, Iterable, cast
|
|
8
8
|
from uuid import UUID
|
|
9
9
|
|
|
10
10
|
import yaml
|
|
@@ -18,10 +18,12 @@ class Format(Enum):
|
|
|
18
18
|
JSON = "JSON"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@dataclass
|
|
21
22
|
class OpenAPIElement:
|
|
22
23
|
"""Base class for all OpenAPI Elements"""
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@dataclass
|
|
25
27
|
class OpenAPIRoot(OpenAPIElement):
|
|
26
28
|
"""Base class for a root OpenAPI Documentation"""
|
|
27
29
|
|
|
@@ -67,8 +69,8 @@ def normalize_key(key: Any) -> str:
|
|
|
67
69
|
return "".join([first.lower(), *map(str.title, others)])
|
|
68
70
|
|
|
69
71
|
|
|
70
|
-
def normalize_dict_factory(items:
|
|
71
|
-
data = {}
|
|
72
|
+
def normalize_dict_factory(items: list[tuple[Any, Any]]) -> dict[str, Any]:
|
|
73
|
+
data: dict[str, Any] = {}
|
|
72
74
|
for key, value in items:
|
|
73
75
|
if value is None:
|
|
74
76
|
continue
|
|
@@ -80,6 +82,10 @@ def normalize_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
|
|
|
80
82
|
data["$ref"] = value
|
|
81
83
|
continue
|
|
82
84
|
|
|
85
|
+
if key == "defs":
|
|
86
|
+
data["$defs"] = value
|
|
87
|
+
continue
|
|
88
|
+
|
|
83
89
|
for handler in TYPES_HANDLERS:
|
|
84
90
|
value = handler.normalize(value)
|
|
85
91
|
|
|
@@ -87,8 +93,8 @@ def normalize_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
|
|
|
87
93
|
return data
|
|
88
94
|
|
|
89
95
|
|
|
90
|
-
def regular_dict_factory(items:
|
|
91
|
-
data = {}
|
|
96
|
+
def regular_dict_factory(items: list[tuple[Any, Any]]) -> dict[Any, Any]:
|
|
97
|
+
data: dict[Any, Any] = {}
|
|
92
98
|
for key, value in items:
|
|
93
99
|
for handler in TYPES_HANDLERS:
|
|
94
100
|
value = handler.normalize(value)
|
|
@@ -100,11 +106,11 @@ def regular_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
|
|
|
100
106
|
# replicates the asdict method from dataclasses module, to support
|
|
101
107
|
# bypassing "asdict" on child properties when they implement a `to_obj`
|
|
102
108
|
# method: some entities require a specific shape when represented
|
|
103
|
-
def _asdict_inner(obj, dict_factory):
|
|
109
|
+
def _asdict_inner(obj: Any, dict_factory: Callable[[Any], Any]) -> Any:
|
|
104
110
|
if hasattr(obj, "to_obj"):
|
|
105
111
|
return obj.to_obj()
|
|
106
112
|
if isinstance(obj, OpenAPIElement):
|
|
107
|
-
result = []
|
|
113
|
+
result: list[tuple[str, Any]] = []
|
|
108
114
|
for f in fields(obj):
|
|
109
115
|
value = _asdict_inner(getattr(obj, f.name), dict_factory)
|
|
110
116
|
result.append((f.name, value))
|
|
@@ -115,20 +121,24 @@ def _asdict_inner(obj, dict_factory):
|
|
|
115
121
|
if hasattr(obj, "dict") and callable(obj.dict):
|
|
116
122
|
# For Pydantic 1
|
|
117
123
|
return obj.dict()
|
|
118
|
-
if is_dataclass(obj):
|
|
124
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
119
125
|
return asdict(obj, dict_factory=regular_dict_factory)
|
|
120
126
|
elif isinstance(obj, (list, tuple)):
|
|
127
|
+
obj = cast(Iterable[Any], obj)
|
|
121
128
|
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
|
|
122
129
|
elif isinstance(obj, dict):
|
|
130
|
+
obj = cast(dict[Any, Any], obj)
|
|
123
131
|
return type(obj)(
|
|
124
132
|
(_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
|
|
125
133
|
for k, v in obj.items()
|
|
126
134
|
)
|
|
127
135
|
else:
|
|
136
|
+
for handler in TYPES_HANDLERS:
|
|
137
|
+
obj = handler.normalize(obj)
|
|
128
138
|
return copy.deepcopy(obj)
|
|
129
139
|
|
|
130
140
|
|
|
131
|
-
def normalize_dict(obj):
|
|
141
|
+
def normalize_dict(obj: Any) -> Any:
|
|
132
142
|
if hasattr(obj, "dict") and callable(obj.dict):
|
|
133
143
|
return obj.dict()
|
|
134
144
|
if hasattr(obj, "to_obj"):
|
|
@@ -5,6 +5,7 @@ from source OAD files.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, cast
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class DocumentsWriter(ABC):
|
|
@@ -14,13 +15,13 @@ class DocumentsWriter(ABC):
|
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
@abstractmethod
|
|
17
|
-
def write(self, data, **kwargs) -> str:
|
|
18
|
+
def write(self, data: object, **kwargs: dict[str, Any]) -> str:
|
|
18
19
|
"""
|
|
19
20
|
Writes markdown.
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def is_reference(data) -> bool:
|
|
24
|
+
def is_reference(data: object) -> bool:
|
|
24
25
|
"""
|
|
25
26
|
Returns a value indicating whether the given dictionary represents
|
|
26
27
|
a reference.
|
|
@@ -32,31 +33,47 @@ def is_reference(data) -> bool:
|
|
|
32
33
|
return "$ref" in data
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
+
def _type_matches(type_val: Any, expected: str) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Returns True if type_val equals expected (OAS 3.0 string) or contains expected
|
|
39
|
+
(OAS 3.1 list).
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(type_val, list):
|
|
42
|
+
return expected in type_val
|
|
43
|
+
return type_val == expected
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_object_schema(data: object) -> bool:
|
|
36
47
|
"""
|
|
37
48
|
Returns a value indicating whether the given schema dictionary represents
|
|
38
49
|
an object schema.
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
Supports both OAS 3.0 (type: "object") and OAS 3.1 (type: ["object", ...]).
|
|
41
52
|
"""
|
|
42
53
|
if not isinstance(data, dict):
|
|
43
54
|
return False
|
|
44
|
-
|
|
55
|
+
data = cast(dict[str, object], data)
|
|
56
|
+
return _type_matches(data.get("type"), "object") and isinstance(
|
|
57
|
+
data.get("properties"), dict
|
|
58
|
+
)
|
|
45
59
|
|
|
46
60
|
|
|
47
|
-
def is_array_schema(data) -> bool:
|
|
61
|
+
def is_array_schema(data: object) -> bool:
|
|
48
62
|
"""
|
|
49
63
|
Returns a value indicating whether the given schema dictionary represents
|
|
50
64
|
an array schema.
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
Supports both OAS 3.0 (type: "array") and OAS 3.1 (type: ["array", ...]).
|
|
53
67
|
"""
|
|
54
68
|
if not isinstance(data, dict):
|
|
55
69
|
return False
|
|
56
|
-
|
|
70
|
+
data = cast(dict[str, object], data)
|
|
71
|
+
return _type_matches(data.get("type"), "array") and isinstance(
|
|
72
|
+
data.get("items"), dict
|
|
73
|
+
)
|
|
57
74
|
|
|
58
75
|
|
|
59
|
-
def get_ref_type_name(reference) -> str:
|
|
76
|
+
def get_ref_type_name(reference: dict[str, str] | str) -> str:
|
|
60
77
|
"""
|
|
61
78
|
Returns the type name of a reference.
|
|
62
79
|
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module contains classes to generate representations of content types by mime type.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from json import JSONEncoder
|
|
9
|
+
from typing import Any, Mapping, Sequence
|
|
8
10
|
from urllib.parse import urlencode
|
|
9
11
|
|
|
10
12
|
from essentials.json import FriendlyEncoder, dumps
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class OADJSONEncoder(JSONEncoder):
|
|
14
|
-
def default(self,
|
|
16
|
+
def default(self, o: object) -> Any:
|
|
15
17
|
try:
|
|
16
|
-
return JSONEncoder.default(self,
|
|
18
|
+
return JSONEncoder.default(self, o)
|
|
17
19
|
except TypeError:
|
|
18
|
-
if isinstance(
|
|
20
|
+
if isinstance(o, datetime):
|
|
19
21
|
datetime_format = os.environ.get("OPENAPI_DATETIME_FORMAT")
|
|
20
22
|
if datetime_format:
|
|
21
|
-
return
|
|
23
|
+
return o.strftime(datetime_format)
|
|
22
24
|
else:
|
|
23
|
-
return
|
|
24
|
-
return FriendlyEncoder.default(self,
|
|
25
|
+
return o.isoformat()
|
|
26
|
+
return FriendlyEncoder.default(self, o) # type: ignore
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class ContentWriter(ABC):
|
|
@@ -37,7 +39,7 @@ class ContentWriter(ABC):
|
|
|
37
39
|
"""
|
|
38
40
|
|
|
39
41
|
@abstractmethod
|
|
40
|
-
def write(self, value) -> str:
|
|
42
|
+
def write(self, value: Any) -> str:
|
|
41
43
|
"""
|
|
42
44
|
Writes markdown to represent a value in a certain type of content.
|
|
43
45
|
"""
|
|
@@ -47,7 +49,7 @@ class JSONContentWriter(ContentWriter):
|
|
|
47
49
|
def handle_content_type(self, content_type: str) -> bool:
|
|
48
50
|
return "json" in content_type.lower()
|
|
49
51
|
|
|
50
|
-
def write(self, value) -> str:
|
|
52
|
+
def write(self, value: object) -> str:
|
|
51
53
|
return dumps(value, indent=4, cls=OADJSONEncoder)
|
|
52
54
|
|
|
53
55
|
|
|
@@ -56,5 +58,5 @@ class FormContentWriter(ContentWriter):
|
|
|
56
58
|
# multipart/form-data. Otherwise, use application/x-www-form-urlencoded.
|
|
57
59
|
return "x-www-form-urlencoded" == content_type.lower()
|
|
58
60
|
|
|
59
|
-
def write(self, value) -> str:
|
|
61
|
+
def write(self, value: Mapping[Any, Any] | Sequence[tuple[Any, Any]]) -> str:
|
|
60
62
|
return urlencode(value)
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from openapidocs.mk.v3 import OpenAPIV3DocumentationHandler
|
|
4
4
|
from openapidocs.utils.source import read_from_source
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def generate_document(
|
|
7
|
+
def generate_document(
|
|
8
|
+
source: str,
|
|
9
|
+
destination: str,
|
|
10
|
+
style: int | str,
|
|
11
|
+
templates_path: Optional[str] = None,
|
|
12
|
+
):
|
|
8
13
|
# Note: if support for more kinds of OAD versions will be added, handle a version
|
|
9
14
|
# parameter in this function
|
|
10
15
|
|
|
11
16
|
data = read_from_source(source)
|
|
12
|
-
handler = OpenAPIV3DocumentationHandler(
|
|
17
|
+
handler = OpenAPIV3DocumentationHandler(
|
|
18
|
+
data, style=style, source=source, templates_path=templates_path
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
html = handler.write()
|
|
15
22
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides a Jinja2 environment.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from jinja2 import (
|
|
11
|
+
ChoiceLoader,
|
|
12
|
+
Environment,
|
|
13
|
+
FileSystemLoader,
|
|
14
|
+
PackageLoader,
|
|
15
|
+
Template,
|
|
16
|
+
select_autoescape,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from . import get_http_status_phrase, highlight_params, read_dict, sort_dict
|
|
20
|
+
from .common import DocumentsWriter, is_reference
|
|
21
|
+
from .md import normalize_link, write_table
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_primary_type(type_val):
|
|
25
|
+
"""
|
|
26
|
+
Returns the primary (first non-null) type from a schema type value.
|
|
27
|
+
|
|
28
|
+
Handles both OAS 3.0 (string) and OAS 3.1 (list) type representations:
|
|
29
|
+
- "string" → "string"
|
|
30
|
+
- ["string", "null"] → "string"
|
|
31
|
+
- ["null"] → "null"
|
|
32
|
+
- ["string", "integer"] → "string"
|
|
33
|
+
"""
|
|
34
|
+
if not type_val:
|
|
35
|
+
return None
|
|
36
|
+
if isinstance(type_val, list):
|
|
37
|
+
non_null = [t for t in type_val if t != "null"]
|
|
38
|
+
return non_null[0] if non_null else "null"
|
|
39
|
+
return type_val
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_nullable_schema(schema) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Returns True if the given schema is nullable.
|
|
45
|
+
|
|
46
|
+
Handles both OAS 3.0 (nullable: true) and OAS 3.1 (type: [..., "null"]) patterns.
|
|
47
|
+
"""
|
|
48
|
+
if not isinstance(schema, dict):
|
|
49
|
+
return False
|
|
50
|
+
if schema.get("nullable"):
|
|
51
|
+
return True
|
|
52
|
+
type_val = schema.get("type")
|
|
53
|
+
if isinstance(type_val, list):
|
|
54
|
+
return "null" in type_val
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_type_display(type_val) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Returns a display string for a schema type value.
|
|
61
|
+
|
|
62
|
+
Handles both OAS 3.0 (string) and OAS 3.1 (list) type representations:
|
|
63
|
+
- "string" → "string"
|
|
64
|
+
- ["string", "null"] → "string | null"
|
|
65
|
+
- ["string", "integer"] → "string | integer"
|
|
66
|
+
"""
|
|
67
|
+
if not type_val:
|
|
68
|
+
return ""
|
|
69
|
+
if isinstance(type_val, list):
|
|
70
|
+
return " | ".join(str(t) for t in type_val)
|
|
71
|
+
return str(type_val)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def configure_filters(env: Environment):
|
|
75
|
+
env.filters.update(
|
|
76
|
+
{"route": highlight_params, "table": write_table, "link": normalize_link}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def configure_functions(env: Environment):
|
|
81
|
+
helpers = {
|
|
82
|
+
"read_dict": read_dict,
|
|
83
|
+
"sort_dict": sort_dict,
|
|
84
|
+
"is_reference": is_reference,
|
|
85
|
+
"scalar_types": {"string", "integer", "boolean", "number"},
|
|
86
|
+
"get_http_status_phrase": get_http_status_phrase,
|
|
87
|
+
"write_md_table": write_table,
|
|
88
|
+
"get_primary_type": get_primary_type,
|
|
89
|
+
"is_nullable_schema": is_nullable_schema,
|
|
90
|
+
"get_type_display": get_type_display,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
env.globals.update(helpers)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OutputStyle(Enum):
|
|
97
|
+
"""
|
|
98
|
+
Output style.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
MKDOCS = 1
|
|
102
|
+
"""Markdown for MkDocs and PyMdown extensions"""
|
|
103
|
+
|
|
104
|
+
MARKDOWN = 2
|
|
105
|
+
"""Basic Markdown"""
|
|
106
|
+
|
|
107
|
+
PLANTUML_SCHEMAS = 100
|
|
108
|
+
"""PlantUML class diagram for components schemas."""
|
|
109
|
+
|
|
110
|
+
PLANTUML_API = 101
|
|
111
|
+
"""PlantUML diagram of the API with request and response bodies."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PackageLoadingError(ValueError):
|
|
115
|
+
def __init__(
|
|
116
|
+
self, style: OutputStyle, templates_folder: str
|
|
117
|
+
) -> None: # pragma: no cover
|
|
118
|
+
super().__init__(
|
|
119
|
+
f"Failed to read the templates for the output style {style.name}. "
|
|
120
|
+
f"Tried to read templates from the folder: {__name__}.{templates_folder}. "
|
|
121
|
+
"This is most probably an issue in `essentials-openapi`."
|
|
122
|
+
)
|
|
123
|
+
self.desired_style = style
|
|
124
|
+
self.attempted_folder = templates_folder
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_environment(
|
|
128
|
+
package_name: str,
|
|
129
|
+
views_style: OutputStyle = OutputStyle.MKDOCS,
|
|
130
|
+
custom_templates_path: Optional[str] = None,
|
|
131
|
+
) -> Environment:
|
|
132
|
+
templates_folder = f"views_{views_style.name}".lower()
|
|
133
|
+
|
|
134
|
+
loaders = []
|
|
135
|
+
|
|
136
|
+
# If custom templates path is provided, validate and add FileSystemLoader first
|
|
137
|
+
if custom_templates_path:
|
|
138
|
+
custom_path = Path(custom_templates_path)
|
|
139
|
+
if not custom_path.exists():
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Custom templates path does not exist: {custom_templates_path}"
|
|
142
|
+
)
|
|
143
|
+
if not custom_path.is_dir():
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Custom templates path is not a directory: {custom_templates_path}"
|
|
146
|
+
)
|
|
147
|
+
loaders.append(FileSystemLoader(str(custom_path)))
|
|
148
|
+
|
|
149
|
+
# Always add the package loader as fallback
|
|
150
|
+
try:
|
|
151
|
+
loaders.append(PackageLoader(package_name, templates_folder))
|
|
152
|
+
except ValueError as package_loading_error: # pragma: no cover
|
|
153
|
+
if not custom_templates_path:
|
|
154
|
+
raise PackageLoadingError(
|
|
155
|
+
views_style, templates_folder
|
|
156
|
+
) from package_loading_error
|
|
157
|
+
|
|
158
|
+
loader = ChoiceLoader(loaders)
|
|
159
|
+
|
|
160
|
+
env = Environment(
|
|
161
|
+
loader=loader,
|
|
162
|
+
autoescape=select_autoescape(["html", "xml"])
|
|
163
|
+
if os.environ.get("SELECT_AUTOESCAPE") in {"YES", "Y", "1"}
|
|
164
|
+
else False,
|
|
165
|
+
auto_reload=True,
|
|
166
|
+
enable_async=False,
|
|
167
|
+
)
|
|
168
|
+
configure_filters(env)
|
|
169
|
+
configure_functions(env)
|
|
170
|
+
|
|
171
|
+
return env
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Jinja2DocumentsWriter(DocumentsWriter):
|
|
175
|
+
"""
|
|
176
|
+
This class uses Jinja2 templating engine to generate other kinds of text output from
|
|
177
|
+
source OpenAPI Documentation data.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
package_name: str,
|
|
183
|
+
views_style: OutputStyle = OutputStyle.MKDOCS,
|
|
184
|
+
custom_templates_path: Optional[str] = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
self._env = get_environment(package_name, views_style, custom_templates_path)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def env(self) -> Environment:
|
|
190
|
+
return self._env
|
|
191
|
+
|
|
192
|
+
def get_template(self) -> Template:
|
|
193
|
+
return self.env.get_template("layout.html")
|
|
194
|
+
|
|
195
|
+
def write(self, data, **kwargs) -> str:
|
|
196
|
+
template = self.get_template()
|
|
197
|
+
return template.render(data, **kwargs)
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
This module provides common functions to handle Markdown.
|
|
3
3
|
These functions apply to any kind of Markdown work.
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from typing import Iterable
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def write_row(
|
|
9
10
|
row: Iterable[str],
|
|
10
|
-
columns_widths:
|
|
11
|
+
columns_widths: dict[int, int],
|
|
11
12
|
padding: int = 1,
|
|
12
13
|
indent: int = 0,
|
|
13
14
|
) -> str:
|
|
@@ -48,7 +49,7 @@ def write_table_lines(
|
|
|
48
49
|
if write_headers:
|
|
49
50
|
# add separator line after headers
|
|
50
51
|
yield write_row(
|
|
51
|
-
["-" * column_len for column_len in columns_widths.values()],
|
|
52
|
+
["-" * max(3, column_len) for column_len in columns_widths.values()],
|
|
52
53
|
columns_widths,
|
|
53
54
|
padding,
|
|
54
55
|
indent,
|