essentials-openapi 1.2.1__tar.gz → 1.3.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 (72) hide show
  1. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/CHANGELOG.md +3 -0
  2. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/PKG-INFO +31 -3
  3. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/README.md +28 -0
  4. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/__init__.py +1 -1
  5. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/commands/docs.py +15 -2
  6. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/common.py +14 -10
  7. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/logs.py +0 -1
  8. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/common.py +8 -5
  9. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/contents.py +11 -9
  10. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/generate.py +10 -3
  11. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/jinja.py +52 -20
  12. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/md.py +3 -2
  13. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/__init__.py +5 -1
  14. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/utils/source.py +9 -4
  15. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/utils/web.py +1 -1
  16. essentials_openapi-1.3.0/openapidocs/v2.py +362 -0
  17. essentials_openapi-1.3.0/openapidocs/v3.py +948 -0
  18. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/pyproject.toml +2 -2
  19. essentials_openapi-1.2.1/openapidocs/v2.py +0 -361
  20. essentials_openapi-1.2.1/openapidocs/v3.py +0 -947
  21. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/.gitignore +0 -0
  22. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/LICENSE +0 -0
  23. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/commands/__init__.py +0 -0
  24. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/main.py +0 -0
  25. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/__init__.py +0 -0
  26. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/texts.py +0 -0
  27. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/examples.py +0 -0
  28. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/README.md +0 -0
  29. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/__init__.py +0 -0
  30. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/layout.html +0 -0
  31. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/components-parameters.html +0 -0
  32. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/components-responses.html +0 -0
  33. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/components-schemas.html +0 -0
  34. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/components-security-schemes.html +0 -0
  35. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/content-examples.html +0 -0
  36. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/external-docs.html +0 -0
  37. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/info.html +0 -0
  38. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/path-items.html +0 -0
  39. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/request-auth.html +0 -0
  40. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/request-body.html +0 -0
  41. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/request-parameters.html +0 -0
  42. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/request-responses.html +0 -0
  43. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/schema-repr.html +0 -0
  44. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/servers.html +0 -0
  45. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/tags.html +0 -0
  46. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_markdown/partial/type.html +0 -0
  47. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/README.md +0 -0
  48. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/__init__.py +0 -0
  49. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/layout.html +0 -0
  50. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/components-parameters.html +0 -0
  51. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/components-responses.html +0 -0
  52. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/components-schemas.html +0 -0
  53. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/components-security-schemes.html +0 -0
  54. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/content-examples.html +0 -0
  55. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/external-docs.html +0 -0
  56. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/info.html +0 -0
  57. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/path-items.html +0 -0
  58. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/request-auth.html +0 -0
  59. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/request-body.html +0 -0
  60. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/request-parameters.html +0 -0
  61. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/request-responses.html +0 -0
  62. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/schema-repr.html +0 -0
  63. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/servers.html +0 -0
  64. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/tags.html +0 -0
  65. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_mkdocs/partial/type.html +0 -0
  66. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_api/README.md +0 -0
  67. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_api/layout.html +0 -0
  68. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_api/partial/schema-repr.html +0 -0
  69. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_schemas/README.md +0 -0
  70. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_schemas/layout.html +0 -0
  71. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/mk/v3/views_plantuml_schemas/partial/schema-repr.html +0 -0
  72. {essentials_openapi-1.2.1 → essentials_openapi-1.3.0}/openapidocs/utils/__init__.py +0 -0
@@ -5,6 +5,9 @@ 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.3.0] - 2025-11-19
9
+ - Add support for passing custom Jinja2 templates as an argument, by @sindrehan.
10
+
8
11
  ## [1.2.1] - 2025-07-30
9
12
 
10
13
  - 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.3.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.3.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
@@ -87,8 +89,8 @@ def normalize_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
87
89
  return data
88
90
 
89
91
 
90
- def regular_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
91
- data = {}
92
+ def regular_dict_factory(items: list[tuple[Any, Any]]) -> dict[Any, Any]:
93
+ data: dict[Any, Any] = {}
92
94
  for key, value in items:
93
95
  for handler in TYPES_HANDLERS:
94
96
  value = handler.normalize(value)
@@ -100,11 +102,11 @@ def regular_dict_factory(items: List[Tuple[Any, Any]]) -> Any:
100
102
  # replicates the asdict method from dataclasses module, to support
101
103
  # bypassing "asdict" on child properties when they implement a `to_obj`
102
104
  # method: some entities require a specific shape when represented
103
- def _asdict_inner(obj, dict_factory):
105
+ def _asdict_inner(obj: Any, dict_factory: Callable[[Any], Any]) -> Any:
104
106
  if hasattr(obj, "to_obj"):
105
107
  return obj.to_obj()
106
108
  if isinstance(obj, OpenAPIElement):
107
- result = []
109
+ result: list[tuple[str, Any]] = []
108
110
  for f in fields(obj):
109
111
  value = _asdict_inner(getattr(obj, f.name), dict_factory)
110
112
  result.append((f.name, value))
@@ -115,11 +117,13 @@ def _asdict_inner(obj, dict_factory):
115
117
  if hasattr(obj, "dict") and callable(obj.dict):
116
118
  # For Pydantic 1
117
119
  return obj.dict()
118
- if is_dataclass(obj):
120
+ if is_dataclass(obj) and not isinstance(obj, type):
119
121
  return asdict(obj, dict_factory=regular_dict_factory)
120
122
  elif isinstance(obj, (list, tuple)):
123
+ obj = cast(Iterable[Any], obj)
121
124
  return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
122
125
  elif isinstance(obj, dict):
126
+ obj = cast(dict[Any, Any], obj)
123
127
  return type(obj)(
124
128
  (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
125
129
  for k, v in obj.items()
@@ -128,7 +132,7 @@ def _asdict_inner(obj, dict_factory):
128
132
  return copy.deepcopy(obj)
129
133
 
130
134
 
131
- def normalize_dict(obj):
135
+ def normalize_dict(obj: Any) -> Any:
132
136
  if hasattr(obj, "dict") and callable(obj.dict):
133
137
  return obj.dict()
134
138
  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,7 +33,7 @@ def is_reference(data) -> bool:
32
33
  return "$ref" in data
33
34
 
34
35
 
35
- def is_object_schema(data) -> bool:
36
+ def is_object_schema(data: object) -> bool:
36
37
  """
37
38
  Returns a value indicating whether the given schema dictionary represents
38
39
  an object schema.
@@ -41,10 +42,11 @@ def is_object_schema(data) -> bool:
41
42
  """
42
43
  if not isinstance(data, dict):
43
44
  return False
45
+ data = cast(dict[str, object], data)
44
46
  return data.get("type") == "object" and isinstance(data.get("properties"), dict)
45
47
 
46
48
 
47
- def is_array_schema(data) -> bool:
49
+ def is_array_schema(data: object) -> bool:
48
50
  """
49
51
  Returns a value indicating whether the given schema dictionary represents
50
52
  an array schema.
@@ -53,10 +55,11 @@ def is_array_schema(data) -> bool:
53
55
  """
54
56
  if not isinstance(data, dict):
55
57
  return False
58
+ data = cast(dict[str, object], data)
56
59
  return data.get("type") == "array" and isinstance(data.get("items"), dict)
57
60
 
58
61
 
59
- def get_ref_type_name(reference) -> str:
62
+ def get_ref_type_name(reference: dict[str, str] | str) -> str:
60
63
  """
61
64
  Returns the type name of a reference.
62
65
 
@@ -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
 
@@ -1,10 +1,20 @@
1
1
  """
2
2
  This module provides a Jinja2 environment.
3
3
  """
4
+
4
5
  import os
5
6
  from enum import Enum
6
-
7
- from jinja2 import Environment, PackageLoader, Template, select_autoescape
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
+ )
8
18
 
9
19
  from . import get_http_status_phrase, highlight_params, read_dict, sort_dict
10
20
  from .common import DocumentsWriter, is_reference
@@ -62,29 +72,50 @@ class PackageLoadingError(ValueError):
62
72
 
63
73
 
64
74
  def get_environment(
65
- package_name: str, views_style: OutputStyle = OutputStyle.MKDOCS
75
+ package_name: str,
76
+ views_style: OutputStyle = OutputStyle.MKDOCS,
77
+ custom_templates_path: Optional[str] = None,
66
78
  ) -> Environment:
67
79
  templates_folder = f"views_{views_style.name}".lower()
68
80
 
81
+ loaders = []
82
+
83
+ # If custom templates path is provided, validate and add FileSystemLoader first
84
+ if custom_templates_path:
85
+ custom_path = Path(custom_templates_path)
86
+ if not custom_path.exists():
87
+ raise ValueError(
88
+ f"Custom templates path does not exist: {custom_templates_path}"
89
+ )
90
+ if not custom_path.is_dir():
91
+ raise ValueError(
92
+ f"Custom templates path is not a directory: {custom_templates_path}"
93
+ )
94
+ loaders.append(FileSystemLoader(str(custom_path)))
95
+
96
+ # Always add the package loader as fallback
69
97
  try:
70
- loader = PackageLoader(package_name, templates_folder)
98
+ loaders.append(PackageLoader(package_name, templates_folder))
71
99
  except ValueError as package_loading_error: # pragma: no cover
72
- raise PackageLoadingError(
73
- views_style, templates_folder
74
- ) from package_loading_error
75
- else:
76
- env = Environment(
77
- loader=loader,
78
- autoescape=select_autoescape(["html", "xml"])
79
- if os.environ.get("SELECT_AUTOESCAPE") in {"YES", "Y", "1"}
80
- else False,
81
- auto_reload=True,
82
- enable_async=False,
83
- )
84
- configure_filters(env)
85
- configure_functions(env)
100
+ if not custom_templates_path:
101
+ raise PackageLoadingError(
102
+ views_style, templates_folder
103
+ ) from package_loading_error
104
+
105
+ loader = ChoiceLoader(loaders)
106
+
107
+ env = Environment(
108
+ loader=loader,
109
+ autoescape=select_autoescape(["html", "xml"])
110
+ if os.environ.get("SELECT_AUTOESCAPE") in {"YES", "Y", "1"}
111
+ else False,
112
+ auto_reload=True,
113
+ enable_async=False,
114
+ )
115
+ configure_filters(env)
116
+ configure_functions(env)
86
117
 
87
- return env
118
+ return env
88
119
 
89
120
 
90
121
  class Jinja2DocumentsWriter(DocumentsWriter):
@@ -97,8 +128,9 @@ class Jinja2DocumentsWriter(DocumentsWriter):
97
128
  self,
98
129
  package_name: str,
99
130
  views_style: OutputStyle = OutputStyle.MKDOCS,
131
+ custom_templates_path: Optional[str] = None,
100
132
  ) -> None:
101
- self._env = get_environment(package_name, views_style)
133
+ self._env = get_environment(package_name, views_style, custom_templates_path)
102
134
 
103
135
  @property
104
136
  def env(self) -> Environment:
@@ -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:
@@ -1,6 +1,7 @@
1
1
  """
2
2
  This module provides functions to generate Markdown for OpenAPI Version 3.
3
3
  """
4
+
4
5
  import copy
5
6
  import os
6
7
  import warnings
@@ -95,11 +96,14 @@ class OpenAPIV3DocumentationHandler:
95
96
  writer: Optional[DocumentsWriter] = None,
96
97
  style: Union[int, str] = 1,
97
98
  source: str = "",
99
+ templates_path: Optional[str] = None,
98
100
  ) -> None:
99
101
  self._source = source
100
102
  self.texts = texts or EnglishTexts()
101
103
  self._writer = writer or Jinja2DocumentsWriter(
102
- __name__, views_style=style_from_value(style)
104
+ __name__,
105
+ views_style=style_from_value(style),
106
+ custom_templates_path=templates_path,
103
107
  )
104
108
  self.doc = self.normalize_data(copy.deepcopy(doc))
105
109
 
@@ -1,8 +1,10 @@
1
1
  """
2
2
  This module provides methods to obtain OpenAPI Documentation from file or web sources.
3
3
  """
4
+
4
5
  import json
5
6
  from pathlib import Path
7
+ from typing import Any
6
8
 
7
9
  import yaml
8
10
 
@@ -11,7 +13,7 @@ from openapidocs.logs import logger
11
13
  from .web import ensure_success, http_get
12
14
 
13
15
 
14
- def read_from_json_file(file_path: Path):
16
+ def read_from_json_file(file_path: Path) -> dict[Any, Any] | list[Any]:
15
17
  """
16
18
  Reads JSON from a given file by path.
17
19
  """
@@ -19,7 +21,7 @@ def read_from_json_file(file_path: Path):
19
21
  return json.loads(source_file.read())
20
22
 
21
23
 
22
- def read_from_yaml_file(file_path: Path):
24
+ def read_from_yaml_file(file_path: Path) -> dict[Any, Any] | list[Any]:
23
25
  """
24
26
  Reads YAML from a given file by path.
25
27
  """
@@ -32,7 +34,7 @@ class SourceError(Exception):
32
34
  super().__init__(message)
33
35
 
34
36
 
35
- def read_from_url(url: str):
37
+ def read_from_url(url: str) -> dict[Any, Any] | list[Any]:
36
38
  """
37
39
  Tries to read OpenAPI Documentation from the given source URL.
38
40
  This method will try to fetch JSON or YAML from the given source, in case of
@@ -63,7 +65,10 @@ def read_from_url(url: str):
63
65
  )
64
66
 
65
67
 
66
- def read_from_source(source: str, cwd: Path = None):
68
+ def read_from_source(
69
+ source: str,
70
+ cwd: Path | None = None,
71
+ ) -> dict[Any, Any] | list[Any]:
67
72
  """
68
73
  Tries to read a JSON or YAML file from a given source.
69
74
  The source can be a path to a file, or a URL.
@@ -6,7 +6,7 @@ http_client = httpx.Client(verify=False, timeout=20)
6
6
 
7
7
 
8
8
  class FailedRequestError(Exception):
9
- def __init__(self, message) -> None:
9
+ def __init__(self, message: str) -> None:
10
10
  super().__init__(
11
11
  f"Failed request: {message}. "
12
12
  "Inspect the inner exception (__context__) for more information."