Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add if*generation*Match support, pt1 #123

Merged
merged 21 commits into from May 15, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
207 changes: 201 additions & 6 deletions google/cloud/storage/_helpers.py
Expand Up @@ -30,6 +30,14 @@

_DEFAULT_STORAGE_HOST = u"https://storage.googleapis.com"

# generation match parameters in camel and snake cases
_GENERATION_MATCH_PARAMETERS = (
("if_generation_match", "ifGenerationMatch"),
("if_generation_not_match", "ifGenerationNotMatch"),
("if_metageneration_match", "ifMetagenerationMatch"),
("if_metageneration_not_match", "ifMetagenerationNotMatch"),
)


def _get_storage_host():
return os.environ.get(STORAGE_EMULATOR_ENV_VAR, _DEFAULT_STORAGE_HOST)
Expand Down Expand Up @@ -121,27 +129,75 @@ def _query_params(self):
params["userProject"] = self.user_project
return params

def reload(self, client=None, timeout=_DEFAULT_TIMEOUT):
def reload(
self,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Reload properties from Cloud Storage.

If :attr:`user_project` is set, bills the API request to that project.

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.

:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.

Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.

:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.

:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.

:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.

:type if_metageneration_not_match: long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.

:raises: :class:`ValueError` if ``if_metageneration_match`` and
``if_metageneration_not_match`` are both set.
"""
_raise_for_more_than_one_none(
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
)
_raise_for_more_than_one_none(
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
client = self._require_client(client)
query_params = self._query_params
# Pass only '?projection=noAcl' here because 'acl' and related
# are handled via custom endpoints.
query_params["projection"] = "noAcl"
_add_generation_match_parameters(
query_params,
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
api_response = client._connection.api_request(
method="GET",
path=self.path,
Expand Down Expand Up @@ -180,7 +236,15 @@ def _set_properties(self, value):
# If the values are reset, the changes must as well.
self._changes = set()

def patch(self, client=None, timeout=_DEFAULT_TIMEOUT):
def patch(
self,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Sends all changed properties in a PATCH request.

Updates the ``_properties`` with the response from the backend.
Expand All @@ -189,20 +253,62 @@ def patch(self, client=None, timeout=_DEFAULT_TIMEOUT):

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.

:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.

Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.

:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.

:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.

:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.

:type if_metageneration_not_match: long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.

:raises: :class:`ValueError` if ``if_metageneration_match`` and
``if_metageneration_not_match``, or
``if_generation_match`` and ``if_generation_not_match``
are both set.
"""
_raise_for_more_than_one_none(
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
)
_raise_for_more_than_one_none(
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
client = self._require_client(client)
query_params = self._query_params
# Pass '?projection=full' here because 'PATCH' documented not
# to work properly w/ 'noAcl'.
query_params["projection"] = "full"
_add_generation_match_parameters(
query_params,
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
update_properties = {key: self._properties[key] for key in self._changes}

# Make the API call.
Expand All @@ -216,7 +322,15 @@ def patch(self, client=None, timeout=_DEFAULT_TIMEOUT):
)
self._set_properties(api_response)

def update(self, client=None, timeout=_DEFAULT_TIMEOUT):
def update(
self,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Sends all properties in a PUT request.

Updates the ``_properties`` with the response from the backend.
Expand All @@ -225,18 +339,58 @@ def update(self, client=None, timeout=_DEFAULT_TIMEOUT):

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.

:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.

Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.

:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.

:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.

:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.

:raises: :class:`ValueError` if ``if_metageneration_match`` and
``if_metageneration_not_match``, or
``if_generation_match`` and ``if_generation_not_match``
are both set.

:raises: :class:`ValueError` if ``if_metageneration_match`` and
``if_metageneration_not_match`` are both set.
"""
_raise_for_more_than_one_none(
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
)
_raise_for_more_than_one_none(
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
client = self._require_client(client)

query_params = self._query_params
query_params["projection"] = "full"
_add_generation_match_parameters(
query_params,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
api_response = client._connection.api_request(
method="PUT",
path=self.path,
Expand Down Expand Up @@ -312,3 +466,44 @@ def _convert_to_timestamp(value):
utc_naive = value.replace(tzinfo=None) - value.utcoffset()
mtime = (utc_naive - datetime(1970, 1, 1)).total_seconds()
return mtime


def _add_generation_match_parameters(parameters, **match_parameters):
"""Add generation match parameters into the given parameters list.

:type parameters: list or dict
:param parameters: Parameters list or dict.

:type match_parameters: dict
:param match_parameters: if*generation*match parameters to add.
"""
for snakecase_name, camelcase_name in _GENERATION_MATCH_PARAMETERS:
value = match_parameters.get(snakecase_name)

if value is not None:
if isinstance(parameters, list):
parameters.append((camelcase_name, value))

elif isinstance(parameters, dict):
parameters[camelcase_name] = value
andrewsg marked this conversation as resolved.
Show resolved Hide resolved


def _raise_for_more_than_one_none(**kwargs):
"""Raise ``ValueError`` exception if more than one parameter was set.

:type error: :exc:`ValueError`
:param error: Description of which fields were set

:raises: :class:`~ValueError` containing the fields that were set
"""
if sum(arg is not None for arg in kwargs.values()) > 1:
escaped_keys = ["'%s'" % name for name in kwargs.keys()]

keys_but_last = ", ".join(escaped_keys[:-1])
last_key = escaped_keys[-1]

msg = "Pass at most one of {keys_but_last} and {last_key}".format(
keys_but_last=keys_but_last, last_key=last_key
)

raise ValueError(msg)
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved