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.
Files changed (75) hide show
  1. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/CHANGELOG.md +30 -0
  2. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/PKG-INFO +31 -3
  3. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/README.md +28 -0
  4. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/__init__.py +1 -1
  5. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/commands/docs.py +15 -2
  6. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/common.py +20 -10
  7. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/logs.py +0 -1
  8. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/common.py +26 -9
  9. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/contents.py +11 -9
  10. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/generate.py +10 -3
  11. essentials_openapi-1.4.0/openapidocs/mk/jinja.py +197 -0
  12. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/md.py +4 -3
  13. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/__init__.py +170 -3
  14. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/examples.py +13 -6
  15. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-responses.html +1 -1
  16. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/content-examples.html +1 -1
  17. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/external-docs.html +1 -1
  18. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/info.html +1 -1
  19. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/path-items.html +2 -2
  20. essentials_openapi-1.4.0/openapidocs/mk/v3/views_markdown/partial/request-parameters.html +9 -0
  21. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-responses.html +1 -1
  22. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/schema-repr.html +2 -4
  23. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/type.html +4 -4
  24. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-responses.html +1 -1
  25. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/external-docs.html +1 -1
  26. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/info.html +1 -1
  27. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/path-items.html +2 -2
  28. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-parameters.html +2 -2
  29. essentials_openapi-1.4.0/openapidocs/mk/v3/views_mkdocs/partial/request-responses.html +42 -0
  30. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/schema-repr.html +2 -4
  31. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/type.html +3 -1
  32. {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
  33. {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
  34. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/utils/source.py +9 -4
  35. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/utils/web.py +1 -1
  36. essentials_openapi-1.4.0/openapidocs/v2.py +362 -0
  37. essentials_openapi-1.4.0/openapidocs/v3.py +981 -0
  38. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/pyproject.toml +2 -2
  39. essentials_openapi-1.2.1/openapidocs/mk/jinja.py +0 -112
  40. essentials_openapi-1.2.1/openapidocs/mk/v3/views_markdown/partial/request-parameters.html +0 -9
  41. essentials_openapi-1.2.1/openapidocs/mk/v3/views_mkdocs/partial/request-responses.html +0 -63
  42. essentials_openapi-1.2.1/openapidocs/v2.py +0 -361
  43. essentials_openapi-1.2.1/openapidocs/v3.py +0 -947
  44. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/.gitignore +0 -0
  45. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/LICENSE +0 -0
  46. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/commands/__init__.py +0 -0
  47. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/main.py +0 -0
  48. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/__init__.py +0 -0
  49. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/texts.py +0 -0
  50. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/README.md +0 -0
  51. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/__init__.py +0 -0
  52. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/layout.html +0 -0
  53. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-parameters.html +0 -0
  54. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-schemas.html +0 -0
  55. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/components-security-schemes.html +0 -0
  56. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-auth.html +0 -0
  57. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/request-body.html +0 -0
  58. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/servers.html +0 -0
  59. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_markdown/partial/tags.html +0 -0
  60. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/README.md +0 -0
  61. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/__init__.py +0 -0
  62. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/layout.html +0 -0
  63. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-parameters.html +0 -0
  64. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-schemas.html +0 -0
  65. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/components-security-schemes.html +0 -0
  66. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/content-examples.html +0 -0
  67. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-auth.html +0 -0
  68. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/request-body.html +0 -0
  69. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/servers.html +0 -0
  70. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_mkdocs/partial/tags.html +0 -0
  71. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_api/README.md +0 -0
  72. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_api/layout.html +0 -0
  73. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_schemas/README.md +0 -0
  74. {essentials_openapi-1.2.1 → essentials_openapi-1.4.0}/openapidocs/mk/v3/views_plantuml_schemas/layout.html +0 -0
  75. {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.2.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
- Requires-Python: >=3.8
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.2.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
- def generate_documents_command(source: str, destination: str, style: Union[int, str]):
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, List, Tuple
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: List[Tuple[Any, Any]]) -> Any:
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: List[Tuple[Any, Any]]) -> Any:
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"):
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import logging.handlers
3
2
 
4
3
  from rich.logging import RichHandler
5
4
 
@@ -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 is_object_schema(data) -> bool:
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
- is_reference({"type": "array", "items": {...}}) -> True
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
- return data.get("type") == "object" and isinstance(data.get("properties"), dict)
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
- is_reference({"type": "array", "items": {...}}) -> True
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
- return data.get("type") == "array" and isinstance(data.get("items"), dict)
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, obj):
16
+ def default(self, o: object) -> Any:
15
17
  try:
16
- return JSONEncoder.default(self, obj)
18
+ return JSONEncoder.default(self, o)
17
19
  except TypeError:
18
- if isinstance(obj, datetime):
20
+ if isinstance(o, datetime):
19
21
  datetime_format = os.environ.get("OPENAPI_DATETIME_FORMAT")
20
22
  if datetime_format:
21
- return obj.strftime(datetime_format)
23
+ return o.strftime(datetime_format)
22
24
  else:
23
- return obj.isoformat()
24
- return FriendlyEncoder.default(self, obj) # type: ignore
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 Union
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(source: str, destination: str, style: Union[int, str]):
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(data, style=style, source=source)
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
- from typing import Dict, Iterable
5
+
6
+ from typing import Iterable
6
7
 
7
8
 
8
9
  def write_row(
9
10
  row: Iterable[str],
10
- columns_widths: Dict[int, int],
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,