Skip to content

Commit 671afaf

Browse files
HemangChothanijkwluitseaverfrankyn
authored andcommitted
feat: add support for 'Blob.custom_time' and lifecycle rules (googleapis#199)
* feat(storage): add support of custom time metadata and timestamp * feat(storage): change the return type of custom_time_before * feat(storage): add setter method * feat(storage): add test for None value * feat(storage): changes in unittest * feat(storage): change custom_time type to date * feat: change custom_time to datetime * feat: nit Co-authored-by: Jonathan Lui <[email protected]> Co-authored-by: Tres Seaver <[email protected]> Co-authored-by: Frank Natividad <[email protected]>
1 parent 5d70a1b commit 671afaf

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
lines changed

google/cloud/storage/blob.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from google.api_core.iam import Policy
5353
from google.cloud import exceptions
5454
from google.cloud._helpers import _bytes_to_unicode
55+
from google.cloud._helpers import _datetime_to_rfc3339
5556
from google.cloud._helpers import _rfc3339_to_datetime
5657
from google.cloud._helpers import _to_bytes
5758
from google.cloud.exceptions import NotFound
@@ -3348,6 +3349,39 @@ def updated(self):
33483349
if value is not None:
33493350
return _rfc3339_to_datetime(value)
33503351

3352+
@property
3353+
def custom_time(self):
3354+
"""Retrieve the custom time for the object.
3355+
3356+
See https://cloud.google.com/storage/docs/json_api/v1/objects
3357+
3358+
:rtype: :class:`datetime.datetime` or ``NoneType``
3359+
:returns: Datetime object parsed from RFC3339 valid timestamp, or
3360+
``None`` if the blob's resource has not been loaded from
3361+
the server (see :meth:`reload`).
3362+
"""
3363+
value = self._properties.get("customTime")
3364+
if value is not None:
3365+
return _rfc3339_to_datetime(value)
3366+
3367+
@custom_time.setter
3368+
def custom_time(self, value):
3369+
"""Set the custom time for the object. Once set it can't be unset
3370+
and only changed to a custom datetime in the future. If the
3371+
custom_time must be unset, you must either perform a rewrite operation
3372+
or upload the data again.
3373+
3374+
See https://cloud.google.com/storage/docs/json_api/v1/objects
3375+
3376+
:type value: :class:`datetime.datetime`
3377+
:param value: (Optional) Set the custom time of blob. Datetime object
3378+
parsed from RFC3339 valid timestamp.
3379+
"""
3380+
if value is not None:
3381+
value = _datetime_to_rfc3339(value)
3382+
3383+
self._properties["customTime"] = value
3384+
33513385

33523386
def _get_encryption_headers(key, source=False):
33533387
"""Builds customer encryption key headers

google/cloud/storage/bucket.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,18 @@ class LifecycleRuleConditions(dict):
170170
:param number_of_newer_versions: (Optional) Apply rule action to versioned
171171
items having N newer versions.
172172
173+
:type days_since_custom_time: int
174+
:param days_since_custom_time: (Optional) Apply rule action to items whose number of days
175+
elapsed since the custom timestamp. This condition is relevant
176+
only for versioned objects. The value of the field must be a non
177+
negative integer. If it's zero, the object version will become
178+
eligible for lifecycle action as soon as it becomes custom.
179+
180+
:type custom_time_before: :class:`datetime.date`
181+
:param custom_time_before: (Optional) Date object parsed from RFC3339 valid date, apply rule action
182+
to items whose custom time is before this date. This condition is relevant
183+
only for versioned objects, e.g., 2019-03-16.
184+
173185
:type days_since_noncurrent_time: int
174186
:param days_since_noncurrent_time: (Optional) Apply rule action to items whose number of days
175187
elapsed since the non current timestamp. This condition
@@ -193,6 +205,8 @@ def __init__(
193205
is_live=None,
194206
matches_storage_class=None,
195207
number_of_newer_versions=None,
208+
days_since_custom_time=None,
209+
custom_time_before=None,
196210
days_since_noncurrent_time=None,
197211
noncurrent_time_before=None,
198212
_factory=False,
@@ -214,6 +228,12 @@ def __init__(
214228
if number_of_newer_versions is not None:
215229
conditions["numNewerVersions"] = number_of_newer_versions
216230

231+
if days_since_custom_time is not None:
232+
conditions["daysSinceCustomTime"] = days_since_custom_time
233+
234+
if custom_time_before is not None:
235+
conditions["customTimeBefore"] = custom_time_before.isoformat()
236+
217237
if not _factory and not conditions:
218238
raise ValueError("Supply at least one condition")
219239

@@ -266,6 +286,18 @@ def number_of_newer_versions(self):
266286
"""Conditon's 'number_of_newer_versions' value."""
267287
return self.get("numNewerVersions")
268288

289+
@property
290+
def days_since_custom_time(self):
291+
"""Conditon's 'days_since_custom_time' value."""
292+
return self.get("daysSinceCustomTime")
293+
294+
@property
295+
def custom_time_before(self):
296+
"""Conditon's 'custom_time_before' value."""
297+
before = self.get("customTimeBefore")
298+
if before is not None:
299+
return datetime_helpers.from_iso8601_date(before)
300+
269301
@property
270302
def days_since_noncurrent_time(self):
271303
"""Conditon's 'days_since_noncurrent_time' value."""

tests/system/test_system.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,21 @@ def test_lifecycle_rules(self):
196196
from google.cloud.storage import constants
197197

198198
new_bucket_name = "w-lifcycle-rules" + unique_resource_id("-")
199+
custom_time_before = datetime.date(2018, 8, 1)
199200
noncurrent_before = datetime.date(2018, 8, 1)
201+
200202
self.assertRaises(
201203
exceptions.NotFound, Config.CLIENT.get_bucket, new_bucket_name
202204
)
203205
bucket = Config.CLIENT.bucket(new_bucket_name)
204206
bucket.add_lifecycle_delete_rule(
205207
age=42,
206208
number_of_newer_versions=3,
209+
days_since_custom_time=2,
210+
custom_time_before=custom_time_before,
207211
days_since_noncurrent_time=2,
208212
noncurrent_time_before=noncurrent_before,
209213
)
210-
211214
bucket.add_lifecycle_set_storage_class_rule(
212215
constants.COLDLINE_STORAGE_CLASS,
213216
is_live=False,
@@ -218,6 +221,8 @@ def test_lifecycle_rules(self):
218221
LifecycleRuleDelete(
219222
age=42,
220223
number_of_newer_versions=3,
224+
days_since_custom_time=2,
225+
custom_time_before=custom_time_before,
221226
days_since_noncurrent_time=2,
222227
noncurrent_time_before=noncurrent_before,
223228
),

tests/unit/test_blob.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def _set_properties_helper(self, kms_key_name=None):
157157
"crc32c": CRC32C,
158158
"componentCount": COMPONENT_COUNT,
159159
"etag": ETAG,
160+
"customTime": NOW,
160161
}
161162

162163
if kms_key_name is not None:
@@ -188,6 +189,7 @@ def _set_properties_helper(self, kms_key_name=None):
188189
self.assertEqual(blob.crc32c, CRC32C)
189190
self.assertEqual(blob.component_count, COMPONENT_COUNT)
190191
self.assertEqual(blob.etag, ETAG)
192+
self.assertEqual(blob.custom_time, now)
191193

192194
if kms_key_name is not None:
193195
self.assertEqual(blob.kms_key_name, kms_key_name)
@@ -4248,6 +4250,48 @@ def test_updated_unset(self):
42484250
blob = self._make_one("blob-name", bucket=BUCKET)
42494251
self.assertIsNone(blob.updated)
42504252

4253+
def test_custom_time_getter(self):
4254+
from google.cloud._helpers import _RFC3339_MICROS
4255+
from google.cloud._helpers import UTC
4256+
4257+
BLOB_NAME = "blob-name"
4258+
bucket = _Bucket()
4259+
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
4260+
TIME_CREATED = TIMESTAMP.strftime(_RFC3339_MICROS)
4261+
properties = {"customTime": TIME_CREATED}
4262+
blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
4263+
self.assertEqual(blob.custom_time, TIMESTAMP)
4264+
4265+
def test_custom_time_setter(self):
4266+
from google.cloud._helpers import UTC
4267+
4268+
BLOB_NAME = "blob-name"
4269+
bucket = _Bucket()
4270+
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
4271+
blob = self._make_one(BLOB_NAME, bucket=bucket)
4272+
self.assertIsNone(blob.custom_time)
4273+
blob.custom_time = TIMESTAMP
4274+
self.assertEqual(blob.custom_time, TIMESTAMP)
4275+
4276+
def test_custom_time_setter_none_value(self):
4277+
from google.cloud._helpers import _RFC3339_MICROS
4278+
from google.cloud._helpers import UTC
4279+
4280+
BLOB_NAME = "blob-name"
4281+
bucket = _Bucket()
4282+
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
4283+
TIME_CREATED = TIMESTAMP.strftime(_RFC3339_MICROS)
4284+
properties = {"customTime": TIME_CREATED}
4285+
blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
4286+
self.assertEqual(blob.custom_time, TIMESTAMP)
4287+
blob.custom_time = None
4288+
self.assertIsNone(blob.custom_time)
4289+
4290+
def test_custom_time_unset(self):
4291+
BUCKET = object()
4292+
blob = self._make_one("blob-name", bucket=BUCKET)
4293+
self.assertIsNone(blob.custom_time)
4294+
42514295
def test_from_string_w_valid_uri(self):
42524296
from google.cloud.storage.blob import Blob
42534297

tests/unit/test_bucket.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def test_ctor_w_created_before_and_is_live(self):
7777
self.assertEqual(conditions.is_live, False)
7878
self.assertIsNone(conditions.matches_storage_class)
7979
self.assertIsNone(conditions.number_of_newer_versions)
80+
self.assertIsNone(conditions.days_since_custom_time)
81+
self.assertIsNone(conditions.custom_time_before)
8082
self.assertIsNone(conditions.noncurrent_time_before)
8183

8284
def test_ctor_w_number_of_newer_versions(self):
@@ -89,6 +91,19 @@ def test_ctor_w_number_of_newer_versions(self):
8991
self.assertIsNone(conditions.matches_storage_class)
9092
self.assertEqual(conditions.number_of_newer_versions, 3)
9193

94+
def test_ctor_w_days_since_custom_time(self):
95+
conditions = self._make_one(
96+
number_of_newer_versions=3, days_since_custom_time=2
97+
)
98+
expected = {"numNewerVersions": 3, "daysSinceCustomTime": 2}
99+
self.assertEqual(dict(conditions), expected)
100+
self.assertIsNone(conditions.age)
101+
self.assertIsNone(conditions.created_before)
102+
self.assertIsNone(conditions.is_live)
103+
self.assertIsNone(conditions.matches_storage_class)
104+
self.assertEqual(conditions.number_of_newer_versions, 3)
105+
self.assertEqual(conditions.days_since_custom_time, 2)
106+
92107
def test_ctor_w_days_since_noncurrent_time(self):
93108
conditions = self._make_one(
94109
number_of_newer_versions=3, days_since_noncurrent_time=2
@@ -102,6 +117,25 @@ def test_ctor_w_days_since_noncurrent_time(self):
102117
self.assertEqual(conditions.number_of_newer_versions, 3)
103118
self.assertEqual(conditions.days_since_noncurrent_time, 2)
104119

120+
def test_ctor_w_custom_time_before(self):
121+
import datetime
122+
123+
custom_time_before = datetime.date(2018, 8, 1)
124+
conditions = self._make_one(
125+
number_of_newer_versions=3, custom_time_before=custom_time_before
126+
)
127+
expected = {
128+
"numNewerVersions": 3,
129+
"customTimeBefore": custom_time_before.isoformat(),
130+
}
131+
self.assertEqual(dict(conditions), expected)
132+
self.assertIsNone(conditions.age)
133+
self.assertIsNone(conditions.created_before)
134+
self.assertIsNone(conditions.is_live)
135+
self.assertIsNone(conditions.matches_storage_class)
136+
self.assertEqual(conditions.number_of_newer_versions, 3)
137+
self.assertEqual(conditions.custom_time_before, custom_time_before)
138+
105139
def test_ctor_w_noncurrent_time_before(self):
106140
import datetime
107141

@@ -125,16 +159,18 @@ def test_ctor_w_noncurrent_time_before(self):
125159
def test_from_api_repr(self):
126160
import datetime
127161

162+
custom_time_before = datetime.date(2018, 8, 1)
128163
noncurrent_before = datetime.date(2018, 8, 1)
129164
before = datetime.date(2018, 8, 1)
130165
klass = self._get_target_class()
131-
132166
resource = {
133167
"age": 10,
134168
"createdBefore": "2018-08-01",
135169
"isLive": True,
136170
"matchesStorageClass": ["COLDLINE"],
137171
"numNewerVersions": 3,
172+
"daysSinceCustomTime": 2,
173+
"customTimeBefore": custom_time_before.isoformat(),
138174
"daysSinceNoncurrentTime": 2,
139175
"noncurrentTimeBefore": noncurrent_before.isoformat(),
140176
}
@@ -144,6 +180,8 @@ def test_from_api_repr(self):
144180
self.assertEqual(conditions.is_live, True)
145181
self.assertEqual(conditions.matches_storage_class, ["COLDLINE"])
146182
self.assertEqual(conditions.number_of_newer_versions, 3)
183+
self.assertEqual(conditions.days_since_custom_time, 2)
184+
self.assertEqual(conditions.custom_time_before, custom_time_before)
147185
self.assertEqual(conditions.days_since_noncurrent_time, 2)
148186
self.assertEqual(conditions.noncurrent_time_before, noncurrent_before)
149187

0 commit comments

Comments
 (0)