ab-openapi-python-generator 2.1.4__tar.gz → 2.2.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.
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/PKG-INFO +1 -1
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/pyproject.toml +1 -1
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/generate_data.py +60 -60
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/service_generator.py → ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/client_generator.py +83 -167
- ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/exception_generator.py +23 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/generator.py +9 -11
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/jinja_config.py +3 -4
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/model_generator.py +33 -91
- ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +80 -0
- ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +8 -0
- ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +80 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/models.py +5 -5
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/version_detector.py +1 -4
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generate_data.py +30 -25
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_service_generator.py +5 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_service_generator_edges.py +6 -0
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/api_config_generator.py +0 -35
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/aiohttp.jinja2 +0 -49
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/apiconfig.jinja2 +0 -38
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/apiconfig_pydantic_2.jinja2 +0 -42
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/httpx.jinja2 +0 -126
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/requests.jinja2 +0 -50
- ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/service.jinja2 +0 -12
- ab_openapi_python_generator-2.1.4/tests/regression/__init__.py +0 -0
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_11.py +0 -33
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_117.py +0 -36
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_120.py +0 -36
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_17.py +0 -27
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_30_87.py +0 -24
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_51.py +0 -27
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_55.py +0 -23
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_7.py +0 -34
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_71.py +0 -27
- ab_openapi_python_generator-2.1.4/tests/regression/test_issue_illegal_py_symbols.py +0 -51
- ab_openapi_python_generator-2.1.4/tests/test_api_config.py +0 -10
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.envrc.example +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.gitattributes +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/dependabot.yml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/workflows/ci.yaml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/workflows/publish.yaml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.gitignore +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.pre-commit-config.yaml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.vscode/launch.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.vscode/tasks.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/LICENSE +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/Makefile +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/README.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/acknowledgements/index.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/css/custom.css +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/css/termynal.css +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/index.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/js/custom.js +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/js/termynal.js +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/openapi-definition.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/quick_start.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/references/index.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/references/module_usage.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/advanced.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/authentication.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/index.md +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/logo.png +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/__init__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/__main__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/common.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/__init__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/common.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/__init__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/openapi_30.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/openapi_31.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/py.typed +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/__init__.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/build_test_api/api.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/conftest.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_common_normalize_symbol.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/failing_api.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/gitea_issue_11.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_117.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_120.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_17.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_30_87.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_51.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_55.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_71.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_71_31.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_illegal_character_in_operation_id.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_keyword_parameter_name.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/openapi_gitea_converted.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/swagger_petstore_3_0_4.yaml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/swagger_petstore_3_1.yaml +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/test_api.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/test_api_31.json +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generate_data_negative.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generated_code.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_main.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_docstring.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_generator.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_generator_edges.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_30.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_completeness.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_coverage.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_schema_features.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_swagger_petstore_30.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_swagger_petstore_31.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_version_detector_edges.py +0 -0
- {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ab-openapi-python-generator
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Openapi Python Generator
|
|
5
5
|
Project-URL: Homepage, https://github.com/auth-broker/openapi-python-generator
|
|
6
6
|
Project-URL: Repository, https://github.com/auth-broker/openapi-python-generator
|
|
@@ -12,7 +12,6 @@ from httpx import ConnectError, ConnectTimeout
|
|
|
12
12
|
from pydantic import ValidationError
|
|
13
13
|
|
|
14
14
|
from .common import FormatOptions, Formatter, HTTPLibrary, PydanticVersion
|
|
15
|
-
from .language_converters.python.jinja_config import SERVICE_TEMPLATE, create_jinja_env
|
|
16
15
|
from .models import ConversionResult
|
|
17
16
|
from .parsers import (
|
|
18
17
|
generate_code_3_0,
|
|
@@ -35,9 +34,7 @@ def write_code(path: Path, content: str, formatter: Formatter) -> None:
|
|
|
35
34
|
elif formatter == Formatter.NONE:
|
|
36
35
|
formatted_contend = content
|
|
37
36
|
else:
|
|
38
|
-
raise NotImplementedError(
|
|
39
|
-
f"Missing implementation for formatter {formatter!r}."
|
|
40
|
-
)
|
|
37
|
+
raise NotImplementedError(f"Missing implementation for formatter {formatter!r}.")
|
|
41
38
|
with open(path, "w") as f:
|
|
42
39
|
f.write(formatted_contend)
|
|
43
40
|
|
|
@@ -74,9 +71,7 @@ def get_open_api(source: Union[str, Path]):
|
|
|
74
71
|
"""
|
|
75
72
|
try:
|
|
76
73
|
# Handle remote files
|
|
77
|
-
if not isinstance(source, Path) and (
|
|
78
|
-
source.startswith("http://") or source.startswith("https://")
|
|
79
|
-
):
|
|
74
|
+
if not isinstance(source, Path) and (source.startswith("http://") or source.startswith("https://")):
|
|
80
75
|
content = httpx.get(source).text
|
|
81
76
|
# Try JSON first, then YAML for remote files
|
|
82
77
|
try:
|
|
@@ -96,9 +91,7 @@ def get_open_api(source: Union[str, Path]):
|
|
|
96
91
|
try:
|
|
97
92
|
data = yaml.safe_load(file_content)
|
|
98
93
|
except yaml.YAMLError as e:
|
|
99
|
-
click.echo(
|
|
100
|
-
f"File {source} is neither a valid JSON nor YAML file: {str(e)}"
|
|
101
|
-
)
|
|
94
|
+
click.echo(f"File {source} is neither a valid JSON nor YAML file: {str(e)}")
|
|
102
95
|
raise
|
|
103
96
|
|
|
104
97
|
# Detect version and parse with appropriate parser
|
|
@@ -110,16 +103,12 @@ def get_open_api(source: Union[str, Path]):
|
|
|
110
103
|
openapi_obj = parse_openapi_3_1(data) # type: ignore[assignment]
|
|
111
104
|
else:
|
|
112
105
|
# Unsupported version detected (version detection already limited to 3.0 / 3.1)
|
|
113
|
-
raise ValueError(
|
|
114
|
-
f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported."
|
|
115
|
-
)
|
|
106
|
+
raise ValueError(f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported.")
|
|
116
107
|
|
|
117
108
|
return openapi_obj, version
|
|
118
109
|
|
|
119
110
|
except FileNotFoundError:
|
|
120
|
-
click.echo(
|
|
121
|
-
f"File {source} not found. Please make sure to pass the path to the OpenAPI specification."
|
|
122
|
-
)
|
|
111
|
+
click.echo(f"File {source} not found. Please make sure to pass the path to the OpenAPI specification.")
|
|
123
112
|
raise
|
|
124
113
|
except (ConnectError, ConnectTimeout):
|
|
125
114
|
click.echo(f"Could not connect to {source}.")
|
|
@@ -129,70 +118,81 @@ def get_open_api(source: Union[str, Path]):
|
|
|
129
118
|
raise
|
|
130
119
|
|
|
131
120
|
|
|
132
|
-
def write_data(
|
|
133
|
-
data: ConversionResult, output: Union[str, Path], formatter: Formatter
|
|
134
|
-
) -> None:
|
|
135
|
-
"""
|
|
136
|
-
This function will firstly create the folder structure of output, if it doesn't exist. Then it will create the
|
|
137
|
-
models from data.models into the models sub module of the output folder. After this, the services will be created
|
|
138
|
-
into the services sub module of the output folder.
|
|
139
|
-
:param data: The data to write.
|
|
140
|
-
:param output: The path to the output folder.
|
|
141
|
-
:param formatter: The formatter applied to the code written.
|
|
121
|
+
def write_data(data: ConversionResult, output: Union[str, Path], formatter: Formatter) -> None:
|
|
142
122
|
"""
|
|
123
|
+
Write generated code to disk.
|
|
143
124
|
|
|
144
|
-
|
|
145
|
-
|
|
125
|
+
Creates:
|
|
126
|
+
- models/ (and models/__init__.py)
|
|
127
|
+
- clients/ (and clients/__init__.py)
|
|
128
|
+
- exceptions.py (package root, if present)
|
|
129
|
+
- __init__.py (package root)
|
|
130
|
+
"""
|
|
131
|
+
out = Path(output)
|
|
132
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
146
133
|
|
|
147
|
-
#
|
|
148
|
-
|
|
134
|
+
# ----------------------------
|
|
135
|
+
# models/
|
|
136
|
+
# ----------------------------
|
|
137
|
+
models_path = out / "models"
|
|
149
138
|
models_path.mkdir(parents=True, exist_ok=True)
|
|
150
139
|
|
|
151
|
-
|
|
152
|
-
services_path = Path(output) / "services"
|
|
153
|
-
services_path.mkdir(parents=True, exist_ok=True)
|
|
154
|
-
|
|
155
|
-
files: List[str] = []
|
|
156
|
-
|
|
157
|
-
# Write the models.
|
|
140
|
+
model_files: List[str] = []
|
|
158
141
|
for model in data.models:
|
|
159
|
-
|
|
142
|
+
model_files.append(model.file_name)
|
|
160
143
|
write_code(models_path / f"{model.file_name}.py", model.content, formatter)
|
|
161
144
|
|
|
162
|
-
# Create models.__init__.py file containing imports to all models.
|
|
163
145
|
write_code(
|
|
164
146
|
models_path / "__init__.py",
|
|
165
|
-
"\n".join([f"from .{
|
|
147
|
+
"\n".join([f"from .{f} import *" for f in model_files]) + ("\n" if model_files else ""),
|
|
166
148
|
formatter,
|
|
167
149
|
)
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
151
|
+
# ----------------------------
|
|
152
|
+
# clients/
|
|
153
|
+
# ----------------------------
|
|
154
|
+
clients_path = out / "clients"
|
|
155
|
+
clients_path.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
|
|
157
|
+
client_files: List[str] = []
|
|
158
|
+
for client in data.clients:
|
|
159
|
+
client_files.append(client.file_name)
|
|
160
|
+
write_code(clients_path / f"{client.file_name}.py", client.content, formatter)
|
|
161
|
+
|
|
162
|
+
write_code(
|
|
163
|
+
clients_path / "__init__.py",
|
|
164
|
+
"\n".join([f"from .{f} import *" for f in client_files]) + ("\n" if client_files else ""),
|
|
165
|
+
formatter,
|
|
166
|
+
)
|
|
182
167
|
|
|
183
|
-
#
|
|
184
|
-
|
|
168
|
+
# ----------------------------
|
|
169
|
+
# exceptions/
|
|
170
|
+
# ----------------------------
|
|
171
|
+
exceptions_path = out / "exceptions"
|
|
172
|
+
exceptions_path.mkdir(parents=True, exist_ok=True)
|
|
185
173
|
|
|
186
|
-
|
|
187
|
-
|
|
174
|
+
exception_files: List[str] = []
|
|
175
|
+
for ex in data.exceptions:
|
|
176
|
+
exception_files.append(ex.file_name)
|
|
177
|
+
write_code(exceptions_path / f"{ex.file_name}.py", ex.content, formatter)
|
|
188
178
|
|
|
189
|
-
# Write the __init__.py file.
|
|
190
179
|
write_code(
|
|
191
|
-
|
|
192
|
-
"from .
|
|
180
|
+
exceptions_path / "__init__.py",
|
|
181
|
+
"\n".join([f"from .{f} import *" for f in exception_files]) + ("\n" if exception_files else ""),
|
|
193
182
|
formatter,
|
|
194
183
|
)
|
|
195
184
|
|
|
185
|
+
# ----------------------------
|
|
186
|
+
# package __init__.py (root)
|
|
187
|
+
# ----------------------------
|
|
188
|
+
init_lines: List[str] = [
|
|
189
|
+
"from .models import *",
|
|
190
|
+
"from .clients import *",
|
|
191
|
+
"from .exceptions import *",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
write_code(out / "__init__.py", "\n".join(init_lines) + "\n", formatter)
|
|
195
|
+
|
|
196
196
|
|
|
197
197
|
def generate_data(
|
|
198
198
|
source: Union[str, Path],
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
3
3
|
|
|
4
|
-
import click
|
|
5
4
|
from openapi_pydantic.v3 import (
|
|
6
5
|
Operation,
|
|
7
6
|
PathItem,
|
|
@@ -38,9 +37,11 @@ from openapi_pydantic.v3.v3_1 import (
|
|
|
38
37
|
)
|
|
39
38
|
from openapi_pydantic.v3.v3_1.parameter import Parameter as Parameter31
|
|
40
39
|
|
|
40
|
+
from ab_openapi_python_generator.common import PydanticVersion
|
|
41
41
|
from ab_openapi_python_generator.language_converters.python import common
|
|
42
|
-
from ab_openapi_python_generator.language_converters.python.common import normalize_symbol
|
|
43
42
|
from ab_openapi_python_generator.language_converters.python.jinja_config import (
|
|
43
|
+
ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2,
|
|
44
|
+
SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2,
|
|
44
45
|
create_jinja_env,
|
|
45
46
|
)
|
|
46
47
|
from ab_openapi_python_generator.language_converters.python.model_generator import (
|
|
@@ -48,8 +49,8 @@ from ab_openapi_python_generator.language_converters.python.model_generator impo
|
|
|
48
49
|
)
|
|
49
50
|
from ab_openapi_python_generator.models import (
|
|
50
51
|
LibraryConfig,
|
|
52
|
+
Model,
|
|
51
53
|
OpReturnType,
|
|
52
|
-
Service,
|
|
53
54
|
ServiceOperation,
|
|
54
55
|
TypeConversion,
|
|
55
56
|
)
|
|
@@ -123,9 +124,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
|
123
124
|
if operation.requestBody is None:
|
|
124
125
|
return None
|
|
125
126
|
else:
|
|
126
|
-
if isinstance(operation.requestBody, Reference30) or isinstance(
|
|
127
|
-
operation.requestBody, Reference31
|
|
128
|
-
):
|
|
127
|
+
if isinstance(operation.requestBody, Reference30) or isinstance(operation.requestBody, Reference31):
|
|
129
128
|
return "data.dict()"
|
|
130
129
|
|
|
131
130
|
if operation.requestBody.content is None:
|
|
@@ -139,9 +138,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
|
139
138
|
if media_type is None:
|
|
140
139
|
return None # pragma: no cover
|
|
141
140
|
|
|
142
|
-
if isinstance(
|
|
143
|
-
media_type.media_type_schema, (Reference, Reference30, Reference31)
|
|
144
|
-
):
|
|
141
|
+
if isinstance(media_type.media_type_schema, (Reference, Reference30, Reference31)):
|
|
145
142
|
return "data.dict()"
|
|
146
143
|
elif hasattr(media_type.media_type_schema, "ref"):
|
|
147
144
|
# Handle Reference objects from different OpenAPI versions
|
|
@@ -153,9 +150,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
|
153
150
|
elif schema.type == "object":
|
|
154
151
|
return "data"
|
|
155
152
|
else:
|
|
156
|
-
raise Exception(
|
|
157
|
-
f"Unsupported schema type for request body: {schema.type}"
|
|
158
|
-
) # pragma: no cover
|
|
153
|
+
raise Exception(f"Unsupported schema type for request body: {schema.type}") # pragma: no cover
|
|
159
154
|
else:
|
|
160
155
|
raise Exception(
|
|
161
156
|
f"Unsupported schema type for request body: {type(media_type.media_type_schema)}"
|
|
@@ -185,26 +180,17 @@ def generate_params(operation: Operation) -> str:
|
|
|
185
180
|
required = False
|
|
186
181
|
param_name_cleaned = common.normalize_symbol(param.name)
|
|
187
182
|
|
|
188
|
-
if isinstance(param.param_schema, Schema30) or isinstance(
|
|
189
|
-
param.param_schema, Schema31
|
|
190
|
-
):
|
|
183
|
+
if isinstance(param.param_schema, Schema30) or isinstance(param.param_schema, Schema31):
|
|
191
184
|
converted_result = (
|
|
192
185
|
f"{param_name_cleaned} : {type_converter(param.param_schema, param.required).converted_type}"
|
|
193
186
|
+ ("" if param.required else " = None")
|
|
194
187
|
)
|
|
195
188
|
required = param.required
|
|
196
|
-
elif isinstance(param.param_schema, Reference30) or isinstance(
|
|
197
|
-
param.param_schema
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
+ (
|
|
202
|
-
""
|
|
203
|
-
if isinstance(param, Reference30)
|
|
204
|
-
or isinstance(param, Reference31)
|
|
205
|
-
or param.required
|
|
206
|
-
else " = None"
|
|
207
|
-
)
|
|
189
|
+
elif isinstance(param.param_schema, Reference30) or isinstance(param.param_schema, Reference31):
|
|
190
|
+
converted_result = f"{param_name_cleaned} : {param.param_schema.ref.split('/')[-1]}" + (
|
|
191
|
+
""
|
|
192
|
+
if isinstance(param, Reference30) or isinstance(param, Reference31) or param.required
|
|
193
|
+
else " = None"
|
|
208
194
|
)
|
|
209
195
|
required = isinstance(param, Reference) or param.required
|
|
210
196
|
|
|
@@ -220,17 +206,11 @@ def generate_params(operation: Operation) -> str:
|
|
|
220
206
|
"application/octet-stream",
|
|
221
207
|
]
|
|
222
208
|
|
|
223
|
-
if operation.requestBody is not None and not is_reference_type(
|
|
224
|
-
operation.requestBody
|
|
225
|
-
):
|
|
209
|
+
if operation.requestBody is not None and not is_reference_type(operation.requestBody):
|
|
226
210
|
# Safe access only if it's a concrete RequestBody object
|
|
227
211
|
rb_content = getattr(operation.requestBody, "content", None)
|
|
228
|
-
if isinstance(rb_content, dict) and any(
|
|
229
|
-
|
|
230
|
-
):
|
|
231
|
-
get_keyword = [
|
|
232
|
-
i for i in operation_request_body_types if rb_content.get(i)
|
|
233
|
-
][0]
|
|
212
|
+
if isinstance(rb_content, dict) and any(rb_content.get(i) is not None for i in operation_request_body_types):
|
|
213
|
+
get_keyword = [i for i in operation_request_body_types if rb_content.get(i)][0]
|
|
234
214
|
content = rb_content.get(get_keyword)
|
|
235
215
|
if content is not None and hasattr(content, "media_type_schema"):
|
|
236
216
|
mts = getattr(content, "media_type_schema", None)
|
|
@@ -240,9 +220,7 @@ def generate_params(operation: Operation) -> str:
|
|
|
240
220
|
):
|
|
241
221
|
params += f"{_generate_params_from_content(mts)}, "
|
|
242
222
|
else: # pragma: no cover
|
|
243
|
-
raise Exception(
|
|
244
|
-
f"Unsupported media type schema for {str(operation)}: {type(mts)}"
|
|
245
|
-
)
|
|
223
|
+
raise Exception(f"Unsupported media type schema for {str(operation)}: {type(mts)}")
|
|
246
224
|
# else: silently ignore unsupported body shapes (could extend later)
|
|
247
225
|
# Replace - with _ in params
|
|
248
226
|
params = params.replace("-", "_")
|
|
@@ -251,9 +229,7 @@ def generate_params(operation: Operation) -> str:
|
|
|
251
229
|
return params + default_params
|
|
252
230
|
|
|
253
231
|
|
|
254
|
-
def generate_operation_id(
|
|
255
|
-
operation: Operation, http_op: str, path_name: Optional[str] = None
|
|
256
|
-
) -> str:
|
|
232
|
+
def generate_operation_id(operation: Operation, http_op: str, path_name: Optional[str] = None) -> str:
|
|
257
233
|
if operation.operationId is not None:
|
|
258
234
|
return common.normalize_symbol(operation.operationId)
|
|
259
235
|
elif path_name is not None:
|
|
@@ -264,9 +240,7 @@ def generate_operation_id(
|
|
|
264
240
|
) # pragma: no cover
|
|
265
241
|
|
|
266
242
|
|
|
267
|
-
def _generate_params(
|
|
268
|
-
operation: Operation, param_in: Literal["query", "header"] = "query"
|
|
269
|
-
):
|
|
243
|
+
def _generate_params(operation: Operation, param_in: Literal["query", "header"] = "query"):
|
|
270
244
|
if operation.parameters is None:
|
|
271
245
|
return []
|
|
272
246
|
|
|
@@ -319,9 +293,7 @@ def generate_return_type(operation: Operation) -> OpReturnType:
|
|
|
319
293
|
media_type_schema = create_media_type_for_reference(chosen_response)
|
|
320
294
|
|
|
321
295
|
if media_type_schema is None:
|
|
322
|
-
return OpReturnType(
|
|
323
|
-
type=None, status_code=good_responses[0][0], complex_type=False
|
|
324
|
-
)
|
|
296
|
+
return OpReturnType(type=None, status_code=good_responses[0][0], complex_type=False)
|
|
325
297
|
|
|
326
298
|
if is_media_type(media_type_schema):
|
|
327
299
|
inner_schema = getattr(media_type_schema, "media_type_schema", None)
|
|
@@ -338,25 +310,18 @@ def generate_return_type(operation: Operation) -> OpReturnType:
|
|
|
338
310
|
)
|
|
339
311
|
elif is_schema_type(inner_schema):
|
|
340
312
|
converted_result = type_converter(inner_schema, True) # type: ignore
|
|
341
|
-
if "array" in converted_result.original_type and isinstance(
|
|
342
|
-
converted_result.import_types, list
|
|
343
|
-
):
|
|
313
|
+
if "array" in converted_result.original_type and isinstance(converted_result.import_types, list):
|
|
344
314
|
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
|
|
345
315
|
if len(matched) > 0:
|
|
346
316
|
list_type = matched[0]
|
|
347
317
|
else: # pragma: no cover
|
|
348
|
-
raise Exception(
|
|
349
|
-
f"Unable to parse list type from {converted_result.converted_type}"
|
|
350
|
-
)
|
|
318
|
+
raise Exception(f"Unable to parse list type from {converted_result.converted_type}")
|
|
351
319
|
else:
|
|
352
320
|
list_type = None
|
|
353
321
|
return OpReturnType(
|
|
354
322
|
type=converted_result,
|
|
355
323
|
status_code=good_responses[0][0],
|
|
356
|
-
complex_type=bool(
|
|
357
|
-
converted_result.import_types
|
|
358
|
-
and len(converted_result.import_types) > 0
|
|
359
|
-
),
|
|
324
|
+
complex_type=bool(converted_result.import_types and len(converted_result.import_types) > 0),
|
|
360
325
|
list_type=list_type,
|
|
361
326
|
)
|
|
362
327
|
else: # pragma: no cover
|
|
@@ -371,63 +336,62 @@ def generate_return_type(operation: Operation) -> OpReturnType:
|
|
|
371
336
|
raise Exception("Unknown media type schema type") # pragma: no cover
|
|
372
337
|
|
|
373
338
|
|
|
374
|
-
def
|
|
375
|
-
|
|
376
|
-
)
|
|
339
|
+
def clean_up_path_name(path_name: str) -> str:
|
|
340
|
+
# Clean up path name: only replace dashes inside curly brackets for f-string compatibility, keep other dashes
|
|
341
|
+
def _replace_bracket_dashes(match):
|
|
342
|
+
return "{" + match.group(1).replace("-", "_") + "}"
|
|
343
|
+
|
|
344
|
+
return re.sub(r"\{([^}/]+)\}", _replace_bracket_dashes, path_name)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def generate_clients(
|
|
348
|
+
openapi: Any,
|
|
349
|
+
paths: Dict[str, PathItem],
|
|
350
|
+
library_config: LibraryConfig,
|
|
351
|
+
env_token_name: Optional[str],
|
|
352
|
+
pydantic_version: PydanticVersion,
|
|
353
|
+
) -> List[Model]:
|
|
377
354
|
"""
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
355
|
+
Generate two client modules:
|
|
356
|
+
- sync_client.py (SyncClient)
|
|
357
|
+
- async_client.py (AsyncClient)
|
|
381
358
|
"""
|
|
382
359
|
jinja_env = create_jinja_env()
|
|
383
360
|
|
|
384
|
-
|
|
385
|
-
|
|
361
|
+
service_ops: List[ServiceOperation] = []
|
|
362
|
+
|
|
363
|
+
def _generate_service_operation(
|
|
364
|
+
op: Operation, path_obj: PathItem, path_name: str, http_operation: str, async_type: bool
|
|
386
365
|
) -> ServiceOperation:
|
|
387
|
-
# Merge path-level parameters (always required by spec) into the
|
|
388
|
-
# operation-level parameters so they get turned into function args.
|
|
389
366
|
try:
|
|
390
367
|
path_level_params = []
|
|
391
|
-
if hasattr(
|
|
392
|
-
path_level_params = [p for p in
|
|
368
|
+
if hasattr(path_obj, "parameters") and path_obj.parameters is not None:
|
|
369
|
+
path_level_params = [p for p in path_obj.parameters if p is not None]
|
|
393
370
|
if path_level_params:
|
|
394
371
|
existing_names = set()
|
|
395
372
|
if op.parameters is not None:
|
|
396
|
-
for p in op.parameters:
|
|
373
|
+
for p in op.parameters:
|
|
397
374
|
if isinstance(p, (Parameter30, Parameter31)):
|
|
398
375
|
existing_names.add(p.name)
|
|
399
376
|
for p in path_level_params:
|
|
400
|
-
if (
|
|
401
|
-
isinstance(p, (Parameter30, Parameter31))
|
|
402
|
-
and p.name not in existing_names
|
|
403
|
-
):
|
|
377
|
+
if isinstance(p, (Parameter30, Parameter31)) and p.name not in existing_names:
|
|
404
378
|
if op.parameters is None:
|
|
405
379
|
op.parameters = [] # type: ignore
|
|
406
380
|
op.parameters.append(p) # type: ignore
|
|
407
|
-
except Exception:
|
|
408
|
-
print(
|
|
409
|
-
f"Error merging path-level parameters for {path_name}"
|
|
410
|
-
) # pragma: no cover
|
|
381
|
+
except Exception:
|
|
411
382
|
pass
|
|
412
383
|
|
|
413
384
|
params = generate_params(op)
|
|
414
|
-
# Fallback: ensure all {placeholders} in path are present as function params
|
|
415
385
|
try:
|
|
416
|
-
placeholder_names = [
|
|
417
|
-
|
|
418
|
-
]
|
|
419
|
-
existing_param_names = {
|
|
420
|
-
p.split(":")[0].strip() for p in params.split(",") if ":" in p
|
|
421
|
-
}
|
|
386
|
+
placeholder_names = [m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)]
|
|
387
|
+
existing_param_names = {p.split(":")[0].strip() for p in params.split(",") if ":" in p}
|
|
422
388
|
for ph in placeholder_names:
|
|
423
389
|
norm_ph = common.normalize_symbol(ph)
|
|
424
390
|
if norm_ph not in existing_param_names and norm_ph:
|
|
425
391
|
params = f"{norm_ph}: Any, " + params
|
|
426
|
-
except Exception:
|
|
427
|
-
print(
|
|
428
|
-
f"Error ensuring path placeholders in params for {path_name}"
|
|
429
|
-
) # pragma: no cover
|
|
392
|
+
except Exception:
|
|
430
393
|
pass
|
|
394
|
+
|
|
431
395
|
operation_id = generate_operation_id(op, http_operation, path_name)
|
|
432
396
|
query_params = generate_query_params(op)
|
|
433
397
|
header_params = generate_header_params(op)
|
|
@@ -441,7 +405,7 @@ def generate_services(
|
|
|
441
405
|
header_params=header_params,
|
|
442
406
|
return_type=return_type,
|
|
443
407
|
operation=op,
|
|
444
|
-
pathItem=
|
|
408
|
+
pathItem=path_obj,
|
|
445
409
|
content="",
|
|
446
410
|
async_client=async_type,
|
|
447
411
|
body_param=body_param,
|
|
@@ -451,90 +415,42 @@ def generate_services(
|
|
|
451
415
|
use_orjson=common.get_use_orjson(),
|
|
452
416
|
)
|
|
453
417
|
|
|
454
|
-
so.content = jinja_env.get_template(library_config.template_name).render(
|
|
455
|
-
**so.model_dump()
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
if op.tags is not None and len(op.tags) > 0:
|
|
459
|
-
so.tag = normalize_symbol(op.tags[0])
|
|
460
|
-
|
|
461
|
-
try:
|
|
462
|
-
compile(so.content, "<string>", "exec")
|
|
463
|
-
except SyntaxError as e: # pragma: no cover
|
|
464
|
-
click.echo(f"Error in service {so.operation_id}: {e}") # pragma: no cover
|
|
465
|
-
|
|
466
418
|
return so
|
|
467
419
|
|
|
468
|
-
services = []
|
|
469
|
-
service_ops = []
|
|
470
420
|
for path_name, path in paths.items():
|
|
471
421
|
clean_path_name = clean_up_path_name(path_name)
|
|
472
422
|
for http_operation in HTTP_OPERATIONS:
|
|
473
|
-
op = path
|
|
423
|
+
op = getattr(path, http_operation)
|
|
474
424
|
if op is None:
|
|
475
425
|
continue
|
|
476
426
|
|
|
477
427
|
if library_config.include_sync:
|
|
478
|
-
|
|
479
|
-
service_ops.append(sync_so)
|
|
480
|
-
|
|
428
|
+
service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, False))
|
|
481
429
|
if library_config.include_async:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
library_import=library_config.library_name,
|
|
508
|
-
use_orjson=common.get_use_orjson(),
|
|
509
|
-
)
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
for tag in tags:
|
|
513
|
-
services.append(
|
|
514
|
-
Service(
|
|
515
|
-
file_name=f"async_{tag}_service",
|
|
516
|
-
operations=[
|
|
517
|
-
so for so in service_ops if so.tag == tag and so.async_client
|
|
518
|
-
],
|
|
519
|
-
content="\n".join(
|
|
520
|
-
[
|
|
521
|
-
so.content
|
|
522
|
-
for so in service_ops
|
|
523
|
-
if so.tag == tag and so.async_client
|
|
524
|
-
]
|
|
525
|
-
),
|
|
526
|
-
async_client=True,
|
|
527
|
-
library_import=library_config.library_name,
|
|
528
|
-
use_orjson=common.get_use_orjson(),
|
|
529
|
-
)
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
return services
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def clean_up_path_name(path_name: str) -> str:
|
|
536
|
-
# Clean up path name: only replace dashes inside curly brackets for f-string compatibility, keep other dashes
|
|
537
|
-
def _replace_bracket_dashes(match):
|
|
538
|
-
return "{" + match.group(1).replace("-", "_") + "}"
|
|
430
|
+
service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, True))
|
|
431
|
+
|
|
432
|
+
sync_ops = [so for so in service_ops if not so.async_client]
|
|
433
|
+
async_ops = [so for so in service_ops if so.async_client]
|
|
434
|
+
|
|
435
|
+
openapi_dump = openapi.model_dump() if hasattr(openapi, "model_dump") else {}
|
|
436
|
+
|
|
437
|
+
sync_content = jinja_env.get_template(SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
|
|
438
|
+
**openapi_dump,
|
|
439
|
+
env_token_name=env_token_name,
|
|
440
|
+
operations=[so.model_dump() for so in sync_ops],
|
|
441
|
+
)
|
|
442
|
+
async_content = jinja_env.get_template(ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
|
|
443
|
+
**openapi_dump,
|
|
444
|
+
env_token_name=env_token_name,
|
|
445
|
+
operations=[so.model_dump() for so in async_ops],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
compile(sync_content, "<string>", "exec")
|
|
449
|
+
compile(async_content, "<string>", "exec")
|
|
450
|
+
|
|
451
|
+
clients: List[Model] = [
|
|
452
|
+
Model(file_name="sync_client", content=sync_content, openapi_object={}, properties=[]),
|
|
453
|
+
Model(file_name="async_client", content=async_content, openapi_object={}, properties=[]),
|
|
454
|
+
]
|
|
539
455
|
|
|
540
|
-
return
|
|
456
|
+
return clients
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ab_openapi_python_generator.language_converters.python.jinja_config import (
|
|
4
|
+
HTTP_EXCEPTION_TEMPLATE,
|
|
5
|
+
create_jinja_env,
|
|
6
|
+
)
|
|
7
|
+
from ab_openapi_python_generator.models import Model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_exceptions() -> list[Model]:
|
|
11
|
+
"""
|
|
12
|
+
Generate shared exception modules (package-local support code).
|
|
13
|
+
"""
|
|
14
|
+
jinja_env = create_jinja_env()
|
|
15
|
+
|
|
16
|
+
http_exception = Model(
|
|
17
|
+
file_name="http_exception",
|
|
18
|
+
content=jinja_env.get_template(HTTP_EXCEPTION_TEMPLATE).render(),
|
|
19
|
+
openapi_object=None, # Model.openapi_object is optional now
|
|
20
|
+
properties=[],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return [http_exception]
|