Skip to content

Commit b3f13a0

Browse files
dbantyemann
andauthored
Support inline object schemas (#236)
* Major refactor of models/enums/schemas. * Switch properties to use attr.s instead of dataclass * Fix forward references and properly allow Unset for ModelPropertys * Refactor response handling to use the same schema generation as input properties. * Added improved naming scheme of models/enums using parent elements Co-authored-by: Ethan Mann <emann@triaxtec.com>
1 parent 296ffb6 commit b3f13a0

File tree

91 files changed

+3106
-2837
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+3106
-2837
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ test-reports/
2626
htmlcov/
2727

2828
# Generated end to end test data
29-
my-test-api-client
29+
my-test-api-client/
30+
custom-e2e/

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Any request/response field that is not `required` and wasn't specified is now set to `UNSET` instead of `None`.
1313
- Values that are `UNSET` will not be sent along in API calls
14+
- Schemas defined with `type=object` will now be converted into classes, just like if they were created as ref components.
15+
The previous behavior was a combination of skipping and using generic Dicts for these schemas.
16+
- Response schema handling was unified with input schema handling, meaning that responses will behave differently than before.
17+
Specifically, instead of the content-type deciding what the generated Python type is, the schema itself will.
18+
- As a result of this, endpoints that used to return `bytes` when content-type was application/octet-stream will now return a `File` object if the type of the data is "binary", just like if you were submitting that type instead of receiving it.
19+
- Instead of skipping input properties with no type, enum, anyOf, or oneOf declared, the property will be declared as `None`.
20+
- Class (models and Enums) names will now contain the name of their parent element (if any). For example, a property
21+
declared in an endpoint will be named like {endpoint_name}_{previous_class_name}. Classes will no longer be
22+
deduplicated by appending a number to the end of the generated name, so if two names conflict with this new naming
23+
scheme, there will be an error instead.
1424

1525
### Additions
1626

1727
- Added a `--custom-template-path` option for providing custom jinja2 templates (#231 - Thanks @erichulburd!).
1828
- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar!
29+
- Support for all the same schemas in responses as are supported in parameters.
1930

2031
## 0.6.2 - 2020-11-03
2132

end_to_end_tests/custom_config.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
project_name_override: "custom-e2e"
2+
package_name_override: "custom_e2e"
3+
class_overrides:
4+
_ABCResponse:
5+
class_name: ABCResponse
6+
module_name: abc_response
7+
AnEnumValueItem:
8+
class_name: AnEnumValue
9+
module_name: an_enum_value
10+
NestedListOfEnumsItemItem:
11+
class_name: AnEnumValue
12+
module_name: an_enum_value
13+
field_prefix: attr_

end_to_end_tests/golden-record-custom/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
1-
# my-test-api-client
1+
# custom-e2e
22
A client library for accessing My Test API
33

44
## Usage
55
First, create a client:
66

77
```python
8-
from my_test_api_client import Client
8+
from custom_e2e import Client
99

1010
client = Client(base_url="https://api.example.com")
1111
```
1212

1313
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
1414

1515
```python
16-
from my_test_api_client import AuthenticatedClient
16+
from custom_e2e import AuthenticatedClient
1717

1818
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
1919
```
2020

2121
Now call your endpoint and use your models:
2222

2323
```python
24-
from my_test_api_client.models import MyDataModel
25-
from my_test_api_client.api.my_tag import get_my_data_model
24+
from custom_e2e.models import MyDataModel
25+
from custom_e2e.api.my_tag import get_my_data_model
2626

2727
my_data: MyDataModel = get_my_data_model(client=client)
2828
```
2929

3030
Or do the same thing with an async version:
3131

3232
```python
33-
from my_test_api_client.models import MyDataModel
34-
from my_test_api_client.async_api.my_tag import get_my_data_model
33+
from custom_e2e.models import MyDataModel
34+
from custom_e2e.async_api.my_tag import get_my_data_model
3535

3636
my_data: MyDataModel = await get_my_data_model(client=client)
3737
```
@@ -40,9 +40,9 @@ Things to know:
4040
1. Every path/method combo becomes a Python function with type annotations.
4141
1. All path/query params, and bodies become method arguments.
4242
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
43-
1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default`
43+
1. Any endpoint which did not have a tag will be in `custom_e2e.api.default`
4444
1. If the API returns a response code that was not declared in the OpenAPI document, a
45-
`my_test_api_client.api.errors.ApiResponseError` wil be raised
45+
`custom_e2e.api.errors.ApiResponseError` wil be raised
4646
with the `response` attribute set to the `httpx.Response` that was received.
4747

4848

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

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

79
import datetime
8-
from typing import Dict, List, Optional, Union, cast
10+
from typing import Dict, List, Union
911

1012
from dateutil.parser import isoparse
1113

@@ -16,14 +18,18 @@
1618

1719
def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]:
1820
if response.status_code == 200:
19-
return None
21+
response_200 = None
22+
23+
return response_200
2024
if response.status_code == 422:
21-
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
25+
response_422 = HTTPValidationError.from_dict(response.json())
26+
27+
return response_422
2228
return None
2329

2430

25-
def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]:
26-
return httpx.Response(
31+
def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]:
32+
return Response(
2733
status_code=response.status_code,
2834
content=response.content,
2935
headers=response.headers,
@@ -34,7 +40,6 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H
3440
def httpx_request(
3541
*,
3642
client: Client,
37-
json_body: Dict[Any, Any],
3843
string_prop: Union[Unset, str] = "the default string",
3944
datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"),
4045
date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(),
@@ -44,7 +49,7 @@ def httpx_request(
4449
list_prop: Union[Unset, List[AnEnum]] = UNSET,
4550
union_prop: Union[Unset, float, str] = "not a float",
4651
enum_prop: Union[Unset, AnEnum] = UNSET,
47-
) -> httpx.Response[Union[None, HTTPValidationError]]:
52+
) -> Response[Union[None, HTTPValidationError]]:
4853

4954
json_datetime_prop: Union[Unset, str] = UNSET
5055
if not isinstance(datetime_prop, Unset):
@@ -94,12 +99,9 @@ def httpx_request(
9499
if enum_prop is not UNSET:
95100
params["enum_prop"] = json_enum_prop
96101

97-
json_json_body = json_body
98-
99102
response = client.request(
100103
"post",
101104
"/tests/defaults",
102-
json=json_json_body,
103105
params=params,
104106
)
105107

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

9+
from typing import List, cast
10+
711

812
def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]:
913
if response.status_code == 200:
10-
return [bool(item) for item in cast(List[bool], response.json())]
14+
response_200 = cast(List[bool], response.json())
15+
16+
return response_200
1117
return None
1218

1319

14-
def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]:
15-
return httpx.Response(
20+
def _build_response(*, response: httpx.Response) -> Response[List[bool]]:
21+
return Response(
1622
status_code=response.status_code,
1723
content=response.content,
1824
headers=response.headers,
@@ -23,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]:
2329
def httpx_request(
2430
*,
2531
client: Client,
26-
) -> httpx.Response[List[bool]]:
32+
) -> Response[List[bool]]:
2733

2834
response = client.request(
2935
"get",
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

9+
from typing import List, cast
10+
711

812
def _parse_response(*, response: httpx.Response) -> Optional[List[float]]:
913
if response.status_code == 200:
10-
return [float(item) for item in cast(List[float], response.json())]
14+
response_200 = cast(List[float], response.json())
15+
16+
return response_200
1117
return None
1218

1319

14-
def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]:
15-
return httpx.Response(
20+
def _build_response(*, response: httpx.Response) -> Response[List[float]]:
21+
return Response(
1622
status_code=response.status_code,
1723
content=response.content,
1824
headers=response.headers,
@@ -23,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]:
2329
def httpx_request(
2430
*,
2531
client: Client,
26-
) -> httpx.Response[List[float]]:
32+
) -> Response[List[float]]:
2733

2834
response = client.request(
2935
"get",
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

9+
from typing import List, cast
10+
711

812
def _parse_response(*, response: httpx.Response) -> Optional[List[int]]:
913
if response.status_code == 200:
10-
return [int(item) for item in cast(List[int], response.json())]
14+
response_200 = cast(List[int], response.json())
15+
16+
return response_200
1117
return None
1218

1319

14-
def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]:
15-
return httpx.Response(
20+
def _build_response(*, response: httpx.Response) -> Response[List[int]]:
21+
return Response(
1622
status_code=response.status_code,
1723
content=response.content,
1824
headers=response.headers,
@@ -23,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]:
2329
def httpx_request(
2430
*,
2531
client: Client,
26-
) -> httpx.Response[List[int]]:
32+
) -> Response[List[int]]:
2733

2834
response = client.request(
2935
"get",
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

9+
from typing import List, cast
10+
711

812
def _parse_response(*, response: httpx.Response) -> Optional[List[str]]:
913
if response.status_code == 200:
10-
return [str(item) for item in cast(List[str], response.json())]
14+
response_200 = cast(List[str], response.json())
15+
16+
return response_200
1117
return None
1218

1319

14-
def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]:
15-
return httpx.Response(
20+
def _build_response(*, response: httpx.Response) -> Response[List[str]]:
21+
return Response(
1622
status_code=response.status_code,
1723
content=response.content,
1824
headers=response.headers,
@@ -23,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]:
2329
def httpx_request(
2430
*,
2531
client: Client,
26-
) -> httpx.Response[List[str]]:
32+
) -> Response[List[str]]:
2733

2834
response = client.request(
2935
"get",

end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py renamed to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import httpx
44

5+
from ...types import Response
6+
57
Client = httpx.Client
68

79
import datetime
8-
from typing import Dict, List, Union, cast
10+
from typing import Dict, List, Union
911

1012
from ...models.a_model import AModel
1113
from ...models.an_enum import AnEnum
@@ -14,14 +16,22 @@
1416

1517
def _parse_response(*, response: httpx.Response) -> Optional[Union[List[AModel], HTTPValidationError]]:
1618
if response.status_code == 200:
17-
return [AModel.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())]
19+
response_200 = []
20+
for response_200_item_data in response.json():
21+
response_200_item = AModel.from_dict(response_200_item_data)
22+
23+
response_200.append(response_200_item)
24+
25+
return response_200
1826
if response.status_code == 422:
19-
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
27+
response_422 = HTTPValidationError.from_dict(response.json())
28+
29+
return response_422
2030
return None
2131

2232

23-
def _build_response(*, response: httpx.Response) -> httpx.Response[Union[List[AModel], HTTPValidationError]]:
24-
return httpx.Response(
33+
def _build_response(*, response: httpx.Response) -> Response[Union[List[AModel], HTTPValidationError]]:
34+
return Response(
2535
status_code=response.status_code,
2636
content=response.content,
2737
headers=response.headers,
@@ -34,7 +44,7 @@ def httpx_request(
3444
client: Client,
3545
an_enum_value: List[AnEnum],
3646
some_date: Union[datetime.date, datetime.datetime],
37-
) -> httpx.Response[Union[List[AModel], HTTPValidationError]]:
47+
) -> Response[Union[List[AModel], HTTPValidationError]]:
3848

3949
json_an_enum_value = []
4050
for an_enum_value_item_data in an_enum_value:

0 commit comments

Comments
 (0)