Skip to content

Commit 07df708

Browse files
authored
Merge branch 'main' into fix-generated-readme-async-example
2 parents c65ec61 + d535bd0 commit 07df708

File tree

10 files changed

+124
-4
lines changed

10 files changed

+124
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Attempt to detect and alert users if they are using an unsupported version of OpenAPI (#281).
2222
- Fixes `Enum` deserialization when the value is `UNSET`.
2323
- Add handling of application/vnd.api+json media type.
24+
- Support passing models into query parameters (#316). Thanks @forest-benchling!
2425

2526
### Changes
2627

@@ -30,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3031

3132
### Fixes
3233

34+
- Endpoint tags are now sanitized during parsing to fix an issue where `My Tag` and `MyTag` are seen as two different tags but are then later unified, causing errors when creating directories. Thanks @p1-ra! (#328)
3335
- Parser will softly ignore value error during schema responses' status code convertion from string to integer (not a number). Errors will be reported to the end user and parsing will continue to proceed (#327).
3436
- The generated `from_dict` and `to_dict` methods of models will now properly handle `nullable` and `not required` properties that are themselves generated models (#315). Thanks @forest-benchling!
3537
- Fixed a typo in the async example in generated README.md files (#337). Thanks @synchronizing!

end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ...models.an_enum import AnEnum
1515
from ...models.http_validation_error import HTTPValidationError
16+
from ...models.model_with_union_property import ModelWithUnionProperty
1617
from ...types import UNSET, Unset
1718

1819

@@ -50,6 +51,8 @@ def httpx_request(
5051
union_prop: Union[Unset, float, str] = "not a float",
5152
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
5253
enum_prop: Union[Unset, AnEnum] = UNSET,
54+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
55+
required_model_prop: ModelWithUnionProperty,
5356
) -> Response[Union[None, HTTPValidationError]]:
5457

5558
json_datetime_prop: Union[Unset, str] = UNSET
@@ -89,6 +92,12 @@ def httpx_request(
8992
if not isinstance(enum_prop, Unset):
9093
json_enum_prop = enum_prop
9194

95+
json_model_prop: Union[Unset, Dict[str, Any]] = UNSET
96+
if not isinstance(model_prop, Unset):
97+
json_model_prop = model_prop.to_dict()
98+
99+
json_required_model_prop = required_model_prop.to_dict()
100+
92101
params: Dict[str, Any] = {
93102
"string_prop": string_prop,
94103
"datetime_prop": json_datetime_prop,
@@ -101,6 +110,9 @@ def httpx_request(
101110
"union_prop_with_ref": json_union_prop_with_ref,
102111
"enum_prop": json_enum_prop,
103112
}
113+
if not isinstance(json_model_prop, Unset):
114+
params.update(json_model_prop)
115+
params.update(json_required_model_prop)
104116
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
105117

106118
response = client.request(

end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ...client import Client
88
from ...models.an_enum import AnEnum
99
from ...models.http_validation_error import HTTPValidationError
10+
from ...models.model_with_union_property import ModelWithUnionProperty
1011
from ...types import UNSET, Response, Unset
1112

1213

@@ -23,6 +24,8 @@ def _get_kwargs(
2324
union_prop: Union[Unset, float, str] = "not a float",
2425
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
2526
enum_prop: Union[Unset, AnEnum] = UNSET,
27+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
28+
required_model_prop: ModelWithUnionProperty,
2629
) -> Dict[str, Any]:
2730
url = "{}/tests/defaults".format(client.base_url)
2831

@@ -65,6 +68,12 @@ def _get_kwargs(
6568
if not isinstance(enum_prop, Unset):
6669
json_enum_prop = enum_prop
6770

71+
json_model_prop: Union[Unset, Dict[str, Any]] = UNSET
72+
if not isinstance(model_prop, Unset):
73+
json_model_prop = model_prop.to_dict()
74+
75+
json_required_model_prop = required_model_prop.to_dict()
76+
6877
params: Dict[str, Any] = {
6978
"string_prop": string_prop,
7079
"datetime_prop": json_datetime_prop,
@@ -77,6 +86,9 @@ def _get_kwargs(
7786
"union_prop_with_ref": json_union_prop_with_ref,
7887
"enum_prop": json_enum_prop,
7988
}
89+
if not isinstance(json_model_prop, Unset):
90+
params.update(json_model_prop)
91+
params.update(json_required_model_prop)
8092
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
8193

8294
return {
@@ -122,6 +134,8 @@ def sync_detailed(
122134
union_prop: Union[Unset, float, str] = "not a float",
123135
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
124136
enum_prop: Union[Unset, AnEnum] = UNSET,
137+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
138+
required_model_prop: ModelWithUnionProperty,
125139
) -> Response[Union[None, HTTPValidationError]]:
126140
kwargs = _get_kwargs(
127141
client=client,
@@ -135,6 +149,8 @@ def sync_detailed(
135149
union_prop=union_prop,
136150
union_prop_with_ref=union_prop_with_ref,
137151
enum_prop=enum_prop,
152+
model_prop=model_prop,
153+
required_model_prop=required_model_prop,
138154
)
139155

140156
response = httpx.post(
@@ -157,6 +173,8 @@ def sync(
157173
union_prop: Union[Unset, float, str] = "not a float",
158174
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
159175
enum_prop: Union[Unset, AnEnum] = UNSET,
176+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
177+
required_model_prop: ModelWithUnionProperty,
160178
) -> Optional[Union[None, HTTPValidationError]]:
161179
""" """
162180

@@ -172,6 +190,8 @@ def sync(
172190
union_prop=union_prop,
173191
union_prop_with_ref=union_prop_with_ref,
174192
enum_prop=enum_prop,
193+
model_prop=model_prop,
194+
required_model_prop=required_model_prop,
175195
).parsed
176196

177197

@@ -188,6 +208,8 @@ async def asyncio_detailed(
188208
union_prop: Union[Unset, float, str] = "not a float",
189209
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
190210
enum_prop: Union[Unset, AnEnum] = UNSET,
211+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
212+
required_model_prop: ModelWithUnionProperty,
191213
) -> Response[Union[None, HTTPValidationError]]:
192214
kwargs = _get_kwargs(
193215
client=client,
@@ -201,6 +223,8 @@ async def asyncio_detailed(
201223
union_prop=union_prop,
202224
union_prop_with_ref=union_prop_with_ref,
203225
enum_prop=enum_prop,
226+
model_prop=model_prop,
227+
required_model_prop=required_model_prop,
204228
)
205229

206230
async with httpx.AsyncClient() as _client:
@@ -222,6 +246,8 @@ async def asyncio(
222246
union_prop: Union[Unset, float, str] = "not a float",
223247
union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6,
224248
enum_prop: Union[Unset, AnEnum] = UNSET,
249+
model_prop: Union[ModelWithUnionProperty, Unset] = UNSET,
250+
required_model_prop: ModelWithUnionProperty,
225251
) -> Optional[Union[None, HTTPValidationError]]:
226252
""" """
227253

@@ -238,5 +264,7 @@ async def asyncio(
238264
union_prop=union_prop,
239265
union_prop_with_ref=union_prop_with_ref,
240266
enum_prop=enum_prop,
267+
model_prop=model_prop,
268+
required_model_prop=required_model_prop,
241269
)
242270
).parsed

end_to_end_tests/openapi.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,22 @@
396396
},
397397
"name": "enum_prop",
398398
"in": "query"
399+
},
400+
{
401+
"required": false,
402+
"schema": {
403+
"$ref": "#/components/schemas/ModelWithUnionProperty"
404+
},
405+
"name": "model_prop",
406+
"in": "query"
407+
},
408+
{
409+
"required": true,
410+
"schema": {
411+
"$ref": "#/components/schemas/ModelWithUnionProperty"
412+
},
413+
"name": "required_model_prop",
414+
"in": "query"
399415
}
400416
],
401417
"responses": {

openapi_python_client/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ def _build_api(self) -> None:
235235

236236
endpoint_template = self.env.get_template("endpoint_module.py.jinja")
237237
for tag, collection in self.openapi.endpoint_collections_by_tag.items():
238-
tag = utils.snake_case(tag)
239238
tag_dir = api_dir / tag
240239
tag_dir.mkdir()
241240
(tag_dir / "__init__.py").touch()

openapi_python_client/parser/openapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def from_data(
4848
operation: Optional[oai.Operation] = getattr(path_data, method)
4949
if operation is None:
5050
continue
51-
tag = (operation.tags or ["default"])[0]
51+
tag = utils.snake_case((operation.tags or ["default"])[0])
5252
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
5353
endpoint, schemas = Endpoint.from_data(
5454
data=operation, path=path, method=method, tag=tag, schemas=schemas

openapi_python_client/parser/properties/model_property.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ModelProperty(Property):
1919
additional_properties: Union[bool, Property]
2020

2121
template: ClassVar[str] = "model_property.py.jinja"
22+
json_is_dict: ClassVar[bool] = True
2223

2324
def get_type_string(self, no_optional: bool = False) -> str:
2425
""" Get a string representation of type that should be used when declaring this property """

openapi_python_client/parser/properties/property.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Property:
2828
python_name: str = attr.ib(init=False)
2929

3030
template: ClassVar[Optional[str]] = None
31+
json_is_dict: ClassVar[bool] = False
3132

3233
def __attrs_post_init__(self) -> None:
3334
object.__setattr__(self, "python_name", utils.to_valid_python_identifier(utils.snake_case(self.name)))

openapi_python_client/templates/endpoint_macros.py.jinja

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,26 @@ if {{ parameter.python_name }} is not UNSET:
2222
{% endfor %}
2323
params: Dict[str, Any] = {
2424
{% for property in endpoint.query_parameters %}
25-
{% if property.template %}
25+
{% if not property.json_is_dict %}
26+
{% if property.template %}
2627
"{{ property.name }}": {{ "json_" + property.python_name }},
27-
{% else %}
28+
{% else %}
2829
"{{ property.name }}": {{ property.python_name }},
30+
{% endif %}
2931
{% endif %}
3032
{% endfor %}
3133
}
34+
{% for property in endpoint.query_parameters %}
35+
{% if property.json_is_dict %}
36+
{% set property_name = "json_" + property.python_name %}
37+
{% if property.required and not property.nullable %}
38+
params.update({{ property_name }})
39+
{% else %}
40+
if {% if not property.required %}not isinstance({{ property_name }}, Unset){% endif %}{% if not property.required and property.nullable %} and {% endif %}{% if property.nullable %}{{ property_name }} is not None{% endif %}:
41+
params.update({{ property_name }})
42+
{% endif %}
43+
{% endif %}
44+
{% endfor %}
3245
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
3346
{% endif %}
3447
{% endmacro %}

tests/test_parser/test_openapi.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,3 +829,51 @@ def test_from_data_errors(self, mocker):
829829
assert result["default"].parse_errors[1].data == "3"
830830
assert result["tag_2"].parse_errors[0].data == "2"
831831
assert result_schemas == schemas_3
832+
833+
def test_from_data_tags_snake_case_sanitizer(self, mocker):
834+
from openapi_python_client.parser.openapi import Endpoint, EndpointCollection
835+
836+
path_1_put = oai.Operation.construct()
837+
path_1_post = oai.Operation.construct(tags=["AMF Subscription Info (Document)", "tag_3"])
838+
path_2_get = oai.Operation.construct()
839+
data = {
840+
"path_1": oai.PathItem.construct(post=path_1_post, put=path_1_put),
841+
"path_2": oai.PathItem.construct(get=path_2_get),
842+
}
843+
endpoint_1 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"1", "2"})
844+
endpoint_2 = mocker.MagicMock(autospec=Endpoint, tag="AMFSubscriptionInfo (Document)", relative_imports={"2"})
845+
endpoint_3 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"2", "3"})
846+
schemas_1 = mocker.MagicMock()
847+
schemas_2 = mocker.MagicMock()
848+
schemas_3 = mocker.MagicMock()
849+
endpoint_from_data = mocker.patch.object(
850+
Endpoint,
851+
"from_data",
852+
side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)],
853+
)
854+
schemas = mocker.MagicMock()
855+
856+
result = EndpointCollection.from_data(data=data, schemas=schemas)
857+
858+
endpoint_from_data.assert_has_calls(
859+
[
860+
mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas),
861+
mocker.call(
862+
data=path_1_post,
863+
path="path_1",
864+
method="post",
865+
tag="amf_subscription_info_document",
866+
schemas=schemas_1,
867+
),
868+
mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2),
869+
],
870+
)
871+
assert result == (
872+
{
873+
"default": EndpointCollection("default", endpoints=[endpoint_1, endpoint_3]),
874+
"amf_subscription_info_document": EndpointCollection(
875+
"amf_subscription_info_document", endpoints=[endpoint_2]
876+
),
877+
},
878+
schemas_3,
879+
)

0 commit comments

Comments
 (0)