Skip to content

Commit 8857e2a

Browse files
committed
Support Form bodies
Also a bit of cleanup for References Closes #11
1 parent 3e040d3 commit 8857e2a

File tree

6 files changed

+110
-105
lines changed

6 files changed

+110
-105
lines changed

openapi_python_client/models/openapi.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .properties import Property, property_from_dict, ListProperty, RefProperty, EnumProperty
1010
from .responses import Response, response_from_dict
11+
from .reference import Reference
1112

1213

1314
class ParameterLocation(Enum):
@@ -29,17 +30,18 @@ def from_dict(d: Dict, /) -> Parameter:
2930
""" Construct a parameter from it's OpenAPI dict form """
3031
return Parameter(
3132
location=ParameterLocation(d["in"]),
32-
property=property_from_dict(name=d["name"], required=d["required"], data=d["schema"],),
33+
property=property_from_dict(name=d["name"], required=d["required"], data=d["schema"]),
3334
)
3435

3536

36-
def _import_string_from_ref(ref: str, prefix: str = "") -> str:
37-
return f"from {prefix}.{stringcase.snakecase(ref)} import {ref}"
37+
def _import_string_from_reference(reference: Reference, prefix: str = "") -> str:
38+
return f"from {prefix}.{reference.module_name} import {reference.class_name}"
3839

3940

4041
@dataclass
4142
class EndpointCollection:
4243
""" A bunch of endpoints grouped under a tag that will become a module """
44+
4345
tag: str
4446
endpoints: List[Endpoint] = field(default_factory=list)
4547
relative_imports: Set[str] = field(default_factory=set)
@@ -56,14 +58,11 @@ def from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, EndpointCollection]
5658
parameters.append(Parameter.from_dict(param_dict))
5759
tag = method_data.get("tags", ["default"])[0]
5860
for code, response_dict in method_data["responses"].items():
59-
response = response_from_dict(
60-
status_code=int(code),
61-
data=response_dict,
62-
)
61+
response = response_from_dict(status_code=int(code), data=response_dict)
6362
responses.append(response)
64-
form_body_ref = None
63+
form_body_reference = None
6564
if "requestBody" in method_data:
66-
form_body_ref = Endpoint.parse_request_body(method_data["requestBody"])
65+
form_body_reference = Endpoint.parse_request_body(method_data["requestBody"])
6766

6867
endpoint = Endpoint(
6968
path=path,
@@ -72,13 +71,15 @@ def from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, EndpointCollection]
7271
name=method_data["operationId"],
7372
parameters=parameters,
7473
responses=responses,
75-
form_body_ref=form_body_ref,
74+
form_body_reference=form_body_reference,
7675
)
7776

7877
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
7978
collection.endpoints.append(endpoint)
80-
if form_body_ref:
81-
collection.relative_imports.add(_import_string_from_ref(form_body_ref, prefix="..models"))
79+
if form_body_reference:
80+
collection.relative_imports.add(
81+
_import_string_from_reference(form_body_reference, prefix="..models")
82+
)
8283
return endpoints_by_tag
8384

8485

@@ -94,17 +95,17 @@ class Endpoint:
9495
name: str
9596
parameters: List[Parameter]
9697
responses: List[Response]
97-
form_body_ref: Optional[str]
98+
form_body_reference: Optional[Reference]
9899

99100
@staticmethod
100-
def parse_request_body(body: Dict, /) -> Optional[str]:
101+
def parse_request_body(body: Dict, /) -> Optional[Reference]:
101102
""" Return form_body_ref """
102-
form_body_ref = None
103+
form_body_reference = None
103104
body_content = body["content"]
104105
form_body = body_content.get("application/x-www-form-urlencoded")
105106
if form_body:
106-
form_body_ref = form_body["schema"]["$ref"].split("/")[-1]
107-
return form_body_ref
107+
form_body_reference = Reference(form_body["schema"]["$ref"])
108+
return form_body_reference
108109

109110

110111
@dataclass
@@ -116,21 +117,35 @@ class Schema:
116117
"""
117118

118119
title: str
119-
properties: List[Property]
120+
required_properties: List[Property]
121+
optional_properties: List[Property]
120122
description: str
121-
relative_imports: Set[str] = field(default_factory=set)
123+
relative_imports: Set[str]
122124

123125
@staticmethod
124126
def from_dict(d: Dict, /) -> Schema:
125127
""" A single Schema from its dict representation """
126-
required = set(d.get("required", []))
127-
properties: List[Property] = []
128-
schema = Schema(title=d["title"], properties=properties, description=d.get("description", ""))
128+
required_set = set(d.get("required", []))
129+
required_properties: List[Property] = []
130+
optional_properties: List[Property] = []
131+
relative_imports: Set[str] = set()
132+
129133
for key, value in d["properties"].items():
130-
p = property_from_dict(name=key, required=key in required, data=value)
131-
properties.append(p)
132-
if isinstance(p, (ListProperty, RefProperty)) and p.ref:
133-
schema.relative_imports.add(_import_string_from_ref(p.ref))
134+
required = key in required_set
135+
p = property_from_dict(name=key, required=required, data=value)
136+
if required:
137+
required_properties.append(p)
138+
else:
139+
optional_properties.append(p)
140+
if isinstance(p, (ListProperty, RefProperty)) and p.reference:
141+
relative_imports.add(_import_string_from_reference(p.reference))
142+
schema = Schema(
143+
title=stringcase.pascalcase(d["title"]),
144+
required_properties=required_properties,
145+
optional_properties=optional_properties,
146+
relative_imports=relative_imports,
147+
description=d.get("description", ""),
148+
)
134149
return schema
135150

136151
@staticmethod
@@ -161,7 +176,7 @@ def from_dict(d: Dict, /) -> OpenAPI:
161176
schemas = Schema.dict(d["components"]["schemas"])
162177
enums: Dict[str, EnumProperty] = {}
163178
for schema in schemas.values():
164-
for prop in schema.properties:
179+
for prop in schema.required_properties + schema.optional_properties:
165180
if not isinstance(prop, EnumProperty):
166181
continue
167182
schema.relative_imports.add(f"from .{prop.name} import {prop.class_name}")

openapi_python_client/models/properties.py

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from dataclasses import dataclass, field
22
from typing import Optional, List, Dict, Union, ClassVar
3+
34
import stringcase
45

6+
from .reference import Reference
7+
58

69
@dataclass
710
class Property:
811
""" Describes a single property for a schema """
912

1013
name: str
1114
required: bool
15+
default: Optional[str]
1216

1317
_type_string: ClassVar[str]
1418

@@ -18,23 +22,33 @@ def get_type_string(self):
1822
return self._type_string
1923
return f"Optional[{self._type_string}]"
2024

25+
def to_string(self) -> str:
26+
""" How this should be declared in a dataclass """
27+
if self.default:
28+
default = self.default
29+
elif not self.required:
30+
default = "None"
31+
else:
32+
default = None
33+
34+
if default is not None:
35+
return f"{self.name}: {self.get_type_string()} = {self.default}"
36+
else:
37+
return f"{self.name}: {self.get_type_string()}"
38+
2139

2240
@dataclass
2341
class StringProperty(Property):
2442
""" A property of type str """
2543

2644
max_length: Optional[int] = None
27-
default: Optional[str] = None
2845
pattern: Optional[str] = None
2946

3047
_type_string: ClassVar[str] = "str"
3148

32-
def to_string(self) -> str:
33-
""" How this should be declared in a dataclass """
34-
if self.default:
35-
return f"{self.name}: {self.get_type_string()} = {self.default}"
36-
else:
37-
return f"{self.name}: {self.get_type_string()}"
49+
def __post_init__(self):
50+
if self.default is not None:
51+
self.default = f'"{self.default}"'
3852

3953

4054
@dataclass
@@ -43,10 +57,6 @@ class DateTimeProperty(Property):
4357

4458
_type_string: ClassVar[str] = "datetime"
4559

46-
def to_string(self) -> str:
47-
""" How this should be declared in a dataclass """
48-
return f"{self.name}: {self.get_type_string()}"
49-
5060

5161
@dataclass
5262
class FloatProperty(Property):
@@ -55,13 +65,6 @@ class FloatProperty(Property):
5565
default: Optional[float] = None
5666
_type_string: ClassVar[str] = "float"
5767

58-
def to_string(self) -> str:
59-
""" How this should be declared in a dataclass """
60-
if self.default:
61-
return f"{self.name}: {self.get_type_string()} = {self.default}"
62-
else:
63-
return f"{self.name}: {self.get_type_string()}"
64-
6568

6669
@dataclass
6770
class IntProperty(Property):
@@ -70,42 +73,27 @@ class IntProperty(Property):
7073
default: Optional[int] = None
7174
_type_string: ClassVar[str] = "int"
7275

73-
def to_string(self) -> str:
74-
""" How this should be declared in a dataclass """
75-
if self.default:
76-
return f"{self.name}: {self.get_type_string()} = {self.default}"
77-
else:
78-
return f"{self.name}: {self.get_type_string()}"
79-
8076

8177
@dataclass
8278
class BooleanProperty(Property):
8379
""" Property for bool """
8480

8581
_type_string: ClassVar[str] = "bool"
8682

87-
def to_string(self) -> str:
88-
""" How this should be declared in a dataclass """
89-
return f"{self.name}: {self.get_type_string()}"
90-
9183

9284
@dataclass
9385
class ListProperty(Property):
9486
""" Property for list """
9587

9688
type: Optional[str]
97-
ref: Optional[str]
89+
reference: Optional[Reference]
9890

9991
def get_type_string(self):
10092
""" Get a string representation of type that should be used when declaring this property """
10193
if self.required:
10294
return f"List[{self.type}]"
10395
return f"Optional[List[{self.type}]]"
10496

105-
def to_string(self) -> str:
106-
""" How this should be declared in a dataclass """
107-
return f"{self.name}: {self.get_type_string()}"
108-
10997

11098
@dataclass
11199
class EnumProperty(Property):
@@ -124,10 +112,6 @@ def get_type_string(self):
124112
return self.class_name
125113
return f"Optional[{self.class_name}]"
126114

127-
def to_string(self) -> str:
128-
""" How this should be declared in a dataclass """
129-
return f"{self.name}: {self.get_type_string()}"
130-
131115
@staticmethod
132116
def values_from_list(l: List[str], /) -> Dict[str, str]:
133117
""" Convert a list of values into dict of {name: value} """
@@ -148,13 +132,13 @@ def values_from_list(l: List[str], /) -> Dict[str, str]:
148132
class RefProperty(Property):
149133
""" A property which refers to another Schema """
150134

151-
ref: str
135+
reference: Reference
152136

153137
def get_type_string(self):
154138
""" Get a string representation of type that should be used when declaring this property """
155139
if self.required:
156-
return self.ref
157-
return f"Optional[{self.ref}]"
140+
return self.reference.class_name
141+
return f"Optional[{self.reference.class_name}]"
158142

159143
def to_string(self) -> str:
160144
""" How this should be declared in a dataclass """
@@ -186,31 +170,35 @@ def property_from_dict(
186170
) -> Property:
187171
""" Generate a Property from the OpenAPI dictionary representation of it """
188172
if "enum" in data:
189-
return EnumProperty(name=name, required=required, values=EnumProperty.values_from_list(data["enum"]))
173+
return EnumProperty(
174+
name=name,
175+
required=required,
176+
values=EnumProperty.values_from_list(data["enum"]),
177+
default=data.get("default"),
178+
)
190179
if "$ref" in data:
191-
ref = data["$ref"].split("/")[-1]
192-
return RefProperty(name=name, required=required, ref=ref)
180+
return RefProperty(name=name, required=required, reference=Reference(data["$ref"]), default=None)
193181
if data["type"] == "string":
194182
if "format" not in data:
195183
return StringProperty(
196184
name=name, default=data.get("default"), required=required, pattern=data.get("pattern"),
197185
)
198186
elif data["format"] == "date-time":
199-
return DateTimeProperty(name=name, required=required)
187+
return DateTimeProperty(name=name, required=required, default=data.get("default"))
200188
elif data["type"] == "number":
201189
return FloatProperty(name=name, default=data.get("default"), required=required)
202190
elif data["type"] == "integer":
203191
return IntProperty(name=name, default=data.get("default"), required=required)
204192
elif data["type"] == "boolean":
205-
return BooleanProperty(name=name, required=required)
193+
return BooleanProperty(name=name, required=required, default=data.get("default"))
206194
elif data["type"] == "array":
207-
ref = None
195+
reference = None
208196
if "$ref" in data["items"]:
209-
ref = data["items"]["$ref"].split("/")[-1]
197+
reference = Reference(data["items"]["$ref"])
210198
_type = None
211199
if "type" in data["items"]:
212200
_type = _openapi_types_to_python_type_strings[data["items"]["type"]]
213-
return ListProperty(name=name, required=required, type=_type, ref=ref)
201+
return ListProperty(name=name, required=required, type=_type, reference=reference, default=None)
214202
elif data["type"] == "object":
215-
return DictProperty(name=name, required=required)
203+
return DictProperty(name=name, required=required, default=None)
216204
raise ValueError(f"Did not recognize type of {data}")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import stringcase
2+
3+
4+
class Reference:
5+
""" A reference to a class which will be in models """
6+
def __init__(self, ref: str):
7+
ref_value = ref.split("/")[-1] # get the #/schemas/blahblah part off
8+
self.class_name: str = stringcase.pascalcase(ref_value)
9+
self.module_name: str = stringcase.snakecase(ref_value)

0 commit comments

Comments
 (0)