Skip to content

Commit 227bc5e

Browse files
supermihidbanty
andauthored
feat: Support inlined form data schema in requestBody [#656, #662]. Thanks @supermihi!
Co-authored-by: Dylan Anthony <[email protected]> Co-authored-by: Michael Helmling <[email protected]>
1 parent b20789b commit 227bc5e

File tree

10 files changed

+318
-45
lines changed

10 files changed

+318
-45
lines changed

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
no_response_tests_no_response_get,
1515
octet_stream_tests_octet_stream_get,
1616
post_form_data,
17+
post_form_data_inline,
1718
post_tests_json_body_string,
1819
test_inline_objects,
1920
token_with_cookie_auth_token_with_cookie_get,
@@ -66,6 +67,13 @@ def post_form_data(cls) -> types.ModuleType:
6667
"""
6768
return post_form_data
6869

70+
@classmethod
71+
def post_form_data_inline(cls) -> types.ModuleType:
72+
"""
73+
Post form data (inline schema)
74+
"""
75+
return post_form_data_inline
76+
6977
@classmethod
7078
def upload_file_tests_upload_post(cls) -> types.ModuleType:
7179
"""

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def sync_detailed(
4141
client: Client,
4242
form_data: AFormData,
4343
) -> Response[Any]:
44-
"""Post from data
44+
"""Post form data
4545
4646
Post form data
4747
@@ -67,7 +67,7 @@ async def asyncio_detailed(
6767
client: Client,
6868
form_data: AFormData,
6969
) -> Response[Any]:
70-
"""Post from data
70+
"""Post form data
7171
7272
Post form data
7373
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import Any, Dict
2+
3+
import httpx
4+
5+
from ...client import Client
6+
from ...models.post_form_data_inline_data import PostFormDataInlineData
7+
from ...types import Response
8+
9+
10+
def _get_kwargs(
11+
*,
12+
client: Client,
13+
form_data: PostFormDataInlineData,
14+
) -> Dict[str, Any]:
15+
url = "{}/tests/post_form_data_inline".format(client.base_url)
16+
17+
headers: Dict[str, str] = client.get_headers()
18+
cookies: Dict[str, Any] = client.get_cookies()
19+
20+
return {
21+
"method": "post",
22+
"url": url,
23+
"headers": headers,
24+
"cookies": cookies,
25+
"timeout": client.get_timeout(),
26+
"data": form_data.to_dict(),
27+
}
28+
29+
30+
def _build_response(*, response: httpx.Response) -> Response[Any]:
31+
return Response(
32+
status_code=response.status_code,
33+
content=response.content,
34+
headers=response.headers,
35+
parsed=None,
36+
)
37+
38+
39+
def sync_detailed(
40+
*,
41+
client: Client,
42+
form_data: PostFormDataInlineData,
43+
) -> Response[Any]:
44+
"""Post form data (inline schema)
45+
46+
Post form data (inline schema)
47+
48+
Returns:
49+
Response[Any]
50+
"""
51+
52+
kwargs = _get_kwargs(
53+
client=client,
54+
form_data=form_data,
55+
)
56+
57+
response = httpx.request(
58+
verify=client.verify_ssl,
59+
**kwargs,
60+
)
61+
62+
return _build_response(response=response)
63+
64+
65+
async def asyncio_detailed(
66+
*,
67+
client: Client,
68+
form_data: PostFormDataInlineData,
69+
) -> Response[Any]:
70+
"""Post form data (inline schema)
71+
72+
Post form data (inline schema)
73+
74+
Returns:
75+
Response[Any]
76+
"""
77+
78+
kwargs = _get_kwargs(
79+
client=client,
80+
form_data=form_data,
81+
)
82+
83+
async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
84+
response = await _client.request(**kwargs)
85+
86+
return _build_response(response=response)

end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0
4343
from .model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1
4444
from .none import None_
45+
from .post_form_data_inline_data import PostFormDataInlineData
4546
from .post_responses_unions_simple_before_complex_response_200 import PostResponsesUnionsSimpleBeforeComplexResponse200
4647
from .post_responses_unions_simple_before_complex_response_200a_type_1 import (
4748
PostResponsesUnionsSimpleBeforeComplexResponse200AType1,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from typing import Any, Dict, List, Type, TypeVar, Union
2+
3+
import attr
4+
5+
from ..types import UNSET, Unset
6+
7+
T = TypeVar("T", bound="PostFormDataInlineData")
8+
9+
10+
@attr.s(auto_attribs=True)
11+
class PostFormDataInlineData:
12+
"""
13+
Attributes:
14+
a_required_field (str):
15+
an_optional_field (Union[Unset, str]):
16+
"""
17+
18+
a_required_field: str
19+
an_optional_field: Union[Unset, str] = UNSET
20+
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
21+
22+
def to_dict(self) -> Dict[str, Any]:
23+
a_required_field = self.a_required_field
24+
an_optional_field = self.an_optional_field
25+
26+
field_dict: Dict[str, Any] = {}
27+
field_dict.update(self.additional_properties)
28+
field_dict.update(
29+
{
30+
"a_required_field": a_required_field,
31+
}
32+
)
33+
if an_optional_field is not UNSET:
34+
field_dict["an_optional_field"] = an_optional_field
35+
36+
return field_dict
37+
38+
@classmethod
39+
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
40+
d = src_dict.copy()
41+
a_required_field = d.pop("a_required_field")
42+
43+
an_optional_field = d.pop("an_optional_field", UNSET)
44+
45+
post_form_data_inline_data = cls(
46+
a_required_field=a_required_field,
47+
an_optional_field=an_optional_field,
48+
)
49+
50+
post_form_data_inline_data.additional_properties = d
51+
return post_form_data_inline_data
52+
53+
@property
54+
def additional_keys(self) -> List[str]:
55+
return list(self.additional_properties.keys())
56+
57+
def __getitem__(self, key: str) -> Any:
58+
return self.additional_properties[key]
59+
60+
def __setitem__(self, key: str, value: Any) -> None:
61+
self.additional_properties[key] = value
62+
63+
def __delitem__(self, key: str) -> None:
64+
del self.additional_properties[key]
65+
66+
def __contains__(self, key: str) -> bool:
67+
return key in self.additional_properties

end_to_end_tests/openapi.json

+42-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@
217217
"tags": [
218218
"tests"
219219
],
220-
"summary": "Post from data",
220+
"summary": "Post form data",
221221
"description": "Post form data",
222222
"operationId": "post_form_data",
223223
"requestBody": {
@@ -242,6 +242,47 @@
242242
}
243243
}
244244
},
245+
"/tests/post_form_data_inline": {
246+
"post": {
247+
"tags": [
248+
"tests"
249+
],
250+
"summary": "Post form data (inline schema)",
251+
"description": "Post form data (inline schema)",
252+
"operationId": "post_form_data_inline",
253+
"requestBody": {
254+
"content": {
255+
"application/x-www-form-urlencoded": {
256+
"schema": {
257+
"type": "object",
258+
"properties": {
259+
"an_optional_field": {
260+
"type": "string"
261+
},
262+
"a_required_field": {
263+
"type": "string"
264+
}
265+
},
266+
"required": [
267+
"a_required_field"
268+
]
269+
}
270+
}
271+
},
272+
"required": true
273+
},
274+
"responses": {
275+
"200": {
276+
"description": "Successful Response",
277+
"content": {
278+
"application/json": {
279+
"schema": {}
280+
}
281+
}
282+
}
283+
}
284+
}
285+
},
245286
"/tests/upload": {
246287
"post": {
247288
"tags": [

openapi_python_client/parser/openapi.py

+38-12
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,32 @@ class Endpoint:
118118
header_parameters: Dict[str, Property] = field(default_factory=dict)
119119
cookie_parameters: Dict[str, Property] = field(default_factory=dict)
120120
responses: List[Response] = field(default_factory=list)
121-
form_body_class: Optional[Class] = None
121+
form_body: Optional[Property] = None
122122
json_body: Optional[Property] = None
123123
multipart_body: Optional[Property] = None
124124
errors: List[ParseError] = field(default_factory=list)
125125
used_python_identifiers: Set[PythonIdentifier] = field(default_factory=set)
126126

127127
@staticmethod
128-
def parse_request_form_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]:
129-
"""Return form_body_reference"""
128+
def parse_request_form_body(
129+
*, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config
130+
) -> Tuple[Union[Property, PropertyError, None], Schemas]:
131+
"""Return form_body and updated schemas"""
130132
body_content = body.content
131133
form_body = body_content.get("application/x-www-form-urlencoded")
132-
if form_body is not None and isinstance(form_body.media_type_schema, oai.Reference):
133-
return Class.from_string(string=form_body.media_type_schema.ref, config=config)
134-
return None
134+
if form_body is not None and form_body.media_type_schema is not None:
135+
prop, schemas = property_from_data(
136+
name="data",
137+
required=True,
138+
data=form_body.media_type_schema,
139+
schemas=schemas,
140+
parent_name=parent_name,
141+
config=config,
142+
)
143+
if isinstance(prop, ModelProperty):
144+
schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, prop.class_info.name: prop})
145+
return prop, schemas
146+
return None, schemas
135147

136148
@staticmethod
137149
def parse_multipart_body(
@@ -186,7 +198,20 @@ def _add_body(
186198
if data.requestBody is None or isinstance(data.requestBody, oai.Reference):
187199
return endpoint, schemas
188200

189-
endpoint.form_body_class = Endpoint.parse_request_form_body(body=data.requestBody, config=config)
201+
form_body, schemas = Endpoint.parse_request_form_body(
202+
body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config
203+
)
204+
205+
if isinstance(form_body, ParseError):
206+
return (
207+
ParseError(
208+
header=f"Cannot parse form body of endpoint {endpoint.name}",
209+
detail=form_body.detail,
210+
data=form_body.data,
211+
),
212+
schemas,
213+
)
214+
190215
json_body, schemas = Endpoint.parse_request_json_body(
191216
body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config
192217
)
@@ -213,8 +238,9 @@ def _add_body(
213238
schemas,
214239
)
215240

216-
if endpoint.form_body_class:
217-
endpoint.relative_imports.add(import_string_from_class(endpoint.form_body_class, prefix="...models"))
241+
if form_body is not None:
242+
endpoint.form_body = form_body
243+
endpoint.relative_imports.update(endpoint.form_body.get_imports(prefix="..."))
218244
if multipart_body is not None:
219245
endpoint.multipart_body = multipart_body
220246
endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix="..."))
@@ -285,9 +311,9 @@ def add_parameters(
285311
config: User-provided config for overrides within parameters.
286312
287313
Returns:
288-
`(result, schemas)` where `result` is either an updated Endpoint containing the parameters or a ParseError
289-
describing what went wrong. `schemas` is an updated version of the `schemas` input, adding any new enums
290-
or classes.
314+
`(result, schemas, parameters)` where `result` is either an updated Endpoint containing the parameters or a
315+
ParseError describing what went wrong. `schemas` is an updated version of the `schemas` input, adding any
316+
new enums or classes. `parameters` is an updated version of the `parameters` input, adding new parameters.
291317
292318
See Also:
293319
- https://swagger.io/docs/specification/describing-parameters/

openapi_python_client/templates/endpoint_macros.py.jinja

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ client: AuthenticatedClient,
9090
client: Client,
9191
{% endif %}
9292
{# Form data if any #}
93-
{% if endpoint.form_body_class %}
94-
form_data: {{ endpoint.form_body_class.name }},
93+
{% if endpoint.form_body %}
94+
form_data: {{ endpoint.form_body.get_type_string() }},
9595
{% endif %}
9696
{# Multipart data if any #}
9797
{% if endpoint.multipart_body %}
@@ -120,7 +120,7 @@ json_body: {{ endpoint.json_body.get_type_string() }},
120120
{{ parameter.python_name }}={{ parameter.python_name }},
121121
{% endfor %}
122122
client=client,
123-
{% if endpoint.form_body_class %}
123+
{% if endpoint.form_body %}
124124
form_data=form_data,
125125
{% endif %}
126126
{% if endpoint.multipart_body %}

openapi_python_client/templates/endpoint_module.py.jinja

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _get_kwargs(
4444
"headers": headers,
4545
"cookies": cookies,
4646
"timeout": client.get_timeout(),
47-
{% if endpoint.form_body_class %}
47+
{% if endpoint.form_body %}
4848
"data": form_data.to_dict(),
4949
{% elif endpoint.multipart_body %}
5050
"files": {{ "multipart_" + endpoint.multipart_body.python_name }},

0 commit comments

Comments
 (0)