Skip to content

Commit 257749b

Browse files
committed
Address PR feedback
1 parent b9a4a2c commit 257749b

File tree

7 files changed

+159
-78
lines changed

7 files changed

+159
-78
lines changed

codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpProtocolTestGenerator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,8 @@ private void compareMediaBlob(HttpMessageTestCase testCase, PythonWriter writer)
424424
if (contentType.equals("application/x-www-form-urlencoded")) {
425425
writer.addStdlibImport("urllib.parse", "parse_qsl");
426426
writer.write("""
427-
actual_params = sorted(parse_qsl(actual_body_content.decode()))
428-
expected_params = sorted(parse_qsl(expected_body_content.decode()))
427+
actual_params = sorted(parse_qsl(actual_body_content.decode(), keep_blank_values=True))
428+
expected_params = sorted(parse_qsl(expected_body_content.decode(), keep_blank_values=True))
429429
assert actual_params == expected_params
430430
431431
""");

packages/smithy-aws-core/src/smithy_aws_core/_private/query/errors.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
from typing import Any
3+
from typing import TYPE_CHECKING, Any
44
from xml.etree.ElementTree import Element, ParseError, fromstring
55

66
from smithy_core.documents import TypeRegistry
7-
from smithy_core.exceptions import CallError, ExpectationNotMetError, ModeledError
7+
from smithy_core.exceptions import (
8+
CallError,
9+
ExpectationNotMetError,
10+
MissingDependencyError,
11+
ModeledError,
12+
)
13+
from smithy_core.interfaces import TypedProperties
814
from smithy_core.schemas import APIOperation
915
from smithy_core.shapes import ShapeID
10-
from smithy_xml import XMLCodec
1116

1217
from ...traits import AwsQueryErrorTrait
1318

19+
try:
20+
from smithy_xml import XMLCodec
21+
22+
_HAS_XML = True
23+
except ImportError:
24+
_HAS_XML = False # type: ignore
25+
26+
if TYPE_CHECKING:
27+
from smithy_xml import XMLCodec
28+
29+
30+
def _assert_xml() -> None:
31+
if not _HAS_XML:
32+
raise MissingDependencyError(
33+
"Attempted to use XML codec, but smithy-xml is not installed."
34+
)
35+
1436

1537
def _local_name(tag: str) -> str:
1638
"""Strip namespace URI from an element tag: {uri}local -> local."""
@@ -76,6 +98,7 @@ def create_aws_query_error(
7698
default_namespace: str,
7799
wrapper_elements: tuple[str, ...],
78100
status: int,
101+
context: TypedProperties,
79102
) -> CallError:
80103
"""Create a modeled or generic CallError from an awsQuery error response."""
81104
code = _parse_aws_query_error_code(body, wrapper_elements)
@@ -94,6 +117,7 @@ def create_aws_query_error(
94117
f"but got {error_shape}"
95118
)
96119

120+
_assert_xml()
97121
deserializer = XMLCodec().create_deserializer(
98122
body, wrapper_elements=wrapper_elements
99123
)
@@ -102,5 +126,15 @@ def create_aws_query_error(
102126
message = f"Unknown error for operation {operation.schema.id} - status: {status}"
103127
if code is not None:
104128
message += f", code: {code}"
105-
fault = "client" if 400 <= status < 500 else "server"
106-
return CallError(message=message, fault=fault)
129+
130+
is_timeout = status == 408
131+
is_throttle = status == 429
132+
fault = "client" if status < 500 else "server"
133+
134+
return CallError(
135+
message=message,
136+
fault=fault,
137+
is_throttling_error=is_throttle,
138+
is_timeout_error=is_timeout,
139+
is_retry_safe=is_throttle or is_timeout or None,
140+
)

packages/smithy-aws-core/src/smithy_aws_core/aio/protocols.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@
7676
def _assert_json() -> None:
7777
if not _HAS_JSON:
7878
raise MissingDependencyError(
79-
"Attempted to use JSON protocol support, but smithy-json is not installed."
79+
"Attempted to use JSON codec, but smithy-json is not installed."
8080
)
8181

8282

8383
def _assert_xml() -> None:
8484
if not _HAS_XML:
8585
raise MissingDependencyError(
86-
"Attempted to use XML protocol support, but smithy-xml is not installed."
86+
"Attempted to use XML codec, but smithy-xml is not installed."
8787
)
8888

8989

@@ -114,17 +114,21 @@ def identify(
114114
return None
115115

116116

117-
class AWSJSONDocument(JSONDocument):
118-
@property
119-
def discriminator(self) -> ShapeID:
120-
if self.shape_type is ShapeType.STRUCTURE:
121-
return self._schema.id
122-
parsed = parse_document_discriminator(self, self._settings.default_namespace)
123-
if parsed is None:
124-
raise DiscriminatorError(
125-
f"Unable to parse discriminator for {self.shape_type} document."
117+
if _HAS_JSON:
118+
119+
class AWSJSONDocument(JSONDocument):
120+
@property
121+
def discriminator(self) -> ShapeID:
122+
if self.shape_type is ShapeType.STRUCTURE:
123+
return self._schema.id
124+
parsed = parse_document_discriminator(
125+
self, self._settings.default_namespace
126126
)
127-
return parsed
127+
if parsed is None:
128+
raise DiscriminatorError(
129+
f"Unable to parse discriminator for {self.shape_type} document."
130+
)
131+
return parsed
128132

129133

130134
class RestJsonClientProtocol(HttpBindingClientProtocol):
@@ -242,6 +246,14 @@ def __init__(self, service_schema: Schema, version: str) -> None:
242246
def id(self) -> ShapeID:
243247
return self._id
244248

249+
@property
250+
def payload_codec(self) -> "XMLCodec":
251+
return self._codec
252+
253+
@property
254+
def content_type(self) -> str:
255+
return self._content_type
256+
245257
def serialize_request[
246258
OperationInput: SerializeableShape,
247259
OperationOutput: DeserializeableShape,
@@ -271,7 +283,7 @@ def serialize_request[
271283
destination=_URI(host="", path="/"),
272284
fields=tuples_to_fields(
273285
[
274-
("content-type", self._content_type),
286+
("content-type", self.content_type),
275287
("content-length", str(content_length)),
276288
]
277289
),
@@ -292,36 +304,46 @@ async def deserialize_response[
292304
) -> OperationOutput:
293305
body = await response.consume_body_async()
294306

295-
if response.status >= 300:
296-
raise self._create_error(
307+
if not self._is_success(operation, context, response):
308+
raise await self._create_error(
297309
operation=operation,
298310
response=response,
299311
response_body=body,
300312
error_registry=error_registry,
313+
context=context,
301314
)
302315

303316
if len(body) == 0:
304317
return operation.output.deserialize(
305318
HTTPResponseDeserializer(
306-
payload_codec=self._codec,
319+
payload_codec=self.payload_codec,
307320
response=response,
308321
body=body,
309322
)
310323
)
311324

312325
wrapper_elements = self._response_wrapper_elements(operation)
313-
deserializer = self._codec.create_deserializer(
326+
deserializer = self.payload_codec.create_deserializer(
314327
body, wrapper_elements=wrapper_elements
315328
)
316329
return operation.output.deserialize(deserializer)
317330

318-
def _create_error(
331+
def _is_success(
332+
self,
333+
operation: APIOperation[Any, Any],
334+
context: TypedProperties,
335+
response: HTTPResponse,
336+
) -> bool:
337+
return 200 <= response.status < 300
338+
339+
async def _create_error(
319340
self,
320341
*,
321342
operation: APIOperation[Any, Any],
322343
response: HTTPResponse,
323344
response_body: bytes,
324345
error_registry: TypeRegistry,
346+
context: TypedProperties,
325347
) -> CallError:
326348
return create_aws_query_error(
327349
body=response_body,
@@ -330,6 +352,7 @@ def _create_error(
330352
default_namespace=self._default_namespace,
331353
wrapper_elements=self._error_wrapper_elements(),
332354
status=response.status,
355+
context=context,
333356
)
334357

335358
def _action_name(

packages/smithy-aws-core/src/smithy_aws_core/traits.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ class AwsQueryErrorTrait(Trait, id=ShapeID("aws.protocols#awsQueryError")):
5757
def __post_init__(self):
5858
assert isinstance(self.document_value, Mapping)
5959
assert isinstance(self.document_value.get("code"), str)
60-
assert isinstance(self.document_value.get("httpResponseCode"), int | None)
60+
assert isinstance(self.document_value.get("httpResponseCode"), int)
6161

6262
@property
6363
def code(self) -> str:
6464
return self.document_value["code"] # type: ignore
6565

6666
@property
67-
def http_response_code(self) -> int | None:
68-
return self.document_value.get("httpResponseCode") # type: ignore
67+
def http_response_code(self) -> int:
68+
return self.document_value["httpResponseCode"] # type: ignore
6969

7070

7171
@dataclass(init=False, frozen=True)

packages/smithy-aws-core/tests/unit/aio/test_protocols.py

Lines changed: 68 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from smithy_core.traits import Trait
2424
from smithy_core.types import TypedProperties
2525
from smithy_http import Fields, tuples_to_fields
26-
from smithy_http.aio import HTTPResponse
26+
from smithy_http.aio import HTTPRequest, HTTPResponse
2727
from smithy_json import JSONSettings
2828

2929

@@ -199,60 +199,80 @@ async def test_aws_query_serializes_base_request_shape() -> None:
199199
assert body == b"Action=TestOperation&Version=2020-01-08&name=example"
200200

201201

202-
def test_aws_query_resolves_modeled_error_from_query_error_trait() -> None:
202+
@pytest.mark.asyncio
203+
async def test_aws_query_resolves_modeled_error_from_query_error_trait() -> None:
203204
protocol = AwsQueryClientProtocol(_SERVICE_SCHEMA, "2020-01-08")
204-
error = getattr(protocol, "_create_error")(
205-
operation=_mock_operation(
206-
_operation_schema("FailingOperation"),
207-
error_schemas=[_INVALID_ACTION_ERROR_SCHEMA],
208-
),
209-
response=HTTPResponse(status=400, fields=tuples_to_fields([])),
210-
response_body=(
211-
b"<ErrorResponse><Error><Code>InvalidAction</Code>"
212-
b"<message>bad request</message></Error></ErrorResponse>"
213-
),
214-
error_registry=TypeRegistry(
215-
{ShapeID("com.test#InvalidActionError"): _ModeledQueryError}
216-
),
217-
)
218-
219-
assert isinstance(error, _ModeledQueryError)
220-
assert error.message == "bad request"
205+
with pytest.raises(_ModeledQueryError) as exc_info:
206+
await protocol.deserialize_response(
207+
operation=_mock_operation(
208+
_operation_schema("FailingOperation"),
209+
error_schemas=[_INVALID_ACTION_ERROR_SCHEMA],
210+
),
211+
request=cast(HTTPRequest, Mock()),
212+
response=HTTPResponse(
213+
status=400,
214+
fields=tuples_to_fields([]),
215+
body=(
216+
b"<ErrorResponse><Error><Code>InvalidAction</Code>"
217+
b"<message>bad request</message></Error></ErrorResponse>"
218+
),
219+
),
220+
error_registry=TypeRegistry(
221+
{ShapeID("com.test#InvalidActionError"): _ModeledQueryError}
222+
),
223+
context=TypedProperties(),
224+
)
225+
226+
assert exc_info.value.message == "bad request"
221227

222228

223-
def test_aws_query_resolves_modeled_error_from_default_namespace_fallback() -> None:
229+
@pytest.mark.asyncio
230+
async def test_aws_query_resolves_modeled_error_from_default_namespace_fallback() -> (
231+
None
232+
):
224233
protocol = AwsQueryClientProtocol(_SERVICE_SCHEMA, "2020-01-08")
225-
error = getattr(protocol, "_create_error")(
226-
operation=_mock_operation(_operation_schema("FailingOperation")),
227-
response=HTTPResponse(status=503, fields=tuples_to_fields([])),
228-
response_body=(
229-
b"<ErrorResponse><Error><Code>ServiceUnavailable</Code>"
230-
b"<message>try again</message></Error></ErrorResponse>"
231-
),
232-
error_registry=TypeRegistry(
233-
{ShapeID("com.test#ServiceUnavailable"): _ModeledQueryError}
234-
),
235-
)
234+
with pytest.raises(_ModeledQueryError) as exc_info:
235+
await protocol.deserialize_response(
236+
operation=_mock_operation(_operation_schema("FailingOperation")),
237+
request=cast(HTTPRequest, Mock()),
238+
response=HTTPResponse(
239+
status=503,
240+
fields=tuples_to_fields([]),
241+
body=(
242+
b"<ErrorResponse><Error><Code>ServiceUnavailable</Code>"
243+
b"<message>try again</message></Error></ErrorResponse>"
244+
),
245+
),
246+
error_registry=TypeRegistry(
247+
{ShapeID("com.test#ServiceUnavailable"): _ModeledQueryError}
248+
),
249+
context=TypedProperties(),
250+
)
251+
252+
assert exc_info.value.message == "try again"
236253

237-
assert isinstance(error, _ModeledQueryError)
238-
assert error.message == "try again"
239254

240-
241-
def test_aws_query_returns_generic_error_for_unknown_code() -> None:
255+
@pytest.mark.asyncio
256+
async def test_aws_query_returns_generic_error_for_unknown_code() -> None:
242257
protocol = AwsQueryClientProtocol(_SERVICE_SCHEMA, "2020-01-08")
243-
error = getattr(protocol, "_create_error")(
244-
operation=_mock_operation(_operation_schema("FailingOperation")),
245-
response=HTTPResponse(status=500, fields=tuples_to_fields([])),
246-
response_body=(
247-
b"<ErrorResponse><Error><Code>UnknownThing</Code>"
248-
b"<message>bad request</message></Error></ErrorResponse>"
249-
),
250-
error_registry=TypeRegistry({}),
251-
)
252-
253-
assert isinstance(error, CallError)
254-
assert not isinstance(error, ModeledError)
255-
assert error.message == (
258+
with pytest.raises(CallError) as exc_info:
259+
await protocol.deserialize_response(
260+
operation=_mock_operation(_operation_schema("FailingOperation")),
261+
request=cast(HTTPRequest, Mock()),
262+
response=HTTPResponse(
263+
status=500,
264+
fields=tuples_to_fields([]),
265+
body=(
266+
b"<ErrorResponse><Error><Code>UnknownThing</Code>"
267+
b"<message>bad request</message></Error></ErrorResponse>"
268+
),
269+
),
270+
error_registry=TypeRegistry({}),
271+
context=TypedProperties(),
272+
)
273+
274+
assert not isinstance(exc_info.value, ModeledError)
275+
assert exc_info.value.message == (
256276
"Unknown error for operation com.test#FailingOperation"
257277
" - status: 500, code: UnknownThing"
258278
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "feature",
3+
"description": "Added `error_schemas` to `APIOperation` to expose the operation\u2019s modeled error schemas."
4+
}

packages/smithy-core/.changes/next-release/smithy-core-enhancement-80e80edee8a7461d99ca5f28c2547e59.json renamed to packages/smithy-core/.changes/next-release/smithy-core-feature-f854c5df00ef4bdf9b0947f09b2f53f0.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"type": "enhancement",
2+
"type": "feature",
33
"description": "Added XML binding traits: `XMLNameTrait`, `XMLNamespaceTrait`, `XMLFlattenedTrait`, and `XMLAttributeTrait`."
4-
}
4+
}

0 commit comments

Comments
 (0)