Skip to content

Commit c258fab

Browse files
committed
Implement binary request bodies for endpoints
1 parent e421b21 commit c258fab

File tree

7 files changed

+297
-4
lines changed

7 files changed

+297
-4
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
json_body_tests_json_body_post,
1616
no_response_tests_no_response_get,
1717
octet_stream_tests_octet_stream_get,
18+
octet_stream_tests_octet_stream_post,
1819
post_form_data,
1920
post_form_data_inline,
2021
post_tests_json_body_string,
@@ -118,6 +119,13 @@ def octet_stream_tests_octet_stream_get(cls) -> types.ModuleType:
118119
"""
119120
return octet_stream_tests_octet_stream_get
120121

122+
@classmethod
123+
def octet_stream_tests_octet_stream_post(cls) -> types.ModuleType:
124+
"""
125+
Binary (octet stream) request body
126+
"""
127+
return octet_stream_tests_octet_stream_post
128+
121129
@classmethod
122130
def no_response_tests_no_response_get(cls) -> types.ModuleType:
123131
"""
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from http import HTTPStatus
2+
from typing import Any, Dict, Optional, Union, cast
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...models.http_validation_error import HTTPValidationError
9+
from ...types import File, Response
10+
11+
12+
def _get_kwargs(
13+
*,
14+
binary_body: File,
15+
) -> Dict[str, Any]:
16+
headers = {}
17+
headers["Content-Type"] = binary_body.mime_type if binary_body.mime_type else "application/octet-stream"
18+
19+
return {
20+
"method": "post",
21+
"url": "/tests/octet_stream",
22+
"content": binary_body,
23+
"headers": headers,
24+
}
25+
26+
27+
def _parse_response(
28+
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
29+
) -> Optional[Union[HTTPValidationError, str]]:
30+
if response.status_code == HTTPStatus.OK:
31+
response_200 = cast(str, response.json())
32+
return response_200
33+
if response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
34+
response_422 = HTTPValidationError.from_dict(response.json())
35+
36+
return response_422
37+
if client.raise_on_unexpected_status:
38+
raise errors.UnexpectedStatus(response.status_code, response.content)
39+
else:
40+
return None
41+
42+
43+
def _build_response(
44+
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
45+
) -> Response[Union[HTTPValidationError, str]]:
46+
return Response(
47+
status_code=HTTPStatus(response.status_code),
48+
content=response.content,
49+
headers=response.headers,
50+
parsed=_parse_response(client=client, response=response),
51+
)
52+
53+
54+
def sync_detailed(
55+
*,
56+
client: Union[AuthenticatedClient, Client],
57+
binary_body: File,
58+
) -> Response[Union[HTTPValidationError, str]]:
59+
"""Binary (octet stream) request body
60+
61+
Args:
62+
binary_body (File): A file to upload
63+
64+
Raises:
65+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
66+
httpx.TimeoutException: If the request takes longer than Client.timeout.
67+
68+
Returns:
69+
Response[Union[HTTPValidationError, str]]
70+
"""
71+
72+
kwargs = _get_kwargs(
73+
binary_body=binary_body,
74+
)
75+
76+
response = client.get_httpx_client().request(
77+
**kwargs,
78+
)
79+
80+
return _build_response(client=client, response=response)
81+
82+
83+
def sync(
84+
*,
85+
client: Union[AuthenticatedClient, Client],
86+
binary_body: File,
87+
) -> Optional[Union[HTTPValidationError, str]]:
88+
"""Binary (octet stream) request body
89+
90+
Args:
91+
binary_body (File): A file to upload
92+
93+
Raises:
94+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
95+
httpx.TimeoutException: If the request takes longer than Client.timeout.
96+
97+
Returns:
98+
Union[HTTPValidationError, str]
99+
"""
100+
101+
return sync_detailed(
102+
client=client,
103+
binary_body=binary_body,
104+
).parsed
105+
106+
107+
async def asyncio_detailed(
108+
*,
109+
client: Union[AuthenticatedClient, Client],
110+
binary_body: File,
111+
) -> Response[Union[HTTPValidationError, str]]:
112+
"""Binary (octet stream) request body
113+
114+
Args:
115+
binary_body (File): A file to upload
116+
117+
Raises:
118+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
119+
httpx.TimeoutException: If the request takes longer than Client.timeout.
120+
121+
Returns:
122+
Response[Union[HTTPValidationError, str]]
123+
"""
124+
125+
kwargs = _get_kwargs(
126+
binary_body=binary_body,
127+
)
128+
129+
response = await client.get_async_httpx_client().request(**kwargs)
130+
131+
return _build_response(client=client, response=response)
132+
133+
134+
async def asyncio(
135+
*,
136+
client: Union[AuthenticatedClient, Client],
137+
binary_body: File,
138+
) -> Optional[Union[HTTPValidationError, str]]:
139+
"""Binary (octet stream) request body
140+
141+
Args:
142+
binary_body (File): A file to upload
143+
144+
Raises:
145+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
146+
httpx.TimeoutException: If the request takes longer than Client.timeout.
147+
148+
Returns:
149+
Union[HTTPValidationError, str]
150+
"""
151+
152+
return (
153+
await asyncio_detailed(
154+
client=client,
155+
binary_body=binary_body,
156+
)
157+
).parsed

end_to_end_tests/openapi.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,46 @@
625625
}
626626
}
627627
}
628+
},
629+
"post": {
630+
"tags": [
631+
"tests"
632+
],
633+
"summary": "Binary (octet stream) request body",
634+
"operationId": "octet_stream_tests_octet_stream_post",
635+
"requestBody": {
636+
"content": {
637+
"application/octet-stream": {
638+
"schema": {
639+
"description": "A file to upload",
640+
"type": "string",
641+
"format": "binary"
642+
}
643+
}
644+
}
645+
},
646+
"responses": {
647+
"200": {
648+
"description": "success",
649+
"content": {
650+
"application/json": {
651+
"schema": {
652+
"type": "string"
653+
}
654+
}
655+
}
656+
},
657+
"422": {
658+
"description": "Validation Error",
659+
"content": {
660+
"application/json": {
661+
"schema": {
662+
"$ref": "#/components/schemas/HTTPValidationError"
663+
}
664+
}
665+
}
666+
}
667+
}
628668
}
629669
},
630670
"/tests/no_response": {

openapi_python_client/parser/openapi.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class Endpoint:
134134
form_body: Optional[Property] = None
135135
json_body: Optional[Property] = None
136136
multipart_body: Optional[Property] = None
137+
binary_body: Optional[Property] = None
137138
errors: List[ParseError] = field(default_factory=list)
138139
used_python_identifiers: Set[PythonIdentifier] = field(default_factory=set)
139140

@@ -204,6 +205,30 @@ def parse_request_json_body(
204205
)
205206
return None, schemas
206207

208+
@staticmethod
209+
def parse_request_binary_body(
210+
*, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config
211+
) -> Tuple[Union[Property, PropertyError, None], Schemas]:
212+
"""Return binary_body"""
213+
binary_body = None
214+
for content_type, schema in body.content.items():
215+
content_type = get_content_type(content_type) # noqa: PLW2901
216+
217+
if content_type == "application/octet-stream":
218+
binary_body = schema
219+
break
220+
221+
if binary_body is not None and binary_body.media_type_schema is not None:
222+
return property_from_data(
223+
name="binary_body",
224+
required=True,
225+
data=binary_body.media_type_schema,
226+
schemas=schemas,
227+
parent_name=parent_name,
228+
config=config,
229+
)
230+
return None, schemas
231+
207232
@staticmethod
208233
def _add_body(
209234
*,
@@ -220,6 +245,7 @@ def _add_body(
220245
request_body_parsers: list[tuple[str, RequestBodyParser]] = [
221246
("form_body", Endpoint.parse_request_form_body),
222247
("json_body", Endpoint.parse_request_json_body),
248+
("binary_body", Endpoint.parse_request_binary_body),
223249
("multipart_body", Endpoint.parse_multipart_body),
224250
]
225251

@@ -517,6 +543,8 @@ def iter_all_parameters(self) -> Iterator[Property]:
517543
yield self.multipart_body
518544
if self.json_body:
519545
yield self.json_body
546+
if self.binary_body:
547+
yield self.binary_body
520548

521549
def list_all_parameters(self) -> List[Property]:
522550
"""Return a List of all the parameters of this endpoint"""

openapi_python_client/templates/endpoint_macros.py.jinja

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
{% from "helpers.jinja" import safe_docstring %}
33

44
{% macro header_params(endpoint) %}
5-
{% if endpoint.header_parameters %}
5+
{% if endpoint.header_parameters or endpoint.binary_body %}
66
headers = {}
7+
{% if endpoint.header_parameters %}
78
{% for parameter in endpoint.header_parameters.values() %}
89
{% import "property_templates/" + parameter.template as param_template %}
910
{% if param_template.transform_header %}
@@ -15,6 +16,10 @@ headers = {}
1516
{{ guarded_statement(parameter, parameter.python_name, statement) }}
1617
{% endfor %}
1718
{% endif %}
19+
{% if endpoint.binary_body %}
20+
headers['Content-Type'] = {{ endpoint.binary_body.python_name }}.mime_type if {{ endpoint.binary_body.python_name}}.mime_type else 'application/octet-stream'
21+
{% endif %}
22+
{% endif %}
1823
{% endmacro %}
1924

2025
{% macro cookie_params(endpoint) %}
@@ -108,6 +113,9 @@ multipart_data: {{ endpoint.multipart_body.get_type_string() }},
108113
{% if endpoint.json_body %}
109114
json_body: {{ endpoint.json_body.get_type_string() }},
110115
{% endif %}
116+
{% if endpoint.binary_body %}
117+
binary_body: {{ endpoint.binary_body.get_type_string() }},
118+
{% endif %}
111119
{# query parameters #}
112120
{% for parameter in endpoint.query_parameters.values() %}
113121
{{ parameter.to_string() }},
@@ -138,6 +146,9 @@ multipart_data=multipart_data,
138146
{% if endpoint.json_body %}
139147
json_body=json_body,
140148
{% endif %}
149+
{% if endpoint.binary_body %}
150+
binary_body=binary_body,
151+
{% endif %}
141152
{% for parameter in endpoint.query_parameters.values() %}
142153
{{ parameter.python_name }}={{ parameter.python_name }},
143154
{% endfor %}

openapi_python_client/templates/endpoint_module.py.jinja

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ def _get_kwargs(
4747
"files": {{ "multipart_" + endpoint.multipart_body.python_name }},
4848
{% elif endpoint.json_body %}
4949
"json": {{ "json_" + endpoint.json_body.python_name }},
50+
{% elif endpoint.binary_body %}
51+
"content": {{ endpoint.binary_body.python_name }},
5052
{% endif %}
5153
{% if endpoint.query_parameters %}
5254
"params": params,
5355
{% endif %}
54-
{% if endpoint.header_parameters %}
56+
{% if endpoint.header_parameters or endpoint.binary_body %}
5557
"headers": headers,
5658
{% endif %}
5759
{% if endpoint.cookie_parameters %}

0 commit comments

Comments
 (0)