Skip to content

Commit 2e7754f

Browse files
thervejirikuncar
andauthored
Use custom generator (#853)
* Use new generator for Python Move off openapi-generator to use a custom generator. * Remove legacy bits * Reformat imports * Align blank lines * Remove unused functions * Rebase * Fix enum description * Missing model * Rebase * Add missing nullable * Fix inclusive minimum * Fix missing model * Add missing oneOf imports * Remove ref to openapi-generator. * Rebuild * Split generator * Fix unstable operations * Remove unused file * Generate servers * Fix default * Fix auth sections in config * Remove unused files * It's a collective effort :D * Fix author Co-authored-by: Jiri Kuncar <[email protected]>
1 parent 4230464 commit 2e7754f

File tree

1,225 files changed

+11136
-16151
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,225 files changed

+11136
-16151
lines changed

.generator/openapitools.json

Lines changed: 0 additions & 31 deletions
This file was deleted.

.generator/poetry.lock

Lines changed: 186 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.generator/pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[tool.poetry]
2+
name = "generator"
3+
version = "0.1.0"
4+
description = ""
5+
authors = ["Datadog <[email protected]>"]
6+
license = "Apache-2.0"
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.9"
10+
click = "8.0.1"
11+
PyYAML = "6.0"
12+
jsonref = "0.2"
13+
jinja2 = "3.0.3"
14+
15+
[tool.poetry.dev-dependencies]
16+
17+
[build-system]
18+
requires = ["poetry-core>=1.0.0"]
19+
build-backend = "poetry.core.masonry.api"

.generator/schemas/v2/openapi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3427,6 +3427,7 @@ components:
34273427
readOnly: true
34283428
type: object
34293429
MetricBulkConfigureTagsType:
3430+
type: string
34303431
default: metric_bulk_configure_tags
34313432
description: The metric bulk configure tags resource.
34323433
enum:

.generator/src/generator/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import cli
2+
3+
cli()

.generator/src/generator/cli.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import json
2+
import pathlib
3+
4+
import click
5+
from jinja2 import Environment, FileSystemLoader
6+
7+
from . import openapi
8+
from . import formatter
9+
10+
11+
@click.command()
12+
@click.option(
13+
"-i",
14+
"--input",
15+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=pathlib.Path),
16+
)
17+
@click.option(
18+
"-o",
19+
"--output",
20+
type=click.Path(path_type=pathlib.Path),
21+
)
22+
def cli(input, output):
23+
"""
24+
Generate a Python code snippet from OpenAPI specification.
25+
"""
26+
spec = openapi.load(input)
27+
28+
version = input.parent.name
29+
with (input.parent.parent.parent / "config" / f"{version}.json").open() as fp:
30+
config = json.load(fp)
31+
32+
env = Environment(loader=FileSystemLoader(str(pathlib.Path(__file__).parent / "templates")))
33+
34+
env.filters["accept_headers"] = openapi.accept_headers
35+
env.filters["attribute_name"] = formatter.attribute_name
36+
env.filters["camel_case"] = formatter.camel_case
37+
env.filters["collection_format"] = openapi.collection_format
38+
env.filters["format_value"] = formatter.format_value
39+
env.filters["parameter_schema"] = openapi.parameter_schema
40+
env.filters["parameters"] = openapi.parameters
41+
env.filters["return_type"] = openapi.return_type
42+
env.filters["snake_case"] = formatter.snake_case
43+
44+
env.globals["config"] = config
45+
env.globals["enumerate"] = enumerate
46+
env.globals["version"] = version
47+
env.globals["openapi"] = spec
48+
env.globals["get_name"] = formatter.get_name
49+
env.globals["get_type_for_attribute"] = openapi.get_type_for_attribute
50+
env.globals["get_types_for_attribute"] = openapi.get_types_for_attribute
51+
env.globals["get_type_for_parameter"] = openapi.get_type_for_parameter
52+
env.globals["get_references_for_model"] = openapi.get_references_for_model
53+
env.globals["get_oneof_parameters"] = openapi.get_oneof_parameters
54+
env.globals["get_type_for_items"] = openapi.get_type_for_items
55+
env.globals["get_api_models"] = openapi.get_api_models
56+
env.globals["get_enum_type"] = openapi.get_enum_type
57+
env.globals["get_enum_default"] = openapi.get_enum_default
58+
env.globals["get_oneof_types"] = openapi.get_oneof_types
59+
env.globals["get_oneof_models"] = openapi.get_oneof_models
60+
env.globals["type_to_python"] = openapi.type_to_python
61+
62+
api_j2 = env.get_template("api.j2")
63+
apis_j2 = env.get_template("apis.j2")
64+
model_j2 = env.get_template("model.j2")
65+
models_j2 = env.get_template("models.j2")
66+
init_j2 = env.get_template("init.j2")
67+
configuration_j2 = env.get_template("configuration.j2")
68+
69+
extra_files = {
70+
"api_client.py": env.get_template("api_client.j2"),
71+
"exceptions.py": env.get_template("exceptions.j2"),
72+
"model_utils.py": env.get_template("model_utils.j2"),
73+
"rest.py": env.get_template("rest.j2"),
74+
}
75+
76+
apis = openapi.apis(spec)
77+
models = openapi.models(spec)
78+
79+
package = output / config["packageName"].replace(".", "/")
80+
package.mkdir(parents=True, exist_ok=True)
81+
82+
for name, template in extra_files.items():
83+
filename = package / name
84+
with filename.open("w") as fp:
85+
fp.write(template.render(apis=apis, models=models))
86+
87+
for name, model in models.items():
88+
filename = formatter.snake_case(name) + ".py"
89+
model_path = package / "model" / filename
90+
model_path.parent.mkdir(parents=True, exist_ok=True)
91+
with model_path.open("w") as fp:
92+
fp.write(model_j2.render(name=name, model=model))
93+
94+
model_init_path = package / "model" / "__init__.py"
95+
with model_init_path.open("w") as fp:
96+
fp.write("")
97+
98+
models_path = package / "models" / "__init__.py"
99+
models_path.parent.mkdir(parents=True, exist_ok=True)
100+
with models_path.open("w") as fp:
101+
fp.write(models_j2.render(models=sorted(models)))
102+
103+
for name, operations in apis.items():
104+
filename = formatter.snake_case(name) + "_api.py"
105+
api_path = package / "api" / filename
106+
api_path.parent.mkdir(parents=True, exist_ok=True)
107+
with api_path.open("w") as fp:
108+
fp.write(api_j2.render(name=name, operations=operations))
109+
110+
api_init_path = package / "api" / "__init__.py"
111+
with api_init_path.open("w") as fp:
112+
fp.write("")
113+
114+
apis_path = package / "apis" / "__init__.py"
115+
apis_path.parent.mkdir(parents=True, exist_ok=True)
116+
with apis_path.open("w") as fp:
117+
fp.write(apis_j2.render(apis=sorted(apis)))
118+
119+
init_path = package / "__init__.py"
120+
with init_path.open("w") as fp:
121+
fp.write(init_j2.render())
122+
123+
config_path = package / "configuration.py"
124+
with config_path.open("w") as fp:
125+
fp.write(configuration_j2.render(apis=apis))

.generator/src/generator/formatter.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Data formatter."""
2+
import keyword
3+
import re
4+
5+
KEYWORDS = set(keyword.kwlist)
6+
KEYWORDS.add("property")
7+
8+
PATTERN_DOUBLE_UNDERSCORE = re.compile(r"__+")
9+
PATTERN_LEADING_ALPHA = re.compile(r"(.)([A-Z][a-z]+)")
10+
PATTERN_FOLLOWING_ALPHA = re.compile(r"([a-z0-9])([A-Z])")
11+
PATTERN_WHITESPACE = re.compile(r"\W")
12+
13+
14+
def snake_case(value):
15+
s1 = PATTERN_LEADING_ALPHA.sub(r"\1_\2", value)
16+
s1 = PATTERN_FOLLOWING_ALPHA.sub(r"\1_\2", s1).lower()
17+
s1 = PATTERN_WHITESPACE.sub("_", s1)
18+
s1 = s1.rstrip("_")
19+
return PATTERN_DOUBLE_UNDERSCORE.sub("_", s1)
20+
21+
22+
def camel_case(value):
23+
return "".join(x.title() for x in snake_case(value).split("_"))
24+
25+
26+
def escape_reserved_keyword(word):
27+
"""
28+
Escape reserved language keywords like openapi generator does it
29+
:param word: Word to escape
30+
:return: The escaped word if it was a reserved keyword, the word unchanged otherwise
31+
"""
32+
if word in KEYWORDS:
33+
return f"_{word}"
34+
return word
35+
36+
37+
def attribute_name(attribute):
38+
return escape_reserved_keyword(snake_case(attribute))
39+
40+
41+
def format_value(value, quotes='"'):
42+
if isinstance(value, str):
43+
return f"{quotes}{value}{quotes}"
44+
elif isinstance(value, bool):
45+
return "true" if value else "false"
46+
return value
47+
48+
49+
def get_name(schema):
50+
if hasattr(schema, "__reference__"):
51+
return schema.__reference__["$ref"].split("/")[-1]

0 commit comments

Comments
 (0)