From bf229932f0b77660f937cd2af599df1eb83a38a8 Mon Sep 17 00:00:00 2001 From: Ben Karl Date: Tue, 3 Feb 2026 01:40:10 +0000 Subject: [PATCH] Add configurable metadata header for ads api assistant. --- google/ads/googleads/client.py | 9 ++++- google/ads/googleads/config.py | 3 +- .../interceptors/metadata_interceptor.py | 38 +++++++++++------- tests/client_test.py | 21 ++++++++++ tests/config_test.py | 40 +++++++++++++++++++ 5 files changed, 95 insertions(+), 16 deletions(-) diff --git a/google/ads/googleads/client.py b/google/ads/googleads/client.py index 58b3bfa22..18d3a8cf1 100644 --- a/google/ads/googleads/client.py +++ b/google/ads/googleads/client.py @@ -204,6 +204,7 @@ def _get_client_kwargs(cls, config_data: Dict[str, Any]) -> Dict[str, Any]: "use_cloud_org_for_api_access": config_data.get( "use_cloud_org_for_api_access" ), + "gaada": config_data.get("gaada"), } @classmethod @@ -328,6 +329,7 @@ def __init__( http_proxy: Union[str, None] = None, use_proto_plus: bool = False, use_cloud_org_for_api_access: Union[str, None] = None, + gaada: Union[str, None] = None, ): """Initializer for the GoogleAdsClient. @@ -346,7 +348,8 @@ def __init__( Google Cloud Organization of your Google Cloud project instead of developer token to determine your Google Ads API access levels. Use this flag only if you are enrolled into a limited - pilot that supports this configuration + pilot that supports this configuration. + gaada: a str specifying the Google Ads API Assistant version. """ if logging_config: logging.config.dictConfig(logging_config) @@ -363,6 +366,7 @@ def __init__( use_cloud_org_for_api_access ) self.enums: _EnumGetter = _EnumGetter(self) + self.gaada: Union[str, None] = gaada # If given, write the http_proxy channel option for GRPC to use if http_proxy: @@ -442,12 +446,14 @@ def get_service( self.login_customer_id, self.linked_customer_id, self.use_cloud_org_for_api_access, + gaada=self.gaada, ), AsyncUnaryStreamMetadataInterceptor( self.developer_token, self.login_customer_id, self.linked_customer_id, self.use_cloud_org_for_api_access, + gaada=self.gaada, ), AsyncUnaryUnaryLoggingInterceptor(_logger, version, endpoint), AsyncUnaryStreamLoggingInterceptor(_logger, version, endpoint), @@ -484,6 +490,7 @@ def get_service( self.login_customer_id, self.linked_customer_id, self.use_cloud_org_for_api_access, + gaada=self.gaada, ), LoggingInterceptor(_logger, version, endpoint), ExceptionInterceptor( diff --git a/google/ads/googleads/config.py b/google/ads/googleads/config.py index 364f8ae7f..a8c96d53f 100644 --- a/google/ads/googleads/config.py +++ b/google/ads/googleads/config.py @@ -34,7 +34,8 @@ "linked_customer_id", "http_proxy", "use_cloud_org_for_api_access", - "use_application_default_credentials" + "use_application_default_credentials", + "gaada", ) _CONFIG_FILE_PATH_KEY = ("configuration_file_path",) _OAUTH2_INSTALLED_APP_KEYS = ("client_id", "client_secret", "refresh_token") diff --git a/google/ads/googleads/interceptors/metadata_interceptor.py b/google/ads/googleads/interceptors/metadata_interceptor.py index 7c458cd8f..82f21ef5a 100644 --- a/google/ads/googleads/interceptors/metadata_interceptor.py +++ b/google/ads/googleads/interceptors/metadata_interceptor.py @@ -60,6 +60,7 @@ def __init__( login_customer_id: Optional[str]= None, linked_customer_id: Optional[str] = None, use_cloud_org_for_api_access: Optional[bool] = None, + gaada: Optional[str] = None, ): """Initialization method for this class. @@ -72,6 +73,7 @@ def __init__( of developer token to determine your Google Ads API access levels. Use this flag only if you are enrolled into a limited pilot that supports this configuration + gaada: a str specifying the Google Ads API Assistant version. """ self.developer_token_meta: Tuple[str, str] = ( "developer-token", @@ -87,6 +89,7 @@ def __init__( if linked_customer_id else None ) + self.gaada: Optional[str] = gaada self.use_cloud_org_for_api_access: Optional[ bool ] = use_cloud_org_for_api_access @@ -152,21 +155,28 @@ def _intercept( # fixed: https://github.com/googleapis/python-api-core/issues/416 for i, metadatum_tuple in enumerate(metadata): # Check if the user agent header key is in the current metadatum - if "x-goog-api-client" in metadatum_tuple and _PROTOBUF_VERSION: - # Convert the tuple to a list so it can be modified. + if "x-goog-api-client" in metadatum_tuple: metadatum: List[str] = list(metadatum_tuple) - # Check that "pb" isn't already included in the user agent. - if "pb" not in metadatum[1]: - # Append the protobuf version key value pair to the end of - # the string. - metadatum[1] += f" pb/{_PROTOBUF_VERSION}{_PB_IMPL_HEADER}" - # Convert the metadatum back to a tuple. - metadatum_tuple: Tuple[str, str] = tuple(metadatum) - # Splice the metadatum back in its original position in - # order to preserve the order of the metadata list. - metadata[i] = metadatum_tuple - # Exit the loop since we already found the user agent. - break + + if self.gaada: + metadatum[1] += f" gaada/{self.gaada}" + + if _PROTOBUF_VERSION: + # Convert the tuple to a list so it can be modified. + # Check that "pb" isn't already included in the user agent. + if "pb" not in metadatum[1]: + # Append the protobuf version key value pair to the end of + # the string. + metadatum[1] += f" pb/{_PROTOBUF_VERSION}{_PB_IMPL_HEADER}" + # Convert the metadatum back to a tuple. + metadatum_tuple: Tuple[str, str] = tuple(metadatum) + + # Splice the metadatum back in its original position in + # order to preserve the order of the metadata list. + metadata[i] = metadatum_tuple + # Exit the loop since we already found the user agent. + break + client_call_details: grpc.ClientCallDetails = self._update_client_call_details_metadata( client_call_details, metadata diff --git a/tests/client_test.py b/tests/client_test.py index 3855c6219..757d5188a 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -114,6 +114,7 @@ def test_get_client_kwargs_login_customer_id(self): "linked_customer_id": self.linked_customer_id, "http_proxy": None, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -147,6 +148,7 @@ def test_get_client_kwargs_login_customer_id_as_None(self): "linked_customer_id": None, "http_proxy": None, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -180,6 +182,7 @@ def test_get_client_kwargs_linked_customer_id(self): "linked_customer_id": self.linked_customer_id, "http_proxy": None, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -213,6 +216,7 @@ def test_get_client_kwargs_linked_customer_id_as_none(self): "login_customer_id": None, "http_proxy": None, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -246,6 +250,7 @@ def test_get_client_kwargs(self): "linked_customer_id": None, "http_proxy": self.http_proxy, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -280,6 +285,7 @@ def test_get_client_kwargs_custom_endpoint(self): "linked_customer_id": None, "http_proxy": None, "use_cloud_org_for_api_access": None, + "gaada": None }, ) @@ -316,6 +322,7 @@ def test_load_from_env(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_env_versioned(self): @@ -351,6 +358,7 @@ def test_load_from_env_versioned(self): version="v4", http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_dict(self): @@ -384,6 +392,7 @@ def test_load_from_dict(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_dict_versioned(self): @@ -417,6 +426,7 @@ def test_load_from_dict_versioned(self): version="v4", http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_string(self): @@ -450,6 +460,7 @@ def test_load_from_string(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_string_versioned(self): @@ -485,6 +496,7 @@ def test_load_from_string_versioned(self): version="v4", http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_get_service(self): @@ -881,6 +893,7 @@ def test_load_http_proxy_from_env(self): version=None, http_proxy=self.http_proxy, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_http_proxy_from_dict(self): @@ -915,6 +928,7 @@ def test_load_http_proxy_from_dict(self): version=None, http_proxy=self.http_proxy, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_http_proxy_from_string(self): @@ -949,6 +963,7 @@ def test_load_http_proxy_from_string(self): version=None, http_proxy=self.http_proxy, use_cloud_org_for_api_access=None, + gaada=None ) def test_client_info_package_not_found(self): @@ -1017,6 +1032,7 @@ def test_load_http_proxy_from_storage(self): version=None, http_proxy=self.http_proxy, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_storage(self): @@ -1058,6 +1074,7 @@ def test_load_from_storage(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_storage_versioned(self): @@ -1099,6 +1116,7 @@ def test_load_from_storage_versioned(self): version="v4", http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_storage_login_cid_int(self): @@ -1142,6 +1160,7 @@ def test_load_from_storage_login_cid_int(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_storage_custom_path(self): @@ -1177,6 +1196,7 @@ def test_load_from_storage_custom_path(self): version=None, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) def test_load_from_storage_file_not_found(self): @@ -1239,4 +1259,5 @@ def test_load_from_storage_service_account_config(self): version=latest_version, http_proxy=None, use_cloud_org_for_api_access=None, + gaada=None ) diff --git a/tests/config_test.py b/tests/config_test.py index ba1a7d5f7..819268f8b 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -320,6 +320,17 @@ def test_load_from_dict_logging(self): } self.assertEqual(config.load_from_dict(config_data), config_data) + def test_load_from_dict_gaada(self): + """Should load "gaada" config from dict.""" + config_data = { + **self.default_dict_config, + **{ + "gaada": "1.6.0", + }, + } + result = config.load_from_dict(config_data) + self.assertEqual(result["gaada"], "1.6.0") + def test_load_from_dict_secondary_service_account_keys(self): """Should convert secondary keys to primary keys.""" config_data = { @@ -664,6 +675,35 @@ def test_disambiguate_string_bool_raises_value_error(self): def test_disambiguate_string_bool_raises_type_error(self): self.assertRaises(TypeError, config.disambiguate_string_bool, {}) + def test_load_from_env_gaada(self): + """Should load ads api assistant flag from environment when specified""" + environ = { + **self.default_env_var_config, + **{ + "GOOGLE_ADS_GAADA": "1.6.0", + }, + } + + with mock.patch("os.environ", environ): + results = config.load_from_env() + self.assertEqual( + results["gaada"], + "1.6.0", + ) + + def test_load_from_yaml_file_gaada(self): + """Should load "gaada" config from a yaml.""" + self._create_mock_yaml({"gaada": "1.6.0"}) + + result = config.load_from_yaml_file() + self.assertEqual(result["gaada"], "1.6.0") + + def test_load_from_yaml_file_gaada_not_set(self): + """Should set "gaada" as False when not set.""" + self._create_mock_yaml({}) + result = config.load_from_yaml_file() + self.assertEqual(result.get("gaada"), None) + def test_load_from_env_use_cloud_org_for_api_access(self): """Should load api access flag from environment when specified""" environ = {