From 0f3e282e0f0fe0b92b4fadc8a05f8f02826f2e71 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 13:17:02 -0400 Subject: [PATCH 1/8] feat(secretmanager): integrate eager channel creation and interceptor application - Use _observability.create_channel_with_otel in SecretManagerServiceClient - Use grpc_helpers.apply_interceptors in SecretManagerServiceGrpcTransport - Add unit tests for channel injection and interceptor wiring --- .../services/secret_manager_service/client.py | 44 ++++++---- .../secret_manager_service/transports/grpc.py | 12 ++- .../google-cloud-secret-manager/noxfile.py | 4 +- packages/google-cloud-secret-manager/setup.py | 2 +- .../testing/constraints-3.10.txt | 2 +- .../test_secret_manager_service.py | 80 ++++++++++++++++++- 6 files changed, 121 insertions(+), 23 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index 1fce45758368..fd94a5d7dc4c 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -35,17 +35,14 @@ ) import google.protobuf +from google.api_core import _observability, gapic_v1 from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions -from google.api_core import gapic_v1 from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.auth.transport import mtls # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore -from google.oauth2 import service_account # type: ignore - -from google.cloud.secretmanager_v1 import gapic_version as package_version from google.cloud.secretmanager_v1._compat import ( get_api_endpoint, get_default_mtls_endpoint, @@ -75,7 +72,6 @@ import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore - from google.cloud.secretmanager_v1.services.secret_manager_service import pagers from google.cloud.secretmanager_v1.types import resources, service @@ -609,17 +605,33 @@ def __init__( else cast(Callable[..., SecretManagerServiceTransport], transport) ) # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + } + + if transport_init is SecretManagerServiceGrpcTransport: + if _observability.is_otel_capabilities_enabled(self._client_options): + transport_kwargs["channel"] = ( + _observability.create_channel_with_otel( + SecretManagerServiceGrpcTransport.create_channel, + client_options=self._client_options, + host=self._api_endpoint, + credentials=credentials, + credentials_file=self._client_options.credentials_file, + scopes=self._client_options.scopes, + quota_project_id=self._client_options.quota_project_id, + ) + ) + + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 51530553e705..6dcebc3da558 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -27,12 +27,12 @@ import grpc # type: ignore import proto # type: ignore from google.api_core import gapic_v1, grpc_helpers +from google.api_core.grpc_helpers import ClientInterceptor from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.protobuf.json_format import MessageToJson - from google.cloud.secretmanager_v1.types import resources, service +from google.protobuf.json_format import MessageToJson from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport @@ -148,6 +148,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[ClientInterceptor]] = None, ) -> None: """Instantiate the transport. @@ -198,6 +199,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[ClientInterceptor]]): + Additional interceptors to be injected into the gRPC channel pipeline. + These are executed in order. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -274,6 +278,10 @@ def __init__( ], ) + self._grpc_channel = grpc_helpers.apply_interceptors( + self._grpc_channel, interceptors + ) + self._interceptor = _LoggingClientInterceptor() self._logged_channel = grpc.intercept_channel( self._grpc_channel, self._interceptor diff --git a/packages/google-cloud-secret-manager/noxfile.py b/packages/google-cloud-secret-manager/noxfile.py index 875aa3a54969..2ce34b0e4d35 100644 --- a/packages/google-cloud-secret-manager/noxfile.py +++ b/packages/google-cloud-secret-manager/noxfile.py @@ -71,7 +71,9 @@ "pytest-asyncio", ] UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] -UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] +UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [ + "../google-api-core[tracing,testing]", +] UNIT_TEST_DEPENDENCIES: List[str] = [] UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} diff --git a/packages/google-cloud-secret-manager/setup.py b/packages/google-cloud-secret-manager/setup.py index 71d8d10242fd..551996a45c94 100644 --- a/packages/google-cloud-secret-manager/setup.py +++ b/packages/google-cloud-secret-manager/setup.py @@ -44,7 +44,7 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 2.28.0, <3.0.0", + "google-api-core[grpc] >= 2.35.0, <3.0.0", # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", diff --git a/packages/google-cloud-secret-manager/testing/constraints-3.10.txt b/packages/google-cloud-secret-manager/testing/constraints-3.10.txt index 9c33fedfb264..9e261ce48fe5 100644 --- a/packages/google-cloud-secret-manager/testing/constraints-3.10.txt +++ b/packages/google-cloud-secret-manager/testing/constraints-3.10.txt @@ -4,7 +4,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 +google-api-core==2.35.0 google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 01d17df2d37e..d9c68a4501b7 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -61,8 +61,6 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 -from google.oauth2 import service_account - from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -70,6 +68,7 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service +from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", @@ -476,6 +475,83 @@ def test_secret_manager_service_client_client_options( ) +def test_secret_manager_service_client_otel_channel_injection_enabled(): + mock_wrapped_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", + return_value=True, + ) as mock_is_enabled, + mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel", + return_value=mock_wrapped_channel, + ) as mock_create_channel_with_otel, + mock.patch.object( + transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = SecretManagerServiceClient(transport="grpc") + + mock_is_enabled.assert_called_once() + mock_create_channel_with_otel.assert_called_once_with( + transports.SecretManagerServiceGrpcTransport.create_channel, + client_options=client._client_options, + host=client._api_endpoint, + credentials=None, + credentials_file=None, + scopes=None, + quota_project_id=None, + ) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("channel") is mock_wrapped_channel + + +def test_secret_manager_service_client_otel_channel_injection_disabled(): + with ( + mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", + return_value=False, + ) as mock_is_enabled, + mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel", + ) as mock_create_channel_with_otel, + mock.patch.object( + transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + SecretManagerServiceClient(transport="grpc") + + mock_is_enabled.assert_called_once() + mock_create_channel_with_otel.assert_not_called() + called_kwargs = patched_transport_init.call_args.kwargs + assert "channel" not in called_kwargs + + +def test_secret_manager_service_grpc_transport_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.SecretManagerServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch( + "google.api_core.grpc_helpers.apply_interceptors", + return_value=mock_channel, + ) as mock_apply_interceptors, + ): + transport = transports.SecretManagerServiceGrpcTransport( + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + + @pytest.mark.parametrize( "client_class,transport_class,transport_name,use_client_cert_env", [ From 96c2a4ebaacccdfdf7ad97283f1e314f3a3271e3 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 13:56:53 -0400 Subject: [PATCH 2/8] docs(secretmanager): add descriptive docstrings to OTel and transport unit tests --- .../test_secret_manager_service.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index d9c68a4501b7..f937ff366d44 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -476,6 +476,15 @@ def test_secret_manager_service_client_client_options( def test_secret_manager_service_client_otel_channel_injection_enabled(): + """Proves that when OpenTelemetry tracing is enabled: + + 1. SecretManagerServiceClient detects the feature flag via + _observability.is_otel_capabilities_enabled. + 2. The client eagerly invokes _observability.create_channel_with_otel with + SecretManagerServiceGrpcTransport.create_channel and client configuration. + 3. The eagerly created and wrapped OTel channel is injected into the transport's + constructor kwargs under the 'channel' key. + """ mock_wrapped_channel = mock.Mock() with ( @@ -508,6 +517,13 @@ def test_secret_manager_service_client_otel_channel_injection_enabled(): def test_secret_manager_service_client_otel_channel_injection_disabled(): + """Proves that when OpenTelemetry tracing is disabled: + + 1. SecretManagerServiceClient checks the feature flag and finds it disabled. + 2. Eager channel creation via _observability.create_channel_with_otel is skipped. + 3. No 'channel' argument is passed to the transport constructor, preserving lazy + channel initialization in the transport. + """ with ( mock.patch( "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", @@ -529,6 +545,10 @@ def test_secret_manager_service_client_otel_channel_injection_disabled(): def test_secret_manager_service_grpc_transport_interceptors(): + """Proves that SecretManagerServiceGrpcTransport accepts custom client interceptors + and invokes grpc_helpers.apply_interceptors to inject them into the underlying + gRPC channel pipeline. + """ mock_interceptor = mock.Mock() mock_channel = mock.Mock() From 319fce37e600a7c78246afd1f457a3fdf0d96f79 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 28 Aug 2026 09:19:06 -0400 Subject: [PATCH 3/8] feat(secretmanager): pass lazy partial channel factory when OTel tracing enabled - Use functools.partial to bind create_channel_with_otel with Transport.create_channel and client_options - Eliminate manual extraction of host, credentials, scopes, and quota_project_id in client - Update unit tests to verify functools.partial factory binding and lazy transport kwargs --- .../services/secret_manager_service/client.py | 19 +++++------ .../test_secret_manager_service.py | 34 ++++++++----------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index fd94a5d7dc4c..ebd5ed17e90a 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import functools import json import logging as std_logging import os @@ -617,18 +618,16 @@ def __init__( "api_audience": self._client_options.api_audience, } + # When OpenTelemetry tracing is enabled, bind create_channel_with_otel + # using functools.partial and pass it as the channel factory. + # This preserves lazy channel instantiation inside the Transport and avoids + # duplicating channel initialization arguments here in the client. if transport_init is SecretManagerServiceGrpcTransport: if _observability.is_otel_capabilities_enabled(self._client_options): - transport_kwargs["channel"] = ( - _observability.create_channel_with_otel( - SecretManagerServiceGrpcTransport.create_channel, - client_options=self._client_options, - host=self._api_endpoint, - credentials=credentials, - credentials_file=self._client_options.credentials_file, - scopes=self._client_options.scopes, - quota_project_id=self._client_options.quota_project_id, - ) + transport_kwargs["channel"] = functools.partial( + _observability.create_channel_with_otel, + SecretManagerServiceGrpcTransport.create_channel, + client_options=self._client_options, ) self._transport = transport_init(**transport_kwargs) diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index f937ff366d44..24b5eb3de06f 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -14,6 +14,7 @@ # limitations under the License. # import asyncio +import functools import json import math import os @@ -480,22 +481,16 @@ def test_secret_manager_service_client_otel_channel_injection_enabled(): 1. SecretManagerServiceClient detects the feature flag via _observability.is_otel_capabilities_enabled. - 2. The client eagerly invokes _observability.create_channel_with_otel with - SecretManagerServiceGrpcTransport.create_channel and client configuration. - 3. The eagerly created and wrapped OTel channel is injected into the transport's - constructor kwargs under the 'channel' key. + 2. The client binds _observability.create_channel_with_otel using + functools.partial with SecretManagerServiceGrpcTransport.create_channel and client_options. + 3. The bound channel factory callable is passed into transport kwargs under 'channel', + allowing the Transport to initialize the channel lazily with its own parameters. """ - mock_wrapped_channel = mock.Mock() - with ( mock.patch( "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", return_value=True, ) as mock_is_enabled, - mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel", - return_value=mock_wrapped_channel, - ) as mock_create_channel_with_otel, mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, @@ -503,17 +498,18 @@ def test_secret_manager_service_client_otel_channel_injection_enabled(): client = SecretManagerServiceClient(transport="grpc") mock_is_enabled.assert_called_once() - mock_create_channel_with_otel.assert_called_once_with( + called_kwargs = patched_transport_init.call_args.kwargs + assert "channel" in called_kwargs + channel_factory = called_kwargs["channel"] + assert isinstance(channel_factory, functools.partial) + assert ( + channel_factory.func + is google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel + ) + assert channel_factory.args == ( transports.SecretManagerServiceGrpcTransport.create_channel, - client_options=client._client_options, - host=client._api_endpoint, - credentials=None, - credentials_file=None, - scopes=None, - quota_project_id=None, ) - called_kwargs = patched_transport_init.call_args.kwargs - assert called_kwargs.get("channel") is mock_wrapped_channel + assert channel_factory.keywords == {"client_options": client._client_options} def test_secret_manager_service_client_otel_channel_injection_disabled(): From 4c135929ca61b654a214c84ced252f8454104aec Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 31 Aug 2026 11:42:31 -0400 Subject: [PATCH 4/8] feat(secretmanager): adopt get_otel_channel_wrapper and apply_channel_wrappers --- .../services/secret_manager_service/client.py | 20 ++-- .../secret_manager_service/transports/grpc.py | 17 ++-- .../test_secret_manager_service.py | 96 ++++++++++--------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index ebd5ed17e90a..bc853a87ea1c 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import functools import json import logging as std_logging import os @@ -44,6 +43,7 @@ from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.auth.transport import mtls # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore +from google.cloud.secretmanager_v1 import gapic_version as package_version from google.cloud.secretmanager_v1._compat import ( get_api_endpoint, get_default_mtls_endpoint, @@ -73,6 +73,7 @@ import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore + from google.cloud.secretmanager_v1.services.secret_manager_service import pagers from google.cloud.secretmanager_v1.types import resources, service @@ -618,17 +619,14 @@ def __init__( "api_audience": self._client_options.api_audience, } - # When OpenTelemetry tracing is enabled, bind create_channel_with_otel - # using functools.partial and pass it as the channel factory. - # This preserves lazy channel instantiation inside the Transport and avoids - # duplicating channel initialization arguments here in the client. + # When OpenTelemetry tracing is enabled, obtain the channel wrapper + # and pass it to the transport. if transport_init is SecretManagerServiceGrpcTransport: - if _observability.is_otel_capabilities_enabled(self._client_options): - transport_kwargs["channel"] = functools.partial( - _observability.create_channel_with_otel, - SecretManagerServiceGrpcTransport.create_channel, - client_options=self._client_options, - ) + otel_wrapper = _observability.get_otel_channel_wrapper( + self._client_options + ) + if otel_wrapper is not None: + transport_kwargs["wrappers"] = [otel_wrapper] self._transport = transport_init(**transport_kwargs) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 6dcebc3da558..3a2653346469 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -27,13 +27,14 @@ import grpc # type: ignore import proto # type: ignore from google.api_core import gapic_v1, grpc_helpers -from google.api_core.grpc_helpers import ClientInterceptor +from google.api_core.grpc_helpers import ChannelWrapper from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.cloud.secretmanager_v1.types import resources, service from google.protobuf.json_format import MessageToJson +from google.cloud.secretmanager_v1.types import resources, service + from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport try: @@ -148,7 +149,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, - interceptors: Optional[Sequence[ClientInterceptor]] = None, + wrappers: Optional[Sequence[ChannelWrapper]] = None, ) -> None: """Instantiate the transport. @@ -199,9 +200,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. - interceptors (Optional[Sequence[ClientInterceptor]]): - Additional interceptors to be injected into the gRPC channel pipeline. - These are executed in order. + wrappers (Optional[Sequence[ChannelWrapper]]): + Additional channel wrappers (interceptors or callables) to apply to the + gRPC channel. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -278,8 +279,8 @@ def __init__( ], ) - self._grpc_channel = grpc_helpers.apply_interceptors( - self._grpc_channel, interceptors + self._grpc_channel = grpc_helpers.apply_channel_wrappers( + self._grpc_channel, wrappers ) self._interceptor = _LoggingClientInterceptor() diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 24b5eb3de06f..a1f7516ac593 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -62,6 +62,8 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 +from google.oauth2 import service_account + from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -69,7 +71,6 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service -from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", @@ -479,73 +480,57 @@ def test_secret_manager_service_client_client_options( def test_secret_manager_service_client_otel_channel_injection_enabled(): """Proves that when OpenTelemetry tracing is enabled: - 1. SecretManagerServiceClient detects the feature flag via - _observability.is_otel_capabilities_enabled. - 2. The client binds _observability.create_channel_with_otel using - functools.partial with SecretManagerServiceGrpcTransport.create_channel and client_options. - 3. The bound channel factory callable is passed into transport kwargs under 'channel', - allowing the Transport to initialize the channel lazily with its own parameters. + 1. SecretManagerServiceClient obtains the channel wrapper via + _observability.get_otel_channel_wrapper passing client_options. + 2. The wrapper is passed into transport kwargs under 'wrappers', + allowing the Transport to apply it via apply_channel_wrappers. """ + mock_wrapper = mock.Mock() with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", - return_value=True, - ) as mock_is_enabled, + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_channel_wrapper", + return_value=mock_wrapper, + ) as mock_get_wrapper, mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): client = SecretManagerServiceClient(transport="grpc") - mock_is_enabled.assert_called_once() + mock_get_wrapper.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs - assert "channel" in called_kwargs - channel_factory = called_kwargs["channel"] - assert isinstance(channel_factory, functools.partial) - assert ( - channel_factory.func - is google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel - ) - assert channel_factory.args == ( - transports.SecretManagerServiceGrpcTransport.create_channel, - ) - assert channel_factory.keywords == {"client_options": client._client_options} + assert "wrappers" in called_kwargs + assert called_kwargs["wrappers"] == [mock_wrapper] def test_secret_manager_service_client_otel_channel_injection_disabled(): """Proves that when OpenTelemetry tracing is disabled: - 1. SecretManagerServiceClient checks the feature flag and finds it disabled. - 2. Eager channel creation via _observability.create_channel_with_otel is skipped. - 3. No 'channel' argument is passed to the transport constructor, preserving lazy - channel initialization in the transport. + 1. SecretManagerServiceClient checks for an OTel wrapper and receives None. + 2. No 'wrappers' argument is passed to the transport constructor. """ with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled", - return_value=False, - ) as mock_is_enabled, - mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.create_channel_with_otel", - ) as mock_create_channel_with_otel, + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_channel_wrapper", + return_value=None, + ) as mock_get_wrapper, mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): - SecretManagerServiceClient(transport="grpc") + client = SecretManagerServiceClient(transport="grpc") - mock_is_enabled.assert_called_once() - mock_create_channel_with_otel.assert_not_called() + mock_get_wrapper.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs - assert "channel" not in called_kwargs + assert "wrappers" not in called_kwargs -def test_secret_manager_service_grpc_transport_interceptors(): - """Proves that SecretManagerServiceGrpcTransport accepts custom client interceptors - and invokes grpc_helpers.apply_interceptors to inject them into the underlying - gRPC channel pipeline. +def test_secret_manager_service_grpc_transport_wrappers(): + """Proves that SecretManagerServiceGrpcTransport accepts channel wrappers + and invokes grpc_helpers.apply_channel_wrappers to apply them to the underlying + gRPC channel. """ - mock_interceptor = mock.Mock() + mock_wrapper = mock.Mock() mock_channel = mock.Mock() with ( @@ -555,18 +540,35 @@ def test_secret_manager_service_grpc_transport_interceptors(): return_value=mock_channel, ), mock.patch( - "google.api_core.grpc_helpers.apply_interceptors", + "google.api_core.grpc_helpers.apply_channel_wrappers", return_value=mock_channel, - ) as mock_apply_interceptors, + ) as mock_apply_wrappers, ): - transport = transports.SecretManagerServiceGrpcTransport( - interceptors=[mock_interceptor], + transports.SecretManagerServiceGrpcTransport( + wrappers=[mock_wrapper], ) - mock_apply_interceptors.assert_called_once_with( - mock_channel, [mock_interceptor] + mock_apply_wrappers.assert_called_once_with(mock_channel, [mock_wrapper]) + + +def test_secret_manager_service_grpc_transport_custom_channel_wrappers(): + """Proves that SecretManagerServiceGrpcTransport wraps explicitly passed custom channels + using grpc_helpers.apply_channel_wrappers. + """ + mock_wrapper = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch( + "google.api_core.grpc_helpers.apply_channel_wrappers", + return_value=mock_custom_channel, + ) as mock_apply_wrappers: + transports.SecretManagerServiceGrpcTransport( + channel=mock_custom_channel, + wrappers=[mock_wrapper], ) + mock_apply_wrappers.assert_called_once_with(mock_custom_channel, [mock_wrapper]) + @pytest.mark.parametrize( "client_class,transport_class,transport_name,use_client_cert_env", From f0a6c7640439eac521da04685a10e5f7897a8e2f Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 1 Sep 2026 13:18:21 -0400 Subject: [PATCH 5/8] feat(secretmanager): rename channel wrapper terminology to interceptor --- .../services/secret_manager_service/client.py | 9 ++- .../secret_manager_service/transports/grpc.py | 22 +++--- .../test_secret_manager_service.py | 71 ++++++++++--------- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index bc853a87ea1c..565fb1dc14f9 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -73,7 +73,6 @@ import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore - from google.cloud.secretmanager_v1.services.secret_manager_service import pagers from google.cloud.secretmanager_v1.types import resources, service @@ -619,14 +618,14 @@ def __init__( "api_audience": self._client_options.api_audience, } - # When OpenTelemetry tracing is enabled, obtain the channel wrapper + # When OpenTelemetry tracing is enabled, obtain the channel interceptor # and pass it to the transport. if transport_init is SecretManagerServiceGrpcTransport: - otel_wrapper = _observability.get_otel_channel_wrapper( + otel_interceptor = _observability.get_otel_interceptor( self._client_options ) - if otel_wrapper is not None: - transport_kwargs["wrappers"] = [otel_wrapper] + if otel_interceptor is not None: + transport_kwargs["interceptors"] = [otel_interceptor] self._transport = transport_init(**transport_kwargs) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 3a2653346469..47c93bc38070 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -27,13 +27,12 @@ import grpc # type: ignore import proto # type: ignore from google.api_core import gapic_v1, grpc_helpers -from google.api_core.grpc_helpers import ChannelWrapper +from google.api_core.grpc_helpers import ClientInterceptor from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.protobuf.json_format import MessageToJson - from google.cloud.secretmanager_v1.types import resources, service +from google.protobuf.json_format import MessageToJson from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport @@ -149,7 +148,14 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, - wrappers: Optional[Sequence[ChannelWrapper]] = None, + interceptors: Optional[ + Sequence[ + Union[ + ClientInterceptor, + Callable[[grpc.Channel], grpc.Channel], + ] + ] + ] = None, ) -> None: """Instantiate the transport. @@ -200,8 +206,8 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. - wrappers (Optional[Sequence[ChannelWrapper]]): - Additional channel wrappers (interceptors or callables) to apply to the + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the gRPC channel. Raises: @@ -279,8 +285,8 @@ def __init__( ], ) - self._grpc_channel = grpc_helpers.apply_channel_wrappers( - self._grpc_channel, wrappers + self._grpc_channel = grpc_helpers.apply_channel_interceptors( + self._grpc_channel, interceptors ) self._interceptor = _LoggingClientInterceptor() diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index a1f7516ac593..ac5b3142452a 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -62,8 +62,6 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 -from google.oauth2 import service_account - from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -71,6 +69,7 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service +from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", @@ -480,57 +479,57 @@ def test_secret_manager_service_client_client_options( def test_secret_manager_service_client_otel_channel_injection_enabled(): """Proves that when OpenTelemetry tracing is enabled: - 1. SecretManagerServiceClient obtains the channel wrapper via - _observability.get_otel_channel_wrapper passing client_options. - 2. The wrapper is passed into transport kwargs under 'wrappers', - allowing the Transport to apply it via apply_channel_wrappers. + 1. SecretManagerServiceClient obtains the channel interceptor via + _observability.get_otel_interceptor passing client_options. + 2. The interceptor is passed into transport kwargs under 'interceptors', + allowing the Transport to apply it via apply_channel_interceptors. """ - mock_wrapper = mock.Mock() + mock_interceptor = mock.Mock() with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_channel_wrapper", - return_value=mock_wrapper, - ) as mock_get_wrapper, + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor", + return_value=mock_interceptor, + ) as mock_get_interceptor, mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): client = SecretManagerServiceClient(transport="grpc") - mock_get_wrapper.assert_called_once_with(client._client_options) + mock_get_interceptor.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs - assert "wrappers" in called_kwargs - assert called_kwargs["wrappers"] == [mock_wrapper] + assert "interceptors" in called_kwargs + assert called_kwargs["interceptors"] == [mock_interceptor] def test_secret_manager_service_client_otel_channel_injection_disabled(): """Proves that when OpenTelemetry tracing is disabled: - 1. SecretManagerServiceClient checks for an OTel wrapper and receives None. - 2. No 'wrappers' argument is passed to the transport constructor. + 1. SecretManagerServiceClient checks for an OTel interceptor and receives None. + 2. No 'interceptors' argument is passed to the transport constructor. """ with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_channel_wrapper", + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor", return_value=None, - ) as mock_get_wrapper, + ) as mock_get_interceptor, mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): client = SecretManagerServiceClient(transport="grpc") - mock_get_wrapper.assert_called_once_with(client._client_options) + mock_get_interceptor.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs - assert "wrappers" not in called_kwargs + assert "interceptors" not in called_kwargs -def test_secret_manager_service_grpc_transport_wrappers(): - """Proves that SecretManagerServiceGrpcTransport accepts channel wrappers - and invokes grpc_helpers.apply_channel_wrappers to apply them to the underlying +def test_secret_manager_service_grpc_transport_interceptors(): + """Proves that SecretManagerServiceGrpcTransport accepts channel interceptors + and invokes grpc_helpers.apply_channel_interceptors to apply them to the underlying gRPC channel. """ - mock_wrapper = mock.Mock() + mock_interceptor = mock.Mock() mock_channel = mock.Mock() with ( @@ -540,34 +539,38 @@ def test_secret_manager_service_grpc_transport_wrappers(): return_value=mock_channel, ), mock.patch( - "google.api_core.grpc_helpers.apply_channel_wrappers", + "google.api_core.grpc_helpers.apply_channel_interceptors", return_value=mock_channel, - ) as mock_apply_wrappers, + ) as mock_apply_interceptors, ): transports.SecretManagerServiceGrpcTransport( - wrappers=[mock_wrapper], + interceptors=[mock_interceptor], ) - mock_apply_wrappers.assert_called_once_with(mock_channel, [mock_wrapper]) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) -def test_secret_manager_service_grpc_transport_custom_channel_wrappers(): +def test_secret_manager_service_grpc_transport_custom_channel_interceptors(): """Proves that SecretManagerServiceGrpcTransport wraps explicitly passed custom channels - using grpc_helpers.apply_channel_wrappers. + using grpc_helpers.apply_channel_interceptors. """ - mock_wrapper = mock.Mock() + mock_interceptor = mock.Mock() mock_custom_channel = mock.Mock(spec=grpc.Channel) with mock.patch( - "google.api_core.grpc_helpers.apply_channel_wrappers", + "google.api_core.grpc_helpers.apply_channel_interceptors", return_value=mock_custom_channel, - ) as mock_apply_wrappers: + ) as mock_apply_interceptors: transports.SecretManagerServiceGrpcTransport( channel=mock_custom_channel, - wrappers=[mock_wrapper], + interceptors=[mock_interceptor], ) - mock_apply_wrappers.assert_called_once_with(mock_custom_channel, [mock_wrapper]) + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) @pytest.mark.parametrize( From e73fdcb14dca618bd71c7a8225f53a62de99e14f Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 2 Sep 2026 07:43:31 -0400 Subject: [PATCH 6/8] style(secretmanager): sort imports and restore service_account --- .../services/secret_manager_service/client.py | 4 +++- .../services/secret_manager_service/transports/grpc.py | 3 ++- .../gapic/secretmanager_v1/test_secret_manager_service.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index 565fb1dc14f9..81f4f4cbb0f4 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -42,7 +42,8 @@ from google.auth import credentials as ga_credentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.auth.transport import mtls # type: ignore -from google.auth.transport.grpc import SslCredentials # type: ignore +from google.oauth2 import service_account # type: ignore + from google.cloud.secretmanager_v1 import gapic_version as package_version from google.cloud.secretmanager_v1._compat import ( get_api_endpoint, @@ -73,6 +74,7 @@ import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore + from google.cloud.secretmanager_v1.services.secret_manager_service import pagers from google.cloud.secretmanager_v1.types import resources, service diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 47c93bc38070..1902ebbaf594 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -31,9 +31,10 @@ from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.cloud.secretmanager_v1.types import resources, service from google.protobuf.json_format import MessageToJson +from google.cloud.secretmanager_v1.types import resources, service + from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport try: diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index ac5b3142452a..91c728277bbf 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -62,6 +62,8 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 +from google.oauth2 import service_account + from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -69,7 +71,6 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service -from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", From 0e1c21bc2a8a3eadafafc677f9fdc284883a398a Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 3 Sep 2026 06:12:55 -0400 Subject: [PATCH 7/8] feat(secretmanager): guard _observability import and update interceptor assertions --- .../services/secret_manager_service/client.py | 18 ++++++++++++--- .../secret_manager_service/transports/grpc.py | 13 +++++++---- .../google-cloud-secret-manager/noxfile.py | 4 +--- packages/google-cloud-secret-manager/setup.py | 2 +- .../testing/constraints-3.10.txt | 2 +- .../test_secret_manager_service.py | 23 +++++++++++++++++-- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index 81f4f4cbb0f4..627c933d38ca 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -35,15 +35,21 @@ ) import google.protobuf -from google.api_core import _observability, gapic_v1 from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.auth.transport import mtls # type: ignore from google.oauth2 import service_account # type: ignore +try: + # mypy: _observability was added in google-api-core 2.35.0; guard for older versions + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: + _observability = None # type: ignore[assignment] + from google.cloud.secretmanager_v1 import gapic_version as package_version from google.cloud.secretmanager_v1._compat import ( get_api_endpoint, @@ -622,12 +628,18 @@ def __init__( # When OpenTelemetry tracing is enabled, obtain the channel interceptor # and pass it to the transport. - if transport_init is SecretManagerServiceGrpcTransport: + if ( + transport_init is SecretManagerServiceGrpcTransport + and _observability is not None + ): otel_interceptor = _observability.get_otel_interceptor( self._client_options ) if otel_interceptor is not None: - transport_kwargs["interceptors"] = [otel_interceptor] + interceptors = transport_kwargs.get("interceptors", []) + transport_kwargs["interceptors"] = [otel_interceptor] + list( + interceptors + ) self._transport = transport_init(**transport_kwargs) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 1902ebbaf594..9ff4abdf5233 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -27,7 +27,12 @@ import grpc # type: ignore import proto # type: ignore from google.api_core import gapic_v1, grpc_helpers -from google.api_core.grpc_helpers import ClientInterceptor + +try: + # mypy: ClientInterceptor was added in google-api-core 2.35.0; guard for older versions + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] +except ImportError: + ClientInterceptor = grpc.ClientInterceptor # type: ignore[misc,assignment] from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore @@ -286,9 +291,9 @@ def __init__( ], ) - self._grpc_channel = grpc_helpers.apply_channel_interceptors( - self._grpc_channel, interceptors - ) + apply_interceptors = getattr(grpc_helpers, "apply_channel_interceptors", None) + if apply_interceptors is not None: + self._grpc_channel = apply_interceptors(self._grpc_channel, interceptors) self._interceptor = _LoggingClientInterceptor() self._logged_channel = grpc.intercept_channel( diff --git a/packages/google-cloud-secret-manager/noxfile.py b/packages/google-cloud-secret-manager/noxfile.py index 2ce34b0e4d35..875aa3a54969 100644 --- a/packages/google-cloud-secret-manager/noxfile.py +++ b/packages/google-cloud-secret-manager/noxfile.py @@ -71,9 +71,7 @@ "pytest-asyncio", ] UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] -UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [ - "../google-api-core[tracing,testing]", -] +UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] UNIT_TEST_DEPENDENCIES: List[str] = [] UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} diff --git a/packages/google-cloud-secret-manager/setup.py b/packages/google-cloud-secret-manager/setup.py index 551996a45c94..71d8d10242fd 100644 --- a/packages/google-cloud-secret-manager/setup.py +++ b/packages/google-cloud-secret-manager/setup.py @@ -44,7 +44,7 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 2.35.0, <3.0.0", + "google-api-core[grpc] >= 2.28.0, <3.0.0", # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", diff --git a/packages/google-cloud-secret-manager/testing/constraints-3.10.txt b/packages/google-cloud-secret-manager/testing/constraints-3.10.txt index 9e261ce48fe5..9c33fedfb264 100644 --- a/packages/google-cloud-secret-manager/testing/constraints-3.10.txt +++ b/packages/google-cloud-secret-manager/testing/constraints-3.10.txt @@ -4,7 +4,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.35.0 +google-api-core==2.28.0 google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 91c728277bbf..55102f956875 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -507,7 +507,7 @@ def test_secret_manager_service_client_otel_channel_injection_disabled(): """Proves that when OpenTelemetry tracing is disabled: 1. SecretManagerServiceClient checks for an OTel interceptor and receives None. - 2. No 'interceptors' argument is passed to the transport constructor. + 2. No OTel interceptor is added to the transport constructor kwargs. """ with ( mock.patch( @@ -522,7 +522,26 @@ def test_secret_manager_service_client_otel_channel_injection_disabled(): mock_get_interceptor.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs - assert "interceptors" not in called_kwargs + interceptors = called_kwargs.get("interceptors", []) + assert not interceptors + + +def test_secret_manager_service_client_observability_import_error(): + """Proves that when _observability cannot be imported (older google-api-core), + SecretManagerServiceClient instantiates gracefully without error. + """ + with ( + mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability", + None, + ), + mock.patch.object( + transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + SecretManagerServiceClient(transport="grpc") + called_kwargs = patched_transport_init.call_args.kwargs + assert not called_kwargs.get("interceptors", []) def test_secret_manager_service_grpc_transport_interceptors(): From 8e5e0ade528d5b451f0af2c9bf67a0c7888e5cd1 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 3 Sep 2026 11:25:18 -0400 Subject: [PATCH 8/8] fix(secretmanager): resolve ClientInterceptor type definition and test mocks for older google-api-core --- .../secret_manager_service/transports/grpc.py | 19 +++++++------- .../test_secret_manager_service.py | 25 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 9ff4abdf5233..93bf1742b084 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -17,7 +17,7 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union import google.auth # type: ignore import google.iam.v1.iam_policy_pb2 as iam_policy_pb2 # type: ignore @@ -27,21 +27,22 @@ import grpc # type: ignore import proto # type: ignore from google.api_core import gapic_v1, grpc_helpers - -try: - # mypy: ClientInterceptor was added in google-api-core 2.35.0; guard for older versions - from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] -except ImportError: - ClientInterceptor = grpc.ClientInterceptor # type: ignore[misc,assignment] from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.protobuf.json_format import MessageToJson - from google.cloud.secretmanager_v1.types import resources, service +from google.protobuf.json_format import MessageToJson from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport +# ClientInterceptor type alias for channel interceptors +ClientInterceptor = Union[ + grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor, + grpc.StreamUnaryClientInterceptor, + grpc.StreamStreamClientInterceptor, +] + try: from google.api_core import client_logging # type: ignore diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 55102f956875..57eb3b22d788 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -62,8 +62,6 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 -from google.oauth2 import service_account - from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -71,6 +69,7 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service +from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", @@ -486,18 +485,20 @@ def test_secret_manager_service_client_otel_channel_injection_enabled(): allowing the Transport to apply it via apply_channel_interceptors. """ mock_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_interceptor with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor", - return_value=mock_interceptor, - ) as mock_get_interceptor, + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability", + mock_obs, + ), mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): client = SecretManagerServiceClient(transport="grpc") - mock_get_interceptor.assert_called_once_with(client._client_options) + mock_obs.get_otel_interceptor.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs assert "interceptors" in called_kwargs assert called_kwargs["interceptors"] == [mock_interceptor] @@ -509,18 +510,20 @@ def test_secret_manager_service_client_otel_channel_injection_disabled(): 1. SecretManagerServiceClient checks for an OTel interceptor and receives None. 2. No OTel interceptor is added to the transport constructor kwargs. """ + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = None with ( mock.patch( - "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor", - return_value=None, - ) as mock_get_interceptor, + "google.cloud.secretmanager_v1.services.secret_manager_service.client._observability", + mock_obs, + ), mock.patch.object( transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None ) as patched_transport_init, ): client = SecretManagerServiceClient(transport="grpc") - mock_get_interceptor.assert_called_once_with(client._client_options) + mock_obs.get_otel_interceptor.assert_called_once_with(client._client_options) called_kwargs = patched_transport_init.call_args.kwargs interceptors = called_kwargs.get("interceptors", []) assert not interceptors @@ -561,6 +564,7 @@ def test_secret_manager_service_grpc_transport_interceptors(): mock.patch( "google.api_core.grpc_helpers.apply_channel_interceptors", return_value=mock_channel, + create=True, ) as mock_apply_interceptors, ): transports.SecretManagerServiceGrpcTransport( @@ -582,6 +586,7 @@ def test_secret_manager_service_grpc_transport_custom_channel_interceptors(): with mock.patch( "google.api_core.grpc_helpers.apply_channel_interceptors", return_value=mock_custom_channel, + create=True, ) as mock_apply_interceptors: transports.SecretManagerServiceGrpcTransport( channel=mock_custom_channel,