1# Copyright 2017 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Support for resumable uploads.
16
17Also supported here are simple (media) uploads and multipart
18uploads that contain both metadata and a small file as payload.
19"""
20
21
22from google.resumable_media import _upload
23from google.resumable_media.requests import _request_helpers
24
25
26class SimpleUpload(_request_helpers.RequestsMixin, _upload.SimpleUpload):
27 """Upload a resource to a Google API.
28
29 A **simple** media upload sends no metadata and completes the upload
30 in a single request.
31
32 Args:
33 upload_url (str): The URL where the content will be uploaded.
34 headers (Optional[Mapping[str, str]]): Extra headers that should
35 be sent with the request, e.g. headers for encrypted data.
36
37 Attributes:
38 upload_url (str): The URL where the content will be uploaded.
39 """
40
41 def transmit(
42 self,
43 transport,
44 data,
45 content_type,
46 timeout=(
47 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
48 _request_helpers._DEFAULT_READ_TIMEOUT,
49 ),
50 ):
51 """Transmit the resource to be uploaded.
52
53 Args:
54 transport (~requests.Session): A ``requests`` object which can
55 make authenticated requests.
56 data (bytes): The resource content to be uploaded.
57 content_type (str): The content type of the resource, e.g. a JPEG
58 image has content type ``image/jpeg``.
59 timeout (Optional[Union[float, Tuple[float, float]]]):
60 The number of seconds to wait for the server response.
61 Depending on the retry strategy, a request may be repeated
62 several times using the same timeout each time.
63
64 Can also be passed as a tuple (connect_timeout, read_timeout).
65 See :meth:`requests.Session.request` documentation for details.
66
67 Returns:
68 ~requests.Response: The HTTP response returned by ``transport``.
69 """
70 method, url, payload, headers = self._prepare_request(data, content_type)
71
72 # Wrap the request business logic in a function to be retried.
73 def retriable_request():
74 result = transport.request(
75 method, url, data=payload, headers=headers, timeout=timeout
76 )
77
78 self._process_response(result)
79
80 return result
81
82 return _request_helpers.wait_and_retry(
83 retriable_request, self._get_status_code, self._retry_strategy
84 )
85
86
87class MultipartUpload(_request_helpers.RequestsMixin, _upload.MultipartUpload):
88 """Upload a resource with metadata to a Google API.
89
90 A **multipart** upload sends both metadata and the resource in a single
91 (multipart) request.
92
93 Args:
94 upload_url (str): The URL where the content will be uploaded.
95 headers (Optional[Mapping[str, str]]): Extra headers that should
96 be sent with the request, e.g. headers for encrypted data.
97 checksum Optional([str]): The type of checksum to compute to verify
98 the integrity of the object. The request metadata will be amended
99 to include the computed value. Using this option will override a
100 manually-set checksum value. Supported values are "md5",
101 "crc32c" and None. The default is None.
102
103 Attributes:
104 upload_url (str): The URL where the content will be uploaded.
105 """
106
107 def transmit(
108 self,
109 transport,
110 data,
111 metadata,
112 content_type,
113 timeout=(
114 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
115 _request_helpers._DEFAULT_READ_TIMEOUT,
116 ),
117 ):
118 """Transmit the resource to be uploaded.
119
120 Args:
121 transport (~requests.Session): A ``requests`` object which can
122 make authenticated requests.
123 data (bytes): The resource content to be uploaded.
124 metadata (Mapping[str, str]): The resource metadata, such as an
125 ACL list.
126 content_type (str): The content type of the resource, e.g. a JPEG
127 image has content type ``image/jpeg``.
128 timeout (Optional[Union[float, Tuple[float, float]]]):
129 The number of seconds to wait for the server response.
130 Depending on the retry strategy, a request may be repeated
131 several times using the same timeout each time.
132
133 Can also be passed as a tuple (connect_timeout, read_timeout).
134 See :meth:`requests.Session.request` documentation for details.
135
136 Returns:
137 ~requests.Response: The HTTP response returned by ``transport``.
138 """
139 method, url, payload, headers = self._prepare_request(
140 data, metadata, content_type
141 )
142
143 # Wrap the request business logic in a function to be retried.
144 def retriable_request():
145 result = transport.request(
146 method, url, data=payload, headers=headers, timeout=timeout
147 )
148
149 self._process_response(result)
150
151 return result
152
153 return _request_helpers.wait_and_retry(
154 retriable_request, self._get_status_code, self._retry_strategy
155 )
156
157
158class ResumableUpload(_request_helpers.RequestsMixin, _upload.ResumableUpload):
159 """Initiate and fulfill a resumable upload to a Google API.
160
161 A **resumable** upload sends an initial request with the resource metadata
162 and then gets assigned an upload ID / upload URL to send bytes to.
163 Using the upload URL, the upload is then done in chunks (determined by
164 the user) until all bytes have been uploaded.
165
166 When constructing a resumable upload, only the resumable upload URL and
167 the chunk size are required:
168
169 .. testsetup:: resumable-constructor
170
171 bucket = 'bucket-foo'
172
173 .. doctest:: resumable-constructor
174
175 >>> from google.resumable_media.requests import ResumableUpload
176 >>>
177 >>> url_template = (
178 ... 'https://www.googleapis.com/upload/storage/v1/b/{bucket}/o?'
179 ... 'uploadType=resumable')
180 >>> upload_url = url_template.format(bucket=bucket)
181 >>>
182 >>> chunk_size = 3 * 1024 * 1024 # 3MB
183 >>> upload = ResumableUpload(upload_url, chunk_size)
184
185 When initiating an upload (via :meth:`initiate`), the caller is expected
186 to pass the resource being uploaded as a file-like ``stream``. If the size
187 of the resource is explicitly known, it can be passed in directly:
188
189 .. testsetup:: resumable-explicit-size
190
191 import os
192 import tempfile
193
194 import mock
195 import requests
196 import http.client
197
198 from google.resumable_media.requests import ResumableUpload
199
200 upload_url = 'http://test.invalid'
201 chunk_size = 3 * 1024 * 1024 # 3MB
202 upload = ResumableUpload(upload_url, chunk_size)
203
204 file_desc, filename = tempfile.mkstemp()
205 os.close(file_desc)
206
207 data = b'some bytes!'
208 with open(filename, 'wb') as file_obj:
209 file_obj.write(data)
210
211 fake_response = requests.Response()
212 fake_response.status_code = int(http.client.OK)
213 fake_response._content = b''
214 resumable_url = 'http://test.invalid?upload_id=7up'
215 fake_response.headers['location'] = resumable_url
216
217 post_method = mock.Mock(return_value=fake_response, spec=[])
218 transport = mock.Mock(request=post_method, spec=['request'])
219
220 .. doctest:: resumable-explicit-size
221
222 >>> import os
223 >>>
224 >>> upload.total_bytes is None
225 True
226 >>>
227 >>> stream = open(filename, 'rb')
228 >>> total_bytes = os.path.getsize(filename)
229 >>> metadata = {'name': filename}
230 >>> response = upload.initiate(
231 ... transport, stream, metadata, 'text/plain',
232 ... total_bytes=total_bytes)
233 >>> response
234 <Response [200]>
235 >>>
236 >>> upload.total_bytes == total_bytes
237 True
238
239 .. testcleanup:: resumable-explicit-size
240
241 os.remove(filename)
242
243 If the stream is in a "final" state (i.e. it won't have any more bytes
244 written to it), the total number of bytes can be determined implicitly
245 from the ``stream`` itself:
246
247 .. testsetup:: resumable-implicit-size
248
249 import io
250
251 import mock
252 import requests
253 import http.client
254
255 from google.resumable_media.requests import ResumableUpload
256
257 upload_url = 'http://test.invalid'
258 chunk_size = 3 * 1024 * 1024 # 3MB
259 upload = ResumableUpload(upload_url, chunk_size)
260
261 fake_response = requests.Response()
262 fake_response.status_code = int(http.client.OK)
263 fake_response._content = b''
264 resumable_url = 'http://test.invalid?upload_id=7up'
265 fake_response.headers['location'] = resumable_url
266
267 post_method = mock.Mock(return_value=fake_response, spec=[])
268 transport = mock.Mock(request=post_method, spec=['request'])
269
270 data = b'some MOAR bytes!'
271 metadata = {'name': 'some-file.jpg'}
272 content_type = 'image/jpeg'
273
274 .. doctest:: resumable-implicit-size
275
276 >>> stream = io.BytesIO(data)
277 >>> response = upload.initiate(
278 ... transport, stream, metadata, content_type)
279 >>>
280 >>> upload.total_bytes == len(data)
281 True
282
283 If the size of the resource is **unknown** when the upload is initiated,
284 the ``stream_final`` argument can be used. This might occur if the
285 resource is being dynamically created on the client (e.g. application
286 logs). To use this argument:
287
288 .. testsetup:: resumable-unknown-size
289
290 import io
291
292 import mock
293 import requests
294 import http.client
295
296 from google.resumable_media.requests import ResumableUpload
297
298 upload_url = 'http://test.invalid'
299 chunk_size = 3 * 1024 * 1024 # 3MB
300 upload = ResumableUpload(upload_url, chunk_size)
301
302 fake_response = requests.Response()
303 fake_response.status_code = int(http.client.OK)
304 fake_response._content = b''
305 resumable_url = 'http://test.invalid?upload_id=7up'
306 fake_response.headers['location'] = resumable_url
307
308 post_method = mock.Mock(return_value=fake_response, spec=[])
309 transport = mock.Mock(request=post_method, spec=['request'])
310
311 metadata = {'name': 'some-file.jpg'}
312 content_type = 'application/octet-stream'
313
314 stream = io.BytesIO(b'data')
315
316 .. doctest:: resumable-unknown-size
317
318 >>> response = upload.initiate(
319 ... transport, stream, metadata, content_type,
320 ... stream_final=False)
321 >>>
322 >>> upload.total_bytes is None
323 True
324
325 Args:
326 upload_url (str): The URL where the resumable upload will be initiated.
327 chunk_size (int): The size of each chunk used to upload the resource.
328 headers (Optional[Mapping[str, str]]): Extra headers that should
329 be sent with the :meth:`initiate` request, e.g. headers for
330 encrypted data. These **will not** be sent with
331 :meth:`transmit_next_chunk` or :meth:`recover` requests.
332 checksum Optional([str]): The type of checksum to compute to verify
333 the integrity of the object. After the upload is complete, the
334 server-computed checksum of the resulting object will be checked
335 and google.resumable_media.common.DataCorruption will be raised on
336 a mismatch. The corrupted file will not be deleted from the remote
337 host automatically. Supported values are "md5", "crc32c" and None.
338 The default is None.
339
340 Attributes:
341 upload_url (str): The URL where the content will be uploaded.
342
343 Raises:
344 ValueError: If ``chunk_size`` is not a multiple of
345 :data:`.UPLOAD_CHUNK_SIZE`.
346 """
347
348 def initiate(
349 self,
350 transport,
351 stream,
352 metadata,
353 content_type,
354 total_bytes=None,
355 stream_final=True,
356 timeout=(
357 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
358 _request_helpers._DEFAULT_READ_TIMEOUT,
359 ),
360 ):
361 """Initiate a resumable upload.
362
363 By default, this method assumes your ``stream`` is in a "final"
364 state ready to transmit. However, ``stream_final=False`` can be used
365 to indicate that the size of the resource is not known. This can happen
366 if bytes are being dynamically fed into ``stream``, e.g. if the stream
367 is attached to application logs.
368
369 If ``stream_final=False`` is used, :attr:`chunk_size` bytes will be
370 read from the stream every time :meth:`transmit_next_chunk` is called.
371 If one of those reads produces strictly fewer bites than the chunk
372 size, the upload will be concluded.
373
374 Args:
375 transport (~requests.Session): A ``requests`` object which can
376 make authenticated requests.
377 stream (IO[bytes]): The stream (i.e. file-like object) that will
378 be uploaded. The stream **must** be at the beginning (i.e.
379 ``stream.tell() == 0``).
380 metadata (Mapping[str, str]): The resource metadata, such as an
381 ACL list.
382 content_type (str): The content type of the resource, e.g. a JPEG
383 image has content type ``image/jpeg``.
384 total_bytes (Optional[int]): The total number of bytes to be
385 uploaded. If specified, the upload size **will not** be
386 determined from the stream (even if ``stream_final=True``).
387 stream_final (Optional[bool]): Indicates if the ``stream`` is
388 "final" (i.e. no more bytes will be added to it). In this case
389 we determine the upload size from the size of the stream. If
390 ``total_bytes`` is passed, this argument will be ignored.
391 timeout (Optional[Union[float, Tuple[float, float]]]):
392 The number of seconds to wait for the server response.
393 Depending on the retry strategy, a request may be repeated
394 several times using the same timeout each time.
395
396 Can also be passed as a tuple (connect_timeout, read_timeout).
397 See :meth:`requests.Session.request` documentation for details.
398
399 Returns:
400 ~requests.Response: The HTTP response returned by ``transport``.
401 """
402 method, url, payload, headers = self._prepare_initiate_request(
403 stream,
404 metadata,
405 content_type,
406 total_bytes=total_bytes,
407 stream_final=stream_final,
408 )
409
410 # Wrap the request business logic in a function to be retried.
411 def retriable_request():
412 result = transport.request(
413 method, url, data=payload, headers=headers, timeout=timeout
414 )
415
416 self._process_initiate_response(result)
417
418 return result
419
420 return _request_helpers.wait_and_retry(
421 retriable_request, self._get_status_code, self._retry_strategy
422 )
423
424 def transmit_next_chunk(
425 self,
426 transport,
427 timeout=(
428 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
429 _request_helpers._DEFAULT_READ_TIMEOUT,
430 ),
431 ):
432 """Transmit the next chunk of the resource to be uploaded.
433
434 If the current upload was initiated with ``stream_final=False``,
435 this method will dynamically determine if the upload has completed.
436 The upload will be considered complete if the stream produces
437 fewer than :attr:`chunk_size` bytes when a chunk is read from it.
438
439 In the case of failure, an exception is thrown that preserves the
440 failed response:
441
442 .. testsetup:: bad-response
443
444 import io
445
446 import mock
447 import requests
448 import http.client
449
450 from google import resumable_media
451 import google.resumable_media.requests.upload as upload_mod
452
453 transport = mock.Mock(spec=['request'])
454 fake_response = requests.Response()
455 fake_response.status_code = int(http.client.BAD_REQUEST)
456 transport.request.return_value = fake_response
457
458 upload_url = 'http://test.invalid'
459 upload = upload_mod.ResumableUpload(
460 upload_url, resumable_media.UPLOAD_CHUNK_SIZE)
461 # Fake that the upload has been initiate()-d
462 data = b'data is here'
463 upload._stream = io.BytesIO(data)
464 upload._total_bytes = len(data)
465 upload._resumable_url = 'http://test.invalid?upload_id=nope'
466
467 .. doctest:: bad-response
468 :options: +NORMALIZE_WHITESPACE
469
470 >>> error = None
471 >>> try:
472 ... upload.transmit_next_chunk(transport)
473 ... except resumable_media.InvalidResponse as caught_exc:
474 ... error = caught_exc
475 ...
476 >>> error
477 InvalidResponse('Request failed with status code', 400,
478 'Expected one of', <HTTPStatus.OK: 200>, <HTTPStatus.PERMANENT_REDIRECT: 308>)
479 >>> error.response
480 <Response [400]>
481
482 Args:
483 transport (~requests.Session): A ``requests`` object which can
484 make authenticated requests.
485 timeout (Optional[Union[float, Tuple[float, float]]]):
486 The number of seconds to wait for the server response.
487 Depending on the retry strategy, a request may be repeated
488 several times using the same timeout each time.
489
490 Can also be passed as a tuple (connect_timeout, read_timeout).
491 See :meth:`requests.Session.request` documentation for details.
492
493 Returns:
494 ~requests.Response: The HTTP response returned by ``transport``.
495
496 Raises:
497 ~google.resumable_media.common.InvalidResponse: If the status
498 code is not 200 or http.client.PERMANENT_REDIRECT.
499 ~google.resumable_media.common.DataCorruption: If this is the final
500 chunk, a checksum validation was requested, and the checksum
501 does not match or is not available.
502 """
503 method, url, payload, headers = self._prepare_request()
504
505 # Wrap the request business logic in a function to be retried.
506 def retriable_request():
507 result = transport.request(
508 method, url, data=payload, headers=headers, timeout=timeout
509 )
510
511 self._process_resumable_response(result, len(payload))
512
513 return result
514
515 return _request_helpers.wait_and_retry(
516 retriable_request, self._get_status_code, self._retry_strategy
517 )
518
519 def recover(self, transport):
520 """Recover from a failure and check the status of the current upload.
521
522 This will verify the progress with the server and make sure the
523 current upload is in a valid state before :meth:`transmit_next_chunk`
524 can be used again. See https://cloud.google.com/storage/docs/performing-resumable-uploads#status-check
525 for more information.
526
527 This method can be used when a :class:`ResumableUpload` is in an
528 :attr:`~ResumableUpload.invalid` state due to a request failure.
529
530 Args:
531 transport (~requests.Session): A ``requests`` object which can
532 make authenticated requests.
533
534 Returns:
535 ~requests.Response: The HTTP response returned by ``transport``.
536 """
537 timeout = (
538 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
539 _request_helpers._DEFAULT_READ_TIMEOUT,
540 )
541
542 method, url, payload, headers = self._prepare_recover_request()
543 # NOTE: We assume "payload is None" but pass it along anyway.
544
545 # Wrap the request business logic in a function to be retried.
546 def retriable_request():
547 result = transport.request(
548 method, url, data=payload, headers=headers, timeout=timeout
549 )
550
551 self._process_recover_response(result)
552
553 return result
554
555 return _request_helpers.wait_and_retry(
556 retriable_request, self._get_status_code, self._retry_strategy
557 )
558
559
560class XMLMPUContainer(_request_helpers.RequestsMixin, _upload.XMLMPUContainer):
561 """Initiate and close an upload using the XML MPU API.
562
563 An XML MPU sends an initial request and then receives an upload ID.
564 Using the upload ID, the upload is then done in numbered parts and the
565 parts can be uploaded concurrently.
566
567 In order to avoid concurrency issues with this container object, the
568 uploading of individual parts is handled separately, by XMLMPUPart objects
569 spawned from this container class. The XMLMPUPart objects are not
570 necessarily in the same process as the container, so they do not update the
571 container automatically.
572
573 MPUs are sometimes referred to as "Multipart Uploads", which is ambiguous
574 given the JSON multipart upload, so the abbreviation "MPU" will be used
575 throughout.
576
577 See: https://cloud.google.com/storage/docs/multipart-uploads
578
579 Args:
580 upload_url (str): The URL of the object (without query parameters). The
581 initiate, PUT, and finalization requests will all use this URL, with
582 varying query parameters.
583 headers (Optional[Mapping[str, str]]): Extra headers that should
584 be sent with the :meth:`initiate` request, e.g. headers for
585 encrypted data. These headers will be propagated to individual
586 XMLMPUPart objects spawned from this container as well.
587
588 Attributes:
589 upload_url (str): The URL where the content will be uploaded.
590 upload_id (Optional(int)): The ID of the upload from the initialization
591 response.
592 """
593
594 def initiate(
595 self,
596 transport,
597 content_type,
598 timeout=(
599 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
600 _request_helpers._DEFAULT_READ_TIMEOUT,
601 ),
602 ):
603 """Initiate an MPU and record the upload ID.
604
605 Args:
606 transport (object): An object which can make authenticated
607 requests.
608 content_type (str): The content type of the resource, e.g. a JPEG
609 image has content type ``image/jpeg``.
610 timeout (Optional[Union[float, Tuple[float, float]]]):
611 The number of seconds to wait for the server response.
612 Depending on the retry strategy, a request may be repeated
613 several times using the same timeout each time.
614
615 Can also be passed as a tuple (connect_timeout, read_timeout).
616 See :meth:`requests.Session.request` documentation for details.
617
618 Returns:
619 ~requests.Response: The HTTP response returned by ``transport``.
620 """
621
622 method, url, payload, headers = self._prepare_initiate_request(
623 content_type,
624 )
625
626 # Wrap the request business logic in a function to be retried.
627 def retriable_request():
628 result = transport.request(
629 method, url, data=payload, headers=headers, timeout=timeout
630 )
631
632 self._process_initiate_response(result)
633
634 return result
635
636 return _request_helpers.wait_and_retry(
637 retriable_request, self._get_status_code, self._retry_strategy
638 )
639
640 def finalize(
641 self,
642 transport,
643 timeout=(
644 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
645 _request_helpers._DEFAULT_READ_TIMEOUT,
646 ),
647 ):
648 """Finalize an MPU request with all the parts.
649
650 Args:
651 transport (object): An object which can make authenticated
652 requests.
653 timeout (Optional[Union[float, Tuple[float, float]]]):
654 The number of seconds to wait for the server response.
655 Depending on the retry strategy, a request may be repeated
656 several times using the same timeout each time.
657
658 Can also be passed as a tuple (connect_timeout, read_timeout).
659 See :meth:`requests.Session.request` documentation for details.
660
661 Returns:
662 ~requests.Response: The HTTP response returned by ``transport``.
663 """
664 method, url, payload, headers = self._prepare_finalize_request()
665
666 # Wrap the request business logic in a function to be retried.
667 def retriable_request():
668 result = transport.request(
669 method, url, data=payload, headers=headers, timeout=timeout
670 )
671
672 self._process_finalize_response(result)
673
674 return result
675
676 return _request_helpers.wait_and_retry(
677 retriable_request, self._get_status_code, self._retry_strategy
678 )
679
680 def cancel(
681 self,
682 transport,
683 timeout=(
684 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
685 _request_helpers._DEFAULT_READ_TIMEOUT,
686 ),
687 ):
688 """Cancel an MPU request and permanently delete any uploaded parts.
689
690 This cannot be undone.
691
692 Args:
693 transport (object): An object which can make authenticated
694 requests.
695 timeout (Optional[Union[float, Tuple[float, float]]]):
696 The number of seconds to wait for the server response.
697 Depending on the retry strategy, a request may be repeated
698 several times using the same timeout each time.
699
700 Can also be passed as a tuple (connect_timeout, read_timeout).
701 See :meth:`requests.Session.request` documentation for details.
702
703 Returns:
704 ~requests.Response: The HTTP response returned by ``transport``.
705 """
706 method, url, payload, headers = self._prepare_cancel_request()
707
708 # Wrap the request business logic in a function to be retried.
709 def retriable_request():
710 result = transport.request(
711 method, url, data=payload, headers=headers, timeout=timeout
712 )
713
714 self._process_cancel_response(result)
715
716 return result
717
718 return _request_helpers.wait_and_retry(
719 retriable_request, self._get_status_code, self._retry_strategy
720 )
721
722
723class XMLMPUPart(_request_helpers.RequestsMixin, _upload.XMLMPUPart):
724 def upload(
725 self,
726 transport,
727 timeout=(
728 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
729 _request_helpers._DEFAULT_READ_TIMEOUT,
730 ),
731 ):
732 """Upload the part.
733
734 Args:
735 transport (object): An object which can make authenticated
736 requests.
737 timeout (Optional[Union[float, Tuple[float, float]]]):
738 The number of seconds to wait for the server response.
739 Depending on the retry strategy, a request may be repeated
740 several times using the same timeout each time.
741
742 Can also be passed as a tuple (connect_timeout, read_timeout).
743 See :meth:`requests.Session.request` documentation for details.
744
745 Returns:
746 ~requests.Response: The HTTP response returned by ``transport``.
747 """
748 method, url, payload, headers = self._prepare_upload_request()
749
750 # Wrap the request business logic in a function to be retried.
751 def retriable_request():
752 result = transport.request(
753 method, url, data=payload, headers=headers, timeout=timeout
754 )
755
756 self._process_upload_response(result)
757
758 return result
759
760 return _request_helpers.wait_and_retry(
761 retriable_request, self._get_status_code, self._retry_strategy
762 )