Skip to content

Commit 785bba1

Browse files
authored
Track feature IDs for retry modes, CBOR, and request compression (#9555)
1 parent 4c25ab9 commit 785bba1

File tree

9 files changed

+186
-2
lines changed

9 files changed

+186
-2
lines changed

awscli/botocore/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from botocore.model import ServiceModel
5252
from botocore.paginate import Paginator
5353
from botocore.retries import adaptive, standard
54-
from botocore.useragent import UserAgentString
54+
from botocore.useragent import UserAgentString, register_feature_id
5555
from botocore.utils import (
5656
CachedProperty,
5757
EventbridgeSignerSetter,
@@ -228,6 +228,9 @@ def _register_retries(self, client):
228228
elif retry_mode == 'adaptive':
229229
self._register_v2_standard_retries(client)
230230
self._register_v2_adaptive_retries(client)
231+
else:
232+
return
233+
register_feature_id(f'RETRY_MODE_{retry_mode.upper()}')
231234

232235
def _register_v2_standard_retries(self, client):
233236
max_attempts = client.meta.config.retries.get('max_attempts')

awscli/botocore/compress.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from gzip import compress as gzip_compress
2323

2424
from botocore.compat import urlencode
25+
from botocore.useragent import register_feature_id
2526
from botocore.utils import determine_content_length
2627

2728
logger = logging.getLogger(__name__)
@@ -88,6 +89,7 @@ def _get_body_size(body):
8889

8990

9091
def _gzip_compress_body(body):
92+
register_feature_id('GZIP_REQUEST_COMPRESSION')
9193
if isinstance(body, str):
9294
return gzip_compress(body.encode('utf-8'))
9395
elif isinstance(body, (bytes, bytearray)):

awscli/botocore/serialize.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
from botocore import validate
5151
from botocore.compat import formatdate, json
52+
from botocore.useragent import register_feature_id
5253
from botocore.utils import (
5354
has_header,
5455
is_json_value_header,
@@ -1162,6 +1163,10 @@ def _add_xml_namespace(self, shape, structure_node):
11621163
class RpcV2CBORSerializer(BaseRpcV2Serializer, CBORSerializer):
11631164
TIMESTAMP_FORMAT = 'unixtimestamp'
11641165

1166+
def serialize_to_request(self, parameters, operation_model):
1167+
register_feature_id('PROTOCOL_RPC_V2_CBOR')
1168+
return super().serialize_to_request(parameters, operation_model)
1169+
11651170
def _serialize_body_params(self, parameters, input_shape):
11661171
body = bytearray()
11671172
self._serialize_data_item(body, parameters, input_shape)

awscli/botocore/useragent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@
5656
_USERAGENT_FEATURE_MAPPINGS = {
5757
'WAITER': 'B',
5858
'PAGINATOR': 'C',
59+
'RETRY_MODE_STANDARD': 'E',
60+
'RETRY_MODE_ADAPTIVE': 'F',
5961
'S3_TRANSFER': 'G',
62+
'PROTOCOL_RPC_V2_CBOR': 'M',
63+
'GZIP_REQUEST_COMPRESSION': 'L',
6064
'ENDPOINT_OVERRIDE': 'N',
6165
'SIGV4A_SIGNING': 'S',
6266
}

tests/functional/botocore/test_compress.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
from botocore.config import Config
1919

2020
from tests import ALL_SERVICES, ClientHTTPStubber, patch_load_service_model
21+
from tests.functional.botocore.test_useragent import (
22+
get_captured_ua_strings,
23+
parse_registered_feature_ids,
24+
)
2125

2226
FAKE_MODEL = {
2327
"version": "2.0",
@@ -127,3 +131,24 @@ def test_compression(patched_session, monkeypatch):
127131
serialized_body = f"{additional_params}&{serialized_params}"
128132
actual_body = gzip.decompress(http_stubber.requests[0].body)
129133
assert serialized_body.encode('utf-8') == actual_body
134+
135+
136+
def test_user_agent_has_gzip_feature_id(patched_session, monkeypatch):
137+
patch_load_service_model(
138+
patched_session, monkeypatch, FAKE_MODEL, FAKE_RULESET
139+
)
140+
client = patched_session.create_client(
141+
"otherservice",
142+
region_name="us-west-2",
143+
config=Config(request_min_compression_size_bytes=100),
144+
)
145+
with ClientHTTPStubber(client, strict=True) as http_stubber:
146+
http_stubber.add_response(status=200, body=b"<response/>")
147+
params_list = [
148+
{"MockOpParam": f"MockOpParamValue{i}"} for i in range(1, 21)
149+
]
150+
# The mock operation registers `'GZIP_REQUEST_COMPRESSION': 'L'`
151+
client.mock_operation(MockOpParamList=params_list)
152+
ua_string = get_captured_ua_strings(http_stubber)[0]
153+
feature_list = parse_registered_feature_ids(ua_string)
154+
assert 'L' in feature_list

tests/functional/botocore/test_retry.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
from botocore.exceptions import ClientError
1818

1919
from tests import BaseSessionTest, ClientHTTPStubber, mock
20+
from tests.functional.botocore.test_useragent import (
21+
get_captured_ua_strings,
22+
parse_registered_feature_ids,
23+
)
2024

2125

2226
class TestRetry(BaseSessionTest):
@@ -46,6 +50,18 @@ def assert_will_retry_n_times(
4650
yield
4751
self.assertEqual(len(http_stubber.requests), num_responses)
4852

53+
def _get_feature_id_lists_from_retries(self, client):
54+
with ClientHTTPStubber(client) as http_stubber:
55+
# Add two failed responses followed by a success
56+
http_stubber.add_response(status=502, body=b'{}')
57+
http_stubber.add_response(status=502, body=b'{}')
58+
http_stubber.add_response(status=200, body=b'{}')
59+
client.list_tables()
60+
ua_strings = get_captured_ua_strings(http_stubber)
61+
return [
62+
parse_registered_feature_ids(ua_string) for ua_string in ua_strings
63+
]
64+
4965
def create_client_with_retry_config(
5066
self, service, retry_mode='standard', max_attempts=None
5167
):
@@ -161,3 +177,19 @@ def test_can_exhaust_default_retry_quota(self):
161177
self.assertTrue(
162178
e.exception.response['ResponseMetadata'].get('RetryQuotaReached')
163179
)
180+
181+
def test_user_agent_has_standard_mode_feature_id(self):
182+
client = self.create_client_with_retry_config(
183+
'dynamodb', retry_mode='standard'
184+
)
185+
feature_lists = self._get_feature_id_lists_from_retries(client)
186+
# Confirm all requests register `'RETRY_MODE_STANDARD': 'E'`
187+
assert all('E' in feature_list for feature_list in feature_lists)
188+
189+
def test_user_agent_has_adaptive_mode_feature_id(self):
190+
client = self.create_client_with_retry_config(
191+
'dynamodb', retry_mode='adaptive'
192+
)
193+
feature_lists = self._get_feature_id_lists_from_retries(client)
194+
# Confirm all requests register `'RETRY_MODE_ADAPTIVE': 'F'`
195+
assert all('F' in feature_list for feature_list in feature_lists)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
14+
from tests import ClientHTTPStubber, patch_load_service_model
15+
from tests.functional.botocore.test_useragent import (
16+
get_captured_ua_strings,
17+
parse_registered_feature_ids,
18+
)
19+
20+
MOCK_SERVICE_MODEL = {
21+
"version": "1.0",
22+
"documentation": "",
23+
"metadata": {
24+
"apiVersion": "2020-02-02",
25+
"endpointPrefix": "mockservice",
26+
"protocols": ["smithy-rpc-v2-cbor"],
27+
"protocol": "smithy-rpc-v2-cbor",
28+
"serviceFullName": "Mock Service",
29+
"serviceId": "mockservice",
30+
"signatureVersion": "v4",
31+
"signingName": "mockservice",
32+
"targetPrefix": "mockservice",
33+
"uid": "mockservice-2020-02-02",
34+
},
35+
"operations": {
36+
"MockOperation": {
37+
"name": "MockOperation",
38+
"http": {"method": "GET", "requestUri": "/"},
39+
"input": {"shape": "MockOperationRequest"},
40+
"documentation": "",
41+
},
42+
},
43+
"shapes": {
44+
"MockOpParam": {
45+
"type": "string",
46+
},
47+
"MockOperationRequest": {
48+
"type": "structure",
49+
"required": ["MockOpParam"],
50+
"members": {
51+
"MockOpParam": {
52+
"shape": "MockOpParam",
53+
"documentation": "",
54+
"location": "uri",
55+
"locationName": "param",
56+
},
57+
},
58+
},
59+
},
60+
}
61+
62+
MOCK_RULESET = {
63+
"version": "1.0",
64+
"parameters": {},
65+
"rules": [
66+
{
67+
"conditions": [],
68+
"endpoint": {
69+
"url": "https://mockservice.us-west-2.amazonaws.com/"
70+
},
71+
"type": "endpoint",
72+
},
73+
],
74+
}
75+
76+
77+
def test_user_agent_has_cbor_feature_id(patched_session, monkeypatch):
78+
patch_load_service_model(
79+
patched_session, monkeypatch, MOCK_SERVICE_MODEL, MOCK_RULESET
80+
)
81+
client = patched_session.create_client(
82+
'mockservice', region_name='us-west-2'
83+
)
84+
with ClientHTTPStubber(client) as stub_client:
85+
stub_client.add_response()
86+
# The mock CBOR operation registers `'PROTOCOL_RPC_V2_CBOR': 'M'`
87+
client.mock_operation(MockOpParam='mock-op-param-value')
88+
ua_string = get_captured_ua_strings(stub_client)[0]
89+
feature_list = parse_registered_feature_ids(ua_string)
90+
assert 'M' in feature_list

tests/functional/botocore/test_useragent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@
2222
from tests import ClientHTTPStubber
2323

2424

25+
def get_captured_ua_strings(stubber):
26+
"""Get captured request-level user agent strings from stubber.
27+
28+
:type stubber: tests.BaseHTTPStubber
29+
"""
30+
return [req.headers['User-Agent'].decode() for req in stubber.requests]
31+
32+
33+
def parse_registered_feature_ids(ua_string):
34+
"""Parse registered feature ids in user agent string.
35+
36+
:type ua_string: str
37+
:rtype: list[str]
38+
"""
39+
ua_fields = ua_string.split(' ')
40+
feature_field = [field for field in ua_fields if field.startswith('m/')][0]
41+
return feature_field[2:].split(',')
42+
43+
2544
class UACapHTTPStubber(ClientHTTPStubber):
2645
"""
2746
Wrapper for ClientHTTPStubber that captures UA header from one request.

tests/functional/test_useragent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from awscli import __version__ as awscli_version
22
from tests import CLIRunner
3+
from tests.functional.botocore.test_useragent import (
4+
parse_registered_feature_ids,
5+
)
36

47

58
def assert_expected_user_agent(result, service, operation):
@@ -32,4 +35,5 @@ def test_user_agent_for_customization():
3235
result = cli_runner.run([service, operation])
3336
assert_expected_user_agent(result, service, operation)
3437
ua_string = result.aws_requests[0].http_requests[0].headers['User-Agent']
35-
assert 'm/C' in ua_string
38+
feature_list = parse_registered_feature_ids(ua_string)
39+
assert 'C' in feature_list

0 commit comments

Comments
 (0)