Skip to content

Commit 46d59bb

Browse files
committed
- Updated docs
- Fixed some mypy complaints about pydantic models and typing
1 parent d135bf3 commit 46d59bb

File tree

13 files changed

+259
-47
lines changed

13 files changed

+259
-47
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
select = B,B9,BLK,C,E,F,I,W,S
3-
max-complexity = 15
3+
max-complexity = 20
44
application-import-names = openapi_python_generator
55
import-order-style = google
66
ignore = E203,E501,W503

docs/tutorial/advanced.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Advanced usage
2+
3+
__coming soon__

docs/tutorial/authentication.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Authentication
2+
3+
__coming soon__

docs/tutorial/index.md

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Pre requisits
44

5-
As already denoted in [the quick start section](quick_start.md), the first thing
5+
As already denoted in [the quick start section](../quick_start.md), the first thing
66
you need to do is to actually install the generator. You can do so via pip
77
or any other package manager.
88

@@ -593,7 +593,7 @@ suite of the generator. It has the following structure:
593593
!!! tip "OpenAPI specification"
594594

595595
Take a look at our short introduction to the
596-
[OpenAPI specification](openapi-definition.md) if you need to look up
596+
[OpenAPI specification](../openapi-definition.md) if you need to look up
597597
what the specific nodes mean, or if you just need a refresher or some
598598
links for further information.
599599

@@ -605,7 +605,7 @@ Lets run the generator on this file:
605605
</div>
606606

607607
This will result in the folder structure as denoted in the
608-
[quick start](quick_start.md) section. Lets take a deep dive on what
608+
[quick start](../quick_start.md) section. Lets take a deep dive on what
609609
the generator created for us, starting with the models.
610610

611611
## The models module
@@ -763,3 +763,108 @@ the additional two - four modules.
763763
The next thing is async support: You may want (depending on your usecase)
764764
bot async and sync services. The generator will create both (for __httpx__),
765765
only sync (for __requests__) or only async (for __aiohttp__) services.
766+
767+
=== "async_general_service.py"
768+
``` py
769+
...
770+
async def async_root__get() -> RootResponse:
771+
base_path = APIConfig.base_path
772+
path = f"/"
773+
headers = {
774+
"Content-Type": "application/json",
775+
"Accept": "application/json",
776+
"Authorization": f"Bearer { APIConfig.get_access_token() }",
777+
}
778+
query_params = {}
779+
780+
with httpx.AsyncClient(base_url=base_path) as client:
781+
response = await client.request(
782+
method="get",
783+
url=path,
784+
headers=headers,
785+
params=query_params,
786+
)
787+
788+
if response.status_code != 200:
789+
raise Exception(f" failed with status code: {response.status_code}")
790+
return RootResponse(**response.json())
791+
...
792+
```
793+
794+
=== "general_service.py"
795+
``` py
796+
...
797+
def root__get() -> RootResponse:
798+
base_path = APIConfig.base_path
799+
path = f"/"
800+
headers = {
801+
"Content-Type": "application/json",
802+
"Accept": "application/json",
803+
"Authorization": f"Bearer { APIConfig.get_access_token() }",
804+
}
805+
query_params = {}
806+
807+
with httpx.Client(base_url=base_path) as client:
808+
response = client.request(
809+
method="get",
810+
url=path,
811+
headers=headers,
812+
params=query_params,
813+
)
814+
815+
if response.status_code != 200:
816+
raise Exception(f" failed with status code: {response.status_code}")
817+
return RootResponse(**response.json())
818+
...
819+
```
820+
821+
While we are at the topic of looking at the individual functions, lets walk through the one above:
822+
823+
```py
824+
...
825+
def root__get() -> RootResponse:
826+
...
827+
```
828+
829+
All functions are fully annotated with the proper types, which provides the inspection of your IDE better insight
830+
on what to provide to a given function and what to expect.
831+
832+
```py
833+
...
834+
path = f"/"
835+
...
836+
```
837+
838+
Paths are automatically created from the specification. No need to worry about that.
839+
840+
```py
841+
...
842+
headers = {
843+
"Content-Type": "application/json",
844+
"Accept": "application/json",
845+
"Authorization": f"Bearer { APIConfig.get_access_token() }",
846+
}
847+
query_params = {}
848+
...
849+
```
850+
851+
Authorization token is always passed to the Rest API, you will not need to worry about differentiating between the
852+
calls. Query params are also automatically created, with the input parameters and depending on your spec (for
853+
this root call no params are necessary)
854+
855+
```py
856+
...
857+
if response.status_code != 200:
858+
raise Exception(f" failed with status code: {response.status_code}")
859+
return RootResponse(**response.json())
860+
...
861+
```
862+
863+
The generator will automatically raise an exception if a non-good status code was returned by the API, for
864+
whatever reason. The "good" status code is also determined by the spec - and can be defined through your API.
865+
For a post call for example, the spec will define a 201 status code as a good status code.
866+
867+
Lastly the code will automatically type check and convert the response to the appropriate type (in this case
868+
`RootResponse`). This is really neat, because without doing much in the code, it automatically validates that
869+
your API truly responds the way we expect it to respond, and gives you proper typing latter on in your code -
870+
all thanks to the magic of [pydantic](https://pydantic-docs.helpmanual.io/).

mkdocs.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ theme:
2323
- search.suggest
2424
- search.highlight
2525
- content.tabs.link
26+
- content.code.annotate
2627
icon:
2728
repo: fontawesome/brands/github-alt
2829
markdown_extensions:
29-
- attr_list
30-
- md_in_html
3130
- pymdownx.superfences
3231
- admonition
32+
- attr_list
33+
- md_in_html
3334
- pymdownx.tabbed:
3435
alternate_style: true
3536
nav:
@@ -38,7 +39,8 @@ nav:
3839
- quick_start.md
3940
- Tutorial - User Guide:
4041
- tutorial/index.md
41-
- tutorial/module_usage.md
42+
- tutorial/authentication.md
43+
- tutorial/advanced.md
4244
- References :
4345
- references/index.md
4446
- Acknowledgements:

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[mypy]
2+
plugins = pydantic.mypy
23

34
[mypy-nox.*,pytest]
45
ignore_missing_imports = True

src/openapi_python_generator/generate_data.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import httpx
88
import isort
99
import orjson
10+
from black import NothingChanged
1011
from httpx import ConnectError
1112
from httpx import ConnectTimeout
1213
from openapi_schema_pydantic import OpenAPI
@@ -28,9 +29,12 @@ def write_code(path: Path, content) -> None:
2829
:param content: The content to write.
2930
"""
3031
with open(path, "w") as f:
31-
formatted_contend = black.format_file_contents(
32-
content, fast=False, mode=black.FileMode(line_length=120)
33-
)
32+
try:
33+
formatted_contend = black.format_file_contents(
34+
content, fast=False, mode=black.FileMode(line_length=120)
35+
)
36+
except NothingChanged:
37+
formatted_contend = content
3438
formatted_contend = isort.code(formatted_contend, line_length=120)
3539
f.write(formatted_contend)
3640

src/openapi_python_generator/language_converters/python/generator.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,16 @@ def generator(
2222
Generate Python code from an OpenAPI 3.0 specification.
2323
"""
2424

25-
models = generate_models(data.components)
26-
services = generate_services(data.paths, library_config)
25+
if data.components is not None:
26+
models = generate_models(data.components)
27+
else:
28+
models = []
29+
30+
if data.paths is not None:
31+
services = generate_services(data.paths, library_config)
32+
else:
33+
services = []
34+
2735
api_config = generate_api_config(data, env_token_name)
2836

2937
return ConversionResult(

src/openapi_python_generator/language_converters/python/model_generator.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import itertools
12
from typing import List
23
from typing import Optional
34

@@ -33,12 +34,26 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
3334
post_type = "]"
3435

3536
original_type = schema.type
36-
import_types = None
37+
import_types: Optional[List[str]] = None
3738

3839
if schema.allOf is not None:
39-
conversions = [type_converter(i, True) for i in schema.allOf]
40+
conversions = []
41+
for sub_schema in schema.allOf:
42+
if isinstance(sub_schema, Schema):
43+
conversions.append(type_converter(sub_schema, True))
44+
else:
45+
import_types = [sub_schema.ref.split("/")[-1]]
46+
conversions.append(
47+
TypeConversion(
48+
original_type=sub_schema.ref,
49+
converted_type=import_types[0],
50+
import_types=import_types,
51+
)
52+
)
4053

41-
original_type = "tuple<" + ",".join([i.type for i in schema.allOf]) + ">"
54+
original_type = (
55+
"tuple<" + ",".join([i.original_type for i in conversions]) + ">"
56+
)
4257
converted_type = (
4358
pre_type
4459
+ "Tuple["
@@ -47,23 +62,40 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
4762
+ post_type
4863
)
4964
import_types = [
50-
i.import_types for i in conversions if i.import_types is not None
65+
i.import_types[0] for i in conversions if i.import_types is not None
5166
]
5267

5368
elif schema.oneOf is not None or schema.anyOf is not None:
5469
used = schema.oneOf if schema.oneOf is not None else schema.anyOf
55-
conversions = [type_converter(i, True) for i in used]
56-
original_type = "union<" + ",".join([i.type for i in used]) + ">"
70+
used = used if used is not None else []
71+
conversions = []
72+
for sub_schema in used:
73+
if isinstance(sub_schema, Schema):
74+
conversions.append(type_converter(sub_schema, True))
75+
else:
76+
import_types = [sub_schema.ref.split("/")[-1]]
77+
conversions.append(
78+
TypeConversion(
79+
original_type=sub_schema.ref,
80+
converted_type=import_types[0],
81+
import_types=import_types,
82+
)
83+
)
84+
original_type = (
85+
"union<" + ",".join([i.original_type for i in conversions]) + ">"
86+
)
5787
converted_type = (
5888
pre_type
5989
+ "Union["
6090
+ ",".join([i.converted_type for i in conversions])
6191
+ "]"
6292
+ post_type
6393
)
64-
import_types = [
65-
i.import_types for i in conversions if i.import_types is not None
66-
]
94+
import_types = list(
95+
itertools.chain(
96+
*[i.import_types for i in conversions if i.import_types is not None]
97+
)
98+
)
6799
elif schema.type == "string":
68100
converted_type = pre_type + "str" + post_type
69101
elif schema.type == "integer":
@@ -165,7 +197,10 @@ def generate_models(components: Components) -> List[Model]:
165197
:param components: The components from an OpenAPI 3.0 specification.
166198
:return: A list of models.
167199
"""
168-
models = []
200+
models: List[Model] = []
201+
202+
if components.schemas is None:
203+
return models
169204

170205
for name, schema_or_reference in components.schemas.items():
171206
if schema_or_reference.enum is not None:
@@ -175,7 +210,6 @@ def generate_models(components: Components) -> List[Model]:
175210
name=name, **schema_or_reference.dict()
176211
),
177212
openapi_object=schema_or_reference,
178-
references=[],
179213
properties=[],
180214
)
181215
try:
@@ -187,7 +221,12 @@ def generate_models(components: Components) -> List[Model]:
187221
continue # pragma: no cover
188222

189223
properties = []
190-
for prop_name, property in schema_or_reference.properties.items():
224+
property_iterator = (
225+
schema_or_reference.properties.items()
226+
if schema_or_reference.properties is not None
227+
else {}
228+
)
229+
for prop_name, property in property_iterator:
191230
if isinstance(property, Reference):
192231
conv_property = _generate_property_from_reference(
193232
prop_name, property, schema_or_reference

0 commit comments

Comments
 (0)