Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/googleapis/python-storage
Browse files Browse the repository at this point in the history
…into storage_issue_159
  • Loading branch information
HemangChothani committed Jun 17, 2020
2 parents 39ff644 + 62d1543 commit be50284
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 2 deletions.
1 change: 1 addition & 0 deletions google/cloud/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
.. literalinclude:: snippets.py
:start-after: [START storage_get_started]
:end-before: [END storage_get_started]
:dedent: 4
The main concepts with this API are:
Expand Down
5 changes: 5 additions & 0 deletions google/cloud/storage/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
.. literalinclude:: snippets.py
:start-after: [START client_bucket_acl]
:end-before: [END client_bucket_acl]
:dedent: 4
Adding and removing permissions can be done with the following methods
Expand Down Expand Up @@ -52,13 +53,15 @@
.. literalinclude:: snippets.py
:start-after: [START acl_user_settings]
:end-before: [END acl_user_settings]
:dedent: 4
After that, you can save any changes you make with the
:func:`google.cloud.storage.acl.ACL.save` method:
.. literalinclude:: snippets.py
:start-after: [START acl_save]
:end-before: [END acl_save]
:dedent: 4
You can alternatively save any existing :class:`google.cloud.storage.acl.ACL`
object (whether it was created by a factory method or not) from a
Expand All @@ -67,13 +70,15 @@
.. literalinclude:: snippets.py
:start-after: [START acl_save_bucket]
:end-before: [END acl_save_bucket]
:dedent: 4
To get the list of ``entity`` and ``role`` for each unique pair, the
:class:`ACL` class is iterable:
.. literalinclude:: snippets.py
:start-after: [START acl_print]
:end-before: [END acl_print]
:dedent: 4
This list of tuples can be used as the ``entity`` and ``role`` fields
when sending metadata for ACLs to the API.
Expand Down
101 changes: 99 additions & 2 deletions google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,7 @@ def get_blob(
.. literalinclude:: snippets.py
:start-after: [START get_blob]
:end-before: [END get_blob]
:dedent: 4
If :attr:`user_project` is set, bills the API request to that project.
Expand Down Expand Up @@ -1415,6 +1416,7 @@ def delete_blob(
.. literalinclude:: snippets.py
:start-after: [START delete_blob]
:end-before: [END delete_blob]
:dedent: 4
If :attr:`user_project` is set, bills the API request to that project.
Expand Down Expand Up @@ -1465,6 +1467,7 @@ def delete_blob(
.. literalinclude:: snippets.py
:start-after: [START delete_blobs]
:end-before: [END delete_blobs]
:dedent: 4
"""
client = self._require_client(client)
Expand All @@ -1489,7 +1492,17 @@ def delete_blob(
timeout=timeout,
)

def delete_blobs(self, blobs, on_error=None, client=None, timeout=_DEFAULT_TIMEOUT):
def delete_blobs(
self,
blobs,
on_error=None,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Deletes a list of blobs from the current bucket.
Uses :meth:`delete_blob` to delete each individual blob.
Expand Down Expand Up @@ -1518,15 +1531,74 @@ def delete_blobs(self, blobs, on_error=None, client=None, timeout=_DEFAULT_TIMEO
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:type if_generation_match: list of 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. The list must match
``blobs`` item-to-item.
:type if_generation_not_match: list of 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. The list must match
``blobs`` item-to-item.
:type if_metageneration_match: list of long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.
The list must match ``blobs`` item-to-item.
:type if_metageneration_not_match: list of long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.
The list must match ``blobs`` item-to-item.
:raises: :class:`~google.cloud.exceptions.NotFound` (if
`on_error` is not passed).
Example:
Delete blobs using generation match preconditions.
>>> from google.cloud import storage
>>> client = storage.Client()
>>> bucket = client.bucket("bucket-name")
>>> blobs = [bucket.blob("blob-name-1"), bucket.blob("blob-name-2")]
>>> if_generation_match = [None] * len(blobs)
>>> if_generation_match[0] = "123" # precondition for "blob-name-1"
>>> bucket.delete_blobs(blobs, if_generation_match=if_generation_match)
"""
_raise_if_len_differs(
len(blobs),
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,
)
if_generation_match = iter(if_generation_match or [])
if_generation_not_match = iter(if_generation_not_match or [])
if_metageneration_match = iter(if_metageneration_match or [])
if_metageneration_not_match = iter(if_metageneration_not_match or [])

for blob in blobs:
try:
blob_name = blob
if not isinstance(blob_name, six.string_types):
blob_name = blob.name
self.delete_blob(blob_name, client=client, timeout=timeout)
self.delete_blob(
blob_name,
client=client,
timeout=timeout,
if_generation_match=next(if_generation_match, None),
if_generation_not_match=next(if_generation_not_match, None),
if_metageneration_match=next(if_metageneration_match, None),
if_metageneration_not_match=next(if_metageneration_not_match, None),
)
except NotFound:
if on_error is not None:
on_error(blob)
Expand Down Expand Up @@ -2070,6 +2142,7 @@ def add_lifecycle_delete_rule(self, **kw):
.. literalinclude:: snippets.py
:start-after: [START add_lifecycle_delete_rule]
:end-before: [END add_lifecycle_delete_rule]
:dedent: 4
:type kw: dict
:params kw: arguments passed to :class:`LifecycleRuleConditions`.
Expand All @@ -2087,6 +2160,7 @@ def add_lifecycle_set_storage_class_rule(self, storage_class, **kw):
.. literalinclude:: snippets.py
:start-after: [START add_lifecycle_set_storage_class_rule]
:end-before: [END add_lifecycle_set_storage_class_rule]
:dedent: 4
:type storage_class: str, one of :attr:`STORAGE_CLASSES`.
:param storage_class: new storage class to assign to matching items.
Expand Down Expand Up @@ -2430,12 +2504,14 @@ def configure_website(self, main_page_suffix=None, not_found_page=None):
.. literalinclude:: snippets.py
:start-after: [START configure_website]
:end-before: [END configure_website]
:dedent: 4
You probably should also make the whole bucket public:
.. literalinclude:: snippets.py
:start-after: [START make_public]
:end-before: [END make_public]
:dedent: 4
This says: "Make the bucket public, and all the stuff already in
the bucket, and anything else I add to the bucket. Just make it
Expand Down Expand Up @@ -2769,6 +2845,7 @@ def generate_upload_policy(self, conditions, expiration=None, client=None):
.. literalinclude:: snippets.py
:start-after: [START policy_document]
:end-before: [END policy_document]
:dedent: 4
.. _policy documents:
https://cloud.google.com/storage/docs/xml-api\
Expand Down Expand Up @@ -3014,3 +3091,23 @@ def generate_signed_url(
headers=headers,
query_parameters=query_parameters,
)


def _raise_if_len_differs(expected_len, **generation_match_args):
"""
Raise an error if any generation match argument
is set and its len differs from the given value.
:type expected_len: int
:param expected_len: Expected argument length in case it's set.
:type generation_match_args: dict
:param generation_match_args: Lists, which length must be checked.
:raises: :exc:`ValueError` if any argument set, but has an unexpected length.
"""
for name, value in generation_match_args.items():
if value is not None and len(value) != expected_len:
raise ValueError(
"'{}' length must be the same as 'blobs' length".format(name)
)
4 changes: 4 additions & 0 deletions google/cloud/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def get_bucket(
.. literalinclude:: snippets.py
:start-after: [START get_bucket]
:end-before: [END get_bucket]
:dedent: 4
Get a bucket using a resource.
Expand Down Expand Up @@ -367,6 +368,7 @@ def lookup_bucket(
.. literalinclude:: snippets.py
:start-after: [START lookup_bucket]
:end-before: [END lookup_bucket]
:dedent: 4
:type bucket_name: str
:param bucket_name: The name of the bucket to get.
Expand Down Expand Up @@ -461,6 +463,7 @@ def create_bucket(
.. literalinclude:: snippets.py
:start-after: [START create_bucket]
:end-before: [END create_bucket]
:dedent: 4
Create a bucket using a resource.
Expand Down Expand Up @@ -702,6 +705,7 @@ def list_buckets(
.. literalinclude:: snippets.py
:start-after: [START list_buckets]
:end-before: [END list_buckets]
:dedent: 4
This implements "storage.buckets.list".
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,82 @@ def test_delete_blobs_hit_w_user_project(self):
self.assertEqual(kw[0]["query_params"], {"userProject": USER_PROJECT})
self.assertEqual(kw[0]["timeout"], 42)

def test_delete_blobs_w_generation_match(self):
NAME = "name"
BLOB_NAME = "blob-name"
BLOB_NAME2 = "blob-name2"
GENERATION_NUMBER = 6
GENERATION_NUMBER2 = 9

connection = _Connection({}, {})
client = _Client(connection)
bucket = self._make_one(client=client, name=NAME)
bucket.delete_blobs(
[BLOB_NAME, BLOB_NAME2],
timeout=42,
if_generation_match=[GENERATION_NUMBER, GENERATION_NUMBER2],
)
kw = connection._requested
self.assertEqual(len(kw), 2)

self.assertEqual(kw[0]["method"], "DELETE")
self.assertEqual(kw[0]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME))
self.assertEqual(kw[0]["timeout"], 42)
self.assertEqual(
kw[0]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER}
)
self.assertEqual(kw[1]["method"], "DELETE")
self.assertEqual(kw[1]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME2))
self.assertEqual(kw[1]["timeout"], 42)
self.assertEqual(
kw[1]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER2}
)

def test_delete_blobs_w_generation_match_wrong_len(self):
NAME = "name"
BLOB_NAME = "blob-name"
BLOB_NAME2 = "blob-name2"
GENERATION_NUMBER = 6

connection = _Connection()
client = _Client(connection)
bucket = self._make_one(client=client, name=NAME)
with self.assertRaises(ValueError):
bucket.delete_blobs(
[BLOB_NAME, BLOB_NAME2],
timeout=42,
if_generation_not_match=[GENERATION_NUMBER],
)

def test_delete_blobs_w_generation_match_none(self):
NAME = "name"
BLOB_NAME = "blob-name"
BLOB_NAME2 = "blob-name2"
GENERATION_NUMBER = 6
GENERATION_NUMBER2 = None

connection = _Connection({}, {})
client = _Client(connection)
bucket = self._make_one(client=client, name=NAME)
bucket.delete_blobs(
[BLOB_NAME, BLOB_NAME2],
timeout=42,
if_generation_match=[GENERATION_NUMBER, GENERATION_NUMBER2],
)
kw = connection._requested
self.assertEqual(len(kw), 2)

self.assertEqual(kw[0]["method"], "DELETE")
self.assertEqual(kw[0]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME))
self.assertEqual(kw[0]["timeout"], 42)
self.assertEqual(
kw[0]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER}
)
self.assertEqual(kw[1]["method"], "DELETE")
self.assertEqual(kw[1]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME2))
self.assertEqual(kw[1]["timeout"], 42)
self.assertEqual(kw[1]["query_params"], {})

def test_delete_blobs_miss_no_on_error(self):
from google.cloud.exceptions import NotFound

Expand Down

0 comments on commit be50284

Please sign in to comment.