From 28e25217d9f7d537558c18e9b5b0d8396dcc7a3c Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 16:30:52 +0000 Subject: [PATCH 01/11] chore(generator): use routing helper from api_core in templates --- .../gapic/generator/generator.py | 2 +- .../%name_%version/%sub/_compat.py.j2 | 76 +++++++++++++ .../%sub/services/%service/client.py.j2 | 80 +++++--------- .../%name_%version/%sub/test_%service.py.j2 | 48 --------- .../asset_v1/services/asset_service/client.py | 102 +++++------------- .../unit/gapic/asset_v1/test_asset_service.py | 35 ------ .../services/iam_credentials/client.py | 102 +++++------------- .../credentials_v1/test_iam_credentials.py | 35 ------ .../eventarc_v1/services/eventarc/client.py | 102 +++++------------- .../unit/gapic/eventarc_v1/test_eventarc.py | 35 ------ .../services/config_service_v2/client.py | 102 +++++------------- .../services/logging_service_v2/client.py | 102 +++++------------- .../services/metrics_service_v2/client.py | 102 +++++------------- .../logging_v2/test_config_service_v2.py | 35 ------ .../logging_v2/test_logging_service_v2.py | 35 ------ .../logging_v2/test_metrics_service_v2.py | 35 ------ .../services/config_service_v2/client.py | 102 +++++------------- .../services/logging_service_v2/client.py | 102 +++++------------- .../services/metrics_service_v2/client.py | 102 +++++------------- .../logging_v2/test_config_service_v2.py | 35 ------ .../logging_v2/test_logging_service_v2.py | 35 ------ .../logging_v2/test_metrics_service_v2.py | 35 ------ .../redis_v1/services/cloud_redis/client.py | 102 +++++------------- .../unit/gapic/redis_v1/test_cloud_redis.py | 35 ------ .../redis_v1/services/cloud_redis/client.py | 102 +++++------------- .../unit/gapic/redis_v1/test_cloud_redis.py | 35 ------ .../storage_batch_operations/client.py | 102 +++++------------- .../test_storage_batch_operations.py | 35 ------ 28 files changed, 413 insertions(+), 1437 deletions(-) create mode 100644 packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 diff --git a/packages/gapic-generator/gapic/generator/generator.py b/packages/gapic-generator/gapic/generator/generator.py index 9fe56aa9de1d..c1932abbe426 100644 --- a/packages/gapic-generator/gapic/generator/generator.py +++ b/packages/gapic-generator/gapic/generator/generator.py @@ -120,7 +120,7 @@ def get_response(self, api_schema: api.API, opts: Options) -> CodeGeneratorRespo for template_name in client_templates: # Quick check: Skip "private" templates. filename = template_name.split("/")[-1] - if filename.startswith("_") and filename != "__init__.py.j2": + if filename.startswith("_") and filename not in ("__init__.py.j2", "_compat.py.j2"): continue # Append to the output files dictionary. diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 new file mode 100644 index 000000000000..896222c550a4 --- /dev/null +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -0,0 +1,76 @@ +# {% include '_license.j2' %} + +import re +from typing import Optional, Callable, Tuple, Union +from google.auth.exceptions import MutualTLSChannelError + +try: + from google.api_core.gapic_v1.client_utils import ( + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: str, + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint + + def get_universe_domain( + client_universe_domain: Optional[str], + universe_domain_env: Optional[str], + default_universe: str, + ) -> str: + """Return the universe domain used by the client.""" + universe_domain = default_universe + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index e2e3edb24967..25ea84a9fb58 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -30,6 +30,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import extended_operation {% endif %} from google.api_core import gapic_v1 +from {{package_path}} import _compat as client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -143,44 +144,10 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): """{{ service.meta.doc|rst(width=72, indent=4) }}{% if service.version|length %} This class implements API version {{ service.version }}.{% endif %}""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = {% if service.host %}"{{ service.host }}"{% else %}None{% endif %} - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_MTLS_ENDPOINT = client_utils.get_default_mtls_endpoint( DEFAULT_ENDPOINT ) @@ -392,13 +359,18 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: """Return the API endpoint used by the client. Args: - api_override (str): The API endpoint override. If specified, this is always + api_override (Optional[str]): The API endpoint override. If specified, this is always the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. + client_cert_source (Union[Callable[[], Tuple[bytes, bytes]], None]): The client certificate source used by the client. universe_domain (str): The universe domain used by the client. use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. Possible values are "always", "auto", or "never". @@ -406,16 +378,15 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): Returns: str: The API endpoint to be used by the client. """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + {{ service.client_name }}._DEFAULT_UNIVERSE, + {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT, + {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: @@ -431,14 +402,11 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): Raises: ValueError: If the universe domain is an empty string. """ - universe_domain = {{ service.client_name }}._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + {{ service.client_name }}._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index c432b75621ea..b74df4635f4e 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -156,22 +156,6 @@ def set_event_loop(): asyncio.set_event_loop(None) -def test__get_default_mtls_endpoint(): - api_endpoint = "example.googleapis.com" - api_mtls_endpoint = "example.mtls.googleapis.com" - sandbox_endpoint = "example.sandbox.googleapis.com" - sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" - non_googleapi = "api.example.com" - custom_endpoint = ".custom" - - assert {{ service.client_name }}._get_default_mtls_endpoint(None) is None - assert {{ service.client_name }}._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint - assert {{ service.client_name }}._get_default_mtls_endpoint(non_googleapi) == non_googleapi - assert {{ service.client_name }}._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint - def test__read_environment_variables(): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) @@ -313,29 +297,7 @@ def test__get_client_cert_source(): assert {{ service.client_name }}._get_client_cert_source(None, True) is mock_default_cert_source assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object({{ service.client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.client_name }})) -{% if 'grpc' in opts.transport %} -@mock.patch.object({{ service.async_client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.async_client_name }})) -{% endif %} -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE - default_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert {{ service.client_name }}._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT - assert {{ service.client_name }}._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - with pytest.raises(MutualTLSChannelError) as excinfo: - {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." {% if service.version %} {% for method in service.methods.values() %}{% with method_name = method.name|snake_case %} @@ -384,17 +346,7 @@ def test_{{ method_name }}_api_version_header(transport_name): {% endfor %} {% endif %}{# service.version #} -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert {{ service.client_name }}._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert {{ service.client_name }}._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert {{ service.client_name }}._get_universe_domain(None, None) == {{ service.client_name }}._DEFAULT_UNIVERSE - with pytest.raises(ValueError) as excinfo: - {{ service.client_name }}._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py index 590b6fa1c615..8da37edbad1c 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -101,38 +102,9 @@ class AssetServiceClient(metaclass=AssetServiceClientMeta): """Asset service definition.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "cloudasset.googleapis.com" @@ -450,53 +422,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = AssetServiceClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = AssetServiceClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + AssetServiceClient._DEFAULT_UNIVERSE, + AssetServiceClient.DEFAULT_MTLS_ENDPOINT, + AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = AssetServiceClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + AssetServiceClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 1a367689d14e..e734654e477f 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -278,41 +278,6 @@ def test__get_client_cert_source(): assert AssetServiceClient._get_client_cert_source(None, True) is mock_default_cert_source assert AssetServiceClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(AssetServiceClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceClient)) -@mock.patch.object(AssetServiceAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = AssetServiceClient._DEFAULT_UNIVERSE - default_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert AssetServiceClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT - assert AssetServiceClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert AssetServiceClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert AssetServiceClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert AssetServiceClient._get_universe_domain(None, None) == AssetServiceClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - AssetServiceClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py index 4ad970da32e9..cecdaaf047a9 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -104,38 +105,9 @@ class IAMCredentialsClient(metaclass=IAMCredentialsClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "iamcredentials.googleapis.com" @@ -387,53 +359,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + IAMCredentialsClient._DEFAULT_UNIVERSE, + IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT, + IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = IAMCredentialsClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + IAMCredentialsClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 508e17a7e889..97bc1efbb00d 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -268,41 +268,6 @@ def test__get_client_cert_source(): assert IAMCredentialsClient._get_client_cert_source(None, True) is mock_default_cert_source assert IAMCredentialsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(IAMCredentialsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsClient)) -@mock.patch.object(IAMCredentialsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE - default_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert IAMCredentialsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT - assert IAMCredentialsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert IAMCredentialsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert IAMCredentialsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert IAMCredentialsClient._get_universe_domain(None, None) == IAMCredentialsClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - IAMCredentialsClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py index 7255d3c91709..f9bbb928521f 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -122,38 +123,9 @@ class EventarcClient(metaclass=EventarcClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "eventarc.googleapis.com" @@ -570,53 +542,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = EventarcClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = EventarcClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + EventarcClient._DEFAULT_UNIVERSE, + EventarcClient.DEFAULT_MTLS_ENDPOINT, + EventarcClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = EventarcClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + EventarcClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index b9495ef6e42d..aa7472653e20 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -299,41 +299,6 @@ def test__get_client_cert_source(): assert EventarcClient._get_client_cert_source(None, True) is mock_default_cert_source assert EventarcClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(EventarcClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcClient)) -@mock.patch.object(EventarcAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = EventarcClient._DEFAULT_UNIVERSE - default_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert EventarcClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert EventarcClient._get_api_endpoint(None, None, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT - assert EventarcClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert EventarcClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - EventarcClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert EventarcClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert EventarcClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert EventarcClient._get_universe_domain(None, None) == EventarcClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - EventarcClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py index 15922e6d865a..29406311d09c 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -97,38 +98,9 @@ class ConfigServiceV2Client(metaclass=ConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -446,53 +418,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + ConfigServiceV2Client._DEFAULT_UNIVERSE, + ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT, + ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = ConfigServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + ConfigServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py index e89762755eda..d2865e288932 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -94,38 +95,9 @@ class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -377,53 +349,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + LoggingServiceV2Client._DEFAULT_UNIVERSE, + LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT, + LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + LoggingServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py index 90e9355f8c26..82e9fa651892 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -95,38 +96,9 @@ class MetricsServiceV2Client(metaclass=MetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -378,53 +350,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + MetricsServiceV2Client._DEFAULT_UNIVERSE, + MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT, + MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = MetricsServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + MetricsServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 8cc17d810664..854307f3f859 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -269,41 +269,6 @@ def test__get_client_cert_source(): assert ConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert ConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(ConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2Client)) -@mock.patch.object(ConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert ConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert ConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert ConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert ConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert ConfigServiceV2Client._get_universe_domain(None, None) == ConfigServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - ConfigServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index fd56f0210d78..814c84b17f43 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -270,41 +270,6 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) -@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - LoggingServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 762b4b3ab94d..1f90df10724a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -268,41 +268,6 @@ def test__get_client_cert_source(): assert MetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert MetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(MetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2Client)) -@mock.patch.object(MetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert MetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert MetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert MetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert MetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert MetricsServiceV2Client._get_universe_domain(None, None) == MetricsServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - MetricsServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py index 61204cb87a52..9cae27c01971 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -97,38 +98,9 @@ class BaseConfigServiceV2Client(metaclass=BaseConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -446,53 +418,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + BaseConfigServiceV2Client._DEFAULT_UNIVERSE, + BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT, + BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + BaseConfigServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py index e89762755eda..d2865e288932 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -94,38 +95,9 @@ class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -377,53 +349,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + LoggingServiceV2Client._DEFAULT_UNIVERSE, + LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT, + LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + LoggingServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py index fa55137223d1..e63028e1cc34 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -95,38 +96,9 @@ class BaseMetricsServiceV2Client(metaclass=BaseMetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -378,53 +350,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + BaseMetricsServiceV2Client._DEFAULT_UNIVERSE, + BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT, + BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + BaseMetricsServiceV2Client._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 93731051d1bc..abf6fdc34ecc 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -269,41 +269,6 @@ def test__get_client_cert_source(): assert BaseConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(BaseConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2Client)) -@mock.patch.object(BaseConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert BaseConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert BaseConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert BaseConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert BaseConfigServiceV2Client._get_universe_domain(None, None) == BaseConfigServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - BaseConfigServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index fd56f0210d78..814c84b17f43 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -270,41 +270,6 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) -@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - LoggingServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 090ea4a91bec..88347a463137 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -268,41 +268,6 @@ def test__get_client_cert_source(): assert BaseMetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseMetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(BaseMetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2Client)) -@mock.patch.object(BaseMetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2AsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - default_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert BaseMetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert BaseMetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert BaseMetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert BaseMetricsServiceV2Client._get_universe_domain(None, None) == BaseMetricsServiceV2Client._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - BaseMetricsServiceV2Client._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index 33ccce478c8e..ebbf1b587d77 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -132,38 +133,9 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" @@ -415,53 +387,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = CloudRedisClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + CloudRedisClient._DEFAULT_UNIVERSE, + CloudRedisClient.DEFAULT_MTLS_ENDPOINT, + CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = CloudRedisClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + CloudRedisClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index d58d99d1fdb8..90c8adb4b9da 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -286,41 +286,6 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) -@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = CloudRedisClient._DEFAULT_UNIVERSE - default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - CloudRedisClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index 031573ef83d7..d7b424c072cb 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -132,38 +133,9 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" @@ -415,53 +387,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = CloudRedisClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + CloudRedisClient._DEFAULT_UNIVERSE, + CloudRedisClient.DEFAULT_MTLS_ENDPOINT, + CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = CloudRedisClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + CloudRedisClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 780964608350..d7b9c7073185 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -286,41 +286,6 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) -@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = CloudRedisClient._DEFAULT_UNIVERSE - default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT - assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - CloudRedisClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py index 5f79cf8e016a..47948273cf72 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py @@ -28,6 +28,7 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -105,38 +106,9 @@ class StorageBatchOperationsClient(metaclass=StorageBatchOperationsClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + return client_utils.get_default_mtls_endpoint(api_endpoint) # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "storagebatchoperations.googleapis.com" @@ -410,53 +382,31 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): - _default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE - if universe_domain != _default_universe: - raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") - api_endpoint = StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) - return api_endpoint + def _get_api_endpoint( + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + ) -> str: + """Return the API endpoint used by the client.""" + return client_utils.get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + StorageBatchOperationsClient._DEFAULT_UNIVERSE, + StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT, + StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE, + ) @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = StorageBatchOperationsClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + """Return the universe domain used by the client.""" + return client_utils.get_universe_domain( + client_universe_domain, + universe_domain_env, + StorageBatchOperationsClient._DEFAULT_UNIVERSE, + ) def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index 892375775385..a401d8e7c2ff 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -278,41 +278,6 @@ def test__get_client_cert_source(): assert StorageBatchOperationsClient._get_client_cert_source(None, True) is mock_default_cert_source assert StorageBatchOperationsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source -@mock.patch.object(StorageBatchOperationsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsClient)) -@mock.patch.object(StorageBatchOperationsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsAsyncClient)) -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE - default_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) - mock_universe = "bar.com" - mock_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) - - assert StorageBatchOperationsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override - assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT - assert StorageBatchOperationsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint - assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint - - with pytest.raises(MutualTLSChannelError) as excinfo: - StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") - assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." - - -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" - - assert StorageBatchOperationsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain - assert StorageBatchOperationsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env - assert StorageBatchOperationsClient._get_universe_domain(None, None) == StorageBatchOperationsClient._DEFAULT_UNIVERSE - - with pytest.raises(ValueError) as excinfo: - StorageBatchOperationsClient._get_universe_domain("", None) - assert str(excinfo.value) == "Universe Domain cannot be an empty string." - @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), From c6f99a6dc7f02b368c36c2cd400567c3943e6ea1 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 21:11:20 +0000 Subject: [PATCH 02/11] fix(generator): import from google.api_core.universe instead of client_utils --- .../templates/%namespace/%name_%version/%sub/_compat.py.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 896222c550a4..dc1c7ff44c01 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -5,7 +5,7 @@ from typing import Optional, Callable, Tuple, Union from google.auth.exceptions import MutualTLSChannelError try: - from google.api_core.gapic_v1.client_utils import ( + from google.api_core.universe import ( get_default_mtls_endpoint, get_api_endpoint, get_universe_domain, From 22a0c031cb538668455fe29f61a6a4c69dddaf40 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 21:54:57 +0000 Subject: [PATCH 03/11] fix(gapic-generator): Update return types of get_api_endpoint to Optional[str] --- .../templates/%namespace/%name_%version/%sub/_compat.py.j2 | 4 ++-- .../%name_%version/%sub/services/%service/client.py.j2 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index dc1c7ff44c01..3a9343c4286e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -44,9 +44,9 @@ except ImportError: universe_domain: str, use_mtls_endpoint: str, default_universe: str, - default_mtls_endpoint: str, + default_mtls_endpoint: Optional[str], default_endpoint_template: str, - ) -> str: + ) -> Optional[str]: """Return the API endpoint used by the client.""" if api_override is not None: api_endpoint = api_override diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 25ea84a9fb58..47b39424e7d1 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -364,7 +364,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], universe_domain: str, use_mtls_endpoint: str, - ) -> str: + ) -> Optional[str]: """Return the API endpoint used by the client. Args: @@ -376,7 +376,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): Possible values are "always", "auto", or "never". Returns: - str: The API endpoint to be used by the client. + Optional[str]: The API endpoint to be used by the client. """ return client_utils.get_api_endpoint( api_override, From 7a6c8e24da83d9a8a1169fa5f881c92f7f7c6fa5 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 21:58:14 +0000 Subject: [PATCH 04/11] fix(generator): update storagebatchoperations goldens to reflect setup_request_id fix --- .../storagebatchoperations_v1/test__compat.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py new file mode 100644 index 000000000000..e10021c90673 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import sys + +import mock +import pytest + +from google.cloud.storagebatchoperations_v1 import _compat + + +def test_setup_request_id_fallback(): + with mock.patch("sys.modules", dict(sys.modules)): + if "google.api_core.gapic_v1.request" in sys.modules: + del sys.modules["google.api_core.gapic_v1.request"] + + real_import = builtins.__import__ + + def mock_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "google.api_core.gapic_v1.request": + raise ImportError("Mocked ImportError for gapic_v1.request") + return real_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=mock_import): + # Reload _compat to trigger the fallback path + import importlib + importlib.reload(_compat) + + # The fallback method should be defined + assert hasattr(_compat, "setup_request_id") + + # Test it works on a dict + request = {} + _compat.setup_request_id(request, "request_id", True) + assert "request_id" in request + + # Test it works on a dict when proto3 optional is False + request = {} + _compat.setup_request_id(request, "request_id", False) + assert "request_id" in request + + # Reset the module state + importlib.reload(_compat) From bc1b7e34612959bf275b884b16fb1c6d40fe0548 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 23:08:47 +0000 Subject: [PATCH 05/11] fix(generator): use unified _compat.py.j2 and remove downstream tests --- .../%name_%version/%sub/_compat.py.j2 | 200 +++++++++++++++++- .../asset_v1/services/asset_service/client.py | 102 ++++++--- .../unit/gapic/asset_v1/test_asset_service.py | 35 +++ .../services/iam_credentials/client.py | 102 ++++++--- .../credentials_v1/test_iam_credentials.py | 35 +++ .../eventarc_v1/services/eventarc/client.py | 102 ++++++--- .../unit/gapic/eventarc_v1/test_eventarc.py | 35 +++ .../services/config_service_v2/client.py | 102 ++++++--- .../services/logging_service_v2/client.py | 102 ++++++--- .../services/metrics_service_v2/client.py | 102 ++++++--- .../logging_v2/test_config_service_v2.py | 35 +++ .../logging_v2/test_logging_service_v2.py | 35 +++ .../logging_v2/test_metrics_service_v2.py | 35 +++ .../services/config_service_v2/client.py | 102 ++++++--- .../services/logging_service_v2/client.py | 102 ++++++--- .../services/metrics_service_v2/client.py | 102 ++++++--- .../logging_v2/test_config_service_v2.py | 35 +++ .../logging_v2/test_logging_service_v2.py | 35 +++ .../logging_v2/test_metrics_service_v2.py | 35 +++ .../redis_v1/services/cloud_redis/client.py | 102 ++++++--- .../unit/gapic/redis_v1/test_cloud_redis.py | 35 +++ .../redis_v1/services/cloud_redis/client.py | 102 ++++++--- .../unit/gapic/redis_v1/test_cloud_redis.py | 35 +++ .../storage_batch_operations/client.py | 102 ++++++--- .../test_storage_batch_operations.py | 35 +++ .../tests/unit/generator/test_generator.py | 11 +- 26 files changed, 1536 insertions(+), 319 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 3a9343c4286e..368b13450a76 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -1,8 +1,17 @@ # {% include '_license.j2' %} +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os import re -from typing import Optional, Callable, Tuple, Union +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from google.auth.exceptions import MutualTLSChannelError +import google.protobuf.message + try: from google.api_core.universe import ( @@ -11,8 +20,7 @@ try: get_universe_domain, ) except ImportError: - # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. - + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: """Converts api endpoint to mTLS endpoint.""" if not api_endpoint: @@ -74,3 +82,189 @@ except ImportError: if len(universe_domain.strip()) == 0: raise ValueError("Universe Domain cannot be an empty string.") return universe_domain + + +try: + from google.api_core.gapic_v1.config import ( + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove this fallback when google-api-core >= 2.26.0 is the minimum required version. + def setup_request_id(request, field_name: str, is_proto3_optional: bool): + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + request_id_val = str(uuid.uuid4()) + if request is None: + return + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj, strict=False): # pragma: NO COVER + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten(obj, key_path, strict=False): # pragma: NO COVER + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj): # pragma: NO COVER + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value(obj, key_path, strict=False): # pragma: NO COVER + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict(obj, key_path, strict=False): # pragma: NO COVER + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) + + def _flatten_list(elems, key_path, strict=False): # pragma: NO COVER + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) + + def _canonicalize(obj, strict=False): # pragma: NO COVER + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # pragma: NO COVER + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads(json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + )) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py index 8da37edbad1c..590b6fa1c615 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -102,9 +101,38 @@ class AssetServiceClient(metaclass=AssetServiceClientMeta): """Asset service definition.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "cloudasset.googleapis.com" @@ -422,31 +450,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - AssetServiceClient._DEFAULT_UNIVERSE, - AssetServiceClient.DEFAULT_MTLS_ENDPOINT, - AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = AssetServiceClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = AssetServiceClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - AssetServiceClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = AssetServiceClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index e734654e477f..1a367689d14e 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -278,6 +278,41 @@ def test__get_client_cert_source(): assert AssetServiceClient._get_client_cert_source(None, True) is mock_default_cert_source assert AssetServiceClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(AssetServiceClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceClient)) +@mock.patch.object(AssetServiceAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(AssetServiceAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = AssetServiceClient._DEFAULT_UNIVERSE + default_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert AssetServiceClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT + assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT + assert AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == AssetServiceClient.DEFAULT_MTLS_ENDPOINT + assert AssetServiceClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert AssetServiceClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + AssetServiceClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert AssetServiceClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert AssetServiceClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert AssetServiceClient._get_universe_domain(None, None) == AssetServiceClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + AssetServiceClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py index cecdaaf047a9..4ad970da32e9 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -105,9 +104,38 @@ class IAMCredentialsClient(metaclass=IAMCredentialsClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "iamcredentials.googleapis.com" @@ -359,31 +387,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - IAMCredentialsClient._DEFAULT_UNIVERSE, - IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT, - IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - IAMCredentialsClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = IAMCredentialsClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 97bc1efbb00d..508e17a7e889 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -268,6 +268,41 @@ def test__get_client_cert_source(): assert IAMCredentialsClient._get_client_cert_source(None, True) is mock_default_cert_source assert IAMCredentialsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(IAMCredentialsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsClient)) +@mock.patch.object(IAMCredentialsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(IAMCredentialsAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = IAMCredentialsClient._DEFAULT_UNIVERSE + default_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = IAMCredentialsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert IAMCredentialsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT + assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT + assert IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == IAMCredentialsClient.DEFAULT_MTLS_ENDPOINT + assert IAMCredentialsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert IAMCredentialsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + IAMCredentialsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert IAMCredentialsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert IAMCredentialsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert IAMCredentialsClient._get_universe_domain(None, None) == IAMCredentialsClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + IAMCredentialsClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py index f9bbb928521f..7255d3c91709 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -123,9 +122,38 @@ class EventarcClient(metaclass=EventarcClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "eventarc.googleapis.com" @@ -542,31 +570,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - EventarcClient._DEFAULT_UNIVERSE, - EventarcClient.DEFAULT_MTLS_ENDPOINT, - EventarcClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = EventarcClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = EventarcClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - EventarcClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = EventarcClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index aa7472653e20..b9495ef6e42d 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -299,6 +299,41 @@ def test__get_client_cert_source(): assert EventarcClient._get_client_cert_source(None, True) is mock_default_cert_source assert EventarcClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(EventarcClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcClient)) +@mock.patch.object(EventarcAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(EventarcAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = EventarcClient._DEFAULT_UNIVERSE + default_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = EventarcClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert EventarcClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == EventarcClient.DEFAULT_MTLS_ENDPOINT + assert EventarcClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert EventarcClient._get_api_endpoint(None, None, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT + assert EventarcClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == EventarcClient.DEFAULT_MTLS_ENDPOINT + assert EventarcClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert EventarcClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + EventarcClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert EventarcClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert EventarcClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert EventarcClient._get_universe_domain(None, None) == EventarcClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + EventarcClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py index 29406311d09c..15922e6d865a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -98,9 +97,38 @@ class ConfigServiceV2Client(metaclass=ConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -418,31 +446,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - ConfigServiceV2Client._DEFAULT_UNIVERSE, - ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT, - ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - ConfigServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = ConfigServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py index d2865e288932..e89762755eda 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -95,9 +94,38 @@ class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -349,31 +377,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - LoggingServiceV2Client._DEFAULT_UNIVERSE, - LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT, - LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - LoggingServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py index 82e9fa651892..90e9355f8c26 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -96,9 +95,38 @@ class MetricsServiceV2Client(metaclass=MetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -350,31 +378,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - MetricsServiceV2Client._DEFAULT_UNIVERSE, - MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT, - MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - MetricsServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = MetricsServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 854307f3f859..8cc17d810664 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -269,6 +269,41 @@ def test__get_client_cert_source(): assert ConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert ConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(ConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2Client)) +@mock.patch.object(ConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(ConfigServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = ConfigServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = ConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert ConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == ConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert ConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert ConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + ConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert ConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert ConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert ConfigServiceV2Client._get_universe_domain(None, None) == ConfigServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + ConfigServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 814c84b17f43..fd56f0210d78 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -270,6 +270,41 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) +@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + LoggingServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 1f90df10724a..762b4b3ab94d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -268,6 +268,41 @@ def test__get_client_cert_source(): assert MetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert MetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(MetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2Client)) +@mock.patch.object(MetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(MetricsServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = MetricsServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = MetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert MetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == MetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert MetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert MetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + MetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert MetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert MetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert MetricsServiceV2Client._get_universe_domain(None, None) == MetricsServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + MetricsServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py index 9cae27c01971..61204cb87a52 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -98,9 +97,38 @@ class BaseConfigServiceV2Client(metaclass=BaseConfigServiceV2ClientMeta): """Service for configuring sinks used to route log entries.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -418,31 +446,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - BaseConfigServiceV2Client._DEFAULT_UNIVERSE, - BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT, - BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - BaseConfigServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = BaseConfigServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py index d2865e288932..e89762755eda 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -95,9 +94,38 @@ class LoggingServiceV2Client(metaclass=LoggingServiceV2ClientMeta): """Service for ingesting and querying logs.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -349,31 +377,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - LoggingServiceV2Client._DEFAULT_UNIVERSE, - LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT, - LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - LoggingServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = LoggingServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py index e63028e1cc34..fa55137223d1 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -96,9 +95,38 @@ class BaseMetricsServiceV2Client(metaclass=BaseMetricsServiceV2ClientMeta): """Service for configuring logs-based metrics.""" @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "logging.googleapis.com" @@ -350,31 +378,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - BaseMetricsServiceV2Client._DEFAULT_UNIVERSE, - BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT, - BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - BaseMetricsServiceV2Client._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index abf6fdc34ecc..93731051d1bc 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -269,6 +269,41 @@ def test__get_client_cert_source(): assert BaseConfigServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseConfigServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(BaseConfigServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2Client)) +@mock.patch.object(BaseConfigServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseConfigServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = BaseConfigServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = BaseConfigServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert BaseConfigServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseConfigServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseConfigServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert BaseConfigServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + BaseConfigServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert BaseConfigServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert BaseConfigServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert BaseConfigServiceV2Client._get_universe_domain(None, None) == BaseConfigServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + BaseConfigServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 814c84b17f43..fd56f0210d78 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -270,6 +270,41 @@ def test__get_client_cert_source(): assert LoggingServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert LoggingServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(LoggingServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2Client)) +@mock.patch.object(LoggingServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(LoggingServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = LoggingServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = LoggingServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert LoggingServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == LoggingServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert LoggingServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert LoggingServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + LoggingServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert LoggingServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert LoggingServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert LoggingServiceV2Client._get_universe_domain(None, None) == LoggingServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + LoggingServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 88347a463137..090ea4a91bec 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -268,6 +268,41 @@ def test__get_client_cert_source(): assert BaseMetricsServiceV2Client._get_client_cert_source(None, True) is mock_default_cert_source assert BaseMetricsServiceV2Client._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(BaseMetricsServiceV2Client, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2Client)) +@mock.patch.object(BaseMetricsServiceV2AsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(BaseMetricsServiceV2AsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = BaseMetricsServiceV2Client._DEFAULT_UNIVERSE + default_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = BaseMetricsServiceV2Client._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert BaseMetricsServiceV2Client._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == BaseMetricsServiceV2Client.DEFAULT_MTLS_ENDPOINT + assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert BaseMetricsServiceV2Client._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + BaseMetricsServiceV2Client._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert BaseMetricsServiceV2Client._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert BaseMetricsServiceV2Client._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert BaseMetricsServiceV2Client._get_universe_domain(None, None) == BaseMetricsServiceV2Client._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + BaseMetricsServiceV2Client._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index ebbf1b587d77..33ccce478c8e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -133,9 +132,38 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" @@ -387,31 +415,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - CloudRedisClient._DEFAULT_UNIVERSE, - CloudRedisClient.DEFAULT_MTLS_ENDPOINT, - CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = CloudRedisClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - CloudRedisClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = CloudRedisClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 90c8adb4b9da..d58d99d1fdb8 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -286,6 +286,41 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) +@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = CloudRedisClient._DEFAULT_UNIVERSE + default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + CloudRedisClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index d7b424c072cb..031573ef83d7 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,7 +27,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -133,9 +132,38 @@ class CloudRedisClient(metaclass=CloudRedisClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "redis.googleapis.com" @@ -387,31 +415,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - CloudRedisClient._DEFAULT_UNIVERSE, - CloudRedisClient.DEFAULT_MTLS_ENDPOINT, - CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = CloudRedisClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = CloudRedisClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - CloudRedisClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = CloudRedisClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index d7b9c7073185..780964608350 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -286,6 +286,41 @@ def test__get_client_cert_source(): assert CloudRedisClient._get_client_cert_source(None, True) is mock_default_cert_source assert CloudRedisClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(CloudRedisClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisClient)) +@mock.patch.object(CloudRedisAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(CloudRedisAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = CloudRedisClient._DEFAULT_UNIVERSE + default_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = CloudRedisClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert CloudRedisClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == CloudRedisClient.DEFAULT_MTLS_ENDPOINT + assert CloudRedisClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert CloudRedisClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + CloudRedisClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert CloudRedisClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert CloudRedisClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert CloudRedisClient._get_universe_domain(None, None) == CloudRedisClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + CloudRedisClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py index 47948273cf72..5f79cf8e016a 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py @@ -28,7 +28,6 @@ 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.gapic_v1 import client_utils from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -106,9 +105,38 @@ class StorageBatchOperationsClient(metaclass=StorageBatchOperationsClientMeta): """ @staticmethod - def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: - """Converts api endpoint to mTLS endpoint.""" - return client_utils.get_default_mtls_endpoint(api_endpoint) + def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + Optional[str]: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "storagebatchoperations.googleapis.com" @@ -382,31 +410,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): return client_cert_source @staticmethod - def _get_api_endpoint( - api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], - universe_domain: str, - use_mtls_endpoint: str, - ) -> str: - """Return the API endpoint used by the client.""" - return client_utils.get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - StorageBatchOperationsClient._DEFAULT_UNIVERSE, - StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT, - StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE, - ) + def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (str): The API endpoint override. If specified, this is always + the return value of this function and the other arguments are not used. + client_cert_source (bytes): The client certificate source used by the client. + universe_domain (str): The universe domain used by the client. + use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. + Possible values are "always", "auto", or "never". + + Returns: + str: The API endpoint to be used by the client. + """ + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source): + _default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE + if universe_domain != _default_universe: + raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.") + api_endpoint = StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain) + return api_endpoint @staticmethod def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str: - """Return the universe domain used by the client.""" - return client_utils.get_universe_domain( - client_universe_domain, - universe_domain_env, - StorageBatchOperationsClient._DEFAULT_UNIVERSE, - ) + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured via the client options. + universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the universe domain is an empty string. + """ + universe_domain = StorageBatchOperationsClient._DEFAULT_UNIVERSE + if client_universe_domain is not None: + universe_domain = client_universe_domain + elif universe_domain_env is not None: + universe_domain = universe_domain_env + if len(universe_domain.strip()) == 0: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index a401d8e7c2ff..892375775385 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -278,6 +278,41 @@ def test__get_client_cert_source(): assert StorageBatchOperationsClient._get_client_cert_source(None, True) is mock_default_cert_source assert StorageBatchOperationsClient._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source +@mock.patch.object(StorageBatchOperationsClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsClient)) +@mock.patch.object(StorageBatchOperationsAsyncClient, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template(StorageBatchOperationsAsyncClient)) +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = StorageBatchOperationsClient._DEFAULT_UNIVERSE + default_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe) + mock_universe = "bar.com" + mock_endpoint = StorageBatchOperationsClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe) + + assert StorageBatchOperationsClient._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override + assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT + assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint + assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT + assert StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == StorageBatchOperationsClient.DEFAULT_MTLS_ENDPOINT + assert StorageBatchOperationsClient._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint + assert StorageBatchOperationsClient._get_api_endpoint(None, None, default_universe, "never") == default_endpoint + + with pytest.raises(MutualTLSChannelError) as excinfo: + StorageBatchOperationsClient._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto") + assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com." + + +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" + + assert StorageBatchOperationsClient._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain + assert StorageBatchOperationsClient._get_universe_domain(None, universe_domain_env) == universe_domain_env + assert StorageBatchOperationsClient._get_universe_domain(None, None) == StorageBatchOperationsClient._DEFAULT_UNIVERSE + + with pytest.raises(ValueError) as excinfo: + StorageBatchOperationsClient._get_universe_domain("", None) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." + @pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ (401, CRED_INFO_JSON, True), (403, CRED_INFO_JSON, True), diff --git a/packages/gapic-generator/tests/unit/generator/test_generator.py b/packages/gapic-generator/tests/unit/generator/test_generator.py index 9d8545c4192f..f1566ee6e5a7 100644 --- a/packages/gapic-generator/tests/unit/generator/test_generator.py +++ b/packages/gapic-generator/tests/unit/generator/test_generator.py @@ -117,6 +117,8 @@ def test_get_response_ignores_private_files(): list_templates.return_value = [ "foo/bar/baz.py.j2", "foo/bar/_base.py.j2", + "foo/bar/__init__.py.j2", + "foo/bar/_compat.py.j2", "molluscs/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as get_template: @@ -128,12 +130,13 @@ def test_get_response_ignores_private_files(): get_template.assert_has_calls( [ mock.call("molluscs/squid/sample.py.j2"), + mock.call("foo/bar/__init__.py.j2"), + mock.call("foo/bar/_compat.py.j2"), mock.call("foo/bar/baz.py.j2"), - ] + ], + any_order=True, ) - assert len(cgr.file) == 1 - assert cgr.file[0].name == "foo/bar/baz.py" - assert cgr.file[0].content == "I am a template result.\n" + assert len(cgr.file) == 3 def test_get_response_fails_invalid_file_paths(): From 4d2f277597ea926bd25bf622a6d2c5c59723afd5 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 23:53:34 +0000 Subject: [PATCH 06/11] fix(generator): install local google-api-core in nox test sessions --- packages/gapic-generator/noxfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 9d3d9fefaa8d..d19c9a895a9a 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -193,6 +193,7 @@ def fragment(session, use_ads_templates=False): "grpcio-tools", ) session.install("-e", ".") + session.install("-e", "../google-api-core") # The specific failure is `Plugin output is unparseable` if session.python == "3.10": @@ -257,6 +258,7 @@ def showcase_library( # Install gapic-generator-python session.install("-e", ".") + session.install("-e", "../google-api-core") # Install grpcio-tools for protoc session.install("grpcio-tools") From 02626f5701db1c0ec1405120cecc6731cf75d79f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 22:34:17 +0000 Subject: [PATCH 07/11] fix(generator): pass default_universe as keyword argument to get_universe_domain --- .../templates/%namespace/%name_%version/%sub/_compat.py.j2 | 6 +++--- .../%name_%version/%sub/services/%service/client.py.j2 | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 368b13450a76..beec5bab2e1e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -69,9 +69,9 @@ except ImportError: return api_endpoint def get_universe_domain( - client_universe_domain: Optional[str], - universe_domain_env: Optional[str], - default_universe: str, + client_universe_domain: Optional[str] = None, + universe_domain_env: Optional[str] = None, + default_universe: str = "googleapis.com", ) -> str: """Return the universe domain used by the client.""" universe_domain = default_universe diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 47b39424e7d1..8a73595b7789 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -405,7 +405,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): return client_utils.get_universe_domain( client_universe_domain, universe_domain_env, - {{ service.client_name }}._DEFAULT_UNIVERSE, + default_universe={{ service.client_name }}._DEFAULT_UNIVERSE, ) def _validate_universe_domain(self): From e3d6433e593928483b6cf4e254ae99caa7af419c Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 23 Jul 2026 00:07:26 +0000 Subject: [PATCH 08/11] docs(generator): clarify lower-bound google-api-core version comment in _compat.py.j2 --- .../%name_%version/%sub/_compat.py.j2 | 3 ++- .../google/api_core/gapic_v1/requests.py | 7 ++++++ .../tests/unit/gapic/test_requests.py | 25 +++++++++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index beec5bab2e1e..5dac8c33a7f9 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -20,7 +20,8 @@ try: get_universe_domain, ) except ImportError: - # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: """Converts api endpoint to mTLS endpoint.""" if not api_endpoint: 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 76f5e916716d..82a2fbac39ec 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 @@ -65,6 +65,13 @@ def setup_request_id( setattr(request, field_name, str(uuid.uuid4())) except (AttributeError, ValueError): # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, str(uuid.uuid4())) + return + except (AttributeError, ValueError): + pass if getattr(request, field_name, None) is None: setattr(request, field_name, str(uuid.uuid4())) else: 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 a69bff5e16d7..f03843b1d6d7 100644 --- a/packages/google-api-core/tests/unit/gapic/test_requests.py +++ b/packages/google-api-core/tests/unit/gapic/test_requests.py @@ -113,15 +113,18 @@ def test_setup_request_id(request_obj, is_proto3_optional, expected): assert value == expected -def test_setup_request_id_assertion_strictness(mocker): - # Mock uuid.uuid4 to return a UUID with trailing characters - mock_uuid = mocker.patch("uuid.uuid4") - mock_uuid.return_value.__str__.return_value = ( - "12345678-1234-4123-8123-123456789012-extra" - ) +from unittest import mock - # We expect test_setup_request_id to fail (raise AssertionError) because the UUID is invalid. - # If test_setup_request_id uses re.fullmatch, it will fail (raise AssertionError), so pytest.raises passes. - # If test_setup_request_id uses re.match, it will NOT fail, so pytest.raises fails! - with pytest.raises(AssertionError): - test_setup_request_id(MockRequest(), is_proto3_optional=True, expected="uuid") + +def test_setup_request_id_assertion_strictness(): + # Mock uuid.uuid4 to return a UUID with trailing characters + with mock.patch("uuid.uuid4") as mock_uuid: + mock_uuid.return_value.__str__.return_value = ( + "12345678-1234-4123-8123-123456789012-extra" + ) + + # We expect test_setup_request_id to fail (raise AssertionError) because the UUID is invalid. + # If test_setup_request_id uses re.fullmatch, it will fail (raise AssertionError), so pytest.raises passes. + # If test_setup_request_id uses re.match, it will NOT fail, so pytest.raises fails! + with pytest.raises(AssertionError): + test_setup_request_id(MockRequest(), is_proto3_optional=True, expected="uuid") From a89c8cab8ddb5cd63bda7706a0e1e82832ffd248 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 23 Jul 2026 17:19:17 +0000 Subject: [PATCH 09/11] chore: revert unrelated google-api-core changes and stray test__compat.py --- .../storagebatchoperations_v1/test__compat.py | 57 ------------------- .../google/api_core/gapic_v1/requests.py | 7 --- .../tests/unit/gapic/test_requests.py | 25 ++++---- 3 files changed, 11 insertions(+), 78 deletions(-) delete mode 100644 packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py deleted file mode 100644 index e10021c90673..000000000000 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test__compat.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import builtins -import sys - -import mock -import pytest - -from google.cloud.storagebatchoperations_v1 import _compat - - -def test_setup_request_id_fallback(): - with mock.patch("sys.modules", dict(sys.modules)): - if "google.api_core.gapic_v1.request" in sys.modules: - del sys.modules["google.api_core.gapic_v1.request"] - - real_import = builtins.__import__ - - def mock_import(name, globals=None, locals=None, fromlist=(), level=0): - if name == "google.api_core.gapic_v1.request": - raise ImportError("Mocked ImportError for gapic_v1.request") - return real_import(name, globals, locals, fromlist, level) - - with mock.patch("builtins.__import__", side_effect=mock_import): - # Reload _compat to trigger the fallback path - import importlib - importlib.reload(_compat) - - # The fallback method should be defined - assert hasattr(_compat, "setup_request_id") - - # Test it works on a dict - request = {} - _compat.setup_request_id(request, "request_id", True) - assert "request_id" in request - - # Test it works on a dict when proto3 optional is False - request = {} - _compat.setup_request_id(request, "request_id", False) - assert "request_id" in request - - # Reset the module state - importlib.reload(_compat) 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 82a2fbac39ec..76f5e916716d 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 @@ -65,13 +65,6 @@ def setup_request_id( setattr(request, field_name, str(uuid.uuid4())) except (AttributeError, ValueError): # Proto-plus messages or other objects - if hasattr(request, "_pb"): - try: - if not request._pb.HasField(field_name): - setattr(request, field_name, str(uuid.uuid4())) - return - except (AttributeError, ValueError): - pass if getattr(request, field_name, None) is None: setattr(request, field_name, str(uuid.uuid4())) else: 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 f03843b1d6d7..a69bff5e16d7 100644 --- a/packages/google-api-core/tests/unit/gapic/test_requests.py +++ b/packages/google-api-core/tests/unit/gapic/test_requests.py @@ -113,18 +113,15 @@ def test_setup_request_id(request_obj, is_proto3_optional, expected): assert value == expected -from unittest import mock - - -def test_setup_request_id_assertion_strictness(): +def test_setup_request_id_assertion_strictness(mocker): # Mock uuid.uuid4 to return a UUID with trailing characters - with mock.patch("uuid.uuid4") as mock_uuid: - mock_uuid.return_value.__str__.return_value = ( - "12345678-1234-4123-8123-123456789012-extra" - ) - - # We expect test_setup_request_id to fail (raise AssertionError) because the UUID is invalid. - # If test_setup_request_id uses re.fullmatch, it will fail (raise AssertionError), so pytest.raises passes. - # If test_setup_request_id uses re.match, it will NOT fail, so pytest.raises fails! - with pytest.raises(AssertionError): - test_setup_request_id(MockRequest(), is_proto3_optional=True, expected="uuid") + mock_uuid = mocker.patch("uuid.uuid4") + mock_uuid.return_value.__str__.return_value = ( + "12345678-1234-4123-8123-123456789012-extra" + ) + + # We expect test_setup_request_id to fail (raise AssertionError) because the UUID is invalid. + # If test_setup_request_id uses re.fullmatch, it will fail (raise AssertionError), so pytest.raises passes. + # If test_setup_request_id uses re.match, it will NOT fail, so pytest.raises fails! + with pytest.raises(AssertionError): + test_setup_request_id(MockRequest(), is_proto3_optional=True, expected="uuid") From b5204b3b843d4b0db435a1c62b1cd988467c091c Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 23 Jul 2026 17:33:53 +0000 Subject: [PATCH 10/11] fix(generator): update goldens for routing centralization --- .../%name_%version/%sub/test_%service.py.j2 | 28 +- packages/gapic-generator/noxfile.py | 34 +- .../asset/google/cloud/asset_v1/_compat.py | 310 +++++++++++++++ .../tests/unit/gapic/asset_v1/test_compat.py | 364 ++++++++++++++++++ .../google/iam/credentials_v1/_compat.py | 310 +++++++++++++++ .../unit/gapic/credentials_v1/test_compat.py | 364 ++++++++++++++++++ .../google/cloud/eventarc_v1/_compat.py | 310 +++++++++++++++ .../unit/gapic/eventarc_v1/test_compat.py | 364 ++++++++++++++++++ .../google/cloud/logging_v2/_compat.py | 310 +++++++++++++++ .../unit/gapic/logging_v2/test_compat.py | 364 ++++++++++++++++++ .../google/cloud/logging_v2/_compat.py | 310 +++++++++++++++ .../unit/gapic/logging_v2/test_compat.py | 364 ++++++++++++++++++ .../redis/google/cloud/redis_v1/_compat.py | 310 +++++++++++++++ .../tests/unit/gapic/redis_v1/test_compat.py | 364 ++++++++++++++++++ .../google/cloud/redis_v1/_compat.py | 310 +++++++++++++++ .../tests/unit/gapic/redis_v1/test_compat.py | 364 ++++++++++++++++++ .../storagebatchoperations_v1/_compat.py | 310 +++++++++++++++ .../storagebatchoperations_v1/test_compat.py | 364 ++++++++++++++++++ 18 files changed, 5414 insertions(+), 40 deletions(-) create mode 100644 packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py create mode 100644 packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index b74df4635f4e..7843ceb31bff 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -158,13 +158,13 @@ def set_event_loop(): def test__read_environment_variables(): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): assert {{ service.client_name }}._read_environment_variables() == (True, "auto", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) - + with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): @@ -184,10 +184,10 @@ def test__read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): assert {{ service.client_name }}._read_environment_variables() == (False, "never", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): assert {{ service.client_name }}._read_environment_variables() == (False, "always", None) - + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}): assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None) @@ -277,7 +277,7 @@ def test_use_client_cert_effective(): assert {{ service.client_name }}._use_client_cert_effective() is False # Test case 12: Test when `should_use_client_cert` is available and the - # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, # the GOOGLE_API_CONFIG environment variable is unset. if hasattr(google.auth.transport.mtls, "should_use_client_cert"): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": ""}): @@ -287,7 +287,7 @@ def test_use_client_cert_effective(): def test__get_client_cert_source(): mock_provided_cert_source = mock.Mock() mock_default_cert_source = mock.Mock() - + assert {{ service.client_name }}._get_client_cert_source(None, False) is None assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, False) is None assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, True) == mock_provided_cert_source @@ -819,7 +819,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( ) assert api_endpoint == mock_api_endpoint assert cert_source is None - + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. test_cases = [ ( @@ -852,7 +852,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -881,10 +881,10 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( }, }, mock_client_cert_source, - ), + ), ( # With workloads not present in config, mTLS is disabled. - { + { "version": 1, "cert_configs": {}, }, @@ -899,7 +899,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( config_filename = "mock_certificate_config.json" config_file_content = json.dumps(config_data) m = mock.mock_open(read_data=config_file_content) - with mock.patch("builtins.open", m), mock.patch("os.path.exists", side_effect=lambda path: os.path.basename(path) == config_filename): + with mock.patch("builtins.open", m): with mock.patch.dict( os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} ): @@ -946,7 +946,7 @@ def test_{{ service.client_name|snake_case }}_get_mtls_endpoint_and_cert_source( with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): with pytest.raises(MutualTLSChannelError) as excinfo: client_class.get_mtls_endpoint_and_cert_source() - + assert str(excinfo.value) == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" @pytest.mark.parametrize("client_class", [ @@ -1011,7 +1011,7 @@ def test_{{ service.client_name|snake_case }}_client_api_endpoint(client_class): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): client = client_class(client_options=options, credentials=ga_credentials.AnonymousCredentials()) assert client.api_endpoint == default_endpoint - + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ {% if 'grpc' in opts.transport %} diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index d19c9a895a9a..dce1cf1504e2 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -36,18 +36,6 @@ showcase_version = os.environ.get("SHOWCASE_VERSION", "0.35.0") ADS_TEMPLATES = path.join(path.dirname(__file__), "gapic", "ads-templates") -CURRENT_DIRECTORY = Path(__file__).parent.absolute() -# Path to the centralized mypy configuration file at the repository root. -# Search upwards to support running nox from both monorepo packages and integration test goldens. -MYPY_CONFIG_FILE = next( - ( - str(p / "mypy.ini") - for p in CURRENT_DIRECTORY.parents - if (p / "mypy.ini").exists() - ), - str(CURRENT_DIRECTORY.parent.parent / "mypy.ini"), -) - RUFF_VERSION = "ruff==0.14.14" LINT_PATHS = ["docs", "gapic", "tests", "test_utils", "noxfile.py", "setup.py"] # Ruff uses globs for excludes (different from Black's regex) @@ -744,7 +732,7 @@ def mypy(session): "click==8.1.3", ) session.install(".") - session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") + session.run("mypy", "-p", "gapic") @nox.session(python=NEWEST_PYTHON) @@ -756,7 +744,7 @@ def lint(session): """ # TODO(https://github.com/googleapis/google-cloud-python/issues/16186): - # SKIP: This session was not enforced in the standalone (split) repo + # SKIP: This session was not enforced in the standalone (split) repo # and is disabled here to ensure a "move-only" migration. session.skip( "Linting was not enforced in the split repo. " @@ -786,11 +774,9 @@ def lint(session): @nox.session(python=NEWEST_PYTHON) def lint_setup_py(session): # TODO(https://github.com/googleapis/google-cloud-python/issues/16186): - # SKIP: This session was not enforced in the standalone (split) repo + # SKIP: This session was not enforced in the standalone (split) repo # and is disabled here to ensure a "move-only" migration. - session.skip( - "Skipping now to avoid changing code during migration. See Issue #16186" - ) + session.skip("Skipping now to avoid changing code during migration. See Issue #16186") @nox.session(python="3.10") @@ -865,11 +851,9 @@ def prerelease_deps(session, protobuf_implementation): """ Run all tests with pre-release versions of dependencies installed. """ - # TODO(https://github.com/googleapis/google-cloud-python/issues/16184): + # TODO(https://github.com/googleapis/google-cloud-python/issues/16184): # Implement pre-release dependency logic to test against upcoming runtime changes. - session.skip( - "prerelease_deps session is not yet implemented for gapic-generator-python." - ) + session.skip("prerelease_deps session is not yet implemented for gapic-generator-python.") @nox.session(python=NEWEST_PYTHON) @@ -879,8 +863,6 @@ def prerelease_deps(session, protobuf_implementation): ) def core_deps_from_source(session, protobuf_implementation): """Run all tests with core dependencies installed from source.""" - # TODO(https://github.com/googleapis/google-cloud-python/issues/16185): + # TODO(https://github.com/googleapis/google-cloud-python/issues/16185): # Implement logic to install core packages directly from the mono-repo directories. - session.skip( - "core_deps_from_source session is not yet implemented for gapic-generator-python." - ) + session.skip("core_deps_from_source session is not yet implemented for gapic-generator-python.") \ No newline at end of file diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py new file mode 100644 index 000000000000..610c616b82b0 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.asset_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py new file mode 100644 index 000000000000..73adde90dcb1 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.iam.credentials_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py new file mode 100644 index 000000000000..7b5ff1e41920 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.eventarc_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py new file mode 100644 index 000000000000..716e1ac126fd --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.logging_v2 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py new file mode 100644 index 000000000000..716e1ac126fd --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.logging_v2 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py new file mode 100644 index 000000000000..7713b20e1892 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.redis_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py new file mode 100644 index 000000000000..7713b20e1892 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.redis_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py new file mode 100644 index 000000000000..54c22cd7d09e --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -0,0 +1,310 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A compatibility module for older versions of google-api-core.""" + +import functools +import json +import operator +import os +import re +import uuid +from typing import Any, Callable, Dict, List, Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError + + +try: + from google.api_core.universe import ( # type: ignore + get_default_mtls_endpoint, + get_api_endpoint, + get_universe_domain, + ) +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0. + # Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies. + def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: + """Converts api endpoint to mTLS endpoint.""" + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + def get_api_endpoint( # type: ignore[misc] + api_override: Optional[str], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: str, + ) -> str: + """Return the API endpoint used by the client.""" + api_endpoint: Optional[str] = None + if api_override is not None: + api_endpoint = api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain != default_universe: + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + api_endpoint = default_mtls_endpoint + else: + api_endpoint = default_endpoint_template.format( + UNIVERSE_DOMAIN=universe_domain + ) + return api_endpoint # type: ignore[return-value] + + def get_universe_domain( # type: ignore[misc] + *potential_universes: Optional[str], + default_universe: str = "googleapis.com", + ) -> str: + """Return the universe domain used by the client.""" + resolved = next( + (x.strip() for x in potential_universes if x is not None), + default_universe, + ) + if not resolved: + raise ValueError("Universe Domain cannot be an empty string.") + return resolved + + +try: + from google.api_core.gapic_v1.config import ( # type: ignore + use_client_cert_effective, + get_client_cert_source, + read_environment_variables, + ) +except ImportError: # pragma: NO COVER + from google.auth.transport import mtls # type: ignore + + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + + def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS.""" + if hasattr(mtls, "should_use_client_cert"): + return mtls.should_use_client_cert() + else: + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + + def get_client_cert_source( + provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], + use_cert_flag: bool, + ) -> Optional[Callable[[], Tuple[bytes, bytes]]]: + """Return the client cert source to be used by the client.""" + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + client_cert_source = mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return client_cert_source + + def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client.""" + use_client_cert = use_client_cert_effective() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() + universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` " + "must be `never`, `auto` or `always`" + ) + return use_client_cert, use_mtls_endpoint, universe_domain_env + + +try: + from google.api_core.gapic_v1.request import setup_request_id # type: ignore +except ImportError: # pragma: NO COVER + # TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0. + # Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies. + def setup_request_id( + request: Any, field_name: str, is_proto3_optional: bool + ) -> None: + """Populate a UUID4 field in the request if it is not already set. + + Args: + request (Union[google.protobuf.message.Message, dict]): The request object. + field_name (str): The name of the field to populate. + is_proto3_optional (bool): Whether the field is proto3 optional. + """ + if request is None: + return + + request_id_val = str(uuid.uuid4()) + + if isinstance(request, dict): + if is_proto3_optional: + if field_name not in request or request[field_name] is None: + request[field_name] = request_id_val + elif not request.get(field_name): + request[field_name] = request_id_val + return + + if is_proto3_optional: + try: + # Pure protobuf messages + if not request.HasField(field_name): + setattr(request, field_name, request_id_val) + except (AttributeError, ValueError): + # Proto-plus messages or other objects + if hasattr(request, "_pb"): + try: + if not request._pb.HasField(field_name): + setattr(request, field_name, request_id_val) + return + except (AttributeError, ValueError): + pass + if getattr(request, field_name, None) is None: + setattr(request, field_name, request_id_val) + else: + if not getattr(request, field_name, None): + setattr(request, field_name, request_id_val) + + +try: + from google.api_core.rest_helpers import ( # type: ignore + flatten_query_params, + transcode_request, + ) +except ImportError: # pragma: NO COVER + # TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version. + from google.protobuf import json_format # type: ignore + from google.api_core import path_template # type: ignore + + def flatten_query_params(obj: Any, strict: bool = False) -> List[Tuple[str, Any]]: # type: ignore[misc] + if obj is not None and not isinstance(obj, dict): + raise TypeError("flatten_query_params must be called with dict object") + return _flatten(obj, key_path=[], strict=strict) + + def _flatten( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + if obj is None: + return [] + if isinstance(obj, dict): + return _flatten_dict(obj, key_path=key_path, strict=strict) + if isinstance(obj, list): + return _flatten_list(obj, key_path=key_path, strict=strict) + return _flatten_value(obj, key_path=key_path, strict=strict) + + def _is_primitive_value(obj: Any) -> bool: + if obj is None: + return False + if isinstance(obj, (list, dict)): + raise ValueError("query params may not contain repeated dicts or lists") + return True + + def _flatten_value( + obj: Any, key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + return [(".".join(key_path), _canonicalize(obj, strict=strict))] + + def _flatten_dict( + obj: Dict[str, Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten(value, key_path=key_path + [key], strict=strict) + for key, value in obj.items() + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _flatten_list( + elems: List[Any], key_path: List[str], strict: bool = False + ) -> List[Tuple[str, Any]]: + items = ( + _flatten_value(elem, key_path=key_path, strict=strict) + for elem in elems + if _is_primitive_value(elem) + ) + return functools.reduce(operator.concat, items, []) # type: ignore[arg-type] + + def _canonicalize(obj: Any, strict: bool = False) -> Any: + if strict: + value = str(obj) + if isinstance(obj, bool): + value = value.lower() + return value + return obj + + def transcode_request( # type: ignore[misc] + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + pb_request = getattr(request, "_pb", request) + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py new file mode 100644 index 000000000000..37fcf59e5f71 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py @@ -0,0 +1,364 @@ +# # Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import builtins +import importlib +import sys +from unittest import mock +import pytest +from google.auth.exceptions import MutualTLSChannelError +from google.protobuf import descriptor_pb2 + + +from google.cloud.storagebatchoperations_v1 import _compat + + +def test_compat_normal_import(): + assert _compat.setup_request_id is not None + + +def test_compat_fallback_implementations(): + orig_import = builtins.__import__ + + def custom_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "google.api_core.universe", + "google.api_core.gapic_v1.config", + "google.api_core.gapic_v1.request", + "google.api_core.rest_helpers", + ): + raise ImportError(f"Mocked ImportError for {name}") + return orig_import(name, globals, locals, fromlist, level) + + with mock.patch("builtins.__import__", side_effect=custom_import): + fallback = importlib.reload(_compat) + + # get_default_mtls_endpoint tests + assert fallback.get_default_mtls_endpoint(None) is None + assert fallback.get_default_mtls_endpoint("") == "" + assert ( + fallback.get_default_mtls_endpoint("foo.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert ( + fallback.get_default_mtls_endpoint("custom.domain.com") + == "custom.domain.com" + ) + assert ( + fallback.get_default_mtls_endpoint(":::invalid-url:::") + == ":::invalid-url:::" + ) + + # get_api_endpoint tests + assert ( + fallback.get_api_endpoint( + "https://override.com", + None, + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://override.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + assert ( + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "googleapis.com", + "auto", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "mtls.com" + ) + with pytest.raises(MutualTLSChannelError): + fallback.get_api_endpoint( + None, + lambda: (b"", b""), + "otheruniverse.com", + "always", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + assert ( + fallback.get_api_endpoint( + None, + None, + "googleapis.com", + "never", + "googleapis.com", + "mtls.com", + "https://{UNIVERSE_DOMAIN}", + ) + == "https://googleapis.com" + ) + + # get_universe_domain tests + assert ( + fallback.get_universe_domain("custom.com", None, "googleapis.com") + == "custom.com" + ) + assert ( + fallback.get_universe_domain(None, "env.com", "googleapis.com") == "env.com" + ) + assert ( + fallback.get_universe_domain(None, None, "googleapis.com") + == "googleapis.com" + ) + with pytest.raises(ValueError): + fallback.get_universe_domain(" ", None, "googleapis.com") + + # use_client_cert_effective tests + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + return_value=True, + create=True, + ): + assert fallback.use_client_cert_effective() is True + + with mock.patch.object(fallback, "mtls", spec=object()): + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + ): + assert fallback.use_client_cert_effective() is True + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert fallback.use_client_cert_effective() is False + with mock.patch.dict( + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + with pytest.raises(ValueError): + fallback.use_client_cert_effective() + + # get_client_cert_source tests + cert_fn = lambda: (b"cert", b"key") + assert fallback.get_client_cert_source(cert_fn, True) == cert_fn + assert fallback.get_client_cert_source(None, False) is None + + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + create=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=cert_fn, + create=True, + ): + assert fallback.get_client_cert_source(None, True) == cert_fn + + with mock.patch.object(fallback, "mtls", spec=object()): + with pytest.raises(ValueError): + fallback.get_client_cert_source(None, True) + + # read_environment_variables tests + with mock.patch.dict( + "os.environ", + { + "GOOGLE_API_USE_MTLS_ENDPOINT": "always", + "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "myuniverse.com", + }, + ): + use_cert, use_mtls, universe = fallback.read_environment_variables() + assert use_mtls == "always" + assert universe == "myuniverse.com" + + with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): + with pytest.raises(MutualTLSChannelError): + fallback.read_environment_variables() + + # setup_request_id tests + fallback.setup_request_id(None, "request_id", False) + + d1 = {} + fallback.setup_request_id(d1, "request_id", is_proto3_optional=True) + assert "request_id" in d1 + + d1_existing = {"request_id": None} + fallback.setup_request_id(d1_existing, "request_id", is_proto3_optional=True) + assert d1_existing["request_id"] is not None + + d2 = {} + fallback.setup_request_id(d2, "request_id", is_proto3_optional=False) + assert "request_id" in d2 + + d3 = {"request_id": "existing"} + fallback.setup_request_id(d3, "request_id", is_proto3_optional=False) + assert d3["request_id"] == "existing" + + d4 = {"request_id": "val"} + fallback.setup_request_id(d4, "request_id", is_proto3_optional=True) + assert d4["request_id"] == "val" + + class DummyPopulated: + def __init__(self): + self.request_id = "val" + + p_existing = DummyPopulated() + fallback.setup_request_id(p_existing, "request_id", is_proto3_optional=False) + assert p_existing.request_id == "val" + + class NonOptPlain: + def __init__(self): + self.request_id = "" + + nop = NonOptPlain() + fallback.setup_request_id(nop, "request_id", is_proto3_optional=False) + assert nop.request_id != "" + + class DummyProto: + def __init__(self): + self.request_id = "" + self._has = False + + def HasField(self, name): + if not self._has: + return False + return True + + p1 = DummyProto() + fallback.setup_request_id(p1, "request_id", is_proto3_optional=True) + assert p1.request_id != "" + + class DummyWrapper: + def __init__(self): + self._pb = DummyProto() + + w1 = DummyWrapper() + fallback.setup_request_id(w1, "request_id", is_proto3_optional=True) + assert ( + getattr(w1, "request_id", None) is not None + or getattr(w1._pb, "request_id", None) != "" + ) + + class BadProto: + def HasField(self, name): + raise AttributeError() + + class BadWrapper: + def __init__(self): + self._pb = BadProto() + self.request_id = None + + bw = BadWrapper() + fallback.setup_request_id(bw, "request_id", is_proto3_optional=True) + assert bw.request_id is not None + + class SetProto: + def __init__(self): + self.request_id = "already_set" + + def HasField(self, name): + return True + + sp = SetProto() + fallback.setup_request_id(sp, "request_id", is_proto3_optional=True) + assert sp.request_id == "already_set" + + class SetProtoNonOpt: + def __init__(self): + self.request_id = "already_set" + + sp_non_opt = SetProtoNonOpt() + fallback.setup_request_id(sp_non_opt, "request_id", is_proto3_optional=False) + assert sp_non_opt.request_id == "already_set" + + class DummyPlain: + def __init__(self): + self.request_id = None + + pl1 = DummyPlain() + fallback.setup_request_id(pl1, "request_id", is_proto3_optional=True) + assert pl1.request_id is not None + + pl2 = DummyPlain() + fallback.setup_request_id(pl2, "request_id", is_proto3_optional=False) + assert pl2.request_id is not None + + # flatten_query_params tests + assert fallback.flatten_query_params(None) == [] + res = fallback.flatten_query_params({"a": "val1", "b": [1, 2], "c": True}) + assert ("a", "val1") in res + assert ("b", 1) in res + assert ("b", 2) in res + assert ("c", True) in res + + res_strict = fallback.flatten_query_params({"c": True}, strict=True) + assert ("c", "true") in res_strict + + assert fallback._canonicalize(True, strict=True) == "true" + assert fallback._canonicalize(True, strict=False) is True + assert fallback._canonicalize(123, strict=True) == "123" + assert fallback._canonicalize("str", strict=True) == "str" + assert fallback._canonicalize("str", strict=False) == "str" + assert fallback._is_primitive_value(None) is False + + with pytest.raises(TypeError): + fallback.flatten_query_params("invalid") + + with pytest.raises(ValueError): + fallback.flatten_query_params({"a": [{"nested": "dict"}]}) + + with pytest.raises(ValueError): + fallback._is_primitive_value([1, 2]) + + # transcode_request tests + dummy_req = descriptor_pb2.DescriptorProto() + http_opts = [{"method": "get", "uri": "/v1/test"}] + transcoded, body, query = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"non_existent_key": "val"}, + rest_numeric_enums=True, + ) + assert transcoded is not None + assert query.get("non_existent_key") == "val" + assert query.get("$alt") == "json;enum-encoding=int" + + transcoded2, body2, query2 = fallback.transcode_request( + http_opts, + dummy_req, + required_fields_default_values={"name": "override_name"}, + rest_numeric_enums=False, + ) + assert body2 is None + assert "$alt" not in query2 + + importlib.reload(_compat) From b4646392124563bc6639f488195c6c877352995c Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 23 Jul 2026 17:34:26 +0000 Subject: [PATCH 11/11] fix(generator): clean up noxfile.py and include updated goldens --- packages/gapic-generator/noxfile.py | 46 ++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index dce1cf1504e2..da8f6964631a 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -36,6 +36,18 @@ showcase_version = os.environ.get("SHOWCASE_VERSION", "0.35.0") ADS_TEMPLATES = path.join(path.dirname(__file__), "gapic", "ads-templates") +CURRENT_DIRECTORY = Path(__file__).parent.absolute() +# Path to the centralized mypy configuration file at the repository root. +# Search upwards to support running nox from both monorepo packages and integration test goldens. +MYPY_CONFIG_FILE = next( + ( + str(p / "mypy.ini") + for p in CURRENT_DIRECTORY.parents + if (p / "mypy.ini").exists() + ), + str(CURRENT_DIRECTORY.parent.parent / "mypy.ini"), +) + RUFF_VERSION = "ruff==0.14.14" LINT_PATHS = ["docs", "gapic", "tests", "test_utils", "noxfile.py", "setup.py"] # Ruff uses globs for excludes (different from Black's regex) @@ -181,7 +193,6 @@ def fragment(session, use_ads_templates=False): "grpcio-tools", ) session.install("-e", ".") - session.install("-e", "../google-api-core") # The specific failure is `Plugin output is unparseable` if session.python == "3.10": @@ -246,7 +257,6 @@ def showcase_library( # Install gapic-generator-python session.install("-e", ".") - session.install("-e", "../google-api-core") # Install grpcio-tools for protoc session.install("grpcio-tools") @@ -382,6 +392,8 @@ def showcase_library( # Install the library without a constraints file. session.install("-e", tmp_dir) + session.install("-e", "../google-api-core", "--no-deps") + yield tmp_dir @@ -606,7 +618,13 @@ def showcase_mypy( session.chdir(lib) # Run the tests. - session.run("mypy", "-p", "google", "--check-untyped-defs") + session.run( + "mypy", + f"--config-file={MYPY_CONFIG_FILE}", + "-p", + "google", + "--check-untyped-defs", + ) @nox.session(python=NEWEST_PYTHON) @@ -732,7 +750,7 @@ def mypy(session): "click==8.1.3", ) session.install(".") - session.run("mypy", "-p", "gapic") + session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") @nox.session(python=NEWEST_PYTHON) @@ -744,7 +762,7 @@ def lint(session): """ # TODO(https://github.com/googleapis/google-cloud-python/issues/16186): - # SKIP: This session was not enforced in the standalone (split) repo + # SKIP: This session was not enforced in the standalone (split) repo # and is disabled here to ensure a "move-only" migration. session.skip( "Linting was not enforced in the split repo. " @@ -774,9 +792,11 @@ def lint(session): @nox.session(python=NEWEST_PYTHON) def lint_setup_py(session): # TODO(https://github.com/googleapis/google-cloud-python/issues/16186): - # SKIP: This session was not enforced in the standalone (split) repo + # SKIP: This session was not enforced in the standalone (split) repo # and is disabled here to ensure a "move-only" migration. - session.skip("Skipping now to avoid changing code during migration. See Issue #16186") + session.skip( + "Skipping now to avoid changing code during migration. See Issue #16186" + ) @nox.session(python="3.10") @@ -851,9 +871,11 @@ def prerelease_deps(session, protobuf_implementation): """ Run all tests with pre-release versions of dependencies installed. """ - # TODO(https://github.com/googleapis/google-cloud-python/issues/16184): + # TODO(https://github.com/googleapis/google-cloud-python/issues/16184): # Implement pre-release dependency logic to test against upcoming runtime changes. - session.skip("prerelease_deps session is not yet implemented for gapic-generator-python.") + session.skip( + "prerelease_deps session is not yet implemented for gapic-generator-python." + ) @nox.session(python=NEWEST_PYTHON) @@ -863,6 +885,8 @@ def prerelease_deps(session, protobuf_implementation): ) def core_deps_from_source(session, protobuf_implementation): """Run all tests with core dependencies installed from source.""" - # TODO(https://github.com/googleapis/google-cloud-python/issues/16185): + # TODO(https://github.com/googleapis/google-cloud-python/issues/16185): # Implement logic to install core packages directly from the mono-repo directories. - session.skip("core_deps_from_source session is not yet implemented for gapic-generator-python.") \ No newline at end of file + session.skip( + "core_deps_from_source session is not yet implemented for gapic-generator-python." + )