Skip to content

Commit 43e186e

Browse files
committed
- Added "use-orjson" option
- Added orjson implementation for generated code - Datetimes are now converted properly if orjson is used - Adapted doc - Adapted tests
1 parent 70975a7 commit 43e186e

File tree

16 files changed

+202
-50
lines changed

16 files changed

+202
-50
lines changed

docs/references/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Arguments:
1616
Options:
1717
```console
1818
--library The library to use. Defaults to `httpx`.
19-
--autoformat Specifies which auto formattool to apply to the generated code. Defaults to `black`.
19+
--env-token-name The name of the environment variable to use for the API key. Defaults to `access_token`.
20+
--use-orjson Use the `orjson` library for serialization. Defaults to `false`.
2021
-h, --help Show this help message and exit.
2122
```

docs/tutorial/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,11 @@ suite of the generator. It has the following structure:
534534
"is_active": {
535535
"title": "Is Active",
536536
"type": "boolean"
537+
},
538+
"created_at": {
539+
"title": "Created At",
540+
"type": "string",
541+
"format": "date-time"
537542
}
538543
}
539544
},
@@ -657,6 +662,7 @@ take a look at the `User.py` and the `Team.py` files:
657662
email: str
658663
password: str
659664
is_active: Optional[bool] = None
665+
created_at: Optional[str] = None
660666
```
661667

662668
=== "Team.py"

src/openapi_python_generator/__main__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,39 @@
1414
"--library",
1515
default=HTTPLibrary.httpx,
1616
type=HTTPLibrary,
17+
show_default=True,
1718
help="HTTP library to use in the generation of the client.",
1819
)
1920
@click.option(
2021
"--env-token-name",
2122
default="access_token",
23+
show_default=True,
2224
help="Name of the environment variable that contains the token. If you set this, the code expects this environment "
2325
"variable to be set and will raise an error if it is not.",
2426
)
27+
@click.option(
28+
"--use-orjson",
29+
is_flag=True,
30+
show_default=True,
31+
default=False,
32+
help="Use the orjson library to serialize the data. This is faster than the default json library and provides "
33+
"serialization of datetimes and other types that are not supported by the default json library.",
34+
)
2535
@click.version_option(version=__version__)
2636
def main(
2737
source: str,
2838
output: str,
2939
library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
3040
env_token_name: Optional[str] = None,
41+
use_orjson: bool = False,
3142
) -> None:
3243
"""
3344
Generate Python code from an OpenAPI 3.0 specification.
3445
3546
Provide a SOURCE (file or URL) containing the OpenAPI 3 specification and
3647
an OUTPUT path, where the resulting client is created.
3748
"""
38-
generate_data(source, output, library, env_token_name)
49+
generate_data(source, output, library, env_token_name, use_orjson)
3950

4051

4152
if __name__ == "__main__": # pragma: no cover

src/openapi_python_generator/generate_data.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,7 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
115115
)
116116

117117
# Create services.__init__.py file containing imports to all services.
118-
write_code(
119-
services_path / "__init__.py",
120-
"\n".join([f"from .{file} import *" for file in files]),
121-
)
118+
write_code(services_path / "__init__.py", "")
122119

123120
# Write the api_config.py file.
124121
write_code(Path(output) / "api_config.py", data.api_config.content)
@@ -135,11 +132,12 @@ def generate_data(
135132
output: Union[str, Path],
136133
library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
137134
env_token_name: Optional[str] = None,
135+
use_orjson: bool = False,
138136
) -> None:
139137
"""
140138
Generate Python code from an OpenAPI 3.0 specification.
141139
"""
142140
data = get_open_api(source)
143141
click.echo(f"Generating data from {source}")
144-
result = generator(data, library_config_dict[library], env_token_name)
142+
result = generator(data, library_config_dict[library], env_token_name, use_orjson)
145143
write_data(result, output)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
_use_orjson: bool = False
2+
3+
4+
def set_use_orjson(value: bool) -> None:
5+
"""
6+
Set the value of the global variable _use_orjson.
7+
:param value: value of the variable
8+
"""
9+
global _use_orjson
10+
_use_orjson = value
11+
12+
13+
def get_use_orjson() -> bool:
14+
"""
15+
Get the value of the global variable _use_orjson.
16+
:return: value of the variable
17+
"""
18+
global _use_orjson
19+
return _use_orjson

src/openapi_python_generator/language_converters/python/generator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from openapi_schema_pydantic import OpenAPI
44

5+
from openapi_python_generator.language_converters.python import common
56
from openapi_python_generator.language_converters.python.api_config_generator import (
67
generate_api_config,
78
)
@@ -16,12 +17,17 @@
1617

1718

1819
def generator(
19-
data: OpenAPI, library_config: LibraryConfig, env_token_name: Optional[str] = None
20+
data: OpenAPI,
21+
library_config: LibraryConfig,
22+
env_token_name: Optional[str] = None,
23+
use_orjson: bool = False,
2024
) -> ConversionResult:
2125
"""
2226
Generate Python code from an OpenAPI 3.0 specification.
2327
"""
2428

29+
common.set_use_orjson(use_orjson)
30+
2531
if data.components is not None:
2632
models = generate_models(data.components)
2733
else:

src/openapi_python_generator/language_converters/python/model_generator.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from openapi_schema_pydantic import Reference
88
from openapi_schema_pydantic import Schema
99

10+
from openapi_python_generator.language_converters.python import common
1011
from openapi_python_generator.language_converters.python.jinja_config import (
1112
ENUM_TEMPLATE,
1213
)
@@ -42,11 +43,12 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
4243
if isinstance(sub_schema, Schema):
4344
conversions.append(type_converter(sub_schema, True))
4445
else:
45-
import_types = [sub_schema.ref.split("/")[-1]]
46+
import_type = sub_schema.ref.split("/")[-1]
47+
import_types = [f"from .{import_type} import {import_type}"]
4648
conversions.append(
4749
TypeConversion(
4850
original_type=sub_schema.ref,
49-
converted_type=import_types[0],
51+
converted_type=import_type,
5052
import_types=import_types,
5153
)
5254
)
@@ -73,11 +75,12 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
7375
if isinstance(sub_schema, Schema):
7476
conversions.append(type_converter(sub_schema, True))
7577
else:
76-
import_types = [sub_schema.ref.split("/")[-1]]
78+
import_type = sub_schema.ref.split("/")[-1]
79+
import_types = [f"from .{import_type} import {import_type}"]
7780
conversions.append(
7881
TypeConversion(
7982
original_type=sub_schema.ref,
80-
converted_type=import_types[0],
83+
converted_type=import_type,
8184
import_types=import_types,
8285
)
8386
)
@@ -96,8 +99,15 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
9699
*[i.import_types for i in conversions if i.import_types is not None]
97100
)
98101
)
99-
elif schema.type == "string":
102+
# We only want to auto convert to datetime if orjson is used throghout the code, otherwise we can not
103+
# serialize it to JSON.
104+
elif schema.type == "string" and (
105+
schema.schema_format is None or not common.get_use_orjson()
106+
):
100107
converted_type = pre_type + "str" + post_type
108+
elif schema.type == "string" and schema.schema_format == "date-time":
109+
converted_type = pre_type + "datetime" + post_type
110+
import_types = ["from datetime import datetime"]
101111
elif schema.type == "integer":
102112
converted_type = pre_type + "int" + post_type
103113
elif schema.type == "number":
@@ -107,7 +117,8 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
107117
elif schema.type == "array":
108118
retVal = pre_type + "List["
109119
if isinstance(schema.items, Reference):
110-
import_types = [schema.items.ref.split("/")[-1]]
120+
import_type = schema.items.ref.split("/")[-1]
121+
import_types = [f"from .{import_type} import {import_type}"]
111122
original_type = "array<" + schema.items.ref.split("/")[-1] + ">"
112123
retVal += schema.items.ref.split("/")[-1]
113124
elif isinstance(schema.items, Schema):
@@ -177,7 +188,7 @@ def _generate_property_from_reference(
177188
type_conv = TypeConversion(
178189
original_type=reference.ref,
179190
converted_type=import_model if required else "Optional[" + import_model + "]",
180-
import_types=[import_model],
191+
import_types=[f"from .{import_model} import {import_model}"],
181192
)
182193
return Property(
183194
name=name,

src/openapi_python_generator/language_converters/python/service_generator.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from typing import Dict
23
from typing import List
34
from typing import Tuple
@@ -13,6 +14,7 @@
1314
from openapi_schema_pydantic import Response
1415
from openapi_schema_pydantic import Schema
1516

17+
from openapi_python_generator.language_converters.python import common
1618
from openapi_python_generator.language_converters.python.jinja_config import JINJA_ENV
1719
from openapi_python_generator.language_converters.python.model_generator import (
1820
type_converter,
@@ -189,7 +191,13 @@ def generate_return_type(operation: Operation) -> OpReturnType:
189191
if "array" in converted_result.original_type and isinstance(
190192
converted_result.import_types, list
191193
):
192-
list_type = converted_result.import_types[0]
194+
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
195+
if len(matched) > 0:
196+
list_type = matched[0]
197+
else:
198+
raise Exception(
199+
f"Unable to parse list type from {converted_result.converted_type}"
200+
) # pragma: no cover
193201
else:
194202
list_type = None
195203
return OpReturnType(
@@ -235,6 +243,7 @@ def generate_service_operation(
235243
body_param=body_param,
236244
path_name=path_name,
237245
method=http_operation,
246+
use_orjson=common.get_use_orjson(),
238247
)
239248

240249
so.content = JINJA_ENV.get_template(library_config.template_name).render(
@@ -285,6 +294,7 @@ def generate_service_operation(
285294
),
286295
async_client=False,
287296
library_import=library_config.library_name,
297+
use_orjson=common.get_use_orjson(),
288298
)
289299
)
290300

@@ -304,6 +314,7 @@ def generate_service_operation(
304314
),
305315
async_client=True,
306316
library_import=library_config.library_name,
317+
use_orjson=common.get_use_orjson(),
307318
)
308319
)
309320

src/openapi_python_generator/language_converters/python/templates/httpx.jinja2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ with httpx.Client(base_url=base_path) as client:
2424
headers=headers,
2525
params=query_params,
2626
{% if body_param %}
27+
{% if use_orjson %}
28+
content=orjson.dumps({{ body_param }})
29+
{% else %}
2730
json = json.dumps({{ body_param }})
2831
{% endif %}
32+
{% endif %}
2933
)
3034

3135
if response.status_code != {{ return_type.status_code }}:

src/openapi_python_generator/language_converters/python/templates/models.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from pydantic import BaseModel
33
{% for property in properties %}
44
{% if property.type.import_types is not none %}
55
{% for import_type in property.type.import_types %}
6-
from .{{ import_type }} import {{ import_type}}
6+
{{ import_type }}
77
{% endfor %}
88
{% endif %}
99
{% endfor %}

0 commit comments

Comments
 (0)