Skip to content

Commit 0155398

Browse files
authored
Merge pull request #277 from sbkok/fix/gen-params-intrinsic-upload-error-handling
Fix error handling of generate_params intrinsic upload function
2 parents 6a07310 + 0fbe83c commit 0155398

File tree

2 files changed

+336
-15
lines changed
  • src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python

2 files changed

+336
-15
lines changed

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/s3.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,84 @@ def build_pathing_style(self, style, key):
5858
"virtual-hosted.".format(style=style)
5959
)
6060

61-
6261
def put_object(self, key, file_path, style="path", pre_check=False):
6362
"""
64-
Put the object into S3 and return the S3 URL of the object
63+
Put the object into S3 and return the reference to the object
64+
in the requested path style.
65+
66+
Args:
67+
key (str): The S3 object key to check and/or write to.
68+
69+
file_path (str): The file to upload using binary write mode.
70+
71+
style (str): The path style to use when returning the S3 object
72+
location. Valid values are listed in this class using the
73+
static method: supported_path_styles.
74+
75+
pre_check (bool): Whether or not to check if the file exists
76+
in the S3 bucket already. When set to True, it will only
77+
upload if the object does not exist yet. When set to False
78+
it will always perform the upload, whether the object already
79+
exists or not. Be aware, the contents of the object and the
80+
given file are not compared. Only whether the given object key
81+
exists in the bucket or not.
82+
83+
Returns:
84+
str: The S3 object reference in the requested path style. This
85+
will be returned regardless of whether or not an upload was
86+
performed or not. In case the object key existed before, and
87+
pre_check was set to True, calling this function will only
88+
return the reference path to the object.
6589
"""
66-
if pre_check:
67-
try:
68-
self.client.get_object(Bucket=self.bucket, Key=key)
69-
except self.client.exceptions.NoSuchKey:
70-
LOGGER.info("Uploading %s as %s to S3 Bucket %s in %s", file_path, key, self.bucket, self.region)
71-
self.resource.Object(self.bucket, key).put(Body=open(file_path, 'rb'))
72-
finally:
73-
return self.build_pathing_style(style, key) #pylint: disable=W0150
74-
self.resource.Object(self.bucket, key).put(Body=open(file_path, 'rb'))
90+
# Do we need to upload the file to the bucket?
91+
# If we don't need to check first, do. Otherwise, check if it exists
92+
# first and only upload if it does not exist.
93+
if not pre_check or not self._does_object_exist(key):
94+
self._perform_put_object(key, file_path)
7595
return self.build_pathing_style(style, key)
7696

97+
def _does_object_exist(self, key):
98+
"""
99+
Check whether the given S3 object key exists in this bucket or not.
100+
101+
Args:
102+
key (str): The S3 object key to check.
103+
104+
Returns:
105+
bool: True when the object exists, False when it does not.
106+
"""
107+
try:
108+
self.client.get_object(Bucket=self.bucket, Key=key)
109+
return True
110+
except self.client.exceptions.NoSuchKey:
111+
return False
112+
113+
def _perform_put_object(self, key, file_path):
114+
"""
115+
Perform actual put operation without any checks.
116+
This is called internally by the put_object method when the
117+
requested file needs to be uploaded.
118+
119+
Args:
120+
key (str): They S3 key of the object to put the file contents to.
121+
122+
file_path (str): The file to upload using binary write mode.
123+
"""
124+
try:
125+
LOGGER.info(
126+
"Uploading %s as %s to S3 Bucket %s in %s",
127+
file_path,
128+
key,
129+
self.bucket,
130+
self.region,
131+
)
132+
with open(file_path, 'rb') as file_handler:
133+
self.resource.Object(self.bucket, key).put(Body=file_handler)
134+
LOGGER.debug("Upload of %s was successful.", key)
135+
except BaseException:
136+
LOGGER.error("Failed to upload %s", key, exc_info=True)
137+
raise
138+
77139
def read_object(self, key):
78140
s3_object = self.resource.Object(self.bucket, key)
79141
return s3_object.get()['Body'].read().decode('utf-8')

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_s3.py

Lines changed: 263 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
# pylint: skip-file
55

6-
import os
7-
import boto3
6+
import botocore
7+
from botocore.stub import Stubber
88
from pytest import fixture, raises
9-
from stubs import stub_s3
10-
from mock import Mock
9+
from mock import Mock, patch, mock_open
1110
from s3 import S3
1211

1312
@fixture
@@ -117,3 +116,263 @@ def test_build_pathing_style_unknown_style(us_east_1_cls):
117116

118117
error_message = str(excinfo.value)
119118
assert error_message.find(correct_error_message) >= 0
119+
120+
121+
@patch('s3.S3.build_pathing_style')
122+
@patch('s3.S3._perform_put_object')
123+
@patch('s3.S3._does_object_exist')
124+
def test_put_object_no_checks_always_upload(does_exist, perform_put,
125+
build_path, eu_west_1_cls):
126+
object_key = "some"
127+
object_path = "s3://bucket/{key}".format(key=object_key)
128+
file_path = "some_imaginary_file.json"
129+
path_style = "s3-url"
130+
does_exist.return_value = True
131+
build_path.return_value = object_path
132+
133+
return_value = eu_west_1_cls.put_object(
134+
key=object_key,
135+
file_path=file_path,
136+
style=path_style,
137+
pre_check=False,
138+
)
139+
140+
assert return_value == object_path
141+
142+
does_exist.assert_not_called()
143+
perform_put.assert_called_once_with(object_key, file_path)
144+
build_path.assert_called_once_with(path_style, object_key)
145+
146+
147+
@patch('s3.S3.build_pathing_style')
148+
@patch('s3.S3._perform_put_object')
149+
@patch('s3.S3._does_object_exist')
150+
def test_put_object_do_check_upload_when_missing(
151+
does_exist, perform_put, build_path, eu_west_1_cls):
152+
object_key = "some"
153+
object_path = "s3://bucket/{key}".format(key=object_key)
154+
file_path = "some_imaginary_file.json"
155+
path_style = "s3-url"
156+
does_exist.return_value = False
157+
build_path.return_value = object_path
158+
159+
return_value = eu_west_1_cls.put_object(
160+
key=object_key,
161+
file_path=file_path,
162+
style=path_style,
163+
pre_check=True,
164+
)
165+
166+
assert return_value == object_path
167+
168+
does_exist.assert_called_once_with(object_key)
169+
perform_put.assert_called_once_with(object_key, file_path)
170+
build_path.assert_called_once_with(path_style, object_key)
171+
172+
173+
@patch('s3.S3.build_pathing_style')
174+
@patch('s3.S3._perform_put_object')
175+
@patch('s3.S3._does_object_exist')
176+
def test_put_object_do_check_no_upload_object_present(
177+
does_exist, perform_put, build_path, eu_west_1_cls):
178+
object_key = "some"
179+
object_path = "s3://bucket/{key}".format(key=object_key)
180+
file_path = "some_imaginary_file.json"
181+
path_style = "s3-url"
182+
does_exist.return_value = True
183+
build_path.return_value = object_path
184+
185+
return_value = eu_west_1_cls.put_object(
186+
key=object_key,
187+
file_path=file_path,
188+
style=path_style,
189+
pre_check=True,
190+
)
191+
192+
assert return_value == object_path
193+
194+
does_exist.assert_called_once_with(object_key)
195+
perform_put.assert_not_called()
196+
build_path.assert_called_once_with(path_style, object_key)
197+
198+
199+
@patch('s3.boto3.client')
200+
def test_does_object_exist_yes(boto3_client):
201+
s3_client = botocore.session.get_session().create_client('s3')
202+
s3_client_stubber = Stubber(s3_client)
203+
boto3_client.return_value = s3_client
204+
object_key = "some"
205+
206+
s3_cls = S3(
207+
'eu-west-1',
208+
'some_bucket'
209+
)
210+
response = {}
211+
expected_params = {
212+
'Bucket': s3_cls.bucket,
213+
'Key': object_key,
214+
}
215+
s3_client_stubber.add_response('get_object', response, expected_params)
216+
s3_client_stubber.activate()
217+
218+
assert s3_cls._does_object_exist(key=object_key)
219+
220+
boto3_client.assert_called_once_with('s3', region_name='eu-west-1')
221+
s3_client_stubber.assert_no_pending_responses()
222+
223+
224+
@patch('s3.boto3.client')
225+
def test_does_object_exist_no(boto3_client):
226+
s3_client = botocore.session.get_session().create_client('s3')
227+
s3_client_stubber = Stubber(s3_client)
228+
boto3_client.return_value = s3_client
229+
object_key = "some"
230+
231+
s3_cls = S3(
232+
'eu-west-1',
233+
'some_bucket'
234+
)
235+
s3_client_stubber.add_client_error(
236+
'get_object',
237+
expected_params={'Bucket': s3_cls.bucket, 'Key': object_key},
238+
http_status_code=404,
239+
service_error_code='NoSuchKey',
240+
)
241+
s3_client_stubber.activate()
242+
243+
assert not s3_cls._does_object_exist(key=object_key)
244+
245+
boto3_client.assert_called_once_with('s3', region_name='eu-west-1')
246+
s3_client_stubber.assert_no_pending_responses()
247+
248+
249+
@patch('s3.boto3.resource')
250+
@patch('s3.LOGGER')
251+
def test_perform_put_object_success(logger, boto3_resource):
252+
s3_resource = Mock()
253+
s3_object = Mock()
254+
s3_resource.Object.return_value = s3_object
255+
boto3_resource.return_value = s3_resource
256+
object_key = "some"
257+
file_path = "some-file.json"
258+
file_data = 'some file data'
259+
260+
s3_cls = S3(
261+
'eu-west-1',
262+
'some_bucket'
263+
)
264+
with patch("builtins.open", mock_open(read_data=file_data)) as mock_file:
265+
s3_cls._perform_put_object(
266+
key=object_key,
267+
file_path=file_path,
268+
)
269+
mock_file.assert_called_with(file_path, 'rb')
270+
s3_resource.Object.assert_called_once_with(s3_cls.bucket, object_key)
271+
s3_object.put.assert_called_once_with(Body=mock_file.return_value)
272+
273+
logger.info.assert_called_once_with(
274+
"Uploading %s as %s to S3 Bucket %s in %s",
275+
file_path,
276+
object_key,
277+
s3_cls.bucket,
278+
s3_cls.region,
279+
)
280+
logger.debug.assert_called_once_with(
281+
"Upload of %s was successful.",
282+
object_key,
283+
)
284+
logger.error.assert_not_called()
285+
boto3_resource.assert_called_with('s3', region_name='eu-west-1')
286+
287+
288+
@patch('s3.boto3.resource')
289+
@patch('s3.LOGGER')
290+
def test_perform_put_object_no_such_file(logger, boto3_resource):
291+
s3_resource = Mock()
292+
s3_object = Mock()
293+
s3_resource.Object.return_value = s3_object
294+
boto3_resource.return_value = s3_resource
295+
object_key = "some"
296+
file_path = "some-file.json"
297+
298+
s3_cls = S3(
299+
'eu-west-1',
300+
'some_bucket'
301+
)
302+
correct_error_message = "File not found exception"
303+
with patch("builtins.open") as mock_file:
304+
mock_file.side_effect = Exception(correct_error_message)
305+
with raises(Exception) as excinfo:
306+
s3_cls._perform_put_object(
307+
key=object_key,
308+
file_path=file_path,
309+
)
310+
311+
error_message = str(excinfo.value)
312+
assert error_message.find(correct_error_message) >= 0
313+
314+
mock_file.assert_called_with(file_path, 'rb')
315+
s3_resource.Object.assert_not_called()
316+
s3_object.put.assert_not_called()
317+
318+
logger.info.assert_called_once_with(
319+
"Uploading %s as %s to S3 Bucket %s in %s",
320+
file_path,
321+
object_key,
322+
s3_cls.bucket,
323+
s3_cls.region,
324+
)
325+
logger.debug.assert_not_called()
326+
logger.error.assert_called_once_with(
327+
"Failed to upload %s",
328+
object_key,
329+
exc_info=True,
330+
)
331+
boto3_resource.assert_called_with('s3', region_name='eu-west-1')
332+
333+
334+
@patch('s3.boto3.resource')
335+
@patch('s3.LOGGER')
336+
def test_perform_put_object_failed(logger, boto3_resource):
337+
s3_resource = Mock()
338+
s3_object = Mock()
339+
s3_resource.Object.return_value = s3_object
340+
boto3_resource.return_value = s3_resource
341+
object_key = "some"
342+
file_path = "some-file.json"
343+
file_data = 'some file data'
344+
345+
s3_cls = S3(
346+
'eu-west-1',
347+
'some_bucket'
348+
)
349+
correct_error_message = "Test exception"
350+
s3_object.put.side_effect = Exception(correct_error_message)
351+
with patch("builtins.open", mock_open(read_data=file_data)) as mock_file:
352+
with raises(Exception) as excinfo:
353+
s3_cls._perform_put_object(
354+
key=object_key,
355+
file_path=file_path,
356+
)
357+
358+
error_message = str(excinfo.value)
359+
assert error_message.find(correct_error_message) >= 0
360+
361+
mock_file.assert_called_with(file_path, 'rb')
362+
s3_resource.Object.assert_called_once_with(s3_cls.bucket, object_key)
363+
s3_object.put.assert_called_once_with(Body=mock_file.return_value)
364+
365+
logger.info.assert_called_once_with(
366+
"Uploading %s as %s to S3 Bucket %s in %s",
367+
file_path,
368+
object_key,
369+
s3_cls.bucket,
370+
s3_cls.region,
371+
)
372+
logger.debug.assert_not_called()
373+
logger.error.assert_called_once_with(
374+
"Failed to upload %s",
375+
object_key,
376+
exc_info=True,
377+
)
378+
boto3_resource.assert_called_with('s3', region_name='eu-west-1')

0 commit comments

Comments
 (0)