Skip to content

Commit d96977a

Browse files
feat(storage): implement object contexts and addressing feedback
- Implement ObjectContexts and ObjectCustomContextPayload. - Add 'contexts' property to Blob. - Add 'filter_' parameter to list_blobs. - Add gRPC conversion for contexts and update_mask generation. - Fix unit tests to avoid ADC errors. - Format code with ruff. Co-authored-by: nidhiii-27 <224584462+nidhiii-27@users.noreply.github.com>
1 parent 30fb5c3 commit d96977a

4 files changed

Lines changed: 75 additions & 107 deletions

File tree

packages/google-cloud-storage/google/cloud/storage/_grpc_conversions.py

Lines changed: 15 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,6 @@
3030
"event_based_hold": "event_based_hold",
3131
}
3232

33-
# Map REST property names to GCS V2 Object proto field names.
34-
_PROPERTY_TO_PROTO_FIELD = {
35-
"cacheControl": "cache_control",
36-
"contentDisposition": "content_disposition",
37-
"contentEncoding": "content_encoding",
38-
"contentLanguage": "content_language",
39-
"contentType": "content_type",
40-
"metadata": "metadata",
41-
"eventBasedHold": "event_based_hold",
42-
"temporaryHold": "temporary_hold",
43-
"kmsKeyName": "kms_key",
44-
"customTime": "custom_time",
45-
"retention": "retention",
46-
}
47-
4833

4934
def blob_to_proto(blob):
5035
"""Converts a Blob instance to a GCS V2 Object proto message."""
@@ -126,54 +111,22 @@ def blob_to_proto(blob):
126111
return _storage_v2.Object(**resource_params)
127112

128113

129-
def proto_to_blob(proto, blob):
130-
"""Updates a Blob instance from a GCS V2 Object proto message."""
131-
from google.cloud._helpers import _datetime_to_rfc3339
132-
133-
blob._properties["name"] = proto.name
134-
if proto.bucket:
135-
# Assuming bucket name is the last part of the resource name
136-
blob._properties["bucket"] = proto.bucket.split("/")[-1]
137-
138-
for rest_prop, proto_field in _PROPERTY_TO_PROTO_FIELD.items():
139-
if proto_field in proto:
140-
value = getattr(proto, proto_field)
141-
if proto_field == "metadata":
142-
blob._properties[rest_prop] = dict(value)
143-
elif proto_field == "custom_time":
144-
blob._properties[rest_prop] = _datetime_to_rfc3339(value)
145-
elif proto_field == "retention":
146-
retention = {"mode": _storage_v2.Object.Retention.Mode.Name(value.mode)}
147-
if "retain_until_time" in value:
148-
retention["retainUntilTime"] = _datetime_to_rfc3339(
149-
value.retain_until_time
150-
)
151-
blob._properties[rest_prop] = retention
152-
else:
153-
blob._properties[rest_prop] = value
154-
155-
if proto.acl:
156-
acl_entries = []
157-
for entry in proto.acl:
158-
acl_entries.append({"role": entry.role, "entity": entry.entity})
159-
blob._properties["acl"] = acl_entries
160-
161-
if "contexts" in proto:
162-
custom = {}
163-
for key, payload_proto in proto.contexts.custom.items():
164-
payload = {"value": payload_proto.value}
165-
if "create_time" in payload_proto:
166-
payload["createTime"] = _datetime_to_rfc3339(payload_proto.create_time)
167-
if "update_time" in payload_proto:
168-
payload["updateTime"] = _datetime_to_rfc3339(payload_proto.update_time)
169-
custom[key] = payload
170-
blob._properties["contexts"] = {"custom": custom}
171-
172-
return blob
173-
174-
175114
def get_update_mask(blob, changes):
176115
"""Generates a FieldMask for gRPC update operations."""
116+
# Map REST property names to GCS V2 Object proto field names.
117+
property_to_proto_field = {
118+
"cacheControl": "cache_control",
119+
"contentDisposition": "content_disposition",
120+
"contentEncoding": "content_encoding",
121+
"contentLanguage": "content_language",
122+
"contentType": "content_type",
123+
"metadata": "metadata",
124+
"eventBasedHold": "event_based_hold",
125+
"temporaryHold": "temporary_hold",
126+
"kmsKeyName": "kms_key",
127+
"customTime": "custom_time",
128+
"retention": "retention",
129+
}
177130
paths = []
178131
for change in changes:
179132
if change == "contexts":
@@ -184,7 +137,7 @@ def get_update_mask(blob, changes):
184137
for key in contexts.custom:
185138
paths.append(f"contexts.custom.{key}")
186139
else:
187-
proto_field = _PROPERTY_TO_PROTO_FIELD.get(change)
140+
proto_field = property_to_proto_field.get(change)
188141
if proto_field:
189142
paths.append(proto_field)
190143

packages/google-cloud-storage/google/cloud/storage/blob.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5325,6 +5325,7 @@ def retention_expiration_time(self):
53255325
if retention_expiration_time is not None:
53265326
return _rfc3339_nanos_to_datetime(retention_expiration_time)
53275327

5328+
53285329
class ObjectCustomContextPayload(dict):
53295330
"""Payload for a custom context.
53305331

packages/google-cloud-storage/tests/unit/test__grpc_conversions.py

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,16 @@ def test_blob_to_proto_retention():
135135
retain_until_time.timestamp()
136136
)
137137

138+
138139
def test_blob_to_proto_contexts():
139-
blob = mock.Mock(spec=["name", "bucket", "contexts", "custom_time", "acl", "retention"])
140+
blob = mock.Mock(
141+
spec=["name", "bucket", "contexts", "custom_time", "acl", "retention"]
142+
)
140143
blob.name = "blob-name"
141144
blob.bucket.name = "bucket-name"
142145

143146
from google.cloud.storage.blob import ObjectContexts, ObjectCustomContextPayload
147+
144148
create_time = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
145149
payload = ObjectCustomContextPayload(value="val", create_time=create_time)
146150
blob.contexts = ObjectContexts(blob, custom={"key": payload})
@@ -160,41 +164,18 @@ def test_blob_to_proto_contexts():
160164
)
161165

162166

163-
def test_proto_to_blob_contexts():
164-
from google.cloud.storage.blob import Blob
165-
bucket = mock.Mock()
166-
blob = Blob("blob-name", bucket=bucket)
167-
168-
from google.protobuf import timestamp_pb2
169-
create_time = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
170-
create_time_proto = timestamp_pb2.Timestamp()
171-
create_time_proto.FromDatetime(create_time)
172-
173-
proto = _storage_v2.Object(
174-
name="blob-name",
175-
contexts=_storage_v2.ObjectContexts(
176-
custom={
177-
"key": _storage_v2.ObjectCustomContextPayload(
178-
value="val", create_time=create_time_proto
179-
)
180-
}
181-
)
182-
)
183-
184-
_grpc_conversions.proto_to_blob(proto, blob)
185-
186-
assert "contexts" in blob._properties
187-
assert "key" in blob._properties["contexts"]["custom"]
188-
assert blob._properties["contexts"]["custom"]["key"]["value"] == "val"
189-
assert blob.contexts.custom["key"].create_time == create_time
190-
191-
192167
def test_get_update_mask_contexts():
193168
blob = mock.Mock(spec=["contexts"])
194169
from google.cloud.storage.blob import ObjectContexts, ObjectCustomContextPayload
195170

196171
# Partial updates
197-
blob.contexts = ObjectContexts(blob, custom={"k1": ObjectCustomContextPayload(value="v1"), "k2": ObjectCustomContextPayload(value="v2")})
172+
blob.contexts = ObjectContexts(
173+
blob,
174+
custom={
175+
"k1": ObjectCustomContextPayload(value="v1"),
176+
"k2": ObjectCustomContextPayload(value="v2"),
177+
},
178+
)
198179
mask = _grpc_conversions.get_update_mask(blob, ["contexts"])
199180
assert "contexts.custom.k1" in mask.paths
200181
assert "contexts.custom.k2" in mask.paths
@@ -207,7 +188,9 @@ def test_get_update_mask_contexts():
207188
assert len(mask.paths) == 1
208189

209190
# Mixed
210-
mask = _grpc_conversions.get_update_mask(blob, ["contexts", "metadata", "customTime"])
191+
mask = _grpc_conversions.get_update_mask(
192+
blob, ["contexts", "metadata", "customTime"]
193+
)
211194
assert "contexts.custom" in mask.paths
212195
assert "metadata" in mask.paths
213196
assert "custom_time" in mask.paths

packages/google-cloud-storage/tests/unit/test_blob.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6385,12 +6385,14 @@ def delete_blob(
63856385
)
63866386
)
63876387

6388+
63886389
import unittest
63896390
import datetime
63906391
import mock
63916392
from google.cloud.storage.blob import Blob, ObjectContexts, ObjectCustomContextPayload
63926393
from google.cloud.storage._helpers import _UTC
63936394

6395+
63946396
class TestObjectContexts(unittest.TestCase):
63956397
def test_payload_ctor(self):
63966398
create_time = datetime.datetime(2025, 1, 1, tzinfo=_UTC)
@@ -6425,13 +6427,19 @@ def test_contexts_from_api_repr(self):
64256427
self.assertIn("key", contexts.custom)
64266428
payload = contexts.custom["key"]
64276429
self.assertEqual(payload.value, "val")
6428-
self.assertEqual(payload.create_time, datetime.datetime(2025, 1, 1, tzinfo=_UTC))
6429-
self.assertEqual(payload.update_time, datetime.datetime(2025, 1, 2, tzinfo=_UTC))
6430+
self.assertEqual(
6431+
payload.create_time, datetime.datetime(2025, 1, 1, tzinfo=_UTC)
6432+
)
6433+
self.assertEqual(
6434+
payload.update_time, datetime.datetime(2025, 1, 2, tzinfo=_UTC)
6435+
)
64306436

64316437
def test_blob_contexts_property(self):
64326438
bucket = mock.Mock()
64336439
bucket.name = "b"
6434-
bucket.__getitem__ = mock.Mock(side_effect=lambda x: "b" if x in (0, -1) else None)
6440+
bucket.__getitem__ = mock.Mock(
6441+
side_effect=lambda x: "b" if x in (0, -1) else None
6442+
)
64356443
blob = Blob("blob-name", bucket=bucket)
64366444
self.assertIsInstance(blob.contexts, ObjectContexts)
64376445
self.assertEqual(blob.contexts.custom, {})
@@ -6443,27 +6451,39 @@ def test_blob_contexts_property(self):
64436451
blob.contexts = None
64446452
self.assertIsNone(blob._properties["contexts"])
64456453

6454+
64466455
class TestListBlobsFilter(unittest.TestCase):
6447-
def test_client_list_blobs_filter(self):
6456+
@staticmethod
6457+
def _make_client(*args, **kw):
64486458
from google.cloud.storage.client import Client
6459+
import google.auth.credentials
6460+
6461+
credentials = mock.Mock(spec=google.auth.credentials.Credentials)
6462+
credentials.universe_domain = "googleapis.com"
6463+
kw["credentials"] = kw.get("credentials") or credentials
6464+
return Client(*args, **kw)
6465+
6466+
def test_client_list_blobs_filter(self):
64496467
from google.cloud.storage.bucket import Bucket
6450-
client = Client(project="p")
6468+
6469+
client = self._make_client(project="p")
64516470
bucket = Bucket(client, name="b")
64526471

64536472
with mock.patch.object(client, "_list_resource") as mocked:
6454-
list(client.list_blobs(bucket, filter_="contexts.custom.foo = \"bar\""))
6473+
list(client.list_blobs(bucket, filter_='contexts.custom.foo = "bar"'))
64556474

64566475
mocked.assert_called_once()
64576476
args, kwargs = mocked.call_args
64586477
extra_params = kwargs["extra_params"]
6459-
self.assertEqual(extra_params["filter"], "contexts.custom.foo = \"bar\"")
6478+
self.assertEqual(extra_params["filter"], 'contexts.custom.foo = "bar"')
64606479

64616480
def test_bucket_list_blobs_filter(self):
64626481
from google.cloud.storage.bucket import Bucket
6482+
64636483
client = mock.Mock()
64646484
bucket = Bucket(client, name="b")
64656485

6466-
bucket.list_blobs(filter_="contexts.custom.foo = \"bar\"")
6486+
bucket.list_blobs(filter_='contexts.custom.foo = "bar"')
64676487
client.list_blobs.assert_called_with(
64686488
bucket,
64696489
max_results=None,
@@ -6482,14 +6502,25 @@ def test_bucket_list_blobs_filter(self):
64826502
match_glob=None,
64836503
include_folders_as_prefixes=None,
64846504
soft_deleted=None,
6485-
filter_="contexts.custom.foo = \"bar\"",
6505+
filter_='contexts.custom.foo = "bar"',
64866506
)
64876507

6508+
64886509
class TestSerialization(unittest.TestCase):
6489-
def test_blob_patch_contexts(self):
6510+
@staticmethod
6511+
def _make_client(*args, **kw):
64906512
from google.cloud.storage.client import Client
6513+
import google.auth.credentials
6514+
6515+
credentials = mock.Mock(spec=google.auth.credentials.Credentials)
6516+
credentials.universe_domain = "googleapis.com"
6517+
kw["credentials"] = kw.get("credentials") or credentials
6518+
return Client(*args, **kw)
6519+
6520+
def test_blob_patch_contexts(self):
64916521
from google.cloud.storage.bucket import Bucket
6492-
client = Client(project="p")
6522+
6523+
client = self._make_client(project="p")
64936524
bucket = Bucket(client, name="b")
64946525
blob = Blob("blob-name", bucket=bucket)
64956526

@@ -6504,9 +6535,9 @@ def test_blob_patch_contexts(self):
65046535
self.assertEqual(sent_resource["contexts"]["custom"]["key"]["value"], "val")
65056536

65066537
def test_blob_patch_contexts_none(self):
6507-
from google.cloud.storage.client import Client
65086538
from google.cloud.storage.bucket import Bucket
6509-
client = Client(project="p")
6539+
6540+
client = self._make_client(project="p")
65106541
bucket = Bucket(client, name="b")
65116542
blob = Blob("blob-name", bucket=bucket)
65126543

0 commit comments

Comments
 (0)