Skip to content

Commit 3b50c31

Browse files
Merge branch 'release-0.8.1'
* release-0.8.1: Bumping version to 0.8.1 Update minimum required botocore version to 1.33.2 (#289) Add support for CRC32 default for s3express
2 parents c459ba8 + 823dc6b commit 3b50c31

File tree

9 files changed

+162
-19
lines changed

9 files changed

+162
-19
lines changed

.changes/0.8.1.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"category": "``s3``",
4+
"description": "Added support for defaulting checksums to CRC32 for s3express.",
5+
"type": "enhancement"
6+
}
7+
]

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
CHANGELOG
33
=========
44

5+
0.8.1
6+
=====
7+
8+
* enhancement:``s3``: Added support for defaulting checksums to CRC32 for s3express.
9+
10+
511
0.8.0
612
=====
713

s3transfer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def __call__(self, bytes_amount):
144144
from s3transfer.exceptions import RetriesExceededError, S3UploadFailedError
145145

146146
__author__ = 'Amazon Web Services'
147-
__version__ = '0.8.0'
147+
__version__ = '0.8.1'
148148

149149

150150
class NullHandler(logging.Handler):

s3transfer/manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
OSUtils,
3636
SlidingWindowSemaphore,
3737
TaskSemaphore,
38+
add_s3express_defaults,
3839
get_callbacks,
3940
signal_not_transferring,
4041
signal_transferring,
@@ -320,6 +321,7 @@ def upload(self, fileobj, bucket, key, extra_args=None, subscribers=None):
320321
subscribers = []
321322
self._validate_all_known_args(extra_args, self.ALLOWED_UPLOAD_ARGS)
322323
self._validate_if_bucket_supported(bucket)
324+
self._add_operation_defaults(bucket, extra_args)
323325
call_args = CallArgs(
324326
fileobj=fileobj,
325327
bucket=bucket,
@@ -502,6 +504,9 @@ def _validate_all_known_args(self, actual, allowed):
502504
"must be one of: %s" % (kwarg, ', '.join(allowed))
503505
)
504506

507+
def _add_operation_defaults(self, bucket, extra_args):
508+
add_s3express_defaults(bucket, extra_args)
509+
505510
def _submit_transfer(
506511
self, call_args, submission_task_cls, extra_main_kwargs=None
507512
):

s3transfer/utils.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from collections import defaultdict
2323

2424
from botocore.exceptions import IncompleteReadError, ReadTimeoutError
25+
from botocore.httpchecksum import AwsChunkedWrapper
26+
from botocore.utils import is_s3express_bucket
2527

2628
from s3transfer.compat import SOCKET_ERROR, fallocate, rename_file
2729

@@ -54,10 +56,12 @@ def signal_not_transferring(request, operation_name, **kwargs):
5456

5557

5658
def signal_transferring(request, operation_name, **kwargs):
57-
if operation_name in ['PutObject', 'UploadPart'] and hasattr(
58-
request.body, 'signal_transferring'
59-
):
60-
request.body.signal_transferring()
59+
if operation_name in ['PutObject', 'UploadPart']:
60+
body = request.body
61+
if isinstance(body, AwsChunkedWrapper):
62+
body = getattr(body, '_raw', None)
63+
if hasattr(body, 'signal_transferring'):
64+
body.signal_transferring()
6165

6266

6367
def calculate_num_parts(size, part_size):
@@ -800,3 +804,9 @@ def _adjust_for_max_parts(self, current_chunksize, file_size):
800804
)
801805

802806
return chunksize
807+
808+
809+
def add_s3express_defaults(bucket, extra_args):
810+
if is_s3express_bucket(bucket) and "ChecksumAlgorithm" not in extra_args:
811+
# Default Transfer Operations to S3Express to use CRC32
812+
extra_args["ChecksumAlgorithm"] = "crc32"

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ universal = 0
33

44
[metadata]
55
requires_dist =
6-
botocore>=1.32.7,<2.0a.0
6+
botocore>=1.33.2,<2.0a.0
77

88
[options.extras_require]
9-
crt = botocore[crt]>=1.32.7,<2.0a0
9+
crt = botocore[crt]>=1.33.2,<2.0a0
1010

1111
[flake8]
1212
ignore = E203,E226,E501,W503,W504

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
requires = [
12-
'botocore>=1.32.7,<2.0a.0',
12+
'botocore>=1.33.2,<2.0a.0',
1313
]
1414

1515

@@ -30,7 +30,7 @@ def get_version():
3030
include_package_data=True,
3131
install_requires=requires,
3232
extras_require={
33-
'crt': 'botocore[crt]>=1.32.7,<2.0a.0',
33+
'crt': 'botocore[crt]>=1.33.2,<2.0a.0',
3434
},
3535
license="Apache License 2.0",
3636
python_requires=">= 3.7",

tests/functional/test_upload.py

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,12 @@ class TestNonMultipartUpload(BaseUploadTest):
142142
__test__ = True
143143

144144
def add_put_object_response_with_default_expected_params(
145-
self, extra_expected_params=None
145+
self, extra_expected_params=None, bucket=None
146146
):
147-
expected_params = {'Body': ANY, 'Bucket': self.bucket, 'Key': self.key}
147+
if bucket is None:
148+
bucket = self.bucket
149+
150+
expected_params = {'Body': ANY, 'Bucket': bucket, 'Key': self.key}
148151
if extra_expected_params:
149152
expected_params.update(extra_expected_params)
150153
upload_response = self.create_stubbed_responses()[0]
@@ -167,9 +170,9 @@ def test_upload(self):
167170
self.assert_put_object_body_was_correct()
168171

169172
def test_upload_with_checksum(self):
170-
self.extra_args['ChecksumAlgorithm'] = 'crc32'
173+
self.extra_args['ChecksumAlgorithm'] = 'sha256'
171174
self.add_put_object_response_with_default_expected_params(
172-
extra_expected_params={'ChecksumAlgorithm': 'crc32'}
175+
extra_expected_params={'ChecksumAlgorithm': 'sha256'}
173176
)
174177
future = self.manager.upload(
175178
self.filename, self.bucket, self.key, self.extra_args
@@ -178,6 +181,21 @@ def test_upload_with_checksum(self):
178181
self.assert_expected_client_calls_were_correct()
179182
self.assert_put_object_body_was_correct()
180183

184+
def test_upload_with_s3express_default_checksum(self):
185+
s3express_bucket = "mytestbucket--usw2-az6--x-s3"
186+
self.assertFalse("ChecksumAlgorithm" in self.extra_args)
187+
188+
self.add_put_object_response_with_default_expected_params(
189+
extra_expected_params={'ChecksumAlgorithm': 'crc32'},
190+
bucket=s3express_bucket,
191+
)
192+
future = self.manager.upload(
193+
self.filename, s3express_bucket, self.key, self.extra_args
194+
)
195+
future.result()
196+
self.assert_expected_client_calls_were_correct()
197+
self.assert_put_object_body_was_correct()
198+
181199
def test_upload_for_fileobj(self):
182200
self.add_put_object_response_with_default_expected_params()
183201
with open(self.filename, 'rb') as f:
@@ -342,24 +360,34 @@ def assert_upload_part_bodies_were_correct(self):
342360
self.assertEqual(self.sent_bodies, expected_contents)
343361

344362
def add_create_multipart_response_with_default_expected_params(
345-
self, extra_expected_params=None
363+
self,
364+
extra_expected_params=None,
365+
bucket=None,
346366
):
347-
expected_params = {'Bucket': self.bucket, 'Key': self.key}
367+
if bucket is None:
368+
bucket = self.bucket
369+
370+
expected_params = {'Bucket': bucket, 'Key': self.key}
348371
if extra_expected_params:
349372
expected_params.update(extra_expected_params)
350373
response = self.create_stubbed_responses()[0]
351374
response['expected_params'] = expected_params
352375
self.stubber.add_response(**response)
353376

354377
def add_upload_part_responses_with_default_expected_params(
355-
self, extra_expected_params=None
378+
self,
379+
extra_expected_params=None,
380+
bucket=None,
356381
):
382+
if bucket is None:
383+
bucket = self.bucket
384+
357385
num_parts = 3
358386
upload_part_responses = self.create_stubbed_responses()[1:-1]
359387
for i in range(num_parts):
360388
upload_part_response = upload_part_responses[i]
361389
expected_params = {
362-
'Bucket': self.bucket,
390+
'Bucket': bucket,
363391
'Key': self.key,
364392
'UploadId': self.multipart_id,
365393
'Body': ANY,
@@ -378,10 +406,15 @@ def add_upload_part_responses_with_default_expected_params(
378406
self.stubber.add_response(**upload_part_response)
379407

380408
def add_complete_multipart_response_with_default_expected_params(
381-
self, extra_expected_params=None
409+
self,
410+
extra_expected_params=None,
411+
bucket=None,
382412
):
413+
if bucket is None:
414+
bucket = self.bucket
415+
383416
expected_params = {
384-
'Bucket': self.bucket,
417+
'Bucket': bucket,
385418
'Key': self.key,
386419
'UploadId': self.multipart_id,
387420
'MultipartUpload': {
@@ -600,6 +633,54 @@ def test_multipart_upload_passes_checksums(self):
600633
future.result()
601634
self.assert_expected_client_calls_were_correct()
602635

636+
def test_multipart_upload_sets_s3express_default_checksum(self):
637+
s3express_bucket = "mytestbucket--usw2-az6--x-s3"
638+
self.assertFalse('ChecksumAlgorithm' in self.extra_args)
639+
640+
# ChecksumAlgorithm should be passed on the create_multipart call
641+
self.add_create_multipart_response_with_default_expected_params(
642+
extra_expected_params={'ChecksumAlgorithm': 'crc32'},
643+
bucket=s3express_bucket,
644+
)
645+
646+
# ChecksumAlgorithm should be forwarded and a SHA1 will come back
647+
self.add_upload_part_responses_with_default_expected_params(
648+
extra_expected_params={'ChecksumAlgorithm': 'crc32'},
649+
bucket=s3express_bucket,
650+
)
651+
652+
# The checksums should be used in the complete call like etags
653+
self.add_complete_multipart_response_with_default_expected_params(
654+
extra_expected_params={
655+
'MultipartUpload': {
656+
'Parts': [
657+
{
658+
'ETag': 'etag-1',
659+
'PartNumber': 1,
660+
'ChecksumCRC32': 'sum1==',
661+
},
662+
{
663+
'ETag': 'etag-2',
664+
'PartNumber': 2,
665+
'ChecksumCRC32': 'sum2==',
666+
},
667+
{
668+
'ETag': 'etag-3',
669+
'PartNumber': 3,
670+
'ChecksumCRC32': 'sum3==',
671+
},
672+
]
673+
}
674+
},
675+
bucket=s3express_bucket,
676+
)
677+
678+
future = self.manager.upload(
679+
self.filename, s3express_bucket, self.key, self.extra_args
680+
)
681+
future.result()
682+
self.assert_expected_client_calls_were_correct()
683+
603684
def test_multipart_upload_with_ssec_args(self):
604685
params = {
605686
'RequestPayer': 'requester',

tests/unit/test_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import time
2121
from io import BytesIO, StringIO
2222

23+
import pytest
24+
2325
from s3transfer.futures import TransferFuture, TransferMeta
2426
from s3transfer.utils import (
2527
MAX_PARTS,
@@ -36,6 +38,7 @@
3638
SlidingWindowSemaphore,
3739
StreamReaderProgress,
3840
TaskSemaphore,
41+
add_s3express_defaults,
3942
calculate_num_parts,
4043
calculate_range_parameter,
4144
get_callbacks,
@@ -1187,3 +1190,34 @@ def test_unknown_file_size_above_maximum(self):
11871190
chunksize = MAX_SINGLE_UPLOAD_SIZE + 1
11881191
new_size = self.adjuster.adjust_chunksize(chunksize)
11891192
self.assertEqual(new_size, MAX_SINGLE_UPLOAD_SIZE)
1193+
1194+
1195+
class TestS3ExpressDefaults:
1196+
@pytest.mark.parametrize(
1197+
"bucket,extra_args,expected",
1198+
(
1199+
(
1200+
"mytestbucket--usw2-az2--x-s3",
1201+
{},
1202+
{"ChecksumAlgorithm": "crc32"},
1203+
),
1204+
(
1205+
"mytestbucket--usw2-az2--x-s3",
1206+
{"Some": "Setting"},
1207+
{"ChecksumAlgorithm": "crc32", "Some": "Setting"},
1208+
),
1209+
(
1210+
"mytestbucket",
1211+
{},
1212+
{},
1213+
),
1214+
(
1215+
"mytestbucket--usw2-az2--x-s3",
1216+
{"ChecksumAlgorithm": "sha256"},
1217+
{"ChecksumAlgorithm": "sha256"},
1218+
),
1219+
),
1220+
)
1221+
def test_add_s3express_defaults(self, bucket, extra_args, expected):
1222+
add_s3express_defaults(bucket, extra_args)
1223+
assert extra_args == expected

0 commit comments

Comments
 (0)