Skip to content

feat(storage): support storage_class in _grpc_conversions - #18291

Open
chandra-siri wants to merge 3 commits into
googleapis:mainfrom
chandra-siri:feat/grpc-conversions-storage-class
Open

feat(storage): support storage_class in _grpc_conversions#18291
chandra-siri wants to merge 3 commits into
googleapis:mainfrom
chandra-siri:feat/grpc-conversions-storage-class

Conversation

@chandra-siri

Copy link
Copy Markdown
Contributor

Description

Adds support for mapping storage_class in _grpc_conversions.blob_to_proto(), enabling storage_class from Blob to properly propagate to the proto object during write stream initialization in _AsyncWriteObjectStream.open().

Note: Stacked on #18290 (feat/blob-storage-class).

Changes

  • Added "storage_class": "storage_class" to _BLOB_ATTR_TO_PROTO_FIELD in google/cloud/storage/_grpc_conversions.py.
  • Added fallback to self.storage_class in _AsyncWriteObjectStream.open() if self.blob does not have a storage_class specified.
  • Added unit tests in tests/unit/test__grpc_conversions.py verifying that blob_to_proto maps storage_class for mock and real Blob instances, as well as default handling.
  • Added unit tests in tests/unit/asyncio/test_async_write_object_stream.py verifying that Blob.storage_class is propagated to the initial BidiWriteObjectRequest.write_object_spec.resource.

Add support for specifying storage_class ('STANDARD' or 'RAPID') when writing
objects via AsyncAppendableObjectWriter.

- Add storage_class parameter to AsyncAppendableObjectWriter.__init__ and
  propagate it to _AsyncWriteObjectStream on open().
- Add storage_class parameter to _AsyncWriteObjectStream.__init__ and pass it
  to _storage_v2.Object creation.
- Validate storage_class to ensure only supported values ('STANDARD' or 'RAPID')
  are allowed.
- Add unit tests covering initialization, validation, and open stream requests.
@chandra-siri
chandra-siri requested a review from a team as a code owner September 4, 2026 15:49

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for specifying the storage_class parameter during asynchronous object writes and within the Blob class, updating gRPC conversions and adding corresponding unit tests. The feedback recommends removing restrictive client-side validation of storage classes (such as limiting them to 'STANDARD' and 'RAPID') to ensure forward compatibility with other GCS storage classes, along with removing the associated validation tests. Additionally, it is suggested to use the existing _scalar_property helper in Blob instead of writing boilerplate getter and setter methods for storage_class.

Comment on lines +52 to 53
_SUPPORTED_STORAGE_CLASSES = ("STANDARD", "RAPID")
logger = logging.getLogger(__name__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Client-side validation of storage classes is an anti-pattern in Google Cloud client libraries. GCS supports multiple storage classes (e.g., STANDARD, NEARLINE, COLDLINE, ARCHIVE, RAPID, etc.), and new ones may be introduced in the future. Hardcoding a restricted list of supported storage classes like ('STANDARD', 'RAPID') prevents users from using other valid storage classes (such as NEARLINE or COLDLINE) and breaks forward compatibility when new storage classes are added. The backend already performs robust validation, so we should let the backend handle it.

Suggested change
_SUPPORTED_STORAGE_CLASSES = ("STANDARD", "RAPID")
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)

Comment on lines +191 to +197
if (
storage_class is not None
and storage_class not in _SUPPORTED_STORAGE_CLASSES
):
raise ValueError(
f"storage_class must be either 'STANDARD' or 'RAPID', got '{storage_class}'"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Remove the client-side validation of storage_class to allow all valid GCS storage classes and ensure forward compatibility.

)
from google.cloud.storage.asyncio.async_grpc_client import AsyncGrpcClient

_SUPPORTED_STORAGE_CLASSES = ("STANDARD", "RAPID")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Remove the restricted _SUPPORTED_STORAGE_CLASSES constant to avoid client-side validation of storage classes.

Comment on lines +86 to +92
if (
storage_class is not None
and storage_class not in _SUPPORTED_STORAGE_CLASSES
):
raise ValueError(
f"storage_class must be either 'STANDARD' or 'RAPID', got '{storage_class}'"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Remove the client-side validation of storage_class to allow all valid GCS storage classes and ensure forward compatibility.

Comment on lines +4955 to +4990
@property
def storage_class(self):
"""Retrieve the storage class for the object.

This can only be set at blob / object **creation** time. If you'd
like to change the storage class **after** the blob / object already
exists in a bucket, call :meth:`update_storage_class` (which uses
:meth:`rewrite`).
Default value is None. If nothing specified, its value will be the
same as bucket's storage_class.

See https://cloud.google.com/storage/docs/storage-classes
This can only be set at blob / object **creation** time. If you'd
like to change the storage class **after** the blob / object already
exists in a bucket, call :meth:`update_storage_class` (which uses
:meth:`rewrite`).

:rtype: str or ``NoneType``
:returns:
If set, one of
:attr:`~google.cloud.storage.constants.STANDARD_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.NEARLINE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.COLDLINE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.ARCHIVE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.MULTI_REGIONAL_LEGACY_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.REGIONAL_LEGACY_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.DURABLE_REDUCED_AVAILABILITY_STORAGE_CLASS`,
else ``None``.
"""
See https://cloud.google.com/storage/docs/storage-classes

:rtype: str or ``NoneType``
:returns:
If set, one of
:attr:`~google.cloud.storage.constants.STANDARD_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.NEARLINE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.COLDLINE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.ARCHIVE_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.MULTI_REGIONAL_LEGACY_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.REGIONAL_LEGACY_STORAGE_CLASS`,
:attr:`~google.cloud.storage.constants.DURABLE_REDUCED_AVAILABILITY_STORAGE_CLASS`,
else ``None``.
"""
return self._properties.get("storageClass")

@storage_class.setter
def storage_class(self, value):
"""Set the storage class for the object.

:type value: str or ``NoneType``
:param value: new storage class name (None to clear any existing storage class).
"""
self._patch_property("storageClass", value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using _scalar_property is the established pattern in this file for scalar properties (like temporary_hold, event_based_hold, etc.). Replacing it with explicit getter/setter properties adds unnecessary boilerplate. We should keep _scalar_property("storageClass") and document the default value in its docstring instead.

    storage_class = _scalar_property("storageClass")
    """Retrieve the storage class for the object.

    Default value is None. If nothing specified, its value will be the
    same as bucket's storage_class.

    This can only be set at blob / object **creation** time. If you'd
    like to change the storage class **after** the blob / object already
    exists in a bucket, call :meth:`update_storage_class` (which uses
    :meth:`rewrite`).

    See https://cloud.google.com/storage/docs/storage-classes

    :rtype: str or ``NoneType``
    :returns:
        If set, one of
        :attr:`~google.cloud.storage.constants.STANDARD_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.NEARLINE_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.COLDLINE_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.ARCHIVE_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.MULTI_REGIONAL_LEGACY_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.REGIONAL_LEGACY_STORAGE_CLASS`,
        :attr:`~google.cloud.storage.constants.DURABLE_REDUCED_AVAILABILITY_STORAGE_CLASS`,
        else ``None``.
    """

Comment on lines +151 to +158
def test_init_with_invalid_storage_class_raises(self, mock_appendable_writer):
with pytest.raises(
ValueError, match="storage_class must be either 'STANDARD' or 'RAPID'"
):
self._make_one(
mock_appendable_writer["mock_client"],
storage_class="INVALID",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove the test verifying invalid storage class raises ValueError, as client-side validation has been removed.

Comment on lines +75 to +81
def test_init_with_invalid_storage_class_raises(self, mock_client):
with pytest.raises(
ValueError, match="storage_class must be either 'STANDARD' or 'RAPID'"
):
_AsyncWriteObjectStream(
mock_client, BUCKET, OBJECT, storage_class="INVALID"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove the test verifying invalid storage class raises ValueError, as client-side validation has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant