Skip to content

Commit 0323658

Browse files
Merge branch 'release-0.10.4'
* release-0.10.4: Bumping version to 0.10.4 Add MRAP support to CRT transfer manager (#319) Remove macOS fallback now that setup-python issue is resolved (#317)
2 parents 7878fbf + 1d2c48a commit 0323658

File tree

7 files changed

+122
-24
lines changed

7 files changed

+122
-24
lines changed

.changes/0.10.4.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 Multi-Region Access Points support to CRT transfers.",
5+
"type": "enhancement"
6+
}
7+
]

.github/workflows/run-crt-test.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ jobs:
1414
matrix:
1515
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1616
os: [ubuntu-latest, macOS-latest, windows-latest]
17-
# Python 3.8 and 3.9 do not run on m1 hardware which is now standard for
18-
# macOS-latest.
19-
# https://github.com/actions/setup-python/issues/696#issuecomment-1637587760
20-
exclude:
21-
- { python-version: "3.8", os: "macos-latest" }
22-
- { python-version: "3.9", os: "macos-latest" }
23-
include:
24-
- { python-version: "3.8", os: "macos-13" }
25-
- { python-version: "3.9", os: "macos-13" }
2617

2718
steps:
2819
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3

.github/workflows/run-tests.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ jobs:
1717
matrix:
1818
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1919
os: [ubuntu-latest, macOS-latest, windows-latest]
20-
# Python 3.8 and 3.9 do not run on m1 hardware which is now standard for
21-
# macOS-latest.
22-
# https://github.com/actions/setup-python/issues/696#issuecomment-1637587760
23-
exclude:
24-
- { python-version: "3.8", os: "macos-latest" }
25-
- { python-version: "3.9", os: "macos-latest" }
26-
include:
27-
- { python-version: "3.8", os: "macos-13" }
28-
- { python-version: "3.9", os: "macos-13" }
2920

3021
steps:
3122
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3

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.10.4
6+
======
7+
8+
* enhancement:``s3``: Added Multi-Region Access Points support to CRT transfers.
9+
10+
511
0.10.3
612
======
713

s3transfer/__init__.py

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

147147
__author__ = 'Amazon Web Services'
148-
__version__ = '0.10.3'
148+
__version__ = '0.10.4'
149149

150150

151151
class NullHandler(logging.Handler):

s3transfer/crt.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
import logging
14+
import re
1415
import threading
1516
from io import BytesIO
1617

@@ -36,6 +37,7 @@
3637
from botocore.compat import urlsplit
3738
from botocore.config import Config
3839
from botocore.exceptions import NoCredentialsError
40+
from botocore.utils import ArnParser, InvalidArnException
3941

4042
from s3transfer.constants import MB
4143
from s3transfer.exceptions import TransferNotDoneError
@@ -874,7 +876,18 @@ def _default_get_make_request_args(
874876
x.title() for x in request_type.split('_')
875877
)
876878

877-
if is_s3express_bucket(call_args.bucket):
879+
arn_handler = _S3ArnParamHandler()
880+
if (
881+
accesspoint_arn_details := arn_handler.handle_arn(call_args.bucket)
882+
) and accesspoint_arn_details['region'] == "":
883+
# Configure our region to `*` to propogate in `x-amz-region-set`
884+
# for multi-region support in MRAP accesspoints.
885+
make_request_args['signing_config'] = AwsSigningConfig(
886+
algorithm=AwsSigningAlgorithm.V4_ASYMMETRIC,
887+
region="*",
888+
)
889+
call_args.bucket = accesspoint_arn_details['resource_name']
890+
elif is_s3express_bucket(call_args.bucket):
878891
make_request_args['signing_config'] = AwsSigningConfig(
879892
algorithm=AwsSigningAlgorithm.V4_S3EXPRESS
880893
)
@@ -917,3 +930,41 @@ def __init__(self, fileobj):
917930

918931
def __call__(self, chunk, **kwargs):
919932
self._fileobj.write(chunk)
933+
934+
935+
class _S3ArnParamHandler:
936+
"""Partial port of S3ArnParamHandler from botocore.
937+
938+
This is used to make a determination on MRAP accesspoints for signing
939+
purposes. This should be safe to remove once we properly integrate auth
940+
resolution from Botocore into the CRT transfer integration.
941+
"""
942+
943+
_RESOURCE_REGEX = re.compile(
944+
r'^(?P<resource_type>accesspoint|outpost)[/:](?P<resource_name>.+)$'
945+
)
946+
947+
def __init__(self):
948+
self._arn_parser = ArnParser()
949+
950+
def handle_arn(self, bucket):
951+
arn_details = self._get_arn_details_from_bucket(bucket)
952+
if arn_details is None:
953+
return
954+
if arn_details['resource_type'] == 'accesspoint':
955+
return arn_details
956+
957+
def _get_arn_details_from_bucket(self, bucket):
958+
try:
959+
arn_details = self._arn_parser.parse_arn(bucket)
960+
self._add_resource_type_and_name(arn_details)
961+
return arn_details
962+
except InvalidArnException:
963+
pass
964+
return None
965+
966+
def _add_resource_type_and_name(self, arn_details):
967+
match = self._RESOURCE_REGEX.match(arn_details['resource'])
968+
if match:
969+
arn_details['resource_type'] = match.group('resource_type')
970+
arn_details['resource_name'] = match.group('resource_name')

tests/functional/test_crt.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ def setUp(self):
6969
self.region = 'us-west-2'
7070
self.bucket = "test_bucket"
7171
self.s3express_bucket = 's3expressbucket--usw2-az5--x-s3'
72+
self.mrap_accesspoint = (
73+
'arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap'
74+
)
75+
self.mrap_bucket = 'mfzwi23gnjvgw.mrap'
7276
self.key = "test_key"
7377
self.expected_content = b'my content'
7478
self.expected_download_content = b'new content'
@@ -80,6 +84,10 @@ def setUp(self):
8084
self.expected_host = f"s3.{self.region}.amazonaws.com"
8185
self.expected_s3express_host = f'{self.s3express_bucket}.s3express-usw2-az5.us-west-2.amazonaws.com'
8286
self.expected_s3express_path = f'/{self.key}'
87+
self.expected_mrap_host = (
88+
f'{self.mrap_bucket}.accesspoint.s3-global.amazonaws.com'
89+
)
90+
self.expected_mrap_path = f"/{self.key}"
8391
self.s3_request = mock.Mock(awscrt.s3.S3Request)
8492
self.s3_crt_client = mock.Mock(awscrt.s3.S3Client)
8593
self.s3_crt_client.make_request.side_effect = (
@@ -137,7 +145,7 @@ def _assert_expected_crt_http_request(
137145
for expected_missing_header in expected_missing_headers:
138146
self.assertNotIn(expected_missing_header.lower(), header_names)
139147

140-
def _assert_exected_s3express_request(
148+
def _assert_expected_s3express_request(
141149
self, make_request_kwargs, expected_http_method='GET'
142150
):
143151
self._assert_expected_crt_http_request(
@@ -152,6 +160,22 @@ def _assert_exected_s3express_request(
152160
awscrt.auth.AwsSigningAlgorithm.V4_S3EXPRESS,
153161
)
154162

163+
def _assert_expected_mrap_request(
164+
self, make_request_kwargs, expected_http_method='GET'
165+
):
166+
self._assert_expected_crt_http_request(
167+
make_request_kwargs["request"],
168+
expected_host=self.expected_mrap_host,
169+
expected_path=self.expected_mrap_path,
170+
expected_http_method=expected_http_method,
171+
)
172+
self.assertIn('signing_config', make_request_kwargs)
173+
self.assertEqual(
174+
make_request_kwargs['signing_config'].algorithm,
175+
awscrt.auth.AwsSigningAlgorithm.V4_ASYMMETRIC,
176+
)
177+
self.assertEqual(make_request_kwargs['signing_config'].region, "*")
178+
155179
def _assert_subscribers_called(self, expected_future=None):
156180
self.assertTrue(self.record_subscriber.on_queued_called)
157181
self.assertTrue(self.record_subscriber.on_done_called)
@@ -404,7 +428,21 @@ def test_upload_with_s3express(self):
404428
[self.record_subscriber],
405429
)
406430
future.result()
407-
self._assert_exected_s3express_request(
431+
self._assert_expected_s3express_request(
432+
self.s3_crt_client.make_request.call_args[1],
433+
expected_http_method='PUT',
434+
)
435+
436+
def test_upload_with_mrap(self):
437+
future = self.transfer_manager.upload(
438+
self.filename,
439+
self.mrap_accesspoint,
440+
self.key,
441+
{},
442+
[self.record_subscriber],
443+
)
444+
future.result()
445+
self._assert_expected_mrap_request(
408446
self.s3_crt_client.make_request.call_args[1],
409447
expected_http_method='PUT',
410448
)
@@ -532,7 +570,21 @@ def test_download_with_s3express(self):
532570
[self.record_subscriber],
533571
)
534572
future.result()
535-
self._assert_exected_s3express_request(
573+
self._assert_expected_s3express_request(
574+
self.s3_crt_client.make_request.call_args[1],
575+
expected_http_method='GET',
576+
)
577+
578+
def test_download_with_mrap(self):
579+
future = self.transfer_manager.download(
580+
self.mrap_accesspoint,
581+
self.key,
582+
self.filename,
583+
{},
584+
[self.record_subscriber],
585+
)
586+
future.result()
587+
self._assert_expected_mrap_request(
536588
self.s3_crt_client.make_request.call_args[1],
537589
expected_http_method='GET',
538590
)
@@ -577,7 +629,7 @@ def test_delete_with_s3express(self):
577629
self.s3express_bucket, self.key, {}, [self.record_subscriber]
578630
)
579631
future.result()
580-
self._assert_exected_s3express_request(
632+
self._assert_expected_s3express_request(
581633
self.s3_crt_client.make_request.call_args[1],
582634
expected_http_method='DELETE',
583635
)

0 commit comments

Comments
 (0)