Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 5e60991

Browse files
author
Gurov Ilya
authored
feat: add if*generation*match args into Bucket.delete_blobs() (#130)
Towards #127
1 parent 65b1471 commit 5e60991

2 files changed

Lines changed: 167 additions & 2 deletions

File tree

google/cloud/storage/bucket.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,7 +1455,17 @@ def delete_blob(
14551455
timeout=timeout,
14561456
)
14571457

1458-
def delete_blobs(self, blobs, on_error=None, client=None, timeout=_DEFAULT_TIMEOUT):
1458+
def delete_blobs(
1459+
self,
1460+
blobs,
1461+
on_error=None,
1462+
client=None,
1463+
timeout=_DEFAULT_TIMEOUT,
1464+
if_generation_match=None,
1465+
if_generation_not_match=None,
1466+
if_metageneration_match=None,
1467+
if_metageneration_not_match=None,
1468+
):
14591469
"""Deletes a list of blobs from the current bucket.
14601470
14611471
Uses :meth:`delete_blob` to delete each individual blob.
@@ -1484,15 +1494,74 @@ def delete_blobs(self, blobs, on_error=None, client=None, timeout=_DEFAULT_TIMEO
14841494
Can also be passed as a tuple (connect_timeout, read_timeout).
14851495
See :meth:`requests.Session.request` documentation for details.
14861496
1497+
:type if_generation_match: list of long
1498+
:param if_generation_match: (Optional) Make the operation conditional on whether
1499+
the blob's current generation matches the given value.
1500+
Setting to 0 makes the operation succeed only if there
1501+
are no live versions of the blob. The list must match
1502+
``blobs`` item-to-item.
1503+
1504+
:type if_generation_not_match: list of long
1505+
:param if_generation_not_match: (Optional) Make the operation conditional on whether
1506+
the blob's current generation does not match the given
1507+
value. If no live blob exists, the precondition fails.
1508+
Setting to 0 makes the operation succeed only if there
1509+
is a live version of the blob. The list must match
1510+
``blobs`` item-to-item.
1511+
1512+
:type if_metageneration_match: list of long
1513+
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
1514+
blob's current metageneration matches the given value.
1515+
The list must match ``blobs`` item-to-item.
1516+
1517+
:type if_metageneration_not_match: list of long
1518+
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
1519+
blob's current metageneration does not match the given value.
1520+
The list must match ``blobs`` item-to-item.
1521+
14871522
:raises: :class:`~google.cloud.exceptions.NotFound` (if
14881523
`on_error` is not passed).
1524+
1525+
Example:
1526+
Delete blobs using generation match preconditions.
1527+
1528+
>>> from google.cloud import storage
1529+
1530+
>>> client = storage.Client()
1531+
>>> bucket = client.bucket("bucket-name")
1532+
1533+
>>> blobs = [bucket.blob("blob-name-1"), bucket.blob("blob-name-2")]
1534+
>>> if_generation_match = [None] * len(blobs)
1535+
>>> if_generation_match[0] = "123" # precondition for "blob-name-1"
1536+
1537+
>>> bucket.delete_blobs(blobs, if_generation_match=if_generation_match)
14891538
"""
1539+
_raise_if_len_differs(
1540+
len(blobs),
1541+
if_generation_match=if_generation_match,
1542+
if_generation_not_match=if_generation_not_match,
1543+
if_metageneration_match=if_metageneration_match,
1544+
if_metageneration_not_match=if_metageneration_not_match,
1545+
)
1546+
if_generation_match = iter(if_generation_match or [])
1547+
if_generation_not_match = iter(if_generation_not_match or [])
1548+
if_metageneration_match = iter(if_metageneration_match or [])
1549+
if_metageneration_not_match = iter(if_metageneration_not_match or [])
1550+
14901551
for blob in blobs:
14911552
try:
14921553
blob_name = blob
14931554
if not isinstance(blob_name, six.string_types):
14941555
blob_name = blob.name
1495-
self.delete_blob(blob_name, client=client, timeout=timeout)
1556+
self.delete_blob(
1557+
blob_name,
1558+
client=client,
1559+
timeout=timeout,
1560+
if_generation_match=next(if_generation_match, None),
1561+
if_generation_not_match=next(if_generation_not_match, None),
1562+
if_metageneration_match=next(if_metageneration_match, None),
1563+
if_metageneration_not_match=next(if_metageneration_not_match, None),
1564+
)
14961565
except NotFound:
14971566
if on_error is not None:
14981567
on_error(blob)
@@ -2980,3 +3049,23 @@ def generate_signed_url(
29803049
headers=headers,
29813050
query_parameters=query_parameters,
29823051
)
3052+
3053+
3054+
def _raise_if_len_differs(expected_len, **generation_match_args):
3055+
"""
3056+
Raise an error if any generation match argument
3057+
is set and its len differs from the given value.
3058+
3059+
:type expected_len: int
3060+
:param expected_len: Expected argument length in case it's set.
3061+
3062+
:type generation_match_args: dict
3063+
:param generation_match_args: Lists, which length must be checked.
3064+
3065+
:raises: :exc:`ValueError` if any argument set, but has an unexpected length.
3066+
"""
3067+
for name, value in generation_match_args.items():
3068+
if value is not None and len(value) != expected_len:
3069+
raise ValueError(
3070+
"'{}' length must be the same as 'blobs' length".format(name)
3071+
)

tests/unit/test_bucket.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,82 @@ def test_delete_blobs_hit_w_user_project(self):
11461146
self.assertEqual(kw[0]["query_params"], {"userProject": USER_PROJECT})
11471147
self.assertEqual(kw[0]["timeout"], 42)
11481148

1149+
def test_delete_blobs_w_generation_match(self):
1150+
NAME = "name"
1151+
BLOB_NAME = "blob-name"
1152+
BLOB_NAME2 = "blob-name2"
1153+
GENERATION_NUMBER = 6
1154+
GENERATION_NUMBER2 = 9
1155+
1156+
connection = _Connection({}, {})
1157+
client = _Client(connection)
1158+
bucket = self._make_one(client=client, name=NAME)
1159+
bucket.delete_blobs(
1160+
[BLOB_NAME, BLOB_NAME2],
1161+
timeout=42,
1162+
if_generation_match=[GENERATION_NUMBER, GENERATION_NUMBER2],
1163+
)
1164+
kw = connection._requested
1165+
self.assertEqual(len(kw), 2)
1166+
1167+
self.assertEqual(kw[0]["method"], "DELETE")
1168+
self.assertEqual(kw[0]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME))
1169+
self.assertEqual(kw[0]["timeout"], 42)
1170+
self.assertEqual(
1171+
kw[0]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER}
1172+
)
1173+
self.assertEqual(kw[1]["method"], "DELETE")
1174+
self.assertEqual(kw[1]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME2))
1175+
self.assertEqual(kw[1]["timeout"], 42)
1176+
self.assertEqual(
1177+
kw[1]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER2}
1178+
)
1179+
1180+
def test_delete_blobs_w_generation_match_wrong_len(self):
1181+
NAME = "name"
1182+
BLOB_NAME = "blob-name"
1183+
BLOB_NAME2 = "blob-name2"
1184+
GENERATION_NUMBER = 6
1185+
1186+
connection = _Connection()
1187+
client = _Client(connection)
1188+
bucket = self._make_one(client=client, name=NAME)
1189+
with self.assertRaises(ValueError):
1190+
bucket.delete_blobs(
1191+
[BLOB_NAME, BLOB_NAME2],
1192+
timeout=42,
1193+
if_generation_not_match=[GENERATION_NUMBER],
1194+
)
1195+
1196+
def test_delete_blobs_w_generation_match_none(self):
1197+
NAME = "name"
1198+
BLOB_NAME = "blob-name"
1199+
BLOB_NAME2 = "blob-name2"
1200+
GENERATION_NUMBER = 6
1201+
GENERATION_NUMBER2 = None
1202+
1203+
connection = _Connection({}, {})
1204+
client = _Client(connection)
1205+
bucket = self._make_one(client=client, name=NAME)
1206+
bucket.delete_blobs(
1207+
[BLOB_NAME, BLOB_NAME2],
1208+
timeout=42,
1209+
if_generation_match=[GENERATION_NUMBER, GENERATION_NUMBER2],
1210+
)
1211+
kw = connection._requested
1212+
self.assertEqual(len(kw), 2)
1213+
1214+
self.assertEqual(kw[0]["method"], "DELETE")
1215+
self.assertEqual(kw[0]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME))
1216+
self.assertEqual(kw[0]["timeout"], 42)
1217+
self.assertEqual(
1218+
kw[0]["query_params"], {"ifGenerationMatch": GENERATION_NUMBER}
1219+
)
1220+
self.assertEqual(kw[1]["method"], "DELETE")
1221+
self.assertEqual(kw[1]["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME2))
1222+
self.assertEqual(kw[1]["timeout"], 42)
1223+
self.assertEqual(kw[1]["query_params"], {})
1224+
11491225
def test_delete_blobs_miss_no_on_error(self):
11501226
from google.cloud.exceptions import NotFound
11511227

0 commit comments

Comments
 (0)