Skip to content

Commit adc7630

Browse files
authored
Bump botocore dependency specification (#1374)
1 parent 710b615 commit adc7630

File tree

10 files changed

+243
-22
lines changed

10 files changed

+243
-22
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Changes
22
-------
33

4+
2.23.1 (2025-06-15)
5+
^^^^^^^^^^^^^^^^^^^
6+
* bump botocore dependency specification
7+
48
2.23.0 (2025-06-12)
59
^^^^^^^^^^^^^^^^^^^
610
* drop support for Python 3.8 (EOL)

aiobotocore/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '2.23.0'
1+
__version__ = '2.23.1'

aiobotocore/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from botocore.exceptions import OperationNotPageableError, UnknownServiceError
1414
from botocore.history import get_global_history_recorder
1515
from botocore.hooks import first_non_none_response
16+
from botocore.useragent import register_feature_id
1617
from botocore.utils import get_service_module_name
1718
from botocore.waiter import xform_name
1819

@@ -157,6 +158,9 @@ def _register_retries(self, client):
157158
self._register_v2_adaptive_retries(client)
158159
elif retry_mode == 'legacy':
159160
self._register_legacy_retries(client)
161+
else:
162+
return
163+
register_feature_id(f'RETRY_MODE_{retry_mode.upper()}')
160164

161165
def _register_v2_standard_retries(self, client):
162166
max_attempts = client.meta.config.retries.get('total_max_attempts')

aiobotocore/endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ async def _do_get_response(self, request, operation_model, context):
225225
)
226226
history_recorder.record('HTTP_RESPONSE', http_response_record_dict)
227227

228-
protocol = operation_model.metadata['protocol']
228+
protocol = operation_model.service_model.resolved_protocol
229229
customized_response_dict = {}
230230
await self._event_emitter.emit(
231231
f"before-parse.{service_id}.{operation_model.name}",

aiobotocore/response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
180180

181181

182182
async def get_response(operation_model, http_response):
183-
protocol = operation_model.metadata['protocol']
183+
protocol = operation_model.service_model.resolved_protocol
184184
response_dict = {
185185
'headers': http_response.headers,
186186
'status_code': http_response.status_code,

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ dynamic = ["version", "readme"]
3131
dependencies = [
3232
"aiohttp >= 3.9.2, < 4.0.0",
3333
"aioitertools >= 0.5.1, < 1.0.0",
34-
"botocore >= 1.38.23, < 1.38.28", # NOTE: When updating, always keep `project.optional-dependencies` aligned
34+
"botocore >= 1.38.31, < 1.38.37", # NOTE: When updating, always keep `project.optional-dependencies` aligned
3535
"python-dateutil >= 2.1, < 3.0.0",
3636
"jmespath >= 0.7.1, < 2.0.0",
3737
"multidict >= 6.0.0, < 7.0.0",
@@ -40,10 +40,10 @@ dependencies = [
4040

4141
[project.optional-dependencies]
4242
awscli = [
43-
"awscli >= 1.40.22, < 1.40.27",
43+
"awscli >= 1.40.30, < 1.40.36",
4444
]
4545
boto3 = [
46-
"boto3 >= 1.38.23, < 1.38.28",
46+
"boto3 >= 1.38.31, < 1.38.37",
4747
]
4848
httpx = [
4949
"httpx >= 0.25.1, < 0.29"
@@ -58,7 +58,7 @@ botocore-dev = [
5858
# keep in sync with https://github.com/boto/botocore/blob/develop/requirements-dev.txt
5959
# "wheel==0.43.0",
6060
# "behave==1.2.5",
61-
# "jsonschema==4.21.1",
61+
# "jsonschema==4.23.0",
6262
"coverage==7.2.7",
6363
# "setuptools==71.1.0;python_version>='3.12'",
6464
# "packaging==24.1;python_version>='3.12'", # Requirement for setuptools>=71

tests/botocore_tests/unit/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
2+
# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"). You
5+
# may not use this file except in compliance with the License. A copy of
6+
# the License is located at
7+
#
8+
# http://aws.amazon.com/apache2.0/
9+
#
10+
# or in the "license" file accompanying this file. This file is
11+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12+
# ANY KIND, either express or implied. See the License for the specific
13+
# language governing permissions and limitations under the License.
14+
import copy
15+
16+
17+
class BaseResponseTest:
18+
def assert_response_with_subset_metadata(
19+
self, actual_response, expected_response
20+
):
21+
"""
22+
Compares two parsed service responses. For ResponseMetadata, it will
23+
only assert that the expected is a proper subset of the actual. This
24+
is useful so that when new keys are added to the metadata, tests don't
25+
break.
26+
"""
27+
actual = copy.copy(actual_response)
28+
expected = copy.copy(expected_response)
29+
30+
actual_metadata = actual.pop('ResponseMetadata', {})
31+
expected_metadata = expected.pop('ResponseMetadata', {})
32+
33+
assert actual == expected
34+
self.assert_dict_is_proper_subset(actual_metadata, expected_metadata)
35+
36+
def assert_dict_is_proper_subset(self, superset, subset):
37+
"""
38+
Asserts that a dictionary is a proper subset of another.
39+
"""
40+
assert all(
41+
(k in superset and superset[k] == v) for k, v in subset.items()
42+
)
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright 2012-2014 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+
import datetime
14+
from io import BytesIO
15+
16+
from dateutil.tz import tzutc
17+
18+
from aiobotocore import response
19+
from aiobotocore.awsrequest import AioAWSResponse
20+
from tests.botocore_tests.unit import BaseResponseTest
21+
22+
XMLBODY1 = (
23+
b'<?xml version="1.0" encoding="UTF-8"?><Error>'
24+
b'<Code>AccessDenied</Code>'
25+
b'<Message>Access Denied</Message>'
26+
b'<RequestId>XXXXXXXXXXXXXXXX</RequestId>'
27+
b'<HostId>AAAAAAAAAAAAAAAAAAA</HostId>'
28+
b'</Error>'
29+
)
30+
31+
XMLBODY2 = (
32+
b'<?xml version="1.0" encoding="UTF-8"?>'
33+
b'<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
34+
b'<Name>mybucket</Name><Prefix></Prefix><Marker></Marker>'
35+
b'<MaxKeys>1000</MaxKeys><IsTruncated>false</IsTruncated>'
36+
b'<Contents><Key>test.png</Key><LastModified>2014-03-01T17:06:40.000Z</LastModified>'
37+
b'<ETag>&quot;00000000000000000000000000000000&quot;</ETag><Size>6702</Size>'
38+
b'<Owner><ID>AAAAAAAAAAAAAAAAAAA</ID>'
39+
b'<DisplayName>dummy</DisplayName></Owner>'
40+
b'<StorageClass>STANDARD</StorageClass></Contents></ListBucketResult>'
41+
)
42+
43+
44+
class FakeRawResponse(BytesIO):
45+
async def read(self, amt=-1):
46+
if amt == -1: # aiohttp to regular response
47+
amt = None
48+
return super().read(amt)
49+
50+
51+
class TestGetResponse(BaseResponseTest):
52+
maxDiff = None
53+
54+
async def test_get_response_streaming_ok(self, session):
55+
headers = {
56+
'content-type': 'image/png',
57+
'server': 'AmazonS3',
58+
'AcceptRanges': 'bytes',
59+
'transfer-encoding': 'chunked',
60+
'ETag': '"00000000000000000000000000000000"',
61+
}
62+
raw = FakeRawResponse(b'\x89PNG\r\n\x1a\n\x00\x00')
63+
64+
http_response = AioAWSResponse(None, 200, headers, raw)
65+
66+
service_model = await session.get_service_model('s3')
67+
operation_model = service_model.operation_model('GetObject')
68+
69+
res = await response.get_response(operation_model, http_response)
70+
assert isinstance(res[1]['Body'], response.StreamingBody)
71+
assert res[1]['ETag'] == '"00000000000000000000000000000000"'
72+
73+
async def test_get_response_streaming_ng(self, session):
74+
headers = {
75+
'content-type': 'application/xml',
76+
'date': 'Sat, 08 Mar 2014 12:05:44 GMT',
77+
'server': 'AmazonS3',
78+
'transfer-encoding': 'chunked',
79+
'x-amz-id-2': 'AAAAAAAAAAAAAAAAAAA',
80+
'x-amz-request-id': 'XXXXXXXXXXXXXXXX',
81+
}
82+
raw = FakeRawResponse(XMLBODY1)
83+
http_response = AioAWSResponse(None, 403, headers, raw)
84+
85+
service_model = await session.get_service_model('s3')
86+
operation_model = service_model.operation_model('GetObject')
87+
88+
self.assert_response_with_subset_metadata(
89+
(await response.get_response(operation_model, http_response))[1],
90+
{
91+
'Error': {'Message': 'Access Denied', 'Code': 'AccessDenied'},
92+
'ResponseMetadata': {
93+
'HostId': 'AAAAAAAAAAAAAAAAAAA',
94+
'RequestId': 'XXXXXXXXXXXXXXXX',
95+
'HTTPStatusCode': 403,
96+
},
97+
},
98+
)
99+
100+
async def test_get_response_nonstreaming_ok(self, session):
101+
headers = {
102+
'content-type': 'application/xml',
103+
'date': 'Sun, 09 Mar 2014 02:55:43 GMT',
104+
'server': 'AmazonS3',
105+
'transfer-encoding': 'chunked',
106+
'x-amz-id-2': 'AAAAAAAAAAAAAAAAAAA',
107+
'x-amz-request-id': 'XXXXXXXXXXXXXXXX',
108+
}
109+
raw = FakeRawResponse(XMLBODY1)
110+
http_response = AioAWSResponse(None, 403, headers, raw)
111+
112+
service_model = await session.get_service_model('s3')
113+
operation_model = service_model.operation_model('ListObjects')
114+
115+
self.assert_response_with_subset_metadata(
116+
(await response.get_response(operation_model, http_response))[1],
117+
{
118+
'ResponseMetadata': {
119+
'RequestId': 'XXXXXXXXXXXXXXXX',
120+
'HostId': 'AAAAAAAAAAAAAAAAAAA',
121+
'HTTPStatusCode': 403,
122+
},
123+
'Error': {'Message': 'Access Denied', 'Code': 'AccessDenied'},
124+
},
125+
)
126+
127+
async def test_get_response_nonstreaming_ng(self, session):
128+
headers = {
129+
'content-type': 'application/xml',
130+
'date': 'Sat, 08 Mar 2014 12:05:44 GMT',
131+
'server': 'AmazonS3',
132+
'transfer-encoding': 'chunked',
133+
'x-amz-id-2': 'AAAAAAAAAAAAAAAAAAA',
134+
'x-amz-request-id': 'XXXXXXXXXXXXXXXX',
135+
}
136+
raw = FakeRawResponse(XMLBODY2)
137+
http_response = AioAWSResponse(None, 200, headers, raw)
138+
139+
service_model = await session.get_service_model('s3')
140+
operation_model = service_model.operation_model('ListObjects')
141+
142+
self.assert_response_with_subset_metadata(
143+
(await response.get_response(operation_model, http_response))[1],
144+
{
145+
'Contents': [
146+
{
147+
'ETag': '"00000000000000000000000000000000"',
148+
'Key': 'test.png',
149+
'LastModified': datetime.datetime(
150+
2014, 3, 1, 17, 6, 40, tzinfo=tzutc()
151+
),
152+
'Owner': {
153+
'DisplayName': 'dummy',
154+
'ID': 'AAAAAAAAAAAAAAAAAAA',
155+
},
156+
'Size': 6702,
157+
'StorageClass': 'STANDARD',
158+
}
159+
],
160+
'IsTruncated': False,
161+
'Marker': "",
162+
'MaxKeys': 1000,
163+
'Name': 'mybucket',
164+
'Prefix': "",
165+
'ResponseMetadata': {
166+
'RequestId': 'XXXXXXXXXXXXXXXX',
167+
'HostId': 'AAAAAAAAAAAAAAAAAAA',
168+
'HTTPStatusCode': 200,
169+
},
170+
},
171+
)

tests/test_patches.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def test_protocol_parsers():
216216
(
217217
ClientCreator._register_retries,
218218
{
219-
'16d3064142e5f9e45b0094bbfabf7be30183f255',
219+
'ceecfc40a4ffa83551d7e6f63fd5d17f820664b0',
220220
},
221221
),
222222
(
@@ -701,7 +701,7 @@ def test_protocol_parsers():
701701
(
702702
Endpoint._do_get_response,
703703
{
704-
'abad88892d61b22f2a6decffba2e40d070ba9f38',
704+
'a9f3ae0f375ba9967425b1fb3e4d45537df4f1e3',
705705
},
706706
),
707707
(
@@ -926,7 +926,7 @@ def test_protocol_parsers():
926926
(
927927
get_response,
928928
{
929-
'6515f43730b546419695c26d4bc0d198fde54b10',
929+
'ea8686ae71fae32410e2f1774e5774d6715dd492',
930930
},
931931
),
932932
# session.py

uv.lock

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)