Skip to content

Commit c4bc824

Browse files
committed
Switch OpenAPI parsing to use openapi-schema-pydantic
1 parent e46a9d4 commit c4bc824

File tree

12 files changed

+420
-338
lines changed

12 files changed

+420
-338
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 0.5.0 - Unreleased
8+
### Internal Changes
9+
- Switched OpenAPI document parsing to use
10+
[openapi-schema-pydantic](https://github.com/kuimono/openapi-schema-pydantic/pull/1) (#103)
11+
712
## 0.4.2 - 2020-06-13
813
### Additions
914
- Support for responses with no content (#63 & #66). Thanks @acgray!

openapi_python_client/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from openapi_python_client import utils
1616

17-
from .openapi_parser import OpenAPI, import_string_from_reference
17+
from .openapi_parser import GeneratorData, import_string_from_reference
1818
from .openapi_parser.errors import MultipleParseError
1919

2020
if sys.version_info.minor == 7: # version did not exist in 3.7, need to use a backport
@@ -28,7 +28,7 @@
2828

2929
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> _Project:
3030
data_dict = _get_json(url=url, path=path)
31-
openapi = OpenAPI.from_dict(data_dict)
31+
openapi = GeneratorData.from_dict(data_dict)
3232
return _Project(openapi=openapi)
3333

3434

@@ -72,8 +72,8 @@ def _get_json(*, url: Optional[str], path: Optional[Path]) -> Dict[str, Any]:
7272
class _Project:
7373
TEMPLATE_FILTERS = {"snakecase": utils.snake_case}
7474

75-
def __init__(self, *, openapi: OpenAPI) -> None:
76-
self.openapi: OpenAPI = openapi
75+
def __init__(self, *, openapi: GeneratorData) -> None:
76+
self.openapi: GeneratorData = openapi
7777
self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
7878

7979
self.project_name: str = f"{openapi.title.replace(' ', '-').lower()}-client"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
""" Classes representing the data in the OpenAPI schema """
22

3-
__all__ = ["OpenAPI", "import_string_from_reference"]
3+
__all__ = ["GeneratorData", "import_string_from_reference"]
44

5-
from .openapi import OpenAPI, import_string_from_reference
5+
from .openapi import GeneratorData, import_string_from_reference

openapi_python_client/openapi_parser/openapi.py

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from dataclasses import dataclass, field
44
from enum import Enum
5-
from typing import Any, Dict, List, Optional, Set
5+
from typing import Any, Dict, List, Optional, Set, Union
6+
7+
import openapi_schema_pydantic as oai
68

79
from .errors import ParseError
8-
from .properties import EnumProperty, Property, property_from_dict
10+
from .properties import EnumProperty, Property, property_from_data
911
from .reference import Reference
10-
from .responses import ListRefResponse, RefResponse, Response, response_from_dict
12+
from .responses import ListRefResponse, RefResponse, Response, response_from_data
1113

1214

1315
class ParameterLocation(str, Enum):
@@ -32,16 +34,21 @@ class EndpointCollection:
3234
parse_errors: List[ParseError] = field(default_factory=list)
3335

3436
@staticmethod
35-
def from_dict(d: Dict[str, Dict[str, Dict[str, Any]]]) -> Dict[str, EndpointCollection]:
37+
def from_data(*, data: Dict[str, oai.PathItem]) -> Dict[str, EndpointCollection]:
3638
""" Parse the openapi paths data to get EndpointCollections by tag """
3739
endpoints_by_tag: Dict[str, EndpointCollection] = {}
3840

39-
for path, path_data in d.items():
40-
for method, method_data in path_data.items():
41-
tag = method_data.get("tags", ["default"])[0]
41+
methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
42+
43+
for path, path_data in data.items():
44+
for method in methods:
45+
operation: Optional[oai.Operation] = getattr(path_data, method)
46+
if operation is None:
47+
continue
48+
tag = (operation.tags or ["default"])[0]
4249
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
4350
try:
44-
endpoint = Endpoint.from_data(data=method_data, path=path, method=method, tag=tag)
51+
endpoint = Endpoint.from_data(data=operation, path=path, method=method, tag=tag)
4552
collection.endpoints.append(endpoint)
4653
collection.relative_imports.update(endpoint.relative_imports)
4754
except ParseError as e:
@@ -72,40 +79,40 @@ class Endpoint:
7279
multipart_body_reference: Optional[Reference] = None
7380

7481
@staticmethod
75-
def parse_request_form_body(body: Dict[str, Any]) -> Optional[Reference]:
82+
def parse_request_form_body(body: oai.RequestBody) -> Optional[Reference]:
7683
""" Return form_body_reference """
77-
body_content = body["content"]
84+
body_content = body.content
7885
form_body = body_content.get("application/x-www-form-urlencoded")
79-
if form_body:
80-
return Reference.from_ref(form_body["schema"]["$ref"])
86+
if form_body is not None and isinstance(form_body.media_type_schema, oai.Reference):
87+
return Reference.from_ref(form_body.media_type_schema.ref)
8188
return None
8289

8390
@staticmethod
84-
def parse_multipart_body(body: Dict[str, Any]) -> Optional[Reference]:
91+
def parse_multipart_body(body: oai.RequestBody) -> Optional[Reference]:
8592
""" Return form_body_reference """
86-
body_content = body["content"]
87-
body = body_content.get("multipart/form-data")
88-
if body:
89-
return Reference.from_ref(body["schema"]["$ref"])
93+
body_content = body.content
94+
json_body = body_content.get("multipart/form-data")
95+
if json_body is not None and isinstance(json_body.media_type_schema, oai.Reference):
96+
return Reference.from_ref(json_body.media_type_schema.ref)
9097
return None
9198

9299
@staticmethod
93-
def parse_request_json_body(body: Dict[str, Any]) -> Optional[Property]:
100+
def parse_request_json_body(body: oai.RequestBody) -> Optional[Property]:
94101
""" Return json_body """
95-
body_content = body["content"]
102+
body_content = body.content
96103
json_body = body_content.get("application/json")
97-
if json_body:
98-
return property_from_dict("json_body", required=True, data=json_body["schema"])
104+
if json_body is not None and json_body.media_type_schema is not None:
105+
return property_from_data("json_body", required=True, data=json_body.media_type_schema)
99106
return None
100107

101-
def _add_body(self, data: Dict[str, Any]) -> None:
108+
def _add_body(self, data: oai.Operation) -> None:
102109
""" Adds form or JSON body to Endpoint if included in data """
103-
if "requestBody" not in data:
110+
if data.requestBody is None or isinstance(data.requestBody, oai.Reference):
104111
return
105112

106-
self.form_body_reference = Endpoint.parse_request_form_body(data["requestBody"])
107-
self.json_body = Endpoint.parse_request_json_body(data["requestBody"])
108-
self.multipart_body_reference = Endpoint.parse_multipart_body(data["requestBody"])
113+
self.form_body_reference = Endpoint.parse_request_form_body(data.requestBody)
114+
self.json_body = Endpoint.parse_request_json_body(data.requestBody)
115+
self.multipart_body_reference = Endpoint.parse_multipart_body(data.requestBody)
109116

110117
if self.form_body_reference:
111118
self.relative_imports.add(import_string_from_reference(self.form_body_reference, prefix="..models"))
@@ -114,41 +121,46 @@ def _add_body(self, data: Dict[str, Any]) -> None:
114121
if self.json_body is not None:
115122
self.relative_imports.update(self.json_body.get_imports(prefix="..models"))
116123

117-
def _add_responses(self, data: Dict[str, Any]) -> None:
118-
for code, response_dict in data["responses"].items():
119-
response = response_from_dict(status_code=int(code), data=response_dict)
124+
def _add_responses(self, data: oai.Responses) -> None:
125+
for code, response_data in data.items():
126+
response = response_from_data(status_code=int(code), data=response_data)
120127
if isinstance(response, (RefResponse, ListRefResponse)):
121128
self.relative_imports.add(import_string_from_reference(response.reference, prefix="..models"))
122129
self.responses.append(response)
123130

124-
def _add_parameters(self, data: Dict[str, Any]) -> None:
125-
for param_dict in data.get("parameters", []):
126-
prop = property_from_dict(
127-
name=param_dict["name"], required=param_dict["required"], data=param_dict["schema"]
128-
)
131+
def _add_parameters(self, data: oai.Operation) -> None:
132+
if data.parameters is None:
133+
return
134+
for param in data.parameters:
135+
if isinstance(param, oai.Reference) or param.param_schema is None:
136+
continue
137+
prop = property_from_data(name=param.name, required=param.required, data=param.param_schema)
129138
self.relative_imports.update(prop.get_imports(prefix="..models"))
130139

131-
if param_dict["in"] == ParameterLocation.QUERY:
140+
if param.param_in == ParameterLocation.QUERY:
132141
self.query_parameters.append(prop)
133-
elif param_dict["in"] == ParameterLocation.PATH:
142+
elif param.param_in == ParameterLocation.PATH:
134143
self.path_parameters.append(prop)
135144
else:
136-
raise ValueError(f"Don't know where to put this parameter: {param_dict}")
145+
raise ValueError(f"Don't know where to put this parameter: {param.dict()}")
137146

138147
@staticmethod
139-
def from_data(*, data: Dict[str, Any], path: str, method: str, tag: str) -> Endpoint:
148+
def from_data(*, data: oai.Operation, path: str, method: str, tag: str) -> Endpoint:
140149
""" Construct an endpoint from the OpenAPI data """
141150

151+
if data.operationId is None:
152+
raise ParseError(data=data, message="Path operations with operationId are not yet supported")
153+
142154
endpoint = Endpoint(
143155
path=path,
144156
method=method,
145-
description=data.get("description"),
146-
name=data["operationId"],
147-
requires_security=bool(data.get("security")),
157+
description=data.description,
158+
name=data.operationId,
159+
requires_security=bool(data.security),
148160
tag=tag,
149161
)
150162
endpoint._add_parameters(data)
151-
endpoint._add_responses(data)
163+
endpoint._add_responses(data.responses)
152164
endpoint._add_body(data)
153165

154166
return endpoint
@@ -169,24 +181,26 @@ class Schema:
169181
relative_imports: Set[str]
170182

171183
@staticmethod
172-
def from_dict(d: Dict[str, Any], name: str) -> Schema:
173-
""" A single Schema from its dict representation
184+
def from_data(*, data: Union[oai.Reference, oai.Schema], name: str) -> Schema:
185+
""" A single Schema from its OAI data
174186
175187
Args:
176-
d: Dict representation of the schema
188+
data: Data of a single Schema
177189
name: Name by which the schema is referenced, such as a model name.
178190
Used to infer the type name if a `title` property is not available.
179191
"""
180-
required_set = set(d.get("required", []))
192+
if isinstance(data, oai.Reference):
193+
raise ParseError("Reference schemas are not supported.")
194+
required_set = set(data.required or [])
181195
required_properties: List[Property] = []
182196
optional_properties: List[Property] = []
183197
relative_imports: Set[str] = set()
184198

185-
ref = Reference.from_ref(d.get("title", name))
199+
ref = Reference.from_ref(data.title or name)
186200

187-
for key, value in d.get("properties", {}).items():
201+
for key, value in (data.properties or {}).items():
188202
required = key in required_set
189-
p = property_from_dict(name=key, required=required, data=value)
203+
p = property_from_data(name=key, required=required, data=value)
190204
if required:
191205
required_properties.append(p)
192206
else:
@@ -198,23 +212,23 @@ def from_dict(d: Dict[str, Any], name: str) -> Schema:
198212
required_properties=required_properties,
199213
optional_properties=optional_properties,
200214
relative_imports=relative_imports,
201-
description=d.get("description", ""),
215+
description=data.description or "",
202216
)
203217
return schema
204218

205219
@staticmethod
206-
def dict(d: Dict[str, Dict[str, Any]]) -> Dict[str, Schema]:
220+
def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> Dict[str, Schema]:
207221
""" Get a list of Schemas from an OpenAPI dict """
208222
result = {}
209-
for name, data in d.items():
210-
s = Schema.from_dict(data, name=name)
223+
for name, data in schemas.items():
224+
s = Schema.from_data(data=data, name=name)
211225
result[s.reference.class_name] = s
212226
return result
213227

214228

215229
@dataclass
216-
class OpenAPI:
217-
""" Top level OpenAPI document """
230+
class GeneratorData:
231+
""" All the data needed to generate a client """
218232

219233
title: str
220234
description: Optional[str]
@@ -224,16 +238,20 @@ class OpenAPI:
224238
enums: Dict[str, EnumProperty]
225239

226240
@staticmethod
227-
def from_dict(d: Dict[str, Dict[str, Any]]) -> OpenAPI:
241+
def from_dict(d: Dict[str, Dict[str, Any]]) -> GeneratorData:
228242
""" Create an OpenAPI from dict """
229-
schemas = Schema.dict(d["components"]["schemas"])
230-
endpoint_collections_by_tag = EndpointCollection.from_dict(d["paths"])
243+
openapi = oai.OpenAPI.parse_obj(d)
244+
if openapi.components is None or openapi.components.schemas is None:
245+
schemas = {}
246+
else:
247+
schemas = Schema.build(schemas=openapi.components.schemas)
248+
endpoint_collections_by_tag = EndpointCollection.from_data(data=openapi.paths)
231249
enums = EnumProperty.get_all_enums()
232250

233-
return OpenAPI(
234-
title=d["info"]["title"],
235-
description=d["info"].get("description"),
236-
version=d["info"]["version"],
251+
return GeneratorData(
252+
title=openapi.info.title,
253+
description=openapi.info.description,
254+
version=openapi.info.version,
237255
endpoint_collections_by_tag=endpoint_collections_by_tag,
238256
schemas=schemas,
239257
enums=enums,

0 commit comments

Comments
 (0)