From 6ca591f26cd8c1fb28cba5023de4c753c2b9fb07 Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Mon, 1 Sep 2025 07:36:20 +0000 Subject: [PATCH 1/7] feat(experimental): add async grpc client --- .../_experimental/async_grpc_client.py | 78 +++++++++++++++++++ tests/unit/test_async_grpc_client.py | 49 ++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 google/cloud/storage/_experimental/async_grpc_client.py create mode 100644 tests/unit/test_async_grpc_client.py diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py new file mode 100644 index 000000000..bf850209a --- /dev/null +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -0,0 +1,78 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An async client for interacting with Google Cloud Storage using the gRPC API.""" + +from google.cloud import storage_v2 +from google.api_core import client_options as client_options_lib + + +class AsyncGrpcClient: + """An asynchronous client for interacting with Google Cloud Storage using the gRPC API. + + :type credentials: :class:`~google.auth.credentials.Credentials` + :param credentials: (Optional) The OAuth2 Credentials to use for this + client. If not passed, falls back to the default + inferred from the environment. + + :type client_info: :class:`~google.api_core.client_info.ClientInfo` + :param client_info: + The client info used to send a user-agent string along with API + requests. If ``None``, then default info will be used. + + :type client_options: :class:`~google.api_core.client_options.ClientOptions` or :class:`dict` + :param client_options: (Optional) Client options used to set user options + on the client. + + :type attempt_direct_path: bool + :param attempt_direct_path: + (Optional) Whether to attempt to use DirectPath for gRPC connections. + Defaults to ``False``. + """ + + def __init__( + self, + credentials=None, + client_info=None, + client_options=None, + *, + attempt_direct_path=False, + ): + self._grpc_client = self._create_async_grpc_client( + credentials=credentials, + client_info=client_info, + client_options=client_options, + attempt_direct_path=attempt_direct_path, + ) + + def _create_async_grpc_client( + self, + credentials=None, + client_info=None, + client_options=None, + attempt_direct_path=False, + ): + transport_cls = storage_v2.StorageAsyncClient.get_transport_class("grpc_asyncio") + + transport = transport_cls( + credentials=credentials, + attempt_direct_path=attempt_direct_path, + ) + + return storage_v2.StorageAsyncClient( + credentials=credentials, + transport=transport, + client_info=client_info, + client_options=client_options, + ) diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py new file mode 100644 index 000000000..acedbcbd1 --- /dev/null +++ b/tests/unit/test_async_grpc_client.py @@ -0,0 +1,49 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock +from google.auth import credentials as auth_credentials + + +class TestAsyncGrpcClient(unittest.TestCase): + @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): + from google.cloud.storage._experimental import async_grpc_client + + mock_transport_cls = mock.MagicMock() + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_creds = mock.Mock(spec=auth_credentials.Credentials) + + async_grpc_client.AsyncGrpcClient(credentials=mock_creds) + + mock_transport_cls.assert_called_once_with( + credentials=mock_creds, attempt_direct_path=False + ) + + @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + def test_constructor_respects_directpath_true(self, mock_async_storage_client): + from google.cloud.storage._experimental import async_grpc_client + + mock_transport_cls = mock.MagicMock() + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_creds = mock.Mock(spec=auth_credentials.Credentials) + + async_grpc_client.AsyncGrpcClient( + credentials=mock_creds, attempt_direct_path=True + ) + + mock_transport_cls.assert_called_once_with( + credentials=mock_creds, attempt_direct_path=True + ) From 9dd091e6342c74644d02679306497a6d1ccd88a9 Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Mon, 1 Sep 2025 09:16:05 +0000 Subject: [PATCH 2/7] minor changes --- tests/unit/test_async_grpc_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index acedbcbd1..801bd24ee 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -18,7 +18,7 @@ class TestAsyncGrpcClient(unittest.TestCase): - @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + @mock.patch("google.cloud.storage_v2._experimental.StorageAsyncClient") def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client @@ -32,7 +32,7 @@ def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): credentials=mock_creds, attempt_direct_path=False ) - @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + @mock.patch("google.cloud.storage_v2._experimental.StorageAsyncClient") def test_constructor_respects_directpath_true(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client From 59baeb1ae3b115c10b00d9582800a9a87ba5c8df Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Mon, 1 Sep 2025 11:25:35 +0000 Subject: [PATCH 3/7] remove unused import --- google/cloud/storage/_experimental/async_grpc_client.py | 2 -- tests/unit/test_async_grpc_client.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py index bf850209a..9fb486b12 100644 --- a/google/cloud/storage/_experimental/async_grpc_client.py +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -15,8 +15,6 @@ """An async client for interacting with Google Cloud Storage using the gRPC API.""" from google.cloud import storage_v2 -from google.api_core import client_options as client_options_lib - class AsyncGrpcClient: """An asynchronous client for interacting with Google Cloud Storage using the gRPC API. diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index 801bd24ee..acedbcbd1 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -18,7 +18,7 @@ class TestAsyncGrpcClient(unittest.TestCase): - @mock.patch("google.cloud.storage_v2._experimental.StorageAsyncClient") + @mock.patch("google.cloud.storage_v2.StorageAsyncClient") def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client @@ -32,7 +32,7 @@ def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): credentials=mock_creds, attempt_direct_path=False ) - @mock.patch("google.cloud.storage_v2._experimental.StorageAsyncClient") + @mock.patch("google.cloud.storage_v2.StorageAsyncClient") def test_constructor_respects_directpath_true(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client From f18f8c2c87a9771e886c3d18054855d0223d9626 Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Tue, 2 Sep 2025 06:54:49 +0000 Subject: [PATCH 4/7] update the imports for _storage_v2 --- google/cloud/storage/_experimental/async_grpc_client.py | 2 +- tests/unit/test_async_grpc_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py index 9fb486b12..a5409be75 100644 --- a/google/cloud/storage/_experimental/async_grpc_client.py +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -14,7 +14,7 @@ """An async client for interacting with Google Cloud Storage using the gRPC API.""" -from google.cloud import storage_v2 +from google.cloud import _storage_v2 as storage_v2 class AsyncGrpcClient: """An asynchronous client for interacting with Google Cloud Storage using the gRPC API. diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index acedbcbd1..cfdef80dc 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -18,7 +18,7 @@ class TestAsyncGrpcClient(unittest.TestCase): - @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client @@ -32,7 +32,7 @@ def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): credentials=mock_creds, attempt_direct_path=False ) - @mock.patch("google.cloud.storage_v2.StorageAsyncClient") + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_constructor_respects_directpath_true(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client From df089bf8e1250c55f7a42248cba169e5ab2b9c33 Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Tue, 2 Sep 2025 09:49:11 +0000 Subject: [PATCH 5/7] change the default directpath value to True --- google/cloud/storage/_experimental/async_grpc_client.py | 6 +++--- tests/unit/test_async_grpc_client.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py index a5409be75..9aad4e1a8 100644 --- a/google/cloud/storage/_experimental/async_grpc_client.py +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -36,7 +36,7 @@ class AsyncGrpcClient: :type attempt_direct_path: bool :param attempt_direct_path: (Optional) Whether to attempt to use DirectPath for gRPC connections. - Defaults to ``False``. + Defaults to ``True``. """ def __init__( @@ -45,7 +45,7 @@ def __init__( client_info=None, client_options=None, *, - attempt_direct_path=False, + attempt_direct_path=True, ): self._grpc_client = self._create_async_grpc_client( credentials=credentials, @@ -59,7 +59,7 @@ def _create_async_grpc_client( credentials=None, client_info=None, client_options=None, - attempt_direct_path=False, + attempt_direct_path=True, ): transport_cls = storage_v2.StorageAsyncClient.get_transport_class("grpc_asyncio") diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index cfdef80dc..0cd6a06c9 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -29,11 +29,11 @@ def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): async_grpc_client.AsyncGrpcClient(credentials=mock_creds) mock_transport_cls.assert_called_once_with( - credentials=mock_creds, attempt_direct_path=False + credentials=mock_creds, attempt_direct_path=True ) @mock.patch("google.cloud._storage_v2.StorageAsyncClient") - def test_constructor_respects_directpath_true(self, mock_async_storage_client): + def test_constructor_when_directpath_is_false(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client mock_transport_cls = mock.MagicMock() @@ -41,9 +41,9 @@ def test_constructor_respects_directpath_true(self, mock_async_storage_client): mock_creds = mock.Mock(spec=auth_credentials.Credentials) async_grpc_client.AsyncGrpcClient( - credentials=mock_creds, attempt_direct_path=True + credentials=mock_creds, attempt_direct_path=False ) mock_transport_cls.assert_called_once_with( - credentials=mock_creds, attempt_direct_path=True + credentials=mock_creds, attempt_direct_path=False ) From f287783bf5d865b95c2e884357c002b49ae961d1 Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Wed, 3 Sep 2025 08:36:23 +0000 Subject: [PATCH 6/7] add more comprehensive unit tests --- .../_experimental/async_grpc_client.py | 7 ++-- tests/unit/test_async_grpc_client.py | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py index 9aad4e1a8..0e194d74c 100644 --- a/google/cloud/storage/_experimental/async_grpc_client.py +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -62,11 +62,8 @@ def _create_async_grpc_client( attempt_direct_path=True, ): transport_cls = storage_v2.StorageAsyncClient.get_transport_class("grpc_asyncio") - - transport = transport_cls( - credentials=credentials, - attempt_direct_path=attempt_direct_path, - ) + channel = transport_cls.create_channel(attempt_direct_path=attempt_direct_path) + transport = transport_cls(credentials=credentials, channel=channel) return storage_v2.StorageAsyncClient( credentials=credentials, diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index 0cd6a06c9..1382a0bc7 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -19,31 +19,53 @@ class TestAsyncGrpcClient(unittest.TestCase): @mock.patch("google.cloud._storage_v2.StorageAsyncClient") - def test_constructor_defaults_to_cloudpath(self, mock_async_storage_client): + def test_constructor_default_options(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client mock_transport_cls = mock.MagicMock() - mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_async_storage_client.get_transport_class.return_value = ( + mock_transport_cls + ) mock_creds = mock.Mock(spec=auth_credentials.Credentials) async_grpc_client.AsyncGrpcClient(credentials=mock_creds) + mock_async_storage_client.get_transport_class.assert_called_once_with( + "grpc_asyncio" + ) + mock_transport_cls.create_channel.assert_called_once_with( + attempt_direct_path=True + ) + mock_channel = mock_transport_cls.create_channel.return_value mock_transport_cls.assert_called_once_with( - credentials=mock_creds, attempt_direct_path=True + credentials=mock_creds, channel=mock_channel + ) + mock_transport = mock_transport_cls.return_value + mock_async_storage_client.assert_called_once_with( + credentials=mock_creds, + transport=mock_transport, + client_options=None, + client_info=None, ) @mock.patch("google.cloud._storage_v2.StorageAsyncClient") - def test_constructor_when_directpath_is_false(self, mock_async_storage_client): + def test_constructor_disables_directpath(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client mock_transport_cls = mock.MagicMock() - mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_async_storage_client.get_transport_class.return_value = ( + mock_transport_cls + ) mock_creds = mock.Mock(spec=auth_credentials.Credentials) async_grpc_client.AsyncGrpcClient( credentials=mock_creds, attempt_direct_path=False ) + mock_transport_cls.create_channel.assert_called_once_with( + attempt_direct_path=False + ) + mock_channel = mock_transport_cls.create_channel.return_value mock_transport_cls.assert_called_once_with( - credentials=mock_creds, attempt_direct_path=False + credentials=mock_creds, channel=mock_channel ) From b48362375247167b1ec34e11b0278eb10b3a2e1d Mon Sep 17 00:00:00 2001 From: Pulkit Aggarwal Date: Thu, 11 Sep 2025 05:53:23 +0000 Subject: [PATCH 7/7] adding helper methods --- .../_experimental/async_grpc_client.py | 18 ++++++++++- tests/unit/test_async_grpc_client.py | 30 ++++++++++++++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/google/cloud/storage/_experimental/async_grpc_client.py b/google/cloud/storage/_experimental/async_grpc_client.py index 0e194d74c..e6546908d 100644 --- a/google/cloud/storage/_experimental/async_grpc_client.py +++ b/google/cloud/storage/_experimental/async_grpc_client.py @@ -16,6 +16,7 @@ from google.cloud import _storage_v2 as storage_v2 + class AsyncGrpcClient: """An asynchronous client for interacting with Google Cloud Storage using the gRPC API. @@ -61,7 +62,9 @@ def _create_async_grpc_client( client_options=None, attempt_direct_path=True, ): - transport_cls = storage_v2.StorageAsyncClient.get_transport_class("grpc_asyncio") + transport_cls = storage_v2.StorageAsyncClient.get_transport_class( + "grpc_asyncio" + ) channel = transport_cls.create_channel(attempt_direct_path=attempt_direct_path) transport = transport_cls(credentials=credentials, channel=channel) @@ -71,3 +74,16 @@ def _create_async_grpc_client( client_info=client_info, client_options=client_options, ) + + @property + def grpc_client(self): + """The underlying gRPC client. + + This property gives users direct access to the `_storage_v2.StorageAsyncClient` + instance. This can be useful for accessing + newly added or experimental RPCs that are not yet exposed through + the high-level GrpcClient. + Returns: + google.cloud._storage_v2.StorageAsyncClient: The configured GAPIC client. + """ + return self._grpc_client diff --git a/tests/unit/test_async_grpc_client.py b/tests/unit/test_async_grpc_client.py index 1382a0bc7..322772f8d 100644 --- a/tests/unit/test_async_grpc_client.py +++ b/tests/unit/test_async_grpc_client.py @@ -17,16 +17,20 @@ from google.auth import credentials as auth_credentials +def _make_credentials(spec=None): + if spec is None: + return mock.Mock(spec=auth_credentials.Credentials) + return mock.Mock(spec=spec) + + class TestAsyncGrpcClient(unittest.TestCase): @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_constructor_default_options(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client mock_transport_cls = mock.MagicMock() - mock_async_storage_client.get_transport_class.return_value = ( - mock_transport_cls - ) - mock_creds = mock.Mock(spec=auth_credentials.Credentials) + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_creds = _make_credentials() async_grpc_client.AsyncGrpcClient(credentials=mock_creds) @@ -53,10 +57,8 @@ def test_constructor_disables_directpath(self, mock_async_storage_client): from google.cloud.storage._experimental import async_grpc_client mock_transport_cls = mock.MagicMock() - mock_async_storage_client.get_transport_class.return_value = ( - mock_transport_cls - ) - mock_creds = mock.Mock(spec=auth_credentials.Credentials) + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_creds = _make_credentials() async_grpc_client.AsyncGrpcClient( credentials=mock_creds, attempt_direct_path=False @@ -69,3 +71,15 @@ def test_constructor_disables_directpath(self, mock_async_storage_client): mock_transport_cls.assert_called_once_with( credentials=mock_creds, channel=mock_channel ) + + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") + def test_grpc_client_property(self, mock_async_storage_client): + from google.cloud.storage._experimental import async_grpc_client + + mock_creds = _make_credentials() + + client = async_grpc_client.AsyncGrpcClient(credentials=mock_creds) + + retrieved_client = client.grpc_client + + self.assertIs(retrieved_client, mock_async_storage_client.return_value)