Skip to content

Commit

Permalink
feat: add support for 'Blob.custom_time' and lifecycle rules (googlea…
Browse files Browse the repository at this point in the history
…pis#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 <jonathanlui@google.com>
Co-authored-by: Tres Seaver <tseaver@palladion.com>
Co-authored-by: Frank Natividad <frankyn@users.noreply.github.com>
  • Loading branch information
4 people authored and cojenco committed Oct 13, 2021
1 parent eaadb59 commit cfaf395
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 2 deletions.
34 changes: 34 additions & 0 deletions google/cloud/storage/blob.py
Expand Up @@ -52,6 +52,7 @@
from google.api_core.iam import Policy
from google.cloud import exceptions
from google.cloud._helpers import _bytes_to_unicode
from google.cloud._helpers import _datetime_to_rfc3339
from google.cloud._helpers import _rfc3339_to_datetime
from google.cloud._helpers import _to_bytes
from google.cloud.exceptions import NotFound
Expand Down Expand Up @@ -3348,6 +3349,39 @@ def updated(self):
if value is not None:
return _rfc3339_to_datetime(value)

@property
def custom_time(self):
"""Retrieve the custom time for the object.
See https://cloud.google.com/storage/docs/json_api/v1/objects
:rtype: :class:`datetime.datetime` or ``NoneType``
:returns: Datetime object parsed from RFC3339 valid timestamp, or
``None`` if the blob's resource has not been loaded from
the server (see :meth:`reload`).
"""
value = self._properties.get("customTime")
if value is not None:
return _rfc3339_to_datetime(value)

@custom_time.setter
def custom_time(self, value):
"""Set the custom time for the object. Once set it can't be unset
and only changed to a custom datetime in the future. If the
custom_time must be unset, you must either perform a rewrite operation
or upload the data again.
See https://cloud.google.com/storage/docs/json_api/v1/objects
:type value: :class:`datetime.datetime`
:param value: (Optional) Set the custom time of blob. Datetime object
parsed from RFC3339 valid timestamp.
"""
if value is not None:
value = _datetime_to_rfc3339(value)

self._properties["customTime"] = value


def _get_encryption_headers(key, source=False):
"""Builds customer encryption key headers
Expand Down
32 changes: 32 additions & 0 deletions google/cloud/storage/bucket.py
Expand Up @@ -170,6 +170,18 @@ class LifecycleRuleConditions(dict):
:param number_of_newer_versions: (Optional) Apply rule action to versioned
items having N newer versions.
:type days_since_custom_time: int
:param days_since_custom_time: (Optional) Apply rule action to items whose number of days
elapsed since the custom timestamp. This condition is relevant
only for versioned objects. The value of the field must be a non
negative integer. If it's zero, the object version will become
eligible for lifecycle action as soon as it becomes custom.
:type custom_time_before: :class:`datetime.date`
:param custom_time_before: (Optional) Date object parsed from RFC3339 valid date, apply rule action
to items whose custom time is before this date. This condition is relevant
only for versioned objects, e.g., 2019-03-16.
:type days_since_noncurrent_time: int
:param days_since_noncurrent_time: (Optional) Apply rule action to items whose number of days
elapsed since the non current timestamp. This condition
Expand All @@ -193,6 +205,8 @@ def __init__(
is_live=None,
matches_storage_class=None,
number_of_newer_versions=None,
days_since_custom_time=None,
custom_time_before=None,
days_since_noncurrent_time=None,
noncurrent_time_before=None,
_factory=False,
Expand All @@ -214,6 +228,12 @@ def __init__(
if number_of_newer_versions is not None:
conditions["numNewerVersions"] = number_of_newer_versions

if days_since_custom_time is not None:
conditions["daysSinceCustomTime"] = days_since_custom_time

if custom_time_before is not None:
conditions["customTimeBefore"] = custom_time_before.isoformat()

if not _factory and not conditions:
raise ValueError("Supply at least one condition")

Expand Down Expand Up @@ -266,6 +286,18 @@ def number_of_newer_versions(self):
"""Conditon's 'number_of_newer_versions' value."""
return self.get("numNewerVersions")

@property
def days_since_custom_time(self):
"""Conditon's 'days_since_custom_time' value."""
return self.get("daysSinceCustomTime")

@property
def custom_time_before(self):
"""Conditon's 'custom_time_before' value."""
before = self.get("customTimeBefore")
if before is not None:
return datetime_helpers.from_iso8601_date(before)

@property
def days_since_noncurrent_time(self):
"""Conditon's 'days_since_noncurrent_time' value."""
Expand Down
7 changes: 6 additions & 1 deletion tests/system/test_system.py
Expand Up @@ -196,18 +196,21 @@ def test_lifecycle_rules(self):
from google.cloud.storage import constants

new_bucket_name = "w-lifcycle-rules" + unique_resource_id("-")
custom_time_before = datetime.date(2018, 8, 1)
noncurrent_before = datetime.date(2018, 8, 1)

self.assertRaises(
exceptions.NotFound, Config.CLIENT.get_bucket, new_bucket_name
)
bucket = Config.CLIENT.bucket(new_bucket_name)
bucket.add_lifecycle_delete_rule(
age=42,
number_of_newer_versions=3,
days_since_custom_time=2,
custom_time_before=custom_time_before,
days_since_noncurrent_time=2,
noncurrent_time_before=noncurrent_before,
)

bucket.add_lifecycle_set_storage_class_rule(
constants.COLDLINE_STORAGE_CLASS,
is_live=False,
Expand All @@ -218,6 +221,8 @@ def test_lifecycle_rules(self):
LifecycleRuleDelete(
age=42,
number_of_newer_versions=3,
days_since_custom_time=2,
custom_time_before=custom_time_before,
days_since_noncurrent_time=2,
noncurrent_time_before=noncurrent_before,
),
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_blob.py
Expand Up @@ -157,6 +157,7 @@ def _set_properties_helper(self, kms_key_name=None):
"crc32c": CRC32C,
"componentCount": COMPONENT_COUNT,
"etag": ETAG,
"customTime": NOW,
}

if kms_key_name is not None:
Expand Down Expand Up @@ -188,6 +189,7 @@ def _set_properties_helper(self, kms_key_name=None):
self.assertEqual(blob.crc32c, CRC32C)
self.assertEqual(blob.component_count, COMPONENT_COUNT)
self.assertEqual(blob.etag, ETAG)
self.assertEqual(blob.custom_time, now)

if kms_key_name is not None:
self.assertEqual(blob.kms_key_name, kms_key_name)
Expand Down Expand Up @@ -4248,6 +4250,48 @@ def test_updated_unset(self):
blob = self._make_one("blob-name", bucket=BUCKET)
self.assertIsNone(blob.updated)

def test_custom_time_getter(self):
from google.cloud._helpers import _RFC3339_MICROS
from google.cloud._helpers import UTC

BLOB_NAME = "blob-name"
bucket = _Bucket()
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
TIME_CREATED = TIMESTAMP.strftime(_RFC3339_MICROS)
properties = {"customTime": TIME_CREATED}
blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
self.assertEqual(blob.custom_time, TIMESTAMP)

def test_custom_time_setter(self):
from google.cloud._helpers import UTC

BLOB_NAME = "blob-name"
bucket = _Bucket()
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
blob = self._make_one(BLOB_NAME, bucket=bucket)
self.assertIsNone(blob.custom_time)
blob.custom_time = TIMESTAMP
self.assertEqual(blob.custom_time, TIMESTAMP)

def test_custom_time_setter_none_value(self):
from google.cloud._helpers import _RFC3339_MICROS
from google.cloud._helpers import UTC

BLOB_NAME = "blob-name"
bucket = _Bucket()
TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
TIME_CREATED = TIMESTAMP.strftime(_RFC3339_MICROS)
properties = {"customTime": TIME_CREATED}
blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
self.assertEqual(blob.custom_time, TIMESTAMP)
blob.custom_time = None
self.assertIsNone(blob.custom_time)

def test_custom_time_unset(self):
BUCKET = object()
blob = self._make_one("blob-name", bucket=BUCKET)
self.assertIsNone(blob.custom_time)

def test_from_string_w_valid_uri(self):
from google.cloud.storage.blob import Blob

Expand Down
40 changes: 39 additions & 1 deletion tests/unit/test_bucket.py
Expand Up @@ -77,6 +77,8 @@ def test_ctor_w_created_before_and_is_live(self):
self.assertEqual(conditions.is_live, False)
self.assertIsNone(conditions.matches_storage_class)
self.assertIsNone(conditions.number_of_newer_versions)
self.assertIsNone(conditions.days_since_custom_time)
self.assertIsNone(conditions.custom_time_before)
self.assertIsNone(conditions.noncurrent_time_before)

def test_ctor_w_number_of_newer_versions(self):
Expand All @@ -89,6 +91,19 @@ def test_ctor_w_number_of_newer_versions(self):
self.assertIsNone(conditions.matches_storage_class)
self.assertEqual(conditions.number_of_newer_versions, 3)

def test_ctor_w_days_since_custom_time(self):
conditions = self._make_one(
number_of_newer_versions=3, days_since_custom_time=2
)
expected = {"numNewerVersions": 3, "daysSinceCustomTime": 2}
self.assertEqual(dict(conditions), expected)
self.assertIsNone(conditions.age)
self.assertIsNone(conditions.created_before)
self.assertIsNone(conditions.is_live)
self.assertIsNone(conditions.matches_storage_class)
self.assertEqual(conditions.number_of_newer_versions, 3)
self.assertEqual(conditions.days_since_custom_time, 2)

def test_ctor_w_days_since_noncurrent_time(self):
conditions = self._make_one(
number_of_newer_versions=3, days_since_noncurrent_time=2
Expand All @@ -102,6 +117,25 @@ def test_ctor_w_days_since_noncurrent_time(self):
self.assertEqual(conditions.number_of_newer_versions, 3)
self.assertEqual(conditions.days_since_noncurrent_time, 2)

def test_ctor_w_custom_time_before(self):
import datetime

custom_time_before = datetime.date(2018, 8, 1)
conditions = self._make_one(
number_of_newer_versions=3, custom_time_before=custom_time_before
)
expected = {
"numNewerVersions": 3,
"customTimeBefore": custom_time_before.isoformat(),
}
self.assertEqual(dict(conditions), expected)
self.assertIsNone(conditions.age)
self.assertIsNone(conditions.created_before)
self.assertIsNone(conditions.is_live)
self.assertIsNone(conditions.matches_storage_class)
self.assertEqual(conditions.number_of_newer_versions, 3)
self.assertEqual(conditions.custom_time_before, custom_time_before)

def test_ctor_w_noncurrent_time_before(self):
import datetime

Expand All @@ -125,16 +159,18 @@ def test_ctor_w_noncurrent_time_before(self):
def test_from_api_repr(self):
import datetime

custom_time_before = datetime.date(2018, 8, 1)
noncurrent_before = datetime.date(2018, 8, 1)
before = datetime.date(2018, 8, 1)
klass = self._get_target_class()

resource = {
"age": 10,
"createdBefore": "2018-08-01",
"isLive": True,
"matchesStorageClass": ["COLDLINE"],
"numNewerVersions": 3,
"daysSinceCustomTime": 2,
"customTimeBefore": custom_time_before.isoformat(),
"daysSinceNoncurrentTime": 2,
"noncurrentTimeBefore": noncurrent_before.isoformat(),
}
Expand All @@ -144,6 +180,8 @@ def test_from_api_repr(self):
self.assertEqual(conditions.is_live, True)
self.assertEqual(conditions.matches_storage_class, ["COLDLINE"])
self.assertEqual(conditions.number_of_newer_versions, 3)
self.assertEqual(conditions.days_since_custom_time, 2)
self.assertEqual(conditions.custom_time_before, custom_time_before)
self.assertEqual(conditions.days_since_noncurrent_time, 2)
self.assertEqual(conditions.noncurrent_time_before, noncurrent_before)

Expand Down

0 comments on commit cfaf395

Please sign in to comment.