From 124c0f70b4c870f7b8154efea3cda2b868d8f016 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 21 Jan 2026 04:53:30 +0000 Subject: [PATCH 1/3] feat(async_grpc_client): add client_info support to AsyncGrpcClient for user agent customization --- .../asyncio/async_grpc_client.py | 15 ++++- tests/unit/asyncio/test_async_grpc_client.py | 63 ++++++++++++++++--- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py index a5cccca59..81e53e92b 100644 --- a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py +++ b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py @@ -15,6 +15,9 @@ """An async client for interacting with Google Cloud Storage using the gRPC API.""" from google.cloud import _storage_v2 as storage_v2 +from google.cloud._storage_v2.services.storage.transports.base import ( + DEFAULT_CLIENT_INFO, +) class AsyncGrpcClient: @@ -39,7 +42,8 @@ class AsyncGrpcClient: (Optional) Whether to attempt to use DirectPath for gRPC connections. Defaults to ``True``. """ - + # current scenario: Whatever client_info the users send , you're sending then to end user. + # ie. None. def __init__( self, credentials=None, @@ -65,8 +69,15 @@ def _create_async_grpc_client( transport_cls = storage_v2.StorageAsyncClient.get_transport_class( "grpc_asyncio" ) + + if client_info is None: + client_info = DEFAULT_CLIENT_INFO + primary_user_agent = client_info.to_user_agent() + channel = transport_cls.create_channel( - attempt_direct_path=attempt_direct_path, credentials=credentials + attempt_direct_path=attempt_direct_path, + credentials=credentials, + options=(("grpc.primary_user_agent", primary_user_agent),), ) transport = transport_cls(channel=channel) diff --git a/tests/unit/asyncio/test_async_grpc_client.py b/tests/unit/asyncio/test_async_grpc_client.py index eb06ab938..d2f2decb5 100644 --- a/tests/unit/asyncio/test_async_grpc_client.py +++ b/tests/unit/asyncio/test_async_grpc_client.py @@ -16,6 +16,11 @@ from unittest import mock from google.auth import credentials as auth_credentials from google.auth.credentials import AnonymousCredentials +from google.api_core import client_info as client_info_lib +from google.cloud.storage._experimental.asyncio import async_grpc_client +from google.cloud.storage._experimental.asyncio.async_grpc_client import ( + DEFAULT_CLIENT_INFO, +) def _make_credentials(spec=None): @@ -27,7 +32,6 @@ def _make_credentials(spec=None): 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.asyncio import async_grpc_client mock_transport_cls = mock.MagicMock() mock_async_storage_client.get_transport_class.return_value = mock_transport_cls @@ -35,11 +39,16 @@ def test_constructor_default_options(self, mock_async_storage_client): async_grpc_client.AsyncGrpcClient(credentials=mock_creds) + primary_user_agent = DEFAULT_CLIENT_INFO.to_user_agent() + expected_options = (("grpc.primary_user_agent", primary_user_agent),) + 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, credentials=mock_creds + attempt_direct_path=True, + credentials=mock_creds, + options=expected_options, ) mock_channel = mock_transport_cls.create_channel.return_value mock_transport_cls.assert_called_once_with(channel=mock_channel) @@ -47,12 +56,34 @@ def test_constructor_default_options(self, mock_async_storage_client): mock_async_storage_client.assert_called_once_with( transport=mock_transport, client_options=None, - client_info=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") + def test_constructor_with_client_info(self, mock_async_storage_client): + + mock_transport_cls = mock.MagicMock() + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_creds = _make_credentials() + client_info = client_info_lib.ClientInfo( + client_library_version="1.2.3", + ) + + async_grpc_client.AsyncGrpcClient( + credentials=mock_creds, client_info=client_info + ) + + primary_user_agent = client_info.to_user_agent() + expected_options = (("grpc.primary_user_agent", primary_user_agent),) + + mock_transport_cls.create_channel.assert_called_once_with( + attempt_direct_path=True, + credentials=mock_creds, + options=expected_options, ) @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_constructor_disables_directpath(self, mock_async_storage_client): - from google.cloud.storage._experimental.asyncio import async_grpc_client mock_transport_cls = mock.MagicMock() mock_async_storage_client.get_transport_class.return_value = mock_transport_cls @@ -62,15 +93,19 @@ def test_constructor_disables_directpath(self, mock_async_storage_client): credentials=mock_creds, attempt_direct_path=False ) + primary_user_agent = DEFAULT_CLIENT_INFO.to_user_agent() + expected_options = (("grpc.primary_user_agent", primary_user_agent),) + mock_transport_cls.create_channel.assert_called_once_with( - attempt_direct_path=False, credentials=mock_creds + attempt_direct_path=False, + credentials=mock_creds, + options=expected_options, ) mock_channel = mock_transport_cls.create_channel.return_value mock_transport_cls.assert_called_once_with(channel=mock_channel) @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_grpc_client_property(self, mock_grpc_gapic_client): - from google.cloud.storage._experimental.asyncio import async_grpc_client # Arrange mock_transport_cls = mock.MagicMock() @@ -81,7 +116,8 @@ def test_grpc_client_property(self, mock_grpc_gapic_client): mock_transport_cls.return_value = mock.sentinel.transport mock_creds = _make_credentials() - mock_client_info = mock.sentinel.client_info + mock_client_info = mock.MagicMock(spec=client_info_lib.ClientInfo) + mock_client_info.to_user_agent.return_value = "test-user-agent" mock_client_options = mock.sentinel.client_options mock_attempt_direct_path = mock.sentinel.attempt_direct_path @@ -102,8 +138,11 @@ def test_grpc_client_property(self, mock_grpc_gapic_client): retrieved_client = client.grpc_client # Assert + expected_options = (("grpc.primary_user_agent", "test-user-agent"),) mock_transport_cls.create_channel.assert_called_once_with( - attempt_direct_path=mock_attempt_direct_path, credentials=mock_creds + attempt_direct_path=mock_attempt_direct_path, + credentials=mock_creds, + options=expected_options, ) mock_transport_cls.assere_with(channel=channel_sentinel) mock_grpc_gapic_client.assert_called_once_with( @@ -115,7 +154,6 @@ def test_grpc_client_property(self, mock_grpc_gapic_client): @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_grpc_client_with_anon_creds(self, mock_grpc_gapic_client): - from google.cloud.storage._experimental.asyncio import async_grpc_client # Arrange mock_transport_cls = mock.MagicMock() @@ -133,7 +171,12 @@ def test_grpc_client_with_anon_creds(self, mock_grpc_gapic_client): # Assert self.assertIs(retrieved_client, mock_grpc_gapic_client.return_value) + primary_user_agent = DEFAULT_CLIENT_INFO.to_user_agent() + expected_options = (("grpc.primary_user_agent", primary_user_agent),) + mock_transport_cls.create_channel.assert_called_once_with( - attempt_direct_path=True, credentials=anonymous_creds + attempt_direct_path=True, + credentials=anonymous_creds, + options=expected_options, ) mock_transport_cls.assert_called_once_with(channel=channel_sentinel) From 4b216f575ff014263930d6575eda8d96cc3f870f Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 21 Jan 2026 05:39:30 +0000 Subject: [PATCH 2/3] fix(async_grpc_client): update type hint for client_options parameter in AsyncGrpcClient --- .../storage/_experimental/asyncio/async_grpc_client.py | 2 +- tests/unit/asyncio/test_async_grpc_client.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py index 81e53e92b..89c82f97e 100644 --- a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py +++ b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py @@ -33,7 +33,7 @@ class AsyncGrpcClient: 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` + :type client_options: :class:`~google.api_core.client_options.ClientOptions` :param client_options: (Optional) Client options used to set user options on the client. diff --git a/tests/unit/asyncio/test_async_grpc_client.py b/tests/unit/asyncio/test_async_grpc_client.py index d2f2decb5..02b416f6b 100644 --- a/tests/unit/asyncio/test_async_grpc_client.py +++ b/tests/unit/asyncio/test_async_grpc_client.py @@ -32,16 +32,18 @@ def _make_credentials(spec=None): class TestAsyncGrpcClient(unittest.TestCase): @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_constructor_default_options(self, mock_async_storage_client): - + # Arrange mock_transport_cls = mock.MagicMock() mock_async_storage_client.get_transport_class.return_value = mock_transport_cls mock_creds = _make_credentials() - async_grpc_client.AsyncGrpcClient(credentials=mock_creds) - primary_user_agent = DEFAULT_CLIENT_INFO.to_user_agent() expected_options = (("grpc.primary_user_agent", primary_user_agent),) + # Act + async_grpc_client.AsyncGrpcClient(credentials=mock_creds) + + # Assert mock_async_storage_client.get_transport_class.assert_called_once_with( "grpc_asyncio" ) From 9a97174c162d15605a600d01221ac4c7064d4694 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 21 Jan 2026 06:14:26 +0000 Subject: [PATCH 3/3] remove outdated comment --- google/cloud/storage/_experimental/asyncio/async_grpc_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py index 89c82f97e..b455b1c29 100644 --- a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py +++ b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py @@ -42,8 +42,6 @@ class AsyncGrpcClient: (Optional) Whether to attempt to use DirectPath for gRPC connections. Defaults to ``True``. """ - # current scenario: Whatever client_info the users send , you're sending then to end user. - # ie. None. def __init__( self, credentials=None,