diff --git a/packages/google-cloud-storage/google/cloud/storage/blob.py b/packages/google-cloud-storage/google/cloud/storage/blob.py index 7ef60e0195ab..bc5e5ad9c1f6 100644 --- a/packages/google-cloud-storage/google/cloud/storage/blob.py +++ b/packages/google-cloud-storage/google/cloud/storage/blob.py @@ -5368,20 +5368,10 @@ class ObjectCustomContextPayload(dict): :type value: str or ``NoneType`` :param value: (Optional) The value of the custom context. - - :type create_time: :class:`datetime.datetime` or ``NoneType`` - :param create_time: (Optional) Creation time of the custom context. - - :type update_time: :class:`datetime.datetime` or ``NoneType`` - :param update_time: (Optional) Last update time of the custom context. """ - def __init__(self, value=None, create_time=None, update_time=None): + def __init__(self, value=None): data = {"value": value} - if create_time is not None: - data["createTime"] = _datetime_to_rfc3339(create_time) - if update_time is not None: - data["updateTime"] = _datetime_to_rfc3339(update_time) super(ObjectCustomContextPayload, self).__init__(data) self._contexts = None @@ -5426,6 +5416,8 @@ def update_time(self): class ObjectContexts(dict): """Container for an object's contexts. + See: https://docs.cloud.google.com/storage/docs/object-contexts + :type blob: :class:`Blob` :param blob: blob for which these contexts apply to. @@ -5445,12 +5437,10 @@ def __init__(self, blob, custom=None): raise ValueError( "All values in custom must be ObjectCustomContextPayload instances" ) + payload._contexts = self data["custom"] = custom super(ObjectContexts, self).__init__(data) self._blob = blob - if custom is not None: - for payload in custom.values(): - payload._contexts = self @classmethod def from_api_repr(cls, resource, blob): @@ -5497,9 +5487,14 @@ def custom(self): @custom.setter def custom(self, value): + if value is None: + value = {} if not isinstance(value, dict): raise ValueError( "custom must be a dictionary mapping keys to ObjectCustomContextPayload instances" ) + for payload in value.values(): + if isinstance(payload, ObjectCustomContextPayload): + payload._contexts = self self["custom"] = value self.blob._patch_property("contexts", self) diff --git a/packages/google-cloud-storage/google/cloud/storage/bucket.py b/packages/google-cloud-storage/google/cloud/storage/bucket.py index 7ed8b375cd89..ba5b59c8cb26 100644 --- a/packages/google-cloud-storage/google/cloud/storage/bucket.py +++ b/packages/google-cloud-storage/google/cloud/storage/bucket.py @@ -1518,7 +1518,7 @@ def list_blobs( Note ``soft_deleted`` and ``versions`` cannot be set to True simultaneously. See: https://cloud.google.com/storage/docs/soft-delete - :type filter_: str + :type filter_: str or None :param filter_: (Optional) Filter string used to filter objects. See: https://docs.cloud.google.com/storage/docs/listing-objects#filter-by-object-contexts-syntax diff --git a/packages/google-cloud-storage/tests/system/test_blob.py b/packages/google-cloud-storage/tests/system/test_blob.py index d87faf3c9dda..8cea2add2d4a 100644 --- a/packages/google-cloud-storage/tests/system/test_blob.py +++ b/packages/google-cloud-storage/tests/system/test_blob.py @@ -1308,7 +1308,7 @@ def test_blob_contexts(shared_bucket, blobs_to_delete): blob.reload() assert blob.contexts.custom["k1"].value == "v1-updated" - assert "k2" not in blob.contexts.custom or blob.contexts.custom["k2"].value is None + assert "k2" not in blob.contexts.custom # 3. Clear all blob.contexts = None @@ -1344,3 +1344,4 @@ def test_blob_contexts_custom_setter(shared_bucket, blobs_to_delete): blob.reload() assert blob.contexts.custom["k1"].value == "v1-updated" + assert blob.contexts.custom["k2"].value == "v2" diff --git a/packages/google-cloud-storage/tests/system/test_bucket.py b/packages/google-cloud-storage/tests/system/test_bucket.py index efe38203136a..ae194fe46793 100644 --- a/packages/google-cloud-storage/tests/system/test_bucket.py +++ b/packages/google-cloud-storage/tests/system/test_bucket.py @@ -775,7 +775,7 @@ def test_bucket_list_blobs_w_filter( buckets_to_delete.append(bucket) payload = b"helloworld" - blob_names = ["foo", "bar", "baz"] + blob_names = ["foo", "bar", "baz", "qux"] for name in blob_names: blob = bucket.blob(name) blob.upload_from_string(payload) @@ -783,6 +783,10 @@ def test_bucket_list_blobs_w_filter( custom = {"target": ObjectCustomContextPayload(value="match")} blob.contexts = ObjectContexts(blob, custom=custom) blob.patch() + if name == "qux": + custom = {"target": ObjectCustomContextPayload(value="nomatch")} + blob.contexts = ObjectContexts(blob, custom=custom) + blob.patch() blobs_to_delete.append(blob) # List with filter matching only 'bar' @@ -790,6 +794,21 @@ def test_bucket_list_blobs_w_filter( blobs = list(blob_iter) assert [blob.name for blob in blobs] == ["bar"] + # List with filter matching bar and qux + blob_iter = bucket.list_blobs(filter_='contexts."target":*') + blobs = list(blob_iter) + assert sorted([blob.name for blob in blobs]) == ["bar", "qux"] + + # List with filter matching all except 'bar' + blob_iter = bucket.list_blobs(filter_='-contexts."target"="match"') + blobs = list(blob_iter) + assert sorted([blob.name for blob in blobs]) == ["baz", "foo", "qux"] + + # List with filter matching 'foo' and 'baz' (those without target context) + blob_iter = bucket.list_blobs(filter_='-contexts."target":*') + blobs = list(blob_iter) + assert sorted([blob.name for blob in blobs]) == ["baz", "foo"] + def test_bucket_list_blobs_include_managed_folders( storage_client, diff --git a/packages/google-cloud-storage/tests/unit/test__grpc_conversions.py b/packages/google-cloud-storage/tests/unit/test__grpc_conversions.py index b5029873eb74..2620ce3a5897 100644 --- a/packages/google-cloud-storage/tests/unit/test__grpc_conversions.py +++ b/packages/google-cloud-storage/tests/unit/test__grpc_conversions.py @@ -145,8 +145,7 @@ def test_blob_to_proto_contexts(): from google.cloud.storage.blob import ObjectContexts, ObjectCustomContextPayload - create_time = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc) - payload = ObjectCustomContextPayload(value="val", create_time=create_time) + payload = ObjectCustomContextPayload(value="val") blob.contexts = ObjectContexts(blob, custom={"key": payload}) blob.custom_time = None