From 7658d022c145b9f1a348235c10af4e87bb86a027 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 14:53:10 -0700 Subject: [PATCH 01/17] attempt deduplicating x-goog-api-client headers --- .../google/api_core/gapic_v1/method.py | 27 +++++++++++++------ .../google/api_core/grpc_helpers.py | 19 +++++++++---- .../google-auth/google/auth/transport/grpc.py | 6 ++++- .../google-auth/tests/transport/test_grpc.py | 27 +++++++++++++++++++ 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index b4481ca198a4..f4cc3eaca471 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -114,14 +114,25 @@ def __call__( # Add the user agent metadata to the call. if self._metadata is not None: - metadata = kwargs.get("metadata", []) - # Due to the nature of invocation, None should be treated the same - # as not specified. - if metadata is None: - metadata = [] - metadata = list(metadata) - metadata.extend(self._metadata) - kwargs["metadata"] = metadata + metadata = kwargs.get("metadata") + if not metadata: + kwargs["metadata"] = self._metadata + else: + # Merge user-supplied metadata with library-supplied metadata. + # All keys in gRPC metadata are already lowercase. + from itertools import chain + metadata = list(metadata) + api_client_values = [] + merged_metadata = [] + for key, val in chain(metadata, self._metadata): + if key == "x-goog-api-client": + api_client_values.append(val) + else: + merged_metadata.append((key, val)) + if api_client_values: + merged_metadata.append(("x-goog-api-client", " ".join(api_client_values))) + kwargs["metadata"] = merged_metadata + if self._compression is not None: kwargs["compression"] = compression diff --git a/packages/google-api-core/google/api_core/grpc_helpers.py b/packages/google-api-core/google/api_core/grpc_helpers.py index 30ba19c54f1a..01b9092a2a26 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers.py +++ b/packages/google-api-core/google/api_core/grpc_helpers.py @@ -254,11 +254,20 @@ def _create_composite_credentials( request = google.auth.transport.requests.Request() # Create the metadata plugin for inserting the authorization header. - metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( - credentials, - request, - default_host=default_host, - ) + try: + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, + request, + default_host=default_host, + suppress_metrics_header=True, + ) + except TypeError: + # Support older versions of google-auth that do not accept suppress_metrics_header + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, + request, + default_host=default_host, + ) # Create a set of grpc.CallCredentials using the metadata plugin. google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index e541d20ca0a4..3952bbeae63b 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -49,7 +49,7 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): account credentials. """ - def __init__(self, credentials, request, default_host=None): + def __init__(self, credentials, request, default_host=None, suppress_metrics_header=False): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. @@ -57,6 +57,7 @@ def __init__(self, credentials, request, default_host=None): self._credentials = credentials self._request = request self._default_host = default_host + self._suppress_metrics_header = suppress_metrics_header def _get_authorization_headers(self, context): """Gets the authorization headers for a request. @@ -80,6 +81,9 @@ def _get_authorization_headers(self, context): self._request, context.method_name, context.service_url, headers ) + if self._suppress_metrics_header and "x-goog-api-client" in headers: + del headers["x-goog-api-client"] + return list(headers.items()) def __call__(self, context, callback): diff --git a/packages/google-auth/tests/transport/test_grpc.py b/packages/google-auth/tests/transport/test_grpc.py index 7ebd14758e55..6907d3be8b3c 100644 --- a/packages/google-auth/tests/transport/test_grpc.py +++ b/packages/google-auth/tests/transport/test_grpc.py @@ -132,6 +132,33 @@ def test__get_authorization_headers_with_service_account_and_default_host(self): "https://{}/".format(default_host) ) + def test_suppress_metrics_header(self): + credentials = mock.create_autospec(service_account.Credentials) + # Mock credentials before_request that adds metric and authorization + def mock_before_request(request, method, url, headers): + headers["x-goog-api-client"] = "foo" + headers["authorization"] = "Bearer token" + credentials.before_request.side_effect = mock_before_request + request = mock.create_autospec(transport.Request) + + # By default, suppress_metrics_header=False + plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) + context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) + context.method_name = "methodName" + context.service_url = "https://pubsub.googleapis.com/methodName" + + headers = dict(plugin._get_authorization_headers(context)) + assert "x-goog-api-client" in headers + assert headers["x-goog-api-client"] == "foo" + + # With suppress_metrics_header=True + plugin_suppressed = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, request, suppress_metrics_header=True + ) + headers_suppressed = dict(plugin_suppressed._get_authorization_headers(context)) + assert "x-goog-api-client" not in headers_suppressed + assert headers_suppressed["authorization"] == "Bearer token" + @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True From 7d266a352bb1e188f79f2eaf3397e9d824104701 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:27:36 -0700 Subject: [PATCH 02/17] added test --- .../tests/unit/gapic/test_method.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index 29e8fc2173bd..a6e7f03e7b0d 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -121,6 +121,39 @@ def test_invoke_wrapped_method_with_metadata_as_none(): assert len(metadata) == 1 +def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): + method = mock.Mock(spec=["__call__"]) + + # Create a custom ClientInfo with defined properties so we know exactly what is returned + client_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent="custom-user-agent/1.0", + python_version="3.14.0", + grpc_version="1.76.0", + api_core_version="2.29.0" + ) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method(method, client_info=client_info) + + # Invoke the wrapped method with an explicit user-provided custom header + wrapped_method( + mock.sentinel.request, + metadata=[("x-goog-api-client", "override-client/2.0"), ("other-header", "value")] + ) + + method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY) + metadata = method.call_args[1]["metadata"] + + # There should only be one "x-goog-api-client" header, containing both values joined by space, + # plus the other-header. + assert len(metadata) == 2 + metadata_dict = dict(metadata) + assert "other-header" in metadata_dict + assert metadata_dict["other-header"] == "value" + assert "x-goog-api-client" in metadata_dict + # Verify both the user-provided override value and the library system telemetry are merged explicitly + assert metadata_dict["x-goog-api-client"] == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + + @mock.patch("time.sleep") def test_wrap_method_with_default_retry_and_timeout_and_compression(unused_sleep): method = mock.Mock( From 852c5b1d6e073f8da3f1c06ce0cc3b66a380a390 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:27:52 -0700 Subject: [PATCH 03/17] optimized for hard-coded metadata --- .../google/api_core/gapic_v1/method.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index f4cc3eaca471..49fa7f591f15 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -91,6 +91,14 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata + # separate x-goog-api-client header from provided metadata + self._arbitrary_metadata = [] + self._metrics_values = "" + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + self._metrics_values = val + else: + self._arbitrary_metadata.append((key, val)) def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -116,21 +124,22 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: - kwargs["metadata"] = self._metadata + if self._metrics_values: + kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values), *self._arbitrary_metadata] + else: + kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. - # All keys in gRPC metadata are already lowercase. - from itertools import chain metadata = list(metadata) - api_client_values = [] + metric_values = [self._metrics_values] if self._metrics_values else [] merged_metadata = [] - for key, val in chain(metadata, self._metadata): - if key == "x-goog-api-client": - api_client_values.append(val) + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + metric_values.append(val) else: merged_metadata.append((key, val)) - if api_client_values: - merged_metadata.append(("x-goog-api-client", " ".join(api_client_values))) + if metric_values: + merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(metric_values))) kwargs["metadata"] = merged_metadata if self._compression is not None: From 0d8865a1dda0df0ba6e9c6f3de00415ef0134f18 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:34:29 -0700 Subject: [PATCH 04/17] cleaning up code --- .../google/api_core/gapic_v1/method.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 49fa7f591f15..486dbb3a478f 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -91,14 +91,17 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata - # separate x-goog-api-client header from provided metadata + + # Pre-extract the client metrics header from the initialized metadata. + # This avoids repeating this work on every single RPC request invocation. self._arbitrary_metadata = [] self._metrics_values = "" - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - self._metrics_values = val - else: - self._arbitrary_metadata.append((key, val)) + if metadata: + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + self._metrics_values = val + else: + self._arbitrary_metadata.append((key, val)) def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -124,22 +127,28 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: + # Fast path: in 99% of calls, the user did not pass any custom metadata, + # so we can directly assign the pre-extracted metadata and skip any merging overhead. if self._metrics_values: - kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values), *self._arbitrary_metadata] + kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. + # All keys in gRPC metadata are already lowercase. metadata = list(metadata) - metric_values = [self._metrics_values] if self._metrics_values else [] + api_client_values = [] merged_metadata = [] for key, val in metadata: if key == client_info.METRICS_METADATA_KEY: - metric_values.append(val) + api_client_values.append(val) else: merged_metadata.append((key, val)) - if metric_values: - merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(metric_values))) + if self._metrics_values: + api_client_values.append(self._metrics_values) + if api_client_values: + merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_values))) + merged_metadata.extend(self._arbitrary_metadata) kwargs["metadata"] = merged_metadata if self._compression is not None: From 76274cab6d73b01a27c54b27e3f052808f0f743e Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 16:33:08 -0700 Subject: [PATCH 05/17] pulled out helper method --- .../google/api_core/gapic_v1/method.py | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 486dbb3a478f..278dad9cf51c 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -57,6 +57,34 @@ def _apply_decorators(func, decorators): return func +def _extract_metrics_header(metadata): + """Extract x-google-api-client header from metadata list. + + Args: + metadata (Sequence[Tuple[str, str]]): The metadata to extract from. + + Returns: + Tuple[List[Tuple[str, str]], List[str]]: A tuple containing: + - A list of remaining metadata tuples. + - A list of metrics header values found. + """ + if not metadata: + return [], [] + + for i, (key, val) in enumerate(metadata): + if key == client_info.METRICS_METADATA_KEY: + # Key located. Check the rest of the list for duplicate entries + arbitrary_metadata = list(metadata[:i]) + metric_values = [val] + for k, v in metadata[i+1:]: + if k == client_info.METRICS_METADATA_KEY: + metric_values.append(v) + else: + arbitrary_metadata.append((k, v)) + return arbitrary_metadata, metric_values + # No key found + return list(metadata), [] + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -91,17 +119,9 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata - - # Pre-extract the client metrics header from the initialized metadata. - # This avoids repeating this work on every single RPC request invocation. - self._arbitrary_metadata = [] - self._metrics_values = "" - if metadata: - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - self._metrics_values = val - else: - self._arbitrary_metadata.append((key, val)) + # Pre-extract the x-goog-api-client header from the initialized metadata. + self._arbitrary_metadata, metric_values = _extract_metrics_header(metadata) + self._metrics_values = " ".join(metric_values) if metric_values else "" def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -127,23 +147,13 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: - # Fast path: in 99% of calls, the user did not pass any custom metadata, - # so we can directly assign the pre-extracted metadata and skip any merging overhead. if self._metrics_values: kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. - # All keys in gRPC metadata are already lowercase. - metadata = list(metadata) - api_client_values = [] - merged_metadata = [] - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - api_client_values.append(val) - else: - merged_metadata.append((key, val)) + merged_metadata, api_client_values = _extract_metrics_header(metadata) if self._metrics_values: api_client_values.append(self._metrics_values) if api_client_values: From e42f0a3d8359b077abaa6a44ad1c645cb46de9d5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 16:49:02 -0700 Subject: [PATCH 06/17] added docstring --- packages/google-auth/google/auth/transport/grpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index 3952bbeae63b..93f392731163 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -47,9 +47,11 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): default_host (Optional[str]): A host like "pubsub.googleapis.com". This is used when a self-signed JWT is created from service account credentials. + suppress_metrics_header (bool): When enabled, ``x-goog-api-client`` + will be stripped from authorization headers. """ - def __init__(self, credentials, request, default_host=None, suppress_metrics_header=False): + def __init__(self, credentials, request, default_host=None, *, suppress_metrics_header=False): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. From fc05e08dd21f0f7e86d444a7f93905c0450fccd6 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 17:04:42 -0700 Subject: [PATCH 07/17] fixed format --- .../google/api_core/gapic_v1/method.py | 11 ++++++++--- .../tests/unit/gapic/test_method.py | 16 ++++++++++++---- .../google-auth/google/auth/transport/grpc.py | 4 +++- .../google-auth/tests/transport/test_grpc.py | 2 ++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 278dad9cf51c..b56463cf1770 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -76,7 +76,7 @@ def _extract_metrics_header(metadata): # Key located. Check the rest of the list for duplicate entries arbitrary_metadata = list(metadata[:i]) metric_values = [val] - for k, v in metadata[i+1:]: + for k, v in metadata[i + 1 :]: if k == client_info.METRICS_METADATA_KEY: metric_values.append(v) else: @@ -85,6 +85,7 @@ def _extract_metrics_header(metadata): # No key found return list(metadata), [] + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -148,7 +149,9 @@ def __call__( metadata = kwargs.get("metadata") if not metadata: if self._metrics_values: - kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata + kwargs["metadata"] = [ + (client_info.METRICS_METADATA_KEY, self._metrics_values) + ] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: @@ -157,7 +160,9 @@ def __call__( if self._metrics_values: api_client_values.append(self._metrics_values) if api_client_values: - merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_values))) + merged_metadata.append( + (client_info.METRICS_METADATA_KEY, " ".join(api_client_values)) + ) merged_metadata.extend(self._arbitrary_metadata) kwargs["metadata"] = merged_metadata diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index a6e7f03e7b0d..337451e0ed83 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -129,15 +129,20 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): user_agent="custom-user-agent/1.0", python_version="3.14.0", grpc_version="1.76.0", - api_core_version="2.29.0" + api_core_version="2.29.0", ) - wrapped_method = google.api_core.gapic_v1.method.wrap_method(method, client_info=client_info) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, client_info=client_info + ) # Invoke the wrapped method with an explicit user-provided custom header wrapped_method( mock.sentinel.request, - metadata=[("x-goog-api-client", "override-client/2.0"), ("other-header", "value")] + metadata=[ + ("x-goog-api-client", "override-client/2.0"), + ("other-header", "value"), + ], ) method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY) @@ -151,7 +156,10 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): assert metadata_dict["other-header"] == "value" assert "x-goog-api-client" in metadata_dict # Verify both the user-provided override value and the library system telemetry are merged explicitly - assert metadata_dict["x-goog-api-client"] == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + assert ( + metadata_dict["x-goog-api-client"] + == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + ) @mock.patch("time.sleep") diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index 93f392731163..df3f8a7dcf54 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -51,7 +51,9 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): will be stripped from authorization headers. """ - def __init__(self, credentials, request, default_host=None, *, suppress_metrics_header=False): + def __init__( + self, credentials, request, default_host=None, *, suppress_metrics_header=False + ): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. diff --git a/packages/google-auth/tests/transport/test_grpc.py b/packages/google-auth/tests/transport/test_grpc.py index 6907d3be8b3c..15f7e6b62842 100644 --- a/packages/google-auth/tests/transport/test_grpc.py +++ b/packages/google-auth/tests/transport/test_grpc.py @@ -134,10 +134,12 @@ def test__get_authorization_headers_with_service_account_and_default_host(self): def test_suppress_metrics_header(self): credentials = mock.create_autospec(service_account.Credentials) + # Mock credentials before_request that adds metric and authorization def mock_before_request(request, method, url, headers): headers["x-goog-api-client"] = "foo" headers["authorization"] = "Bearer token" + credentials.before_request.side_effect = mock_before_request request = mock.create_autospec(transport.Request) From e6957fbdb07eb44ec1ebbf524150f89c640f8f2d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 17:57:45 -0700 Subject: [PATCH 08/17] improved function --- .../google/api_core/gapic_v1/method.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index b56463cf1770..101dbaded54a 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -57,33 +57,29 @@ def _apply_decorators(func, decorators): return func -def _extract_metrics_header(metadata): +def _extract_metrics_header(metadata) -> Tuple[List[Tuple[str, str], ...], str] """Extract x-google-api-client header from metadata list. Args: metadata (Sequence[Tuple[str, str]]): The metadata to extract from. Returns: - Tuple[List[Tuple[str, str]], List[str]]: A tuple containing: - - A list of remaining metadata tuples. - - A list of metrics header values found. + A tuple containing: + - A sequence of remaining metadata tuples. + - a string representing the header value. """ if not metadata: - return [], [] - - for i, (key, val) in enumerate(metadata): - if key == client_info.METRICS_METADATA_KEY: - # Key located. Check the rest of the list for duplicate entries - arbitrary_metadata = list(metadata[:i]) - metric_values = [val] - for k, v in metadata[i + 1 :]: - if k == client_info.METRICS_METADATA_KEY: - metric_values.append(v) - else: - arbitrary_metadata.append((k, v)) - return arbitrary_metadata, metric_values - # No key found - return list(metadata), [] + return (), "" + + key_to_find = client_info.METRICS_METADATA_KEY + + metric_str = " ".join([v for k, v in metadata if k == key_to_find]) + + if not metric_str: + return list(metadata), "" + + arbitrary_metadata = [item for item in metadata if item[0] != key_to_find] + return arbitrary_metadata, metric_str class _GapicCallable(object): @@ -121,8 +117,7 @@ def __init__( self._compression = compression self._metadata = metadata # Pre-extract the x-goog-api-client header from the initialized metadata. - self._arbitrary_metadata, metric_values = _extract_metrics_header(metadata) - self._metrics_values = " ".join(metric_values) if metric_values else "" + self._arbitrary_metadata, self._metrics_values = _extract_metrics_header(metadata) def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -158,10 +153,13 @@ def __call__( # Merge user-supplied metadata with library-supplied metadata. merged_metadata, api_client_values = _extract_metrics_header(metadata) if self._metrics_values: - api_client_values.append(self._metrics_values) + if api_client_values: + api_client_values = f"{api_client_values} {self._metrics_values}" + else: + api_client_values = self._metrics_values if api_client_values: merged_metadata.append( - (client_info.METRICS_METADATA_KEY, " ".join(api_client_values)) + (client_info.METRICS_METADATA_KEY, api_client_values) ) merged_metadata.extend(self._arbitrary_metadata) kwargs["metadata"] = merged_metadata From 50283ba91beb46dca5ceb696ad8ec5e14a9e1d4d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:05:20 -0700 Subject: [PATCH 09/17] renamed variables --- .../google/api_core/gapic_v1/method.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 101dbaded54a..7c12d8ea2d5a 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -115,9 +115,10 @@ def __init__( self._retry = retry self._timeout = timeout self._compression = compression - self._metadata = metadata # Pre-extract the x-goog-api-client header from the initialized metadata. - self._arbitrary_metadata, self._metrics_values = _extract_metrics_header(metadata) + remaining_metadata, x_goog_api_client = _extract_metrics_header(metadata) + self._init_metadata = tuple(remaining_metadata) + self._x_goog_api_client = x_goog_api_client def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -140,28 +141,28 @@ def __call__( wrapped_func = _apply_decorators(self._target, [retry, timeout]) # Add the user agent metadata to the call. - if self._metadata is not None: - metadata = kwargs.get("metadata") - if not metadata: - if self._metrics_values: + if self._init_metadata or self._x_goog_api_client: + user_metadata = kwargs.get("metadata") + if not user_metadata: + if self._x_goog_api_client: kwargs["metadata"] = [ - (client_info.METRICS_METADATA_KEY, self._metrics_values) - ] + self._arbitrary_metadata + (client_info.METRICS_METADATA_KEY, self._x_goog_api_client) + ] + self._init_metadata else: - kwargs["metadata"] = self._arbitrary_metadata + kwargs["metadata"] = self._init_metadata else: # Merge user-supplied metadata with library-supplied metadata. - merged_metadata, api_client_values = _extract_metrics_header(metadata) - if self._metrics_values: + merged_metadata, api_client_values = _extract_metrics_header(user_metadata) + if self._x_goog_api_client: if api_client_values: - api_client_values = f"{api_client_values} {self._metrics_values}" + api_client_values = f"{api_client_values} {self._x_goog_api_client}" else: - api_client_values = self._metrics_values + api_client_values = self._x_goog_api_client if api_client_values: merged_metadata.append( (client_info.METRICS_METADATA_KEY, api_client_values) ) - merged_metadata.extend(self._arbitrary_metadata) + merged_metadata.extend(self._init_metadata) kwargs["metadata"] = merged_metadata if self._compression is not None: From 9a1ce952dce527af3499967d965ea2cfcc59ec92 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:17:19 -0700 Subject: [PATCH 10/17] fixed typing --- .../google-api-core/google/api_core/gapic_v1/method.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 7c12d8ea2d5a..36cf61ee661b 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -17,7 +17,7 @@ This is used by gapic clients to provide common error mapping, retry, timeout, compression, pagination, and long-running operations to gRPC methods. """ - +from typing import Tuple, List import enum import functools @@ -57,7 +57,7 @@ def _apply_decorators(func, decorators): return func -def _extract_metrics_header(metadata) -> Tuple[List[Tuple[str, str], ...], str] +def _extract_metrics_header(metadata) -> Tuple[List[Tuple[str, str]], str]: """Extract x-google-api-client header from metadata list. Args: @@ -146,8 +146,9 @@ def __call__( if not user_metadata: if self._x_goog_api_client: kwargs["metadata"] = [ - (client_info.METRICS_METADATA_KEY, self._x_goog_api_client) - ] + self._init_metadata + (client_info.METRICS_METADATA_KEY, self._x_goog_api_client), + *self._init_metadata + ] else: kwargs["metadata"] = self._init_metadata else: From 978b2679ad79dbc05b5bbbaa83538913d7d8633c Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:37:58 -0700 Subject: [PATCH 11/17] refactored --- .../google/api_core/gapic_v1/method.py | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 36cf61ee661b..f7f27e1ca368 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -141,30 +141,18 @@ def __call__( wrapped_func = _apply_decorators(self._target, [retry, timeout]) # Add the user agent metadata to the call. - if self._init_metadata or self._x_goog_api_client: - user_metadata = kwargs.get("metadata") - if not user_metadata: - if self._x_goog_api_client: - kwargs["metadata"] = [ - (client_info.METRICS_METADATA_KEY, self._x_goog_api_client), - *self._init_metadata - ] - else: - kwargs["metadata"] = self._init_metadata - else: - # Merge user-supplied metadata with library-supplied metadata. - merged_metadata, api_client_values = _extract_metrics_header(user_metadata) - if self._x_goog_api_client: - if api_client_values: - api_client_values = f"{api_client_values} {self._x_goog_api_client}" - else: - api_client_values = self._x_goog_api_client - if api_client_values: - merged_metadata.append( - (client_info.METRICS_METADATA_KEY, api_client_values) - ) - merged_metadata.extend(self._init_metadata) - kwargs["metadata"] = merged_metadata + final_metadata = list(self._init_metadata) + if user_metadata := kwargs.get("metadata"): + remaining, user_x_goog = _extract_metrics_header(user_metadata) + api_client_tokens = [t for t in [user_x_goog, self._x_goog_api_client] if t] + if api_client_tokens: + final_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens))) + final_metadata.extend(remaining) + else: + if self._x_goog_api_client: + final_metadata.append((client_info.METRICS_METADATA_KEY, self._x_goog_api_client)) + if final_metadata: + kwargs["metadata"] = final_metadata if self._compression is not None: kwargs["compression"] = compression From 19a00998f3cdddd5346a7b44f69e9293fc96ceef Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:46:44 -0700 Subject: [PATCH 12/17] keep default metadata stored in client init --- .../google/api_core/gapic_v1/method.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index f7f27e1ca368..792bf4a92255 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -117,8 +117,15 @@ def __init__( self._compression = compression # Pre-extract the x-goog-api-client header from the initialized metadata. remaining_metadata, x_goog_api_client = _extract_metrics_header(metadata) - self._init_metadata = tuple(remaining_metadata) + self._static_metadata = tuple(remaining_metadata) self._x_goog_api_client = x_goog_api_client + if x_goog_api_client: + self._default_metadata = ( + (client_info.METRICS_METADATA_KEY, x_goog_api_client), + *self._static_metadata + ) + else: + self._default_metadata = self._static_metadata def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -141,18 +148,16 @@ def __call__( wrapped_func = _apply_decorators(self._target, [retry, timeout]) # Add the user agent metadata to the call. - final_metadata = list(self._init_metadata) if user_metadata := kwargs.get("metadata"): + final_metadata = list(self._static_metadata) remaining, user_x_goog = _extract_metrics_header(user_metadata) api_client_tokens = [t for t in [user_x_goog, self._x_goog_api_client] if t] if api_client_tokens: final_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens))) final_metadata.extend(remaining) - else: - if self._x_goog_api_client: - final_metadata.append((client_info.METRICS_METADATA_KEY, self._x_goog_api_client)) - if final_metadata: kwargs["metadata"] = final_metadata + elif self._default_metadata: + kwargs["metadata"] = self._default_metadata if self._compression is not None: kwargs["compression"] = compression From 358d71cf27c31590d6e8dc49ce0d4c5551faf309 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:48:21 -0700 Subject: [PATCH 13/17] changed return order --- .../google/api_core/gapic_v1/method.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 792bf4a92255..f99008c9d9b6 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -57,7 +57,7 @@ def _apply_decorators(func, decorators): return func -def _extract_metrics_header(metadata) -> Tuple[List[Tuple[str, str]], str]: +def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]: """Extract x-google-api-client header from metadata list. Args: @@ -69,17 +69,17 @@ def _extract_metrics_header(metadata) -> Tuple[List[Tuple[str, str]], str]: - a string representing the header value. """ if not metadata: - return (), "" + return "", () key_to_find = client_info.METRICS_METADATA_KEY metric_str = " ".join([v for k, v in metadata if k == key_to_find]) if not metric_str: - return list(metadata), "" + return "", list(metadata) arbitrary_metadata = [item for item in metadata if item[0] != key_to_find] - return arbitrary_metadata, metric_str + return metric_str, arbitrary_metadata class _GapicCallable(object): @@ -116,8 +116,8 @@ def __init__( self._timeout = timeout self._compression = compression # Pre-extract the x-goog-api-client header from the initialized metadata. - remaining_metadata, x_goog_api_client = _extract_metrics_header(metadata) - self._static_metadata = tuple(remaining_metadata) + x_goog_api_client, remaining = _extract_metrics_header(metadata) + self._static_metadata = tuple(remaining) self._x_goog_api_client = x_goog_api_client if x_goog_api_client: self._default_metadata = ( @@ -150,7 +150,7 @@ def __call__( # Add the user agent metadata to the call. if user_metadata := kwargs.get("metadata"): final_metadata = list(self._static_metadata) - remaining, user_x_goog = _extract_metrics_header(user_metadata) + user_x_goog, remaining = _extract_metrics_header(user_metadata) api_client_tokens = [t for t in [user_x_goog, self._x_goog_api_client] if t] if api_client_tokens: final_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens))) From b2dde9533598888545e303c12dc5523a462af1fe Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 18:52:42 -0700 Subject: [PATCH 14/17] fix lint --- .../google/api_core/gapic_v1/method.py | 11 +++++++---- .../google/api_core/gapic_v1/requests.py | 2 +- .../google-api-core/tests/unit/gapic/test_requests.py | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index f99008c9d9b6..7c227e4ed98c 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -17,9 +17,10 @@ This is used by gapic clients to provide common error mapping, retry, timeout, compression, pagination, and long-running operations to gRPC methods. """ -from typing import Tuple, List + import enum import functools +from typing import List, Tuple from google.api_core import grpc_helpers from google.api_core.gapic_v1 import client_info @@ -69,7 +70,7 @@ def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]: - a string representing the header value. """ if not metadata: - return "", () + return "", [] key_to_find = client_info.METRICS_METADATA_KEY @@ -122,7 +123,7 @@ def __init__( if x_goog_api_client: self._default_metadata = ( (client_info.METRICS_METADATA_KEY, x_goog_api_client), - *self._static_metadata + *self._static_metadata, ) else: self._default_metadata = self._static_metadata @@ -153,7 +154,9 @@ def __call__( user_x_goog, remaining = _extract_metrics_header(user_metadata) api_client_tokens = [t for t in [user_x_goog, self._x_goog_api_client] if t] if api_client_tokens: - final_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens))) + final_metadata.append( + (client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens)) + ) final_metadata.extend(remaining) kwargs["metadata"] = final_metadata elif self._default_metadata: diff --git a/packages/google-api-core/google/api_core/gapic_v1/requests.py b/packages/google-api-core/google/api_core/gapic_v1/requests.py index f440ac69126c..8ce97c9cffa7 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/requests.py +++ b/packages/google-api-core/google/api_core/gapic_v1/requests.py @@ -21,8 +21,8 @@ if they are not already set. """ -from typing import Union import uuid +from typing import Union import google.protobuf.message diff --git a/packages/google-api-core/tests/unit/gapic/test_requests.py b/packages/google-api-core/tests/unit/gapic/test_requests.py index 1e921955d043..e046f31b828b 100644 --- a/packages/google-api-core/tests/unit/gapic/test_requests.py +++ b/packages/google-api-core/tests/unit/gapic/test_requests.py @@ -19,7 +19,6 @@ from google.api_core.gapic_v1.requests import setup_request_id - # --- Mock Request Helper Classes --- From 049f10b959c408506d07e002bb7a99e5004d4ce1 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 20 Jul 2026 17:25:01 -0700 Subject: [PATCH 15/17] Refactor x_goog_api_client assignment in method.py --- .../google-api-core/google/api_core/gapic_v1/method.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 7c227e4ed98c..76ded364937e 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -117,12 +117,11 @@ def __init__( self._timeout = timeout self._compression = compression # Pre-extract the x-goog-api-client header from the initialized metadata. - x_goog_api_client, remaining = _extract_metrics_header(metadata) + self._x_goog_api_client, remaining = _extract_metrics_header(metadata) self._static_metadata = tuple(remaining) - self._x_goog_api_client = x_goog_api_client - if x_goog_api_client: + if self._x_goog_api_client: self._default_metadata = ( - (client_info.METRICS_METADATA_KEY, x_goog_api_client), + (client_info.METRICS_METADATA_KEY, self._x_goog_api_client), *self._static_metadata, ) else: From cc47ba687a41e20621425973fb37be99bba134df Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 24 Jul 2026 14:52:41 -0700 Subject: [PATCH 16/17] added token-level deduplication --- .../google/api_core/gapic_v1/method.py | 33 ++++++++++++--- .../tests/unit/gapic/test_method.py | 42 ++++++++++++++++++- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 76ded364937e..e4ee053d8627 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -58,6 +58,25 @@ def _apply_decorators(func, decorators): return func +def _deduplicate_metadata_tokens(*headers: str) -> str: + """ + Given one or more metadata payload strings, create a combined + string with deduplicated tokens, while preserving token order. + + Inputs are expected contain a set of metadata tokens separated by spaces + Example: `gl-python/3.14.0 grpc/1.76.0 gax/2.29.0 gapic/3.8.0 pb/6.33.4` + + Args: + *headers: one or more metadata payload strings + + Returns: + a single combined payload string + """ + # Split all non-empty headers into individual tokens + token_list = " ".join(filter(None, headers)).split() + # Deduplicate while preserving order + return " ".join(dict.fromkeys(token_list)) + def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]: """Extract x-google-api-client header from metadata list. @@ -74,8 +93,9 @@ def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]: key_to_find = client_info.METRICS_METADATA_KEY - metric_str = " ".join([v for k, v in metadata if k == key_to_find]) - + metric_str = _deduplicate_metadata_tokens( + " ".join([v for k, v in metadata if k == key_to_find]) + ) if not metric_str: return "", list(metadata) @@ -147,14 +167,15 @@ def __call__( # Apply all applicable decorators. wrapped_func = _apply_decorators(self._target, [retry, timeout]) - # Add the user agent metadata to the call. if user_metadata := kwargs.get("metadata"): + # Add the user agent metadata to the call. final_metadata = list(self._static_metadata) user_x_goog, remaining = _extract_metrics_header(user_metadata) - api_client_tokens = [t for t in [user_x_goog, self._x_goog_api_client] if t] - if api_client_tokens: + + merged_header = _deduplicate_metadata_tokens(self._x_goog_api_client, user_x_goog) + if merged_header: final_metadata.append( - (client_info.METRICS_METADATA_KEY, " ".join(api_client_tokens)) + (client_info.METRICS_METADATA_KEY, merged_header) ) final_metadata.extend(remaining) kwargs["metadata"] = final_metadata diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index 68932d0db521..7c94139b6717 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -119,6 +119,21 @@ def test_invoke_wrapped_method_with_metadata_as_none(): assert len(metadata) == 1 +def test_extract_metrics_header_duplicate_tokens(): + metadata = [ + ("x-goog-api-client", "token1 token2"), + ("x-goog-api-client", "token2 token3 token1"), + ("other-header", "value"), + ("x-goog-api-client", "token4 token2"), + ] + + metric_str, arbitrary_metadata = google.api_core.gapic_v1.method._extract_metrics_header(metadata) + + # Should maintain order of first appearance and eliminate duplicates + assert metric_str == "token1 token2 token3 token4" + assert arbitrary_metadata == [("other-header", "value")] + + def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): method = mock.Mock(spec=["__call__"]) @@ -134,11 +149,13 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): method, client_info=client_info ) - # Invoke the wrapped method with an explicit user-provided custom header + # Invoke the wrapped method with an explicit user-provided custom header that contains duplicates + # both within its own items and overlapping with the default client_info wrapped_method( mock.sentinel.request, metadata=[ ("x-goog-api-client", "override-client/2.0"), + ("x-goog-api-client", "override-client/2.0 grpc/1.76.0 custom-user-agent/1.0"), ("other-header", "value"), ], ) @@ -156,7 +173,7 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): # Verify both the user-provided override value and the library system telemetry are merged explicitly assert ( metadata_dict["x-goog-api-client"] - == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + == "custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0 override-client/2.0" ) @@ -289,3 +306,24 @@ def test_wrap_method_with_call_not_supported(): with pytest.raises(ValueError) as exc_info: google.api_core.gapic_v1.method.wrap_method(method, with_call=True) assert "with_call=True is only supported for unary calls" in str(exc_info.value) + + +@pytest.mark.parametrize( + "headers,expected", + [ + ((), ""), + (("",), ""), + ((None,), ""), + (("", None, ""), ""), + (("token1",), "token1"), + (("token1 token1",), "token1"), + (("token1", "token1"), "token1"), + (("token1 token2 token1",), "token1 token2"), + (("token1", "token2", "token1"), "token1 token2"), + (("token1 token2", "token2 token3"), "token1 token2 token3"), + (("token1", None, "token2", "", "token1"), "token1 token2"), + ], +) +def test__deduplicate_metadata_tokens(headers, expected): + dedup = google.api_core.gapic_v1.method._deduplicate_metadata_tokens + assert dedup(*headers) == expected From 0bff55c33fac1c683bf6bfc5852a600f0138e297 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 24 Jul 2026 14:55:52 -0700 Subject: [PATCH 17/17] fixed lint --- .../google/api_core/gapic_v1/method.py | 9 +++++---- .../google-api-core/tests/unit/gapic/test_method.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index e4ee053d8627..bbf122d6a7df 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -77,6 +77,7 @@ def _deduplicate_metadata_tokens(*headers: str) -> str: # Deduplicate while preserving order return " ".join(dict.fromkeys(token_list)) + def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]: """Extract x-google-api-client header from metadata list. @@ -172,11 +173,11 @@ def __call__( final_metadata = list(self._static_metadata) user_x_goog, remaining = _extract_metrics_header(user_metadata) - merged_header = _deduplicate_metadata_tokens(self._x_goog_api_client, user_x_goog) + merged_header = _deduplicate_metadata_tokens( + self._x_goog_api_client, user_x_goog + ) if merged_header: - final_metadata.append( - (client_info.METRICS_METADATA_KEY, merged_header) - ) + final_metadata.append((client_info.METRICS_METADATA_KEY, merged_header)) final_metadata.extend(remaining) kwargs["metadata"] = final_metadata elif self._default_metadata: diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index 7c94139b6717..a6ddfacbf155 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -126,9 +126,11 @@ def test_extract_metrics_header_duplicate_tokens(): ("other-header", "value"), ("x-goog-api-client", "token4 token2"), ] - - metric_str, arbitrary_metadata = google.api_core.gapic_v1.method._extract_metrics_header(metadata) - + + metric_str, arbitrary_metadata = ( + google.api_core.gapic_v1.method._extract_metrics_header(metadata) + ) + # Should maintain order of first appearance and eliminate duplicates assert metric_str == "token1 token2 token3 token4" assert arbitrary_metadata == [("other-header", "value")] @@ -155,7 +157,10 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): mock.sentinel.request, metadata=[ ("x-goog-api-client", "override-client/2.0"), - ("x-goog-api-client", "override-client/2.0 grpc/1.76.0 custom-user-agent/1.0"), + ( + "x-goog-api-client", + "override-client/2.0 grpc/1.76.0 custom-user-agent/1.0", + ), ("other-header", "value"), ], )