| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0500c76 commit 28487f5
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,12 +13,19 @@ | |||
| 13 | 13 | # limitations under the License. | |
| 14 | 14 | ||
| 15 | 15 | from google.cloud import _storage_v2 | |
| 16 | + from google.protobuf import timestamp_pb2 | ||
| 16 | 17 | ||
| 17 | 18 | # Map Python Blob attributes to GCS V2 Object proto field names. | |
| 18 | 19 | _BLOB_ATTR_TO_PROTO_FIELD = { | |
| 19 | 20 | "content_type": "content_type", | |
| 20 | 21 | "metadata": "metadata", | |
| 21 | 22 | "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", | ||
| 22 | 29 | } | |
| 23 | 30 | ||
| 24 | 31 | ||
@@ -37,4 +44,46 @@ def blob_to_proto(blob): | |||
| 37 | 44 | if value is not None: | |
| 38 | 45 | resource_params[proto_field] = value | |
| 39 | 46 | ||
| 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 | + | ||
| 40 | 89 | return _storage_v2.Object(**resource_params) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,6 @@ | |||
| 1 | 1 | # py standard imports | |
| 2 | 2 | import asyncio | |
| 3 | + import datetime | ||
| 3 | 4 | import gc | |
| 4 | 5 | import os | |
| 5 | 6 | import random | |
@@ -347,13 +348,23 @@ def test_write_from_blob( | |||
| 347 | 348 | object_name = f"test_from_blob-{str(uuid.uuid4())[:4]}" | |
| 348 | 349 | content_type = "text/plain" | |
| 349 | 350 | 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) | ||
| 350 | 356 | test_data = b"system-test-data" | |
| 351 | 357 | ||
| 352 | 358 | async def _run(): | |
| 353 | 359 | # 1. Create a Blob instance | |
| 354 | 360 | blob = storage_client.bucket(_ZONAL_BUCKET).blob(object_name) | |
| 355 | 361 | blob.content_type = content_type | |
| 356 | 362 | 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 | ||
| 357 | 368 | ||
| 358 | 369 | # 2. Use from_blob to create the writer | |
| 359 | 370 | writer = AsyncAppendableObjectWriter.from_blob(grpc_client, blob) | |
@@ -369,6 +380,11 @@ async def _run(): | |||
| 369 | 380 | ||
| 370 | 381 | assert obj.content_type == content_type | |
| 371 | 382 | 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()) | ||
| 372 | 388 | ||
| 373 | 389 | blobs_to_delete.append(blob) | |
| 374 | 390 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,7 @@ | |||
| 12 | 12 | # See the License for the specific language governing permissions and | |
| 13 | 13 | # limitations under the License. | |
| 14 | 14 | ||
| 15 | + import datetime | ||
| 15 | 16 | import unittest.mock as mock | |
| 16 | 17 | from unittest.mock import AsyncMock, MagicMock | |
| 17 | 18 | ||
@@ -169,7 +170,33 @@ async def test_open_new_object_with_blob_sync_attrs( | |||
| 169 | 170 | mock_blob.bucket = mock_bucket | |
| 170 | 171 | mock_blob.content_type = "text/plain" | |
| 171 | 172 | 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 | + } | ||
| 173 | 200 | ||
| 174 | 201 | stream = _AsyncWriteObjectStream(mock_client, BUCKET, OBJECT, blob=mock_blob) | |
| 175 | 202 | await stream.open() | |
@@ -180,6 +207,24 @@ async def test_open_new_object_with_blob_sync_attrs( | |||
| 180 | 207 | ||
| 181 | 208 | assert resource.content_type == "text/plain" | |
| 182 | 209 | 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 | + ) | ||
| 183 | 228 | ||
| 184 | 229 | @pytest.mark.asyncio | |
| 185 | 230 | async def test_open_already_open_raises(self, mock_client): | |
| Original file line number | Diff line number | Diff 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 | + ) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments