Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 28487f5

Browse filesBrowse files
feat: add fixed-key metadata support in AAOW (#16817)
Add fixed-key metadata support in AAOW by updating `blob_to_proto` conversion logic. - Updated `_grpc_conversions.py` with simple and complex field mappings. - Added unit tests in `tests/unit/test__grpc_conversions.py`. - Updated system tests in `tests/system/test_zonal.py`. - Fixed regression in `tests/unit/asyncio/test_async_write_object_stream.py`. --- *PR created automatically by Jules for task [11384837182247010380](https://jules.google.com/task/11384837182247010380) started by @nidhiii-27* --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: nidhiii-27 <224584462+nidhiii-27@users.noreply.github.com>
1 parent 0500c76 commit 28487f5
Copy full SHA for 28487f5

4 files changed

+247-1Lines changed: 247 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: packages/google-cloud-storage/google/cloud/storage/_grpc_conversions.py
+49Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@
1313
# limitations under the License.
1414

1515
from google.cloud import _storage_v2
16+
from google.protobuf import timestamp_pb2
1617

1718
# Map Python Blob attributes to GCS V2 Object proto field names.
1819
_BLOB_ATTR_TO_PROTO_FIELD = {
1920
"content_type": "content_type",
2021
"metadata": "metadata",
2122
"kms_key_name": "kms_key",
23+
"cache_control": "cache_control",
24+
"content_disposition": "content_disposition",
25+
"content_encoding": "content_encoding",
26+
"content_language": "content_language",
27+
"temporary_hold": "temporary_hold",
28+
"event_based_hold": "event_based_hold",
2229
}
2330

2431

@@ -37,4 +44,46 @@ def blob_to_proto(blob):
3744
if value is not None:
3845
resource_params[proto_field] = value
3946

47+
custom_time = getattr(blob, "custom_time", None)
48+
if custom_time is not None:
49+
custom_time_proto = timestamp_pb2.Timestamp()
50+
custom_time_proto.FromDatetime(custom_time)
51+
resource_params["custom_time"] = custom_time_proto
52+
53+
acl = getattr(blob, "acl", None)
54+
if acl is not None and getattr(acl, "loaded", False):
55+
acl_entries = []
56+
for entry in acl:
57+
acl_entries.append(
58+
_storage_v2.ObjectAccessControl(
59+
role=entry["role"],
60+
entity=entry["entity"],
61+
)
62+
)
63+
if acl_entries:
64+
resource_params["acl"] = acl_entries
65+
66+
retention = getattr(blob, "retention", None)
67+
if retention:
68+
mode_str = retention.get("mode")
69+
mode = _storage_v2.Object.Retention.Mode.MODE_UNSPECIFIED
70+
if mode_str:
71+
# GCS retention modes are 'Locked' or 'Unlocked'
72+
mode = getattr(
73+
_storage_v2.Object.Retention.Mode,
74+
mode_str.upper(),
75+
_storage_v2.Object.Retention.Mode.MODE_UNSPECIFIED,
76+
)
77+
78+
retain_until_time_proto = None
79+
retain_until_time = retention.get("retain_until_time")
80+
if retain_until_time is not None:
81+
retain_until_time_proto = timestamp_pb2.Timestamp()
82+
retain_until_time_proto.FromDatetime(retain_until_time)
83+
84+
resource_params["retention"] = _storage_v2.Object.Retention(
85+
mode=mode,
86+
retain_until_time=retain_until_time_proto,
87+
)
88+
4089
return _storage_v2.Object(**resource_params)
Collapse file

‎packages/google-cloud-storage/tests/system/test_zonal.py‎

Copy file name to clipboardExpand all lines: packages/google-cloud-storage/tests/system/test_zonal.py
+16Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# py standard imports
22
import asyncio
3+
import datetime
34
import gc
45
import os
56
import random
@@ -347,13 +348,23 @@ def test_write_from_blob(
347348
object_name = f"test_from_blob-{str(uuid.uuid4())[:4]}"
348349
content_type = "text/plain"
349350
metadata = {"environment": "system-test"}
351+
cache_control = "public, max-age=3600"
352+
content_disposition = "attachment; filename=test.txt"
353+
content_encoding = "identity"
354+
content_language = "en"
355+
custom_time = datetime.datetime(2025, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
350356
test_data = b"system-test-data"
351357

352358
async def _run():
353359
# 1. Create a Blob instance
354360
blob = storage_client.bucket(_ZONAL_BUCKET).blob(object_name)
355361
blob.content_type = content_type
356362
blob.metadata = metadata
363+
blob.cache_control = cache_control
364+
blob.content_disposition = content_disposition
365+
blob.content_encoding = content_encoding
366+
blob.content_language = content_language
367+
blob.custom_time = custom_time
357368

358369
# 2. Use from_blob to create the writer
359370
writer = AsyncAppendableObjectWriter.from_blob(grpc_client, blob)
@@ -369,6 +380,11 @@ async def _run():
369380

370381
assert obj.content_type == content_type
371382
assert obj.metadata["environment"] == "system-test"
383+
assert obj.cache_control == cache_control
384+
assert obj.content_disposition == content_disposition
385+
assert obj.content_encoding == content_encoding
386+
assert obj.content_language == content_language
387+
assert int(obj.custom_time.timestamp()) == int(custom_time.timestamp())
372388

373389
blobs_to_delete.append(blob)
374390

Collapse file

‎packages/google-cloud-storage/tests/unit/asyncio/test_async_write_object_stream.py‎

Copy file name to clipboardExpand all lines: packages/google-cloud-storage/tests/unit/asyncio/test_async_write_object_stream.py
+46-1Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import unittest.mock as mock
1617
from unittest.mock import AsyncMock, MagicMock
1718

@@ -169,7 +170,33 @@ async def test_open_new_object_with_blob_sync_attrs(
169170
mock_blob.bucket = mock_bucket
170171
mock_blob.content_type = "text/plain"
171172
mock_blob.metadata = {"test-key": "test-value"}
172-
mock_blob.kms_key_name = None
173+
mock_blob.kms_key_name = "kms-key-name"
174+
mock_blob.cache_control = "cache-control"
175+
mock_blob.content_disposition = "content-disposition"
176+
mock_blob.content_encoding = "content-encoding"
177+
mock_blob.content_language = "content-language"
178+
mock_blob.temporary_hold = True
179+
mock_blob.event_based_hold = True
180+
181+
custom_time = datetime.datetime(
182+
2025, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
183+
)
184+
mock_blob.custom_time = custom_time
185+
186+
acl_mock = MagicMock()
187+
acl_mock.loaded = True
188+
acl_mock.__iter__.return_value = iter(
189+
[{"role": "READER", "entity": "allUsers"}]
190+
)
191+
mock_blob.acl = acl_mock
192+
193+
retain_until_time = datetime.datetime(
194+
2026, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
195+
)
196+
mock_blob.retention = {
197+
"mode": "Locked",
198+
"retain_until_time": retain_until_time,
199+
}
173200

174201
stream = _AsyncWriteObjectStream(mock_client, BUCKET, OBJECT, blob=mock_blob)
175202
await stream.open()
@@ -180,6 +207,24 @@ async def test_open_new_object_with_blob_sync_attrs(
180207

181208
assert resource.content_type == "text/plain"
182209
assert resource.metadata == {"test-key": "test-value"}
210+
assert resource.kms_key == "kms-key-name"
211+
assert resource.cache_control == "cache-control"
212+
assert resource.content_disposition == "content-disposition"
213+
assert resource.content_encoding == "content-encoding"
214+
assert resource.content_language == "content-language"
215+
assert resource.temporary_hold is True
216+
assert resource.event_based_hold is True
217+
218+
assert int(resource.custom_time.timestamp()) == int(custom_time.timestamp())
219+
220+
assert len(resource.acl) == 1
221+
assert resource.acl[0].role == "READER"
222+
assert resource.acl[0].entity == "allUsers"
223+
224+
assert resource.retention.mode == _storage_v2.Object.Retention.Mode.LOCKED
225+
assert int(resource.retention.retain_until_time.timestamp()) == int(
226+
retain_until_time.timestamp()
227+
)
183228

184229
@pytest.mark.asyncio
185230
async def test_open_already_open_raises(self, mock_client):
Collapse file
+136Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import datetime
16+
from unittest import mock
17+
18+
from google.cloud.storage import _grpc_conversions
19+
from google.cloud import _storage_v2
20+
21+
22+
def test_blob_to_proto_simple_fields():
23+
blob = mock.Mock(
24+
spec=[
25+
"name",
26+
"bucket",
27+
"content_type",
28+
"metadata",
29+
"kms_key_name",
30+
"cache_control",
31+
"content_disposition",
32+
"content_encoding",
33+
"content_language",
34+
"temporary_hold",
35+
"event_based_hold",
36+
"custom_time",
37+
"acl",
38+
"retention",
39+
]
40+
)
41+
blob.name = "blob-name"
42+
blob.bucket.name = "bucket-name"
43+
blob.content_type = "text/plain"
44+
blob.metadata = {"key": "value"}
45+
blob.kms_key_name = "kms-key"
46+
blob.cache_control = "no-cache"
47+
blob.content_disposition = "attachment"
48+
blob.content_encoding = "gzip"
49+
blob.content_language = "en"
50+
blob.temporary_hold = True
51+
blob.event_based_hold = False
52+
blob.custom_time = None
53+
blob.acl = None
54+
blob.retention = None
55+
56+
proto = _grpc_conversions.blob_to_proto(blob)
57+
58+
assert proto.name == "blob-name"
59+
assert proto.bucket == "projects/_/buckets/bucket-name"
60+
assert proto.content_type == "text/plain"
61+
assert proto.metadata == {"key": "value"}
62+
assert proto.kms_key == "kms-key"
63+
assert proto.cache_control == "no-cache"
64+
assert proto.content_disposition == "attachment"
65+
assert proto.content_encoding == "gzip"
66+
assert proto.content_language == "en"
67+
assert proto.temporary_hold is True
68+
assert proto.event_based_hold is False
69+
70+
71+
def test_blob_to_proto_custom_time():
72+
blob = mock.Mock(spec=["name", "bucket", "custom_time", "acl", "retention"])
73+
blob.name = "blob-name"
74+
blob.bucket.name = "bucket-name"
75+
blob.custom_time = datetime.datetime(
76+
2025, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
77+
)
78+
blob.acl = None
79+
blob.retention = None
80+
# ensure other fields don't cause issues if missing
81+
for attr in _grpc_conversions._BLOB_ATTR_TO_PROTO_FIELD:
82+
setattr(blob, attr, None)
83+
84+
proto = _grpc_conversions.blob_to_proto(blob)
85+
86+
assert int(proto.custom_time.timestamp()) == int(blob.custom_time.timestamp())
87+
88+
89+
def test_blob_to_proto_acl():
90+
blob = mock.Mock(spec=["name", "bucket", "acl", "custom_time", "retention"])
91+
blob.name = "blob-name"
92+
blob.bucket.name = "bucket-name"
93+
94+
acl_mock = mock.MagicMock()
95+
acl_mock.loaded = True
96+
acl_mock.__iter__.return_value = iter(
97+
[
98+
{"role": "READER", "entity": "allUsers"},
99+
{"role": "OWNER", "entity": "user-123"},
100+
]
101+
)
102+
blob.acl = acl_mock
103+
104+
blob.custom_time = None
105+
blob.retention = None
106+
for attr in _grpc_conversions._BLOB_ATTR_TO_PROTO_FIELD:
107+
setattr(blob, attr, None)
108+
109+
proto = _grpc_conversions.blob_to_proto(blob)
110+
111+
assert len(proto.acl) == 2
112+
assert proto.acl[0].role == "READER"
113+
assert proto.acl[0].entity == "allUsers"
114+
assert proto.acl[1].role == "OWNER"
115+
assert proto.acl[1].entity == "user-123"
116+
117+
118+
def test_blob_to_proto_retention():
119+
blob = mock.Mock(spec=["name", "bucket", "retention", "custom_time", "acl"])
120+
blob.name = "blob-name"
121+
blob.bucket.name = "bucket-name"
122+
123+
retain_until_time = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
124+
blob.retention = {"mode": "Locked", "retain_until_time": retain_until_time}
125+
126+
blob.custom_time = None
127+
blob.acl = None
128+
for attr in _grpc_conversions._BLOB_ATTR_TO_PROTO_FIELD:
129+
setattr(blob, attr, None)
130+
131+
proto = _grpc_conversions.blob_to_proto(blob)
132+
133+
assert proto.retention.mode == _storage_v2.Object.Retention.Mode.LOCKED
134+
assert int(proto.retention.retain_until_time.timestamp()) == int(
135+
retain_until_time.timestamp()
136+
)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.