Skip to content

Commit b2e5150

Browse files
authored
feat: add AbortIncompleteMultipartUpload lifecycle rule (#765)
Fixes #753 🦕
1 parent 50ef911 commit b2e5150

File tree

4 files changed

+136
-6
lines changed

4 files changed

+136
-6
lines changed

google/cloud/storage/bucket.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ class LifecycleRuleDelete(dict):
323323
def __init__(self, **kw):
324324
conditions = LifecycleRuleConditions(**kw)
325325
rule = {"action": {"type": "Delete"}, "condition": dict(conditions)}
326-
super(LifecycleRuleDelete, self).__init__(rule)
326+
super().__init__(rule)
327327

328328
@classmethod
329329
def from_api_repr(cls, resource):
@@ -356,7 +356,7 @@ def __init__(self, storage_class, **kw):
356356
"action": {"type": "SetStorageClass", "storageClass": storage_class},
357357
"condition": dict(conditions),
358358
}
359-
super(LifecycleRuleSetStorageClass, self).__init__(rule)
359+
super().__init__(rule)
360360

361361
@classmethod
362362
def from_api_repr(cls, resource):
@@ -365,7 +365,7 @@ def from_api_repr(cls, resource):
365365
:type resource: dict
366366
:param resource: mapping as returned from API call.
367367
368-
:rtype: :class:`LifecycleRuleDelete`
368+
:rtype: :class:`LifecycleRuleSetStorageClass`
369369
:returns: Instance created from resource.
370370
"""
371371
action = resource["action"]
@@ -374,6 +374,38 @@ def from_api_repr(cls, resource):
374374
return instance
375375

376376

377+
class LifecycleRuleAbortIncompleteMultipartUpload(dict):
378+
"""Map a rule aborting incomplete multipart uploads of matching items.
379+
380+
The "age" lifecycle condition is the only supported condition for this rule.
381+
382+
:type kw: dict
383+
:params kw: arguments passed to :class:`LifecycleRuleConditions`.
384+
"""
385+
386+
def __init__(self, **kw):
387+
conditions = LifecycleRuleConditions(**kw)
388+
rule = {
389+
"action": {"type": "AbortIncompleteMultipartUpload"},
390+
"condition": dict(conditions),
391+
}
392+
super().__init__(rule)
393+
394+
@classmethod
395+
def from_api_repr(cls, resource):
396+
"""Factory: construct instance from resource.
397+
398+
:type resource: dict
399+
:param resource: mapping as returned from API call.
400+
401+
:rtype: :class:`LifecycleRuleAbortIncompleteMultipartUpload`
402+
:returns: Instance created from resource.
403+
"""
404+
instance = cls(_factory=True)
405+
instance.update(resource)
406+
return instance
407+
408+
377409
_default = object()
378410

379411

@@ -2240,6 +2272,8 @@ def lifecycle_rules(self):
22402272
yield LifecycleRuleDelete.from_api_repr(rule)
22412273
elif action_type == "SetStorageClass":
22422274
yield LifecycleRuleSetStorageClass.from_api_repr(rule)
2275+
elif action_type == "AbortIncompleteMultipartUpload":
2276+
yield LifecycleRuleAbortIncompleteMultipartUpload.from_api_repr(rule)
22432277
else:
22442278
warnings.warn(
22452279
"Unknown lifecycle rule type received: {}. Please upgrade to the latest version of google-cloud-storage.".format(
@@ -2289,7 +2323,7 @@ def add_lifecycle_delete_rule(self, **kw):
22892323
self.lifecycle_rules = rules
22902324

22912325
def add_lifecycle_set_storage_class_rule(self, storage_class, **kw):
2292-
"""Add a "delete" rule to lifestyle rules configured for this bucket.
2326+
"""Add a "set storage class" rule to lifestyle rules.
22932327
22942328
See https://cloud.google.com/storage/docs/lifecycle and
22952329
https://cloud.google.com/storage/docs/json_api/v1/buckets
@@ -2309,6 +2343,22 @@ def add_lifecycle_set_storage_class_rule(self, storage_class, **kw):
23092343
rules.append(LifecycleRuleSetStorageClass(storage_class, **kw))
23102344
self.lifecycle_rules = rules
23112345

2346+
def add_lifecycle_abort_incomplete_multipart_upload_rule(self, **kw):
2347+
"""Add a "abort incomplete multipart upload" rule to lifestyle rules.
2348+
2349+
Note that the "age" lifecycle condition is the only supported condition
2350+
for this rule.
2351+
2352+
See https://cloud.google.com/storage/docs/lifecycle and
2353+
https://cloud.google.com/storage/docs/json_api/v1/buckets
2354+
2355+
:type kw: dict
2356+
:params kw: arguments passed to :class:`LifecycleRuleConditions`.
2357+
"""
2358+
rules = list(self.lifecycle_rules)
2359+
rules.append(LifecycleRuleAbortIncompleteMultipartUpload(**kw))
2360+
self.lifecycle_rules = rules
2361+
23122362
_location = _scalar_property("location")
23132363

23142364
@property

tests/system/_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
retry_429 = RetryErrors(exceptions.TooManyRequests)
2424
retry_429_harder = RetryErrors(exceptions.TooManyRequests, max_tries=10)
2525
retry_429_503 = RetryErrors(
26-
[exceptions.TooManyRequests, exceptions.ServiceUnavailable], max_tries=10
26+
(exceptions.TooManyRequests, exceptions.ServiceUnavailable), max_tries=10
2727
)
2828
retry_failures = RetryErrors(AssertionError)
2929

tests/system/test_bucket.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def test_bucket_lifecycle_rules(storage_client, buckets_to_delete):
4242
from google.cloud.storage import constants
4343
from google.cloud.storage.bucket import LifecycleRuleDelete
4444
from google.cloud.storage.bucket import LifecycleRuleSetStorageClass
45+
from google.cloud.storage.bucket import LifecycleRuleAbortIncompleteMultipartUpload
4546

4647
bucket_name = _helpers.unique_name("w-lifcycle-rules")
4748
custom_time_before = datetime.date(2018, 8, 1)
@@ -64,6 +65,9 @@ def test_bucket_lifecycle_rules(storage_client, buckets_to_delete):
6465
is_live=False,
6566
matches_storage_class=[constants.NEARLINE_STORAGE_CLASS],
6667
)
68+
bucket.add_lifecycle_abort_incomplete_multipart_upload_rule(
69+
age=42,
70+
)
6771

6872
expected_rules = [
6973
LifecycleRuleDelete(
@@ -79,6 +83,9 @@ def test_bucket_lifecycle_rules(storage_client, buckets_to_delete):
7983
is_live=False,
8084
matches_storage_class=[constants.NEARLINE_STORAGE_CLASS],
8185
),
86+
LifecycleRuleAbortIncompleteMultipartUpload(
87+
age=42,
88+
),
8289
]
8390

8491
_helpers.retry_429_503(bucket.create)(location="us")
@@ -87,6 +94,16 @@ def test_bucket_lifecycle_rules(storage_client, buckets_to_delete):
8794
assert bucket.name == bucket_name
8895
assert list(bucket.lifecycle_rules) == expected_rules
8996

97+
# Test modifying lifecycle rules
98+
expected_rules[0] = LifecycleRuleDelete(age=30)
99+
rules = list(bucket.lifecycle_rules)
100+
rules[0]["condition"] = {"age": 30}
101+
bucket.lifecycle_rules = rules
102+
bucket.patch()
103+
104+
assert list(bucket.lifecycle_rules) == expected_rules
105+
106+
# Test clearing lifecycle rules
90107
bucket.clear_lifecyle_rules()
91108
bucket.patch()
92109

tests/unit/test_bucket.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,43 @@ def test_from_api_repr(self):
337337
self.assertEqual(dict(rule), resource)
338338

339339

340+
class Test_LifecycleRuleAbortIncompleteMultipartUpload(unittest.TestCase):
341+
@staticmethod
342+
def _get_target_class():
343+
from google.cloud.storage.bucket import (
344+
LifecycleRuleAbortIncompleteMultipartUpload,
345+
)
346+
347+
return LifecycleRuleAbortIncompleteMultipartUpload
348+
349+
def _make_one(self, **kw):
350+
return self._get_target_class()(**kw)
351+
352+
def test_ctor_wo_conditions(self):
353+
with self.assertRaises(ValueError):
354+
self._make_one()
355+
356+
def test_ctor_w_condition(self):
357+
rule = self._make_one(age=10)
358+
expected = {
359+
"action": {"type": "AbortIncompleteMultipartUpload"},
360+
"condition": {"age": 10},
361+
}
362+
self.assertEqual(dict(rule), expected)
363+
364+
def test_from_api_repr(self):
365+
klass = self._get_target_class()
366+
conditions = {
367+
"age": 10,
368+
}
369+
resource = {
370+
"action": {"type": "AbortIncompleteMultipartUpload"},
371+
"condition": conditions,
372+
}
373+
rule = klass.from_api_repr(resource)
374+
self.assertEqual(dict(rule), resource)
375+
376+
340377
class Test_IAMConfiguration(unittest.TestCase):
341378
@staticmethod
342379
def _get_target_class():
@@ -2242,6 +2279,7 @@ def test_lifecycle_rules_getter(self):
22422279
from google.cloud.storage.bucket import (
22432280
LifecycleRuleDelete,
22442281
LifecycleRuleSetStorageClass,
2282+
LifecycleRuleAbortIncompleteMultipartUpload,
22452283
)
22462284

22472285
NAME = "name"
@@ -2250,7 +2288,11 @@ def test_lifecycle_rules_getter(self):
22502288
"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
22512289
"condition": {"isLive": False},
22522290
}
2253-
rules = [DELETE_RULE, SSC_RULE]
2291+
MULTIPART_RULE = {
2292+
"action": {"type": "AbortIncompleteMultipartUpload"},
2293+
"condition": {"age": 42},
2294+
}
2295+
rules = [DELETE_RULE, SSC_RULE, MULTIPART_RULE]
22542296
properties = {"lifecycle": {"rule": rules}}
22552297
bucket = self._make_one(name=NAME, properties=properties)
22562298

@@ -2264,6 +2306,12 @@ def test_lifecycle_rules_getter(self):
22642306
self.assertIsInstance(ssc_rule, LifecycleRuleSetStorageClass)
22652307
self.assertEqual(dict(ssc_rule), SSC_RULE)
22662308

2309+
multipart_rule = found[2]
2310+
self.assertIsInstance(
2311+
multipart_rule, LifecycleRuleAbortIncompleteMultipartUpload
2312+
)
2313+
self.assertEqual(dict(multipart_rule), MULTIPART_RULE)
2314+
22672315
def test_lifecycle_rules_setter_w_dicts(self):
22682316
NAME = "name"
22692317
DELETE_RULE = {"action": {"type": "Delete"}, "condition": {"age": 42}}
@@ -2348,6 +2396,21 @@ def test_add_lifecycle_set_storage_class_rule(self):
23482396
self.assertEqual([dict(rule) for rule in bucket.lifecycle_rules], rules)
23492397
self.assertTrue("lifecycle" in bucket._changes)
23502398

2399+
def test_add_lifecycle_abort_incomplete_multipart_upload_rule(self):
2400+
NAME = "name"
2401+
AIMPU_RULE = {
2402+
"action": {"type": "AbortIncompleteMultipartUpload"},
2403+
"condition": {"age": 42},
2404+
}
2405+
rules = [AIMPU_RULE]
2406+
bucket = self._make_one(name=NAME)
2407+
self.assertEqual(list(bucket.lifecycle_rules), [])
2408+
2409+
bucket.add_lifecycle_abort_incomplete_multipart_upload_rule(age=42)
2410+
2411+
self.assertEqual([dict(rule) for rule in bucket.lifecycle_rules], rules)
2412+
self.assertTrue("lifecycle" in bucket._changes)
2413+
23512414
def test_cors_getter(self):
23522415
NAME = "name"
23532416
CORS_ENTRY = {

0 commit comments

Comments
 (0)