From f1bfc71fe4c34bf378ca2b3f8c626d9bbb545456 Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Fri, 30 Oct 2026 16:22:20 +0000 Subject: [PATCH 1/2] fix(bigtable): avoid DuplicateCredentialArgs in BigtableMetricsExporter When credentials and client_options with credentials_file or api_key are both passed to BigtableMetricsExporter, strip credentials_file and api_key from a shallow copy of client_options before initializing MetricServiceClient. --- .../data/_metrics/handlers/gcp_exporter.py | 21 ++++++++++ .../_metrics/test_gcp_exporter_handler.py | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py index e2e7f2b3871b..fcb53d61b9d9 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py @@ -14,6 +14,7 @@ from __future__ import annotations +import copy import logging import time from collections import defaultdict @@ -167,6 +168,26 @@ class BigtableMetricsExporter(MetricExporter): def __init__(self, *client_args, **client_kwargs): super().__init__() + if client_kwargs.get("credentials") is not None: + client_options = client_kwargs.get("client_options") + if isinstance(client_options, dict): + if client_options.get("credentials_file") or client_options.get( + "api_key" + ): + client_options = { + k: v + for k, v in client_options.items() + if k not in ("credentials_file", "api_key") + } + client_kwargs["client_options"] = client_options + elif client_options is not None and ( + getattr(client_options, "credentials_file", None) is not None + or getattr(client_options, "api_key", None) is not None + ): + client_options = copy.copy(client_options) + client_options.credentials_file = None + client_options.api_key = None + client_kwargs["client_options"] = client_options self.client = MetricServiceClient(*client_args, **client_kwargs) self.prefix = "bigtable.googleapis.com/internal/client" diff --git a/packages/google-cloud-bigtable/tests/unit/data/_metrics/test_gcp_exporter_handler.py b/packages/google-cloud-bigtable/tests/unit/data/_metrics/test_gcp_exporter_handler.py index 6aa608ab3314..b82e21084ad2 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/_metrics/test_gcp_exporter_handler.py +++ b/packages/google-cloud-bigtable/tests/unit/data/_metrics/test_gcp_exporter_handler.py @@ -176,6 +176,47 @@ def test_ctor_mocks(self): assert instance.prefix == "bigtable.googleapis.com/internal/client" mock_client.assert_called_once_with(*args, **kwargs) + def test_ctor_with_credentials_and_credentials_file(self): + """Passing both resolved credentials and a ClientOptions with + credentials_file (or api_key) should not raise DuplicateCredentialArgs + or mutate the caller's ClientOptions.""" + import google.auth.credentials + from google.api_core.client_options import ClientOptions + + class _FakeScopedCreds( + google.auth.credentials.Scoped, google.auth.credentials.Credentials + ): + @property + def requires_scopes(self): + return False + + def with_scopes(self, scopes, default_scopes=None): + return self + + def refresh(self, request): + pass + + creds = _FakeScopedCreds() + opts = ClientOptions( + credentials_file="/fake/sa_key.json", + api_endpoint="monitoring.googleapis.com", + ) + exporter = BigtableMetricsExporter(credentials=creds, client_options=opts) + assert exporter.client is not None + assert opts.credentials_file == "/fake/sa_key.json" + + dict_opts = { + "credentials_file": "/fake/sa_key.json", + "api_key": "fake-key", + "api_endpoint": "monitoring.googleapis.com", + } + exporter_from_dict = BigtableMetricsExporter( + credentials=creds, client_options=dict_opts + ) + assert exporter_from_dict.client is not None + assert dict_opts["credentials_file"] == "/fake/sa_key.json" + assert dict_opts["api_key"] == "fake-key" + @pytest.mark.parametrize( "value,expected_field", [ From 01ad72c88a237f08f935cac2307764239fb60aac Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Fri, 30 Oct 2026 13:10:09 -0400 Subject: [PATCH 2/2] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../cloud/bigtable/data/_metrics/handlers/gcp_exporter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py index fcb53d61b9d9..073728897342 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/handlers/gcp_exporter.py @@ -171,8 +171,9 @@ def __init__(self, *client_args, **client_kwargs): if client_kwargs.get("credentials") is not None: client_options = client_kwargs.get("client_options") if isinstance(client_options, dict): - if client_options.get("credentials_file") or client_options.get( - "api_key" + if ( + client_options.get("credentials_file") is not None + or client_options.get("api_key") is not None ): client_options = { k: v