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 args into Bucket.delete_blobs() #130

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3fa6245
feat: add ifMetageneration*Match support, pt1
May 1, 2020
4cbad43
fix unit tests, add test for helper
May 1, 2020
b905487
fix unit tests
May 1, 2020
ddecf53
add generation match args into more methods
May 1, 2020
74961fa
feat: add if*generation*Match support, pt2
May 4, 2020
7e08361
Lint fix.
May 4, 2020
6f9f910
delete "more than one set "checks
May 5, 2020
ff26d2f
del excess import
May 5, 2020
551df43
Merge branch 'metageneration_match_pt1' into metageneration_match_pt2
May 5, 2020
1f4d9c1
delete "more than one set" checks
May 5, 2020
42fd268
feat: add if*generation*match args into Bucket.delete_blobs()
May 5, 2020
0a34086
Merge branch 'master' into metageneration_match_pt1
frankyn May 5, 2020
6deb7ba
feat: add helper for bucket bound hostname URLs
May 7, 2020
ae516da
Revert "feat: add helper for bucket bound hostname URLs"
May 7, 2020
8ecf93a
lint fix
May 7, 2020
e1b67b5
Merge branch 'master' into metageneration_match_pt1
May 12, 2020
bd5b54f
rename the helper; add error raising in case of wront parameters type
May 12, 2020
ea37318
Merge branch 'metageneration_match_pt2' into metageneration_match_pt1
May 12, 2020
9d19e15
Merge branch 'metageneration_match_pt1' of https://github.com/q-logic…
May 12, 2020
29c435e
Merge branch 'metageneration_match_pt1' into delete_blobs_generation_…
May 12, 2020
d8ca98d
Merge branch 'master' into delete_blobs_generation_match
May 21, 2020
fe0bb8c
add args length check, add more unit tests
May 21, 2020
fabbfbf
erase empty line left from conflict resolving
May 21, 2020
f0190bd
add arg name
Jun 3, 2020
90da4de
Merge branch 'master' into delete_blobs_generation_match
frankyn Jun 9, 2020
5f696bf
Merge branch 'master' into delete_blobs_generation_match
frankyn Jun 15, 2020
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
93 changes: 91 additions & 2 deletions google/cloud/storage/bucket.py
Expand Up @@ -1369,7 +1369,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 @@ -1398,15 +1408,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)
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
"""
_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),
)
Copy link
Author

@IlyaFaer IlyaFaer May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this method we're converting args into iterators to be able to take next item (or None if there is no next item, or items at all) with a single line.

except NotFound:
if on_error is not None:
on_error(blob)
Expand Down Expand Up @@ -2806,3 +2875,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)
)
76 changes: 76 additions & 0 deletions tests/unit/test_bucket.py
Expand Up @@ -1146,6 +1146,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