Skip to content

Commit caabc63

Browse files
committed
Update for 0.2.1
- Fix errors import - Add list of enum support - Bump to 0.2.1
1 parent e5fd39a commit caabc63

25 files changed

+406
-61
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ 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.2.1 - 2020-03-22
8+
### Fixes
9+
- Fixed import of errors.py in generated api modules
10+
11+
### Additions
12+
- Support for lists of Enums
13+
714
## 0.2.0 - 2020-03-22
815
### Changes
916
- Update Typer dependency to 0.1.0 and remove click-completion dependency (#19)

openapi_python_client/openapi_parser/openapi.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from __future__ import annotations
22

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

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

@@ -91,7 +91,7 @@ def _add_body(self, data: Dict[str, Any]) -> None:
9191
self.relative_imports.add(import_string_from_reference(self.form_body_reference, prefix="..models"))
9292
if (
9393
self.json_body is not None
94-
and isinstance(self.json_body, (ListProperty, RefProperty, EnumProperty))
94+
and isinstance(self.json_body, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty))
9595
and self.json_body.reference is not None
9696
):
9797
self.relative_imports.add(import_string_from_reference(self.json_body.reference, prefix="..models"))
@@ -108,7 +108,10 @@ def _add_parameters(self, data: Dict[str, Any]) -> None:
108108
prop = property_from_dict(
109109
name=param_dict["name"], required=param_dict["required"], data=param_dict["schema"]
110110
)
111-
if isinstance(prop, (ListProperty, RefProperty, EnumProperty)) and prop.reference:
111+
if (
112+
isinstance(prop, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty))
113+
and prop.reference
114+
):
112115
self.relative_imports.add(import_string_from_reference(prop.reference, prefix="..models"))
113116
if param_dict["in"] == ParameterLocation.QUERY:
114117
self.query_parameters.append(prop)
@@ -165,7 +168,7 @@ def from_dict(d: Dict[str, Any], /) -> Schema:
165168
required_properties.append(p)
166169
else:
167170
optional_properties.append(p)
168-
if isinstance(p, (ListProperty, RefProperty, EnumProperty)) and p.reference:
171+
if isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference:
169172
relative_imports.add(import_string_from_reference(p.reference))
170173
schema = Schema(
171174
reference=Reference.from_ref(d["title"]),
@@ -195,17 +198,19 @@ class OpenAPI:
195198
version: str
196199
schemas: Dict[str, Schema]
197200
endpoint_collections_by_tag: Dict[str, EndpointCollection]
198-
enums: Dict[str, EnumProperty]
201+
enums: Dict[str, Union[EnumProperty, EnumListProperty]]
199202

200203
@staticmethod
201-
def _check_enums(schemas: Iterable[Schema], collections: Iterable[EndpointCollection]) -> Dict[str, EnumProperty]:
204+
def _check_enums(
205+
schemas: Iterable[Schema], collections: Iterable[EndpointCollection]
206+
) -> Dict[str, Union[EnumProperty, EnumListProperty]]:
202207
"""
203208
Create EnumProperties for every enum in any schema or collection.
204209
Enums are deduplicated by class name.
205210
206211
:raises AssertionError: if two Enums with the same name but different values are detected
207212
"""
208-
enums: Dict[str, EnumProperty] = {}
213+
enums: Dict[str, Union[EnumProperty, EnumListProperty]] = {}
209214

210215
def _iterate_properties() -> Generator[Property, None, None]:
211216
for schema in schemas:
@@ -217,7 +222,7 @@ def _iterate_properties() -> Generator[Property, None, None]:
217222
yield from endpoint.query_parameters
218223

219224
for prop in _iterate_properties():
220-
if not isinstance(prop, EnumProperty):
225+
if not isinstance(prop, (EnumProperty, EnumListProperty)):
221226
continue
222227

223228
if prop.reference.class_name in enums:

openapi_python_client/openapi_parser/properties.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,52 @@ class BooleanProperty(Property):
9393

9494

9595
@dataclass
96-
class ListProperty(Property):
97-
""" Property for list """
96+
class BasicListProperty(Property):
97+
""" A List of basic types """
9898

99-
type: Optional[str]
100-
reference: Optional[Reference]
101-
constructor_template: ClassVar[str] = "list_property.pyi"
99+
type: str
100+
101+
def constructor_from_dict(self, dict_name: str) -> str:
102+
""" How to set this property from a dictionary of values """
103+
return f'{dict_name}.get("{self.name}", [])'
102104

103105
def get_type_string(self) -> str:
104106
""" 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}")
107+
if self.required:
108+
return f"List[{self.type}]"
109+
return f"Optional[List[{self.type}]]"
110+
111+
112+
@dataclass
113+
class ReferenceListProperty(Property):
114+
""" A List of References """
111115

116+
reference: Reference
117+
constructor_template: ClassVar[str] = "reference_list_property.pyi"
118+
119+
def get_type_string(self) -> str:
120+
""" Get a string representation of type that should be used when declaring this property """
112121
if self.required:
113-
return f"List[{this_type}]"
114-
return f"Optional[List[{this_type}]]"
122+
return f"List[{self.reference.class_name}]"
123+
return f"Optional[List[{self.reference.class_name}]]"
124+
125+
126+
@dataclass
127+
class EnumListProperty(Property):
128+
""" List of Enum values """
129+
130+
values: Dict[str, str]
131+
reference: Reference = field(init=False)
132+
constructor_template: ClassVar[str] = "enum_list_property.pyi"
133+
134+
def __post_init__(self) -> None:
135+
self.reference = Reference.from_ref(self.name)
136+
137+
def get_type_string(self) -> str:
138+
""" Get a string representation of type that should be used when declaring this property """
139+
if self.required:
140+
return f"List[{self.reference.class_name}]"
141+
return f"Optional[List[{self.reference.class_name}]]"
115142

116143

117144
@dataclass
@@ -218,13 +245,21 @@ def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Prope
218245
elif data["type"] == "boolean":
219246
return BooleanProperty(name=name, required=required, default=data.get("default"))
220247
elif data["type"] == "array":
221-
reference = None
222248
if "$ref" in data["items"]:
223-
reference = Reference.from_ref(data["items"]["$ref"])
224-
_type = None
249+
return ReferenceListProperty(
250+
name=name, required=required, default=None, reference=Reference.from_ref(data["items"]["$ref"])
251+
)
252+
if "enum" in data["items"]:
253+
return EnumListProperty(
254+
name=name, required=required, default=None, values=EnumProperty.values_from_list(data["items"]["enum"])
255+
)
225256
if "type" in data["items"]:
226-
_type = _openapi_types_to_python_type_strings[data["items"]["type"]]
227-
return ListProperty(name=name, required=required, type=_type, reference=reference, default=None)
257+
return BasicListProperty(
258+
name=name,
259+
required=required,
260+
default=None,
261+
type=_openapi_types_to_python_type_strings[data["items"]["type"]],
262+
)
228263
elif data["type"] == "object":
229264
return DictProperty(name=name, required=required, default=data.get("default"))
230265
raise ValueError(f"Did not recognize type of {data}")

openapi_python_client/templates/async_endpoint_module.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Union
44
import httpx
55

66
from ..client import AuthenticatedClient, Client
7-
from .errors import ApiResponseError
7+
from ..errors import ApiResponseError
88

99
{% for relative in collection.relative_imports %}
1010
{{ relative }}

openapi_python_client/templates/endpoint_module.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Union
44
import httpx
55

66
from ..client import AuthenticatedClient, Client
7-
from .errors import ApiResponseError
7+
from ..errors import ApiResponseError
88

99
{% for relative in collection.relative_imports %}
1010
{{ relative }}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{ property.name }} = []
2+
for {{ property.name }}_item in d.get("{{ property.name }}", []):
3+
{{ property.name }}.append({{ property.reference.class_name }}({{ property.name }}_item))

openapi_python_client/templates/model.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class {{ schema.reference.class_name }}:
3535
{% else %}
3636
{{ property.name }} = {{ property.constructor_from_dict("d") }}
3737
{% endif %}
38+
3839
{% endfor %}
3940
return {{ schema.reference.class_name }}(
4041
{% for property in schema.required_properties + schema.optional_properties %}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
{% if property.type %}
2-
{{ property.name }} = d.get("{{ property.name }}", [])
3-
{% else %}
41
{{ property.name }} = []
52
for {{ property.name }}_item in d.get("{{ property.name }}", []):
63
{{ property.name }}.append({{ property.reference.class_name }}.from_dict({{ property.name }}_item))
7-
{% endif %}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "openapi-python-client"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
description = "Generate modern Python clients from OpenAPI"
55

66
authors = [

tests/test_end_to_end/fastapi/__init__.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
""" A FastAPI app used to create an OpenAPI document for end-to-end testing """
22
import json
3+
from enum import Enum
34
from pathlib import Path
5+
from typing import List
46

5-
from fastapi import FastAPI
7+
from fastapi import APIRouter, FastAPI, Query
68
from pydantic import BaseModel
79

810
app = FastAPI(title="My Test API", description="An API for testing openapi-python-client",)
@@ -18,6 +20,39 @@ async def ping():
1820
return {"success": True}
1921

2022

23+
test_router = APIRouter()
24+
25+
26+
class TestEnum(Enum):
27+
""" For testing Enums in all the ways they can be used """
28+
29+
FIRST_VALUE = "FIRST_VALUE"
30+
SECOND_VALUE = "SECOND_VALUE"
31+
32+
33+
class OtherModel(BaseModel):
34+
""" A different model for calling from TestModel """
35+
36+
a_value: str
37+
38+
39+
class TestModel(BaseModel):
40+
""" A Model for testing all the ways custom objects can be used """
41+
42+
an_enum_value: TestEnum
43+
a_list_of_enums: List[TestEnum]
44+
a_list_of_strings: List[str]
45+
a_list_of_objects: List[OtherModel]
46+
47+
48+
@test_router.get("/", response_model=List[TestModel])
49+
def test_getting_lists(statuses: List[TestEnum] = Query(...),):
50+
""" Get users, filtered by statuses """
51+
return
52+
53+
54+
app.include_router(test_router, prefix="/tests", tags=["users"])
55+
2156
if __name__ == "__main__":
2257
path = Path(__file__).parent / "openapi.json"
2358
path.write_text(json.dumps(app.openapi()))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"openapi": "3.0.2", "info": {"title": "My Test API", "description": "An API for testing openapi-python-client", "version": "0.1.0"}, "paths": {"/ping": {"get": {"summary": "Ping", "description": "A quick check to see if the system is running ", "operationId": "ping_ping_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/_ABCResponse"}}}}}}}}, "components": {"schemas": {"_ABCResponse": {"title": "_ABCResponse", "required": ["success"], "type": "object", "properties": {"success": {"title": "Success", "type": "boolean"}}}}}}
1+
{"openapi": "3.0.2", "info": {"title": "My Test API", "description": "An API for testing openapi-python-client", "version": "0.1.0"}, "paths": {"/ping": {"get": {"summary": "Ping", "description": "A quick check to see if the system is running ", "operationId": "ping_ping_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/_ABCResponse"}}}}}}}, "/tests/": {"get": {"tags": ["users"], "summary": "Test Getting Lists", "description": "Get users, filtered by statuses ", "operationId": "test_getting_lists_tests__get", "parameters": [{"required": true, "schema": {"title": "Statuses", "type": "array", "items": {"enum": ["FIRST_VALUE", "SECOND_VALUE"]}}, "name": "statuses", "in": "query"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Test Getting Lists Tests Get", "type": "array", "items": {"$ref": "#/components/schemas/TestModel"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"HTTPValidationError": {"title": "HTTPValidationError", "type": "object", "properties": {"detail": {"title": "Detail", "type": "array", "items": {"$ref": "#/components/schemas/ValidationError"}}}}, "OtherModel": {"title": "OtherModel", "required": ["a_value"], "type": "object", "properties": {"a_value": {"title": "A Value", "type": "string"}}, "description": "A different model for calling from TestModel "}, "TestModel": {"title": "TestModel", "required": ["an_enum_value", "a_list_of_enums", "a_list_of_strings", "a_list_of_objects"], "type": "object", "properties": {"an_enum_value": {"title": "An Enum Value", "enum": ["FIRST_VALUE", "SECOND_VALUE"]}, "a_list_of_enums": {"title": "A List Of Enums", "type": "array", "items": {"enum": ["FIRST_VALUE", "SECOND_VALUE"]}}, "a_list_of_strings": {"title": "A List Of Strings", "type": "array", "items": {"type": "string"}}, "a_list_of_objects": {"title": "A List Of Objects", "type": "array", "items": {"$ref": "#/components/schemas/OtherModel"}}}, "description": "A Model for testing all the ways custom objects can be used "}, "ValidationError": {"title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", "properties": {"loc": {"title": "Location", "type": "array", "items": {"type": "string"}}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}}, "_ABCResponse": {"title": "_ABCResponse", "required": ["success"], "type": "object", "properties": {"success": {"title": "Success", "type": "boolean"}}}}}}

tests/test_end_to_end/golden-master/my_test_api_client/api/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import httpx
55

66
from ..client import AuthenticatedClient, Client
7+
from ..errors import ApiResponseError
78
from ..models.abc_response import ABCResponse
8-
from .errors import ApiResponseError
99

1010

1111
def ping_ping_get(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from dataclasses import asdict
2+
from typing import Dict, List, Optional, Union
3+
4+
import httpx
5+
6+
from ..client import AuthenticatedClient, Client
7+
from ..errors import ApiResponseError
8+
from ..models.h_t_t_p_validation_error import HTTPValidationError
9+
from ..models.statuses import Statuses
10+
from ..models.test_model import TestModel
11+
12+
13+
def test_getting_lists_tests__get(
14+
*, client: Client, statuses: List[Statuses],
15+
) -> Union[
16+
List[TestModel], HTTPValidationError,
17+
]:
18+
""" Get users, filtered by statuses """
19+
url = f"{client.base_url}/tests/"
20+
21+
params = {
22+
"statuses": statuses,
23+
}
24+
25+
response = httpx.get(url=url, headers=client.get_headers(), params=params,)
26+
27+
if response.status_code == 200:
28+
return [TestModel.from_dict(item) for item in response.json()]
29+
if response.status_code == 422:
30+
return HTTPValidationError.from_dict(response.json())
31+
else:
32+
raise ApiResponseError(response=response)

tests/test_end_to_end/golden-master/my_test_api_client/async_api/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import httpx
55

66
from ..client import AuthenticatedClient, Client
7+
from ..errors import ApiResponseError
78
from ..models.abc_response import ABCResponse
8-
from .errors import ApiResponseError
99

1010

1111
async def ping_ping_get(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from dataclasses import asdict
2+
from typing import Dict, List, Optional, Union
3+
4+
import httpx
5+
6+
from ..client import AuthenticatedClient, Client
7+
from ..errors import ApiResponseError
8+
from ..models.h_t_t_p_validation_error import HTTPValidationError
9+
from ..models.statuses import Statuses
10+
from ..models.test_model import TestModel
11+
12+
13+
async def test_getting_lists_tests__get(
14+
*, client: Client, statuses: List[Statuses],
15+
) -> Union[
16+
List[TestModel], HTTPValidationError,
17+
]:
18+
""" Get users, filtered by statuses """
19+
url = f"{client.base_url}/tests/"
20+
21+
params = {
22+
"statuses": statuses,
23+
}
24+
25+
with httpx.AsyncClient() as client:
26+
response = await client.get(url=url, headers=client.get_headers(), params=params,)
27+
28+
if response.status_code == 200:
29+
return [TestModel.from_dict(item) for item in response.json()]
30+
if response.status_code == 422:
31+
return HTTPValidationError.from_dict(response.json())
32+
else:
33+
raise ApiResponseError(response=response)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
""" Contains all the data models used in inputs/outputs """
22

3+
from .a_list_of_enums import AListOfEnums
34
from .abc_response import ABCResponse
5+
from .an_enum_value import AnEnumValue
6+
from .h_t_t_p_validation_error import HTTPValidationError
7+
from .other_model import OtherModel
8+
from .statuses import Statuses
9+
from .test_model import TestModel
10+
from .validation_error import ValidationError
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class AListOfEnums(str, Enum):
5+
FIRST_VALUE = "FIRST_VALUE"
6+
SECOND_VALUE = "SECOND_VALUE"

tests/test_end_to_end/golden-master/my_test_api_client/models/abc_response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ def to_dict(self) -> Dict:
2020
def from_dict(d: Dict) -> ABCResponse:
2121

2222
success = d["success"]
23+
2324
return ABCResponse(success=success,)

0 commit comments

Comments
 (0)