Skip to content

Commit 350d1b0

Browse files
authored
fix: Default AsyncPredictor upload prefix to endpoint name (#6336)
AsyncPredictor accepts name=None by default, but predict() and predict_async() called name_from_base(self.name) when uploading input data without an input_path, which raised "TypeError: 'NoneType' object is not subscriptable". Fall back to the wrapped predictor's endpoint name when no name is given, and document the name argument. An explicitly provided name is still used as the S3 key prefix. Fixes #3210 Fixes #4774
1 parent c730e10 commit 350d1b0

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

‎sagemaker-serve/src/sagemaker/serve/predictor_async.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def __init__(
4040
predictor (sagemaker.predictor.Predictor): General ``Predictor``
4141
object has useful methods and variables. ``AsyncPredictor``
4242
stands on top of it with capability for async inference.
43+
name (str): Optional. Name used as the prefix of the Amazon S3 key when
44+
input data is uploaded for async inference. If not specified, the
45+
endpoint name is used. (Default: None)
4346
"""
4447
self.predictor = predictor
4548
self.endpoint_name = predictor.endpoint_name
@@ -195,10 +198,13 @@ def _upload_data_to_s3(
195198
my_uuid = str(uuid.uuid4())
196199
timestamp = sagemaker_timestamp()
197200
bucket = self.sagemaker_session.default_bucket()
201+
# ``name`` is optional; fall back to the endpoint name so the default
202+
# ``AsyncPredictor(predictor)`` construction can upload input data.
203+
base_name = self.name or self.endpoint_name
198204
key = s3.s3_path_join(
199205
self.sagemaker_session.default_bucket_prefix,
200206
"async-endpoint-inputs",
201-
name_from_base(self.name, short=True),
207+
name_from_base(base_name, short=True),
202208
"{}-{}".format(timestamp, my_uuid),
203209
)
204210

‎sagemaker-serve/tests/unit/test_predictor_async.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,51 @@ def test_upload_data_to_s3(self, mock_parse):
9090
self.assertEqual(result, "s3://bucket/key")
9191
async_predictor.s3_client.put_object.assert_called_once()
9292

93+
def test_upload_data_to_s3_without_name_uses_endpoint_name(self):
94+
# Regression test for https://github.com/aws/sagemaker-python-sdk/issues/3210
95+
self.mock_predictor.serializer.serialize.return_value = b"serialized_data"
96+
97+
async_predictor = AsyncPredictor(self.mock_predictor)
98+
async_predictor.sagemaker_session.default_bucket.return_value = "default-bucket"
99+
async_predictor.sagemaker_session.default_bucket_prefix = None
100+
101+
result = async_predictor._upload_data_to_s3("test_data")
102+
103+
self.assertIsNone(async_predictor.name)
104+
key = async_predictor.s3_client.put_object.call_args.kwargs["Key"]
105+
self.assertTrue(key.startswith("async-endpoint-inputs/test-endpoint-"))
106+
self.assertEqual(result, "s3://default-bucket/{}".format(key))
107+
108+
def test_upload_data_to_s3_with_name_uses_name(self):
109+
self.mock_predictor.serializer.serialize.return_value = b"serialized_data"
110+
111+
async_predictor = AsyncPredictor(self.mock_predictor, name="my-async")
112+
async_predictor.sagemaker_session.default_bucket.return_value = "default-bucket"
113+
async_predictor.sagemaker_session.default_bucket_prefix = None
114+
115+
async_predictor._upload_data_to_s3("test_data")
116+
117+
key = async_predictor.s3_client.put_object.call_args.kwargs["Key"]
118+
self.assertTrue(key.startswith("async-endpoint-inputs/my-async-"))
119+
120+
@patch.object(AsyncPredictor, "_submit_async_request")
121+
def test_predict_async_with_data_and_no_name(self, mock_submit):
122+
# Regression test for https://github.com/aws/sagemaker-python-sdk/issues/3210
123+
mock_submit.return_value = {
124+
"OutputLocation": "s3://bucket/output",
125+
"FailureLocation": "s3://bucket/failure",
126+
}
127+
self.mock_predictor.serializer.serialize.return_value = b"serialized_data"
128+
129+
async_predictor = AsyncPredictor(self.mock_predictor)
130+
async_predictor.sagemaker_session.default_bucket.return_value = "default-bucket"
131+
async_predictor.sagemaker_session.default_bucket_prefix = None
132+
133+
response = async_predictor.predict_async(data="test_data")
134+
135+
self.assertEqual(response.output_path, "s3://bucket/output")
136+
async_predictor.s3_client.put_object.assert_called_once()
137+
93138
def test_delete_endpoint(self):
94139
async_predictor = AsyncPredictor(self.mock_predictor)
95140
async_predictor.delete_endpoint()

0 commit comments

Comments
 (0)