Skip to content

Commit 0ae35ee

Browse files
authored
feat: add preconditions and retry configuration to blob.create_resumable_upload_session (#484)
* feat: add preconditions and retry configuration to blob.create_resumable_upload_session * move imports
1 parent c027ccf commit 0ae35ee

File tree

2 files changed

+109
-7
lines changed

2 files changed

+109
-7
lines changed

google/cloud/storage/blob.py

+58
Original file line numberDiff line numberDiff line change
@@ -2782,6 +2782,11 @@ def create_resumable_upload_session(
27822782
client=None,
27832783
timeout=_DEFAULT_TIMEOUT,
27842784
checksum=None,
2785+
if_generation_match=None,
2786+
if_generation_not_match=None,
2787+
if_metageneration_match=None,
2788+
if_metageneration_not_match=None,
2789+
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
27852790
):
27862791
"""Create a resumable upload session.
27872792
@@ -2857,6 +2862,41 @@ def create_resumable_upload_session(
28572862
delete the uploaded object automatically. Supported values
28582863
are "md5", "crc32c" and None. The default is None.
28592864
2865+
:type if_generation_match: long
2866+
:param if_generation_match:
2867+
(Optional) See :ref:`using-if-generation-match`
2868+
2869+
:type if_generation_not_match: long
2870+
:param if_generation_not_match:
2871+
(Optional) See :ref:`using-if-generation-not-match`
2872+
2873+
:type if_metageneration_match: long
2874+
:param if_metageneration_match:
2875+
(Optional) See :ref:`using-if-metageneration-match`
2876+
2877+
:type if_metageneration_not_match: long
2878+
:param if_metageneration_not_match:
2879+
(Optional) See :ref:`using-if-metageneration-not-match`
2880+
2881+
:type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy
2882+
:param retry: (Optional) How to retry the RPC. A None value will disable
2883+
retries. A google.api_core.retry.Retry value will enable retries,
2884+
and the object will define retriable response codes and errors and
2885+
configure backoff and timeout options.
2886+
A google.cloud.storage.retry.ConditionalRetryPolicy value wraps a
2887+
Retry object and activates it only if certain conditions are met.
2888+
This class exists to provide safe defaults for RPC calls that are
2889+
not technically safe to retry normally (due to potential data
2890+
duplication or other side-effects) but become safe to retry if a
2891+
condition such as if_generation_match is set.
2892+
See the retry.py source code and docstrings in this package
2893+
(google.cloud.storage.retry) for information on retry types and how
2894+
to configure them.
2895+
Media operations (downloads and uploads) do not support non-default
2896+
predicates in a Retry object. The default will always be used. Other
2897+
configuration changes for Retry objects such as delays and deadlines
2898+
are respected.
2899+
28602900
:rtype: str
28612901
:returns: The resumable upload session URL. The upload can be
28622902
completed by making an HTTP PUT request with the
@@ -2865,6 +2905,19 @@ def create_resumable_upload_session(
28652905
:raises: :class:`google.cloud.exceptions.GoogleCloudError`
28662906
if the session creation response returns an error status.
28672907
"""
2908+
2909+
# Handle ConditionalRetryPolicy.
2910+
if isinstance(retry, ConditionalRetryPolicy):
2911+
# Conditional retries are designed for non-media calls, which change
2912+
# arguments into query_params dictionaries. Media operations work
2913+
# differently, so here we make a "fake" query_params to feed to the
2914+
# ConditionalRetryPolicy.
2915+
query_params = {
2916+
"ifGenerationMatch": if_generation_match,
2917+
"ifMetagenerationMatch": if_metageneration_match,
2918+
}
2919+
retry = retry.get_retry_policy_if_conditions_met(query_params=query_params)
2920+
28682921
extra_headers = {}
28692922
if origin is not None:
28702923
# This header is specifically for client-side uploads, it
@@ -2883,10 +2936,15 @@ def create_resumable_upload_session(
28832936
size,
28842937
None,
28852938
predefined_acl=None,
2939+
if_generation_match=if_generation_match,
2940+
if_generation_not_match=if_generation_not_match,
2941+
if_metageneration_match=if_metageneration_match,
2942+
if_metageneration_not_match=if_metageneration_not_match,
28862943
extra_headers=extra_headers,
28872944
chunk_size=self._CHUNK_SIZE_MULTIPLE,
28882945
timeout=timeout,
28892946
checksum=checksum,
2947+
retry=retry,
28902948
)
28912949

28922950
return upload.resumable_url

tests/unit/test_blob.py

+51-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import pytest
2626
import six
2727
from six.moves import http_client
28+
from six.moves.urllib.parse import urlencode
2829

2930
from google.cloud.storage.retry import DEFAULT_RETRY
3031
from google.cloud.storage.retry import DEFAULT_RETRY_IF_ETAG_IN_JSON
@@ -2041,8 +2042,6 @@ def _do_multipart_success(
20412042
mtls=False,
20422043
retry=None,
20432044
):
2044-
from six.moves.urllib.parse import urlencode
2045-
20462045
bucket = _Bucket(name="w00t", user_project=user_project)
20472046
blob = self._make_one(u"blob-name", bucket=bucket, kms_key_name=kms_key_name)
20482047
self.assertIsNone(blob.chunk_size)
@@ -2286,7 +2285,6 @@ def _initiate_resumable_helper(
22862285
mtls=False,
22872286
retry=None,
22882287
):
2289-
from six.moves.urllib.parse import urlencode
22902288
from google.resumable_media.requests import ResumableUpload
22912289
from google.cloud.storage.blob import _DEFAULT_CHUNKSIZE
22922290

@@ -3248,7 +3246,15 @@ def test_upload_from_string_w_text_w_num_retries(self):
32483246
self._upload_from_string_helper(data, num_retries=2)
32493247

32503248
def _create_resumable_upload_session_helper(
3251-
self, origin=None, side_effect=None, timeout=None
3249+
self,
3250+
origin=None,
3251+
side_effect=None,
3252+
timeout=None,
3253+
if_generation_match=None,
3254+
if_generation_not_match=None,
3255+
if_metageneration_match=None,
3256+
if_metageneration_not_match=None,
3257+
retry=None,
32523258
):
32533259
bucket = _Bucket(name="alex-trebek")
32543260
blob = self._make_one("blob-name", bucket=bucket)
@@ -3280,6 +3286,11 @@ def _create_resumable_upload_session_helper(
32803286
size=size,
32813287
origin=origin,
32823288
client=client,
3289+
if_generation_match=if_generation_match,
3290+
if_generation_not_match=if_generation_not_match,
3291+
if_metageneration_match=if_metageneration_match,
3292+
if_metageneration_not_match=if_metageneration_not_match,
3293+
retry=retry,
32833294
**timeout_kwarg
32843295
)
32853296

@@ -3289,10 +3300,23 @@ def _create_resumable_upload_session_helper(
32893300

32903301
# Check the mocks.
32913302
upload_url = (
3292-
"https://storage.googleapis.com/upload/storage/v1"
3293-
+ bucket.path
3294-
+ "/o?uploadType=resumable"
3303+
"https://storage.googleapis.com/upload/storage/v1" + bucket.path + "/o"
32953304
)
3305+
3306+
qs_params = [("uploadType", "resumable")]
3307+
if if_generation_match is not None:
3308+
qs_params.append(("ifGenerationMatch", if_generation_match))
3309+
3310+
if if_generation_not_match is not None:
3311+
qs_params.append(("ifGenerationNotMatch", if_generation_not_match))
3312+
3313+
if if_metageneration_match is not None:
3314+
qs_params.append(("ifMetagenerationMatch", if_metageneration_match))
3315+
3316+
if if_metageneration_not_match is not None:
3317+
qs_params.append(("ifMetaGenerationNotMatch", if_metageneration_not_match))
3318+
3319+
upload_url += "?" + urlencode(qs_params)
32963320
payload = b'{"name": "blob-name"}'
32973321
expected_headers = {
32983322
"content-type": "application/json; charset=UTF-8",
@@ -3318,6 +3342,26 @@ def test_create_resumable_upload_session_with_custom_timeout(self):
33183342
def test_create_resumable_upload_session_with_origin(self):
33193343
self._create_resumable_upload_session_helper(origin="http://google.com")
33203344

3345+
def test_create_resumable_upload_session_with_generation_match(self):
3346+
self._create_resumable_upload_session_helper(
3347+
if_generation_match=123456, if_metageneration_match=2
3348+
)
3349+
3350+
def test_create_resumable_upload_session_with_generation_not_match(self):
3351+
self._create_resumable_upload_session_helper(
3352+
if_generation_not_match=0, if_metageneration_not_match=3
3353+
)
3354+
3355+
def test_create_resumable_upload_session_with_conditional_retry_success(self):
3356+
self._create_resumable_upload_session_helper(
3357+
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED, if_generation_match=123456
3358+
)
3359+
3360+
def test_create_resumable_upload_session_with_conditional_retry_failure(self):
3361+
self._create_resumable_upload_session_helper(
3362+
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED
3363+
)
3364+
33213365
def test_create_resumable_upload_session_with_failure(self):
33223366
from google.resumable_media import InvalidResponse
33233367
from google.cloud import exceptions

0 commit comments

Comments
 (0)