Skip to content

Commit e47e704

Browse files
[python][Feat] Deserialize error responses (#17038)
* refactor: Clean up _response_types_map formatting It matches black's behavior of having trailing commas now. * test: Add test to reproduce #16967 * fix: deserialize responses even if no returnType Closes #16967 * refactor: Simplify ApiException subclasses * refactor: Move exception subtype choice to ApiException * feat: Deserialize error responses and add to exceptions * test: Add for error responses with model
1 parent 69fcfef commit e47e704

File tree

85 files changed

+4708
-1350
lines changed

Some content is hidden

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

85 files changed

+4708
-1350
lines changed

modules/openapi-generator/src/main/resources/python/api_client.mustache

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ class ApiClient:
289289

290290
def response_deserialize(
291291
self,
292-
response_data=None,
292+
response_data: rest.RESTResponse = None,
293293
response_types_map=None
294294
) -> ApiResponse:
295295
"""Deserializes response into an object.
@@ -304,39 +304,29 @@ class ApiClient:
304304
# if not found, look for '1XX', '2XX', etc.
305305
response_type = response_types_map.get(str(response_data.status)[0] + "XX", None)
306306

307-
if not 200 <= response_data.status <= 299:
308-
if response_data.status == 400:
309-
raise BadRequestException(http_resp=response_data)
310-
311-
if response_data.status == 401:
312-
raise UnauthorizedException(http_resp=response_data)
313-
314-
if response_data.status == 403:
315-
raise ForbiddenException(http_resp=response_data)
316-
317-
if response_data.status == 404:
318-
raise NotFoundException(http_resp=response_data)
319-
320-
if 500 <= response_data.status <= 599:
321-
raise ServiceException(http_resp=response_data)
322-
raise ApiException(http_resp=response_data)
323-
324307
# deserialize response data
325-
326-
if response_type == "bytearray":
327-
return_data = response_data.data
328-
elif response_type is None:
329-
return_data = None
330-
elif response_type == "file":
331-
return_data = self.__deserialize_file(response_data)
332-
else:
333-
match = None
334-
content_type = response_data.getheader('content-type')
335-
if content_type is not None:
336-
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
337-
encoding = match.group(1) if match else "utf-8"
338-
response_text = response_data.data.decode(encoding)
339-
return_data = self.deserialize(response_text, response_type)
308+
response_text = None
309+
return_data = None
310+
try:
311+
if response_type == "bytearray":
312+
return_data = response_data.data
313+
elif response_type == "file":
314+
return_data = self.__deserialize_file(response_data)
315+
elif response_type is not None:
316+
match = None
317+
content_type = response_data.getheader('content-type')
318+
if content_type is not None:
319+
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
320+
encoding = match.group(1) if match else "utf-8"
321+
response_text = response_data.data.decode(encoding)
322+
return_data = self.deserialize(response_text, response_type)
323+
finally:
324+
if not 200 <= response_data.status <= 299:
325+
raise ApiException.from_response(
326+
http_resp=response_data,
327+
body=response_text,
328+
data=return_data,
329+
)
340330

341331
return ApiResponse(
342332
status_code = response_data.status,

modules/openapi-generator/src/main/resources/python/exceptions.mustache

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# coding: utf-8
22

33
{{>partial_header}}
4+
from typing import Any, Optional
5+
6+
from typing_extensions import Self
47

58
class OpenApiException(Exception):
69
"""The base exception class for all OpenAPIExceptions"""
@@ -91,17 +94,56 @@ class ApiKeyError(OpenApiException, KeyError):
9194

9295
class ApiException(OpenApiException):
9396

94-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
97+
def __init__(
98+
self,
99+
status=None,
100+
reason=None,
101+
http_resp=None,
102+
*,
103+
body: Optional[str] = None,
104+
data: Optional[Any] = None,
105+
) -> None:
106+
self.status = status
107+
self.reason = reason
108+
self.body = body
109+
self.data = data
110+
self.headers = None
111+
95112
if http_resp:
96-
self.status = http_resp.status
97-
self.reason = http_resp.reason
98-
self.body = http_resp.data.decode('utf-8')
113+
if self.status is None:
114+
self.status = http_resp.status
115+
if self.reason is None:
116+
self.reason = http_resp.reason
117+
if self.body is None:
118+
try:
119+
self.body = http_resp.data.decode('utf-8')
120+
except Exception:
121+
pass
99122
self.headers = http_resp.getheaders()
100-
else:
101-
self.status = status
102-
self.reason = reason
103-
self.body = None
104-
self.headers = None
123+
124+
@classmethod
125+
def from_response(
126+
cls,
127+
*,
128+
http_resp,
129+
body: Optional[str],
130+
data: Optional[Any],
131+
) -> Self:
132+
if http_resp.status == 400:
133+
raise BadRequestException(http_resp=http_resp, body=body, data=data)
134+
135+
if http_resp.status == 401:
136+
raise UnauthorizedException(http_resp=http_resp, body=body, data=data)
137+
138+
if http_resp.status == 403:
139+
raise ForbiddenException(http_resp=http_resp, body=body, data=data)
140+
141+
if http_resp.status == 404:
142+
raise NotFoundException(http_resp=http_resp, body=body, data=data)
143+
144+
if 500 <= http_resp.status <= 599:
145+
raise ServiceException(http_resp=http_resp, body=body, data=data)
146+
raise ApiException(http_resp=http_resp, body=body, data=data)
105147

106148
def __str__(self):
107149
"""Custom error messages for exception"""
@@ -111,38 +153,30 @@ class ApiException(OpenApiException):
111153
error_message += "HTTP response headers: {0}\n".format(
112154
self.headers)
113155

114-
if self.body:
115-
error_message += "HTTP response body: {0}\n".format(self.body)
156+
if self.data or self.body:
157+
error_message += "HTTP response body: {0}\n".format(self.data or self.body)
116158

117159
return error_message
118160

161+
119162
class BadRequestException(ApiException):
163+
pass
120164

121-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
122-
super(BadRequestException, self).__init__(status, reason, http_resp)
123165

124166
class NotFoundException(ApiException):
125-
126-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
127-
super(NotFoundException, self).__init__(status, reason, http_resp)
167+
pass
128168

129169

130170
class UnauthorizedException(ApiException):
131-
132-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
133-
super(UnauthorizedException, self).__init__(status, reason, http_resp)
171+
pass
134172

135173

136174
class ForbiddenException(ApiException):
137-
138-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
139-
super(ForbiddenException, self).__init__(status, reason, http_resp)
175+
pass
140176

141177

142178
class ServiceException(ApiException):
143-
144-
def __init__(self, status=None, reason=None, http_resp=None) -> None:
145-
super(ServiceException, self).__init__(status, reason, http_resp)
179+
pass
146180

147181

148182
def render_path(path_to_item):

modules/openapi-generator/src/main/resources/python/partial_api.mustache

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
)
4545

4646
_response_types_map: Dict[str, Optional[str]] = {
47-
{{#returnType}}{{#responses}}{{^isWildcard}}'{{code}}': {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}{{/isWildcard}}{{^-last}},{{/-last}}
48-
{{/responses}}{{/returnType}}
47+
{{#responses}}
48+
{{^isWildcard}}
49+
'{{code}}': {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}},
50+
{{/isWildcard}}
51+
{{/responses}}
4952
}

modules/openapi-generator/src/test/resources/3_0/python/petstore-with-fake-endpoints-models-for-testing.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,49 @@ paths:
10631063
schema:
10641064
$ref: '#/components/schemas/User'
10651065
required: true
1066+
/fake/empty_and_non_empty_responses:
1067+
post:
1068+
tags:
1069+
- fake
1070+
summary: test empty and non-empty responses
1071+
description: ''
1072+
operationId: testEmptyAndNonEmptyResponses
1073+
responses:
1074+
'204':
1075+
description: Success, but no response content
1076+
'206':
1077+
description: Partial response content
1078+
content:
1079+
text/plain:
1080+
schema:
1081+
type: string
1082+
/fake/error_responses_with_model:
1083+
post:
1084+
tags:
1085+
- fake
1086+
summary: test error responses with model
1087+
operationId: testErrorResponsesWithModel
1088+
responses:
1089+
'204':
1090+
description: Success, but no response content
1091+
'400':
1092+
description: ''
1093+
content:
1094+
application/json:
1095+
schema:
1096+
type: object
1097+
properties:
1098+
reason400:
1099+
type: string
1100+
'404':
1101+
description: ''
1102+
content:
1103+
application/json:
1104+
schema:
1105+
type: object
1106+
properties:
1107+
reason404:
1108+
type: string
10661109
/another-fake/dummy:
10671110
patch:
10681111
tags:

samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent-true/openapi_client/api/auth_api.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ def test_auth_http_basic(
9393
)
9494

9595
_response_types_map: Dict[str, Optional[str]] = {
96-
'200': "str"
97-
96+
'200': "str",
9897
}
9998
response_data = self.api_client.call_api(
10099
*_param,
@@ -157,8 +156,7 @@ def test_auth_http_basic_with_http_info(
157156
)
158157

159158
_response_types_map: Dict[str, Optional[str]] = {
160-
'200': "str"
161-
159+
'200': "str",
162160
}
163161
response_data = self.api_client.call_api(
164162
*_param,
@@ -221,8 +219,7 @@ def test_auth_http_basic_without_preload_content(
221219
)
222220

223221
_response_types_map: Dict[str, Optional[str]] = {
224-
'200': "str"
225-
222+
'200': "str",
226223
}
227224
response_data = self.api_client.call_api(
228225
*_param,

0 commit comments

Comments
 (0)