Skip to content

Commit c3adca7

Browse files
committed
Added template for enums
improved codecov
1 parent 7f2b1e5 commit c3adca7

15 files changed

+117
-45
lines changed

README.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@
2323

2424
## Features
2525

26-
- TODO
26+
- Easy code generation for OpenAPI 3.0.0+ APIs
27+
- Async and Sync code generation support (with the help of [httpx](https://pypi.org/project/httpx/))])
28+
- Typed services and models for your convinience
29+
- Support for HttpBearer authentication
30+
- Python only
31+
- Usage as CLI tool or as a library
2732

2833
## Requirements
2934

30-
- TODO
35+
- Python 3.7+
3136

3237
## Installation
3338

@@ -41,6 +46,12 @@ $ pip install openapi-python-generator
4146

4247
Please see the [Command-line Reference] for details.
4348

49+
## Roadmap
50+
51+
- Support for all commonly used http libraries in the python ecosystem (requests, urllib, ...)
52+
- Support for multiple languages
53+
- Support for multiple authentication schemes
54+
4455
## Contributing
4556

4657
Contributions are very welcome.

codecov.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ coverage:
33
status:
44
project:
55
default:
6-
target: "100"
6+
target: "85"
77
patch:
88
default:
9-
target: "100"
9+
target: "85"

src/openapi_python_generator/__main__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import typer
44

5-
from .common import HTTPLibrary
6-
from .generate_data import generate_data
5+
from openapi_python_generator.common import HTTPLibrary
6+
from openapi_python_generator.generate_data import generate_data
77

88
app = typer.Typer()
99

@@ -16,5 +16,7 @@ def main(file_name :str, output : str, library : Optional[HTTPLibrary] = HTTPLi
1616
generate_data(file_name, output, library)
1717

1818

19-
if __name__ == "__main__":
19+
20+
if __name__ == "__main__": #pragma: no cover
2021
app()
22+

src/openapi_python_generator/generate_data.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ def get_open_api(path: Union[str,Path]) -> OpenAPI:
3939
return OpenAPI(**orjson.loads(f.read()))
4040
except FileNotFoundError:
4141
typer.echo(f"File {path} not found. Please make sure to pass the path to the OpenAPI 3.0 specification.")
42-
typer.Exit(1)
42+
raise
4343
except ConnectError:
4444
typer.echo(f"Could not connect to {path}.")
45-
typer.Exit(1)
45+
raise
4646
except ValidationError:
4747
typer.echo(f"File {path} is not a valid OpenAPI 3.0 specification, or there may be a problem with your JSON.")
48-
typer.Exit(1)
48+
raise
4949

5050

5151
def write_data(data: ConversionResult, output: str):

src/openapi_python_generator/language_converters/python/jinja_config.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from jinja2 import Environment, FileSystemLoader
44

5+
ENUM_TEMPLATE = "enum.template"
56
MODELS_TEMPLATE = "models.template"
67
SERVICE_TEMPLATE = "service.template"
78
HTTPX_TEMPLATE = "httpx.template"

src/openapi_python_generator/language_converters/python/model_generator.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import typer
44
from openapi_schema_pydantic import Components, Reference, Schema
55

6-
from openapi_python_generator.language_converters.python.jinja_config import JINJA_ENV, MODELS_TEMPLATE
6+
from openapi_python_generator.language_converters.python.jinja_config import JINJA_ENV, MODELS_TEMPLATE, ENUM_TEMPLATE
77
from openapi_python_generator.models import Model, Property
88

9-
109
def type_converter(schema: Schema, required: bool = False) -> str:
1110
"""
1211
Converts an OpenAPI type to a Python type.
@@ -40,10 +39,9 @@ def type_converter(schema: Schema, required: bool = False) -> str:
4039
retVal += schema.items.ref.split("/")[-1]
4140
elif isinstance(schema.items, Schema):
4241
retVal += type_converter(schema.items, True)
43-
elif schema.items is None:
44-
retVal += "Any"
4542
else:
46-
raise Exception(f"Unknown item type: {type(schema.items)}")
43+
retVal += "Any"
44+
4745
return retVal + "]" + post_type
4846
elif schema.type == "object":
4947
return pre_type + "Dict[str, Any]" + post_type
@@ -103,18 +101,29 @@ def generate_models(components: Components) -> List[Model]:
103101

104102
for name, schema in components.schemas.items():
105103
if schema.enum is not None:
106-
continue # TODO generate enum class using different template
104+
m = Model(
105+
file_name=name,
106+
content=JINJA_ENV.get_template(ENUM_TEMPLATE).render(name=name,**schema.dict()),
107+
openapi_object=schema,
108+
references=[],
109+
properties=[],
110+
)
111+
try:
112+
compile(m.content, "<string>", "exec")
113+
models.append(m)
114+
except SyntaxError as e: #pragma: no cover
115+
typer.echo(f"Error in model {name}: {e}")
116+
117+
continue #pragma: no cover
107118

108119
import_models = []
109120
properties = []
110121
for prop_name, property in schema.properties.items():
111122
if isinstance(property, Reference):
112123
conv_property, import_model = _generate_property_from_reference(prop_name, property, schema)
113124
import_models.append(import_model)
114-
elif isinstance(property, Schema):
115-
conv_property = _generate_property_from_schema(prop_name, property, schema)
116125
else:
117-
raise Exception(f"Unknown property type: {type(property)}")
126+
conv_property = _generate_property_from_schema(prop_name, property, schema)
118127
properties.append(conv_property)
119128

120129
generated_content = JINJA_ENV.get_template(MODELS_TEMPLATE).render(
@@ -126,9 +135,8 @@ def generate_models(components: Components) -> List[Model]:
126135

127136
try:
128137
compile(generated_content, "<string>", "exec")
129-
except SyntaxError as e:
130-
typer.echo(f"Error in model {name}: {e}")
131-
typer.Exit()
138+
except SyntaxError as e: #pragma: no cover
139+
typer.echo(f"Error in model {name}: {e}") #pragma: no cover
132140

133141
models.append(Model(
134142
file_name=name,

src/openapi_python_generator/language_converters/python/service_generator.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ def generate_params(operation: Operation) -> List[str]:
2222
def _generate_params_from_content(content: Union[Reference, Schema]):
2323
if isinstance(content, Reference):
2424
return f"data : {content.ref.split('/')[-1]}"
25-
elif isinstance(content, Schema):
26-
return f"data : {type_converter(content, True)}"
2725
else:
28-
raise Exception("Unknown content type")
26+
return f"data : {type_converter(content, True)}"
2927

3028
if operation.parameters is None and operation.requestBody is None:
3129
return []
@@ -36,11 +34,9 @@ def _generate_params_from_content(content: Union[Reference, Schema]):
3634
if isinstance(param.param_schema, Schema):
3735
params.append(f"{param.name} : {type_converter(param.param_schema, param.required)}" + (
3836
"" if param.required else " = None"))
39-
elif isinstance(param.param_schema, Reference):
37+
else:
4038
params.append(
4139
f"{param.name} : {param.param_schema.ref.split('/')[-1]}" + ("" if param.required else " = None"))
42-
else:
43-
raise Exception("Unknown param schema type")
4440

4541
if operation.requestBody is not None:
4642
if isinstance(operation.requestBody.content, MediaType):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import Enum
2+
3+
class {{ name }}(Enum, str):
4+
{% for enumItem in enum %}
5+
6+
{% if enumItem is string %}
7+
{{ enumItem.lower() }} = '{{ enumItem }}'
8+
{% else %}
9+
value_{{ enumItem }} = {{ enumItem }}
10+
{% endif %}
11+
{% endfor %}

tests/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
"""Test suite for the openapi_python_generator package."""

tests/conftest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from openapi_schema_pydantic import OpenAPI
99

10-
test_data_path = Path(__file__).parent / "test_data" / "test_api.json"
10+
test_data_folder = Path(__file__).parent / "test_data"
11+
test_data_path = test_data_folder / "test_api.json"
1112
test_result_path = Path(__file__).parent / "test_result"
1213

1314

tests/test_data/failing_api.json

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

tests/test_data/test_api.json

+9
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,21 @@
6363
"type": "string",
6464
"description": "User token",
6565
"example": "test"
66+
},
67+
"complex_type": {
68+
"ref":"#/components/schemas/TokenUser"
6669
}
6770
},
6871
"required": [
6972
"user_id",
7073
"token"
7174
]
75+
},
76+
"EnumerationString": {
77+
"enum" : ["test","test_2","test_3"]
78+
},
79+
"EnumerationInt": {
80+
"enum" : [1,2,3]
7281
}
7382
}
7483
},

tests/test_generate_data.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
import pytest
2+
from httpx import ConnectError
23

3-
from openapi_python_generator.generate_data import get_open_api, write_data
4+
from openapi_python_generator.generate_data import get_open_api, write_data, generate_data
45
from openapi_python_generator.language_converters.python.generator import generator
5-
from tests.conftest import test_data_path, test_result_path
6+
from pydantic import ValidationError
7+
8+
from tests.conftest import test_data_path, test_result_path, test_data_folder
69

710

811
def test_get_open_api(model_data):
912
assert get_open_api(test_data_path) == model_data
1013

14+
with pytest.raises(ConnectError):
15+
assert get_open_api("http://test.com")
16+
17+
with pytest.raises(ValidationError):
18+
assert get_open_api(test_data_folder / 'failing_api.json')
19+
20+
def test_generate_data(model_data_with_cleanup):
21+
generate_data(test_data_path,test_result_path)
22+
assert test_result_path.exists()
23+
assert test_result_path.is_dir()
24+
assert (test_result_path / "api_config.py").exists()
25+
assert (test_result_path / "models").exists()
26+
assert (test_result_path / "models").is_dir()
27+
assert (test_result_path / "services").exists()
28+
assert (test_result_path / "services").is_dir()
29+
assert (test_result_path / "models" / "__init__.py").exists()
30+
assert (test_result_path / "services" / "__init__.py").exists()
31+
assert (test_result_path / "services" / "__init__.py").is_file()
32+
assert (test_result_path / "models" / "__init__.py").is_file()
33+
assert (test_result_path / "__init__.py").exists()
34+
assert (test_result_path / "__init__.py").is_file()
1135

1236
def test_write_data(model_data_with_cleanup):
1337
result = generator(model_data_with_cleanup)

tests/test_model_generator.py

+11-13
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@
77

88

99
@pytest.mark.parametrize("test_openapi_types,expected_python_types", [
10-
("string", "str"),
11-
("integer", "int"),
12-
("number", "float"),
13-
("boolean", "bool"),
14-
("array", "List[Any]"),
15-
("object", "Dict[str, Any]"),
16-
("null", "Any"),
10+
(Schema(type='string'), "str"),
11+
(Schema(type='integer'), "int"),
12+
(Schema(type='number'), "float"),
13+
(Schema(type='boolean'), "bool"),
14+
(Schema(type='array'), "List[Any]"),
15+
(Schema(type='array', items=Schema(type='string')), "List[str]"),
16+
(Schema(type='array', items=Reference(ref='#/components/schemas/test_name')), "List[test_name]"),
17+
(Schema(type='object'), "Dict[str, Any]"),
18+
(Schema(type='null'), "Any"),
1719
])
1820
def test_type_converter_simple(test_openapi_types, expected_python_types):
19-
# Generate Schema object from test_openapi_types
20-
schema = Schema(type=test_openapi_types)
21-
22-
assert type_converter(schema, True) == expected_python_types
23-
assert type_converter(schema, False) == 'Optional[' + expected_python_types + ']'
21+
assert type_converter(test_openapi_types, True) == expected_python_types
22+
assert type_converter(test_openapi_types, False) == 'Optional[' + expected_python_types + ']'
2423

2524

2625
@pytest.mark.parametrize("test_openapi_types,expected_python_types", [
@@ -85,4 +84,3 @@ def test_model_generation(model_data: OpenAPI):
8584
assert i.content is not None
8685

8786
compile(i.content, '<string>', 'exec')
88-

tests/test_service_generator.py

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ def test_generate_body_param(test_openapi_operation, expected_result):
3333
required=True),
3434
Parameter(name="test2", param_in="path", param_schema=Schema(type="string"), required=False)
3535
], ), ["test : TestModel", "test2 : Optional[str] = None"]),
36+
(Operation(parameters=[
37+
Parameter(name="test", param_in="query", param_schema=Reference(ref="#/components/schemas/TestModel"),
38+
required=True),
39+
Parameter(name="test2", param_in="path", param_schema=Schema(type="string"), required=True),
40+
], requestBody=RequestBody(content={
41+
"application/json": MediaType(
42+
media_type_schema=Reference(ref="#/components/schemas/TestModel")
43+
)
44+
})), ["test : TestModel", "test2 : str", "data : TestModel"]),
3645
(Operation(parameters=[
3746
Parameter(name="test", param_in="query", param_schema=Reference(ref="#/components/schemas/TestModel"),
3847
required=True),
@@ -42,6 +51,7 @@ def test_generate_body_param(test_openapi_operation, expected_result):
4251
media_type_schema=Reference(ref="#/components/schemas/TestModel")
4352
)
4453
})), ["test : TestModel", "test2 : str", "data : TestModel"])
54+
4555
])
4656
def test_generate_params(test_openapi_operation, expected_result):
4757
assert generate_params(test_openapi_operation) == expected_result

0 commit comments

Comments
 (0)