Skip to content

Commit 2d13f24

Browse files
committed
Add response deserialization
Closes #7
1 parent e461977 commit 2d13f24

14 files changed

+157
-68
lines changed

mypy.ini

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
[mypy]
2-
disallow_any_explicit = True
32
disallow_any_generics = True
43
disallow_untyped_defs = True
54
warn_redundant_casts = True
65
warn_unused_ignores = True
76
strict_equality = True
7+
8+
[mypy-stringcase]
9+
ignore_missing_imports = True

openapi_python_client/__init__.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" Generate modern Python clients from OpenAPI """
22
import sys
33
from pathlib import Path
4-
from typing import Dict
4+
from typing import Any, Dict
55

66
import orjson
77
import requests
@@ -10,7 +10,7 @@
1010
from .models import OpenAPI, import_string_from_reference
1111

1212

13-
def main():
13+
def main() -> None:
1414
""" Generate the client library """
1515
url = sys.argv[1]
1616
json = _get_json(url)
@@ -19,16 +19,16 @@ def main():
1919
_build_project(openapi)
2020

2121

22-
def _get_json(url) -> bytes:
22+
def _get_json(url: str) -> bytes:
2323
response = requests.get(url)
2424
return response.content
2525

2626

27-
def _parse_json(json: bytes) -> Dict:
27+
def _parse_json(json: bytes) -> Dict[str, Any]:
2828
return orjson.loads(json)
2929

3030

31-
def _build_project(openapi: OpenAPI):
31+
def _build_project(openapi: OpenAPI) -> None:
3232
env = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
3333

3434
# Create output directories
@@ -46,7 +46,9 @@ def _build_project(openapi: OpenAPI):
4646
# Create a pyproject.toml file
4747
pyproject_template = env.get_template("pyproject.toml")
4848
pyproject_path = project_dir / "pyproject.toml"
49-
pyproject_path.write_text(pyproject_template.render(project_name=project_name, package_name=package_name, description=package_description))
49+
pyproject_path.write_text(
50+
pyproject_template.render(project_name=project_name, package_name=package_name, description=package_description)
51+
)
5052

5153
readme = project_dir / "README.md"
5254
readme_template = env.get_template("README.md")
@@ -88,5 +90,3 @@ def _build_project(openapi: OpenAPI):
8890
for tag, collection in openapi.endpoint_collections_by_tag.items():
8991
module_path = api_dir / f"{tag}.py"
9092
module_path.write_text(endpoint_template.render(collection=collection))
91-
92-

openapi_python_client/models/openapi.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from dataclasses import dataclass, field
44
from enum import Enum
5-
from typing import Dict, List, Optional, Set, Iterable, Generator
5+
from typing import Any, Dict, Generator, Iterable, List, Optional, Set
66

7-
from .properties import Property, property_from_dict, ListProperty, RefProperty, EnumProperty
7+
from .properties import EnumProperty, ListProperty, Property, RefProperty, property_from_dict
88
from .reference import Reference
9-
from .responses import Response, response_from_dict
9+
from .responses import ListRefResponse, RefResponse, Response, response_from_dict
1010

1111

1212
class ParameterLocation(str, Enum):
@@ -30,7 +30,7 @@ class EndpointCollection:
3030
relative_imports: Set[str] = field(default_factory=set)
3131

3232
@staticmethod
33-
def from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, EndpointCollection]:
33+
def from_dict(d: Dict[str, Dict[str, Dict[str, Any]]], /) -> Dict[str, EndpointCollection]:
3434
""" Parse the openapi paths data to get EndpointCollections by tag """
3535
endpoints_by_tag: Dict[str, EndpointCollection] = {}
3636
for path, path_data in d.items():
@@ -55,6 +55,10 @@ def from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, EndpointCollection]
5555

5656
for code, response_dict in method_data["responses"].items():
5757
response = response_from_dict(status_code=int(code), data=response_dict)
58+
if isinstance(response, (RefResponse, ListRefResponse)):
59+
collection.relative_imports.add(
60+
import_string_from_reference(response.reference, prefix="..models")
61+
)
5862
responses.append(response)
5963
form_body_reference = None
6064
if "requestBody" in method_data:
@@ -69,7 +73,7 @@ def from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, EndpointCollection]
6973
path_parameters=path_parameters,
7074
responses=responses,
7175
form_body_reference=form_body_reference,
72-
requires_security=method_data.get("security"),
76+
requires_security=bool(method_data.get("security")),
7377
)
7478

7579
collection.endpoints.append(endpoint)
@@ -97,7 +101,7 @@ class Endpoint:
97101
form_body_reference: Optional[Reference]
98102

99103
@staticmethod
100-
def parse_request_body(body: Dict, /) -> Optional[Reference]:
104+
def parse_request_body(body: Dict[str, Any], /) -> Optional[Reference]:
101105
""" Return form_body_ref """
102106
form_body_reference = None
103107
body_content = body["content"]
@@ -122,7 +126,7 @@ class Schema:
122126
relative_imports: Set[str]
123127

124128
@staticmethod
125-
def from_dict(d: Dict, /) -> Schema:
129+
def from_dict(d: Dict[str, Any], /) -> Schema:
126130
""" A single Schema from its dict representation """
127131
required_set = set(d.get("required", []))
128132
required_properties: List[Property] = []
@@ -148,7 +152,7 @@ def from_dict(d: Dict, /) -> Schema:
148152
return schema
149153

150154
@staticmethod
151-
def dict(d: Dict, /) -> Dict[str, Schema]:
155+
def dict(d: Dict[str, Dict[str, Any]], /) -> Dict[str, Schema]:
152156
""" Get a list of Schemas from an OpenAPI dict """
153157
result = {}
154158
for data in d.values():
@@ -164,7 +168,7 @@ class OpenAPI:
164168
title: str
165169
description: str
166170
version: str
167-
security_schemes: Dict
171+
# security_schemes: Dict
168172
schemas: Dict[str, Schema]
169173
endpoint_collections_by_tag: Dict[str, EndpointCollection]
170174
enums: Dict[str, EnumProperty]
@@ -173,7 +177,7 @@ class OpenAPI:
173177
def check_enums(schemas: Iterable[Schema], collections: Iterable[EndpointCollection]) -> Dict[str, EnumProperty]:
174178
enums: Dict[str, EnumProperty] = {}
175179

176-
def _iterate_properties() -> Generator[Property]:
180+
def _iterate_properties() -> Generator[Property, None, None]:
177181
for schema in schemas:
178182
yield from schema.required_properties
179183
yield from schema.optional_properties
@@ -196,7 +200,7 @@ def _iterate_properties() -> Generator[Property]:
196200
return enums
197201

198202
@staticmethod
199-
def from_dict(d: Dict, /) -> OpenAPI:
203+
def from_dict(d: Dict[str, Dict[str, Any]], /) -> OpenAPI:
200204
""" Create an OpenAPI from dict """
201205
schemas = Schema.dict(d["components"]["schemas"])
202206
endpoint_collections_by_tag = EndpointCollection.from_dict(d["paths"])
@@ -208,6 +212,6 @@ def from_dict(d: Dict, /) -> OpenAPI:
208212
version=d["info"]["version"],
209213
endpoint_collections_by_tag=endpoint_collections_by_tag,
210214
schemas=schemas,
211-
security_schemes=d["components"]["securitySchemes"],
215+
# security_schemes=d["components"]["securitySchemes"],
212216
enums=enums,
213217
)

openapi_python_client/models/properties.py

+34-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass, field
2-
from typing import Optional, List, Dict, Union, ClassVar
2+
from typing import Any, ClassVar, Dict, List, Optional, Union
33

44
from .reference import Reference
55

@@ -10,11 +10,12 @@ class Property:
1010

1111
name: str
1212
required: bool
13-
default: Optional[str]
13+
default: Optional[Any]
1414

15+
constructor_template: ClassVar[Optional[str]] = None
1516
_type_string: ClassVar[str]
1617

17-
def get_type_string(self):
18+
def get_type_string(self) -> str:
1819
""" Get a string representation of type that should be used when declaring this property """
1920
if self.required:
2021
return self._type_string
@@ -38,6 +39,13 @@ def transform(self) -> str:
3839
""" What it takes to turn this object into a native python type """
3940
return self.name
4041

42+
def constructor_from_dict(self, dict_name: str) -> str:
43+
""" How to load this property from a dict (used in generated model from_dict function """
44+
if self.required:
45+
return f'{dict_name}["{self.name}"]'
46+
else:
47+
return f'{dict_name}.get("{self.name}")'
48+
4149

4250
@dataclass
4351
class StringProperty(Property):
@@ -48,7 +56,7 @@ class StringProperty(Property):
4856

4957
_type_string: ClassVar[str] = "str"
5058

51-
def __post_init__(self):
59+
def __post_init__(self) -> None:
5260
if self.default is not None:
5361
self.default = f'"{self.default}"'
5462

@@ -58,6 +66,7 @@ class DateTimeProperty(Property):
5866
""" A property of type datetime.datetime """
5967

6068
_type_string: ClassVar[str] = "datetime"
69+
constructor_template: ClassVar[str] = "datetime_property.pyi"
6170

6271

6372
@dataclass
@@ -89,12 +98,20 @@ class ListProperty(Property):
8998

9099
type: Optional[str]
91100
reference: Optional[Reference]
101+
constructor_template: ClassVar[str] = "list_property.pyi"
92102

93-
def get_type_string(self):
103+
def get_type_string(self) -> str:
94104
""" Get a string representation of type that should be used when declaring this property """
105+
if self.type:
106+
this_type = self.type
107+
elif self.reference:
108+
this_type = self.reference.class_name
109+
else:
110+
raise ValueError(f"Could not figure out type of ListProperty {self.name}")
111+
95112
if self.required:
96-
return f"List[{self.type}]"
97-
return f"Optional[List[{self.type}]]"
113+
return f"List[{this_type}]"
114+
return f"Optional[List[{this_type}]]"
98115

99116

100117
@dataclass
@@ -105,13 +122,13 @@ class EnumProperty(Property):
105122
inverse_values: Dict[str, str] = field(init=False)
106123
reference: Reference = field(init=False)
107124

108-
def __post_init__(self):
125+
def __post_init__(self) -> None:
109126
self.reference = Reference(self.name)
110127
self.inverse_values = {v: k for k, v in self.values.items()}
111128
if self.default is not None:
112129
self.default = f"{self.reference.class_name}.{self.inverse_values[self.default]}"
113130

114-
def get_type_string(self):
131+
def get_type_string(self) -> str:
115132
""" Get a string representation of type that should be used when declaring this property """
116133

117134
if self.required:
@@ -122,6 +139,10 @@ def transform(self) -> str:
122139
""" Output to the template, convert this Enum into a JSONable value """
123140
return f"{self.name}.value"
124141

142+
def constructor_from_dict(self, dict_name: str) -> str:
143+
""" How to load this property from a dict (used in generated model from_dict function """
144+
return f'{self.reference.class_name}({dict_name}["{self.name}"]) if "{self.name}" in {dict_name} else None'
145+
125146
@staticmethod
126147
def values_from_list(l: List[str], /) -> Dict[str, str]:
127148
""" Convert a list of values into dict of {name: value} """
@@ -144,7 +165,9 @@ class RefProperty(Property):
144165

145166
reference: Reference
146167

147-
def get_type_string(self):
168+
constructor_template: ClassVar[str] = "ref_property.pyi"
169+
170+
def get_type_string(self) -> str:
148171
""" Get a string representation of type that should be used when declaring this property """
149172
if self.required:
150173
return self.reference.class_name
@@ -167,9 +190,7 @@ class DictProperty(Property):
167190
}
168191

169192

170-
def property_from_dict(
171-
name: str, required: bool, data: Dict[str, Union[float, int, str, List[str], Dict[str, str]]]
172-
) -> Property:
193+
def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Property:
173194
""" Generate a Property from the OpenAPI dictionary representation of it """
174195
if "enum" in data:
175196
return EnumProperty(

openapi_python_client/models/reference.py

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
class Reference:
55
""" A reference to a class which will be in models """
6+
67
def __init__(self, ref: str):
78
ref_value = ref.split("/")[-1] # get the #/schemas/blahblah part off
89
self.class_name: str = stringcase.pascalcase(ref_value)

0 commit comments

Comments
 (0)