From 8808918355959b3dafb8b7dad365dc1288d34f9f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 14:54:01 +0000 Subject: [PATCH 01/18] feat(api-core): move universe and endpoint routing logic to gapic_v1 public helpers Introduces routing module containing get_api_endpoint, get_default_mtls_endpoint, and get_universe_domain helpers. Exposes the module as public in gapic_v1. --- .../google/api_core/gapic_v1/__init__.py | 4 +- .../google/api_core/gapic_v1/routing.py | 101 ++++++++++ packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/tests/conftest.py | 31 ++++ .../tests/unit/gapic/test_routing.py | 175 ++++++++++++++++++ .../google-api-core/tests/unit/test_bidi.py | 5 +- 6 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 packages/google-api-core/google/api_core/gapic_v1/routing.py create mode 100644 packages/google-api-core/tests/conftest.py create mode 100644 packages/google-api-core/tests/unit/gapic/test_routing.py diff --git a/packages/google-api-core/google/api_core/gapic_v1/__init__.py b/packages/google-api-core/google/api_core/gapic_v1/__init__.py index 48a27ec21d24..dd17bfd55975 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/__init__.py +++ b/packages/google-api-core/google/api_core/gapic_v1/__init__.py @@ -25,9 +25,10 @@ # Older Python versions safely ignore this variable. __lazy_modules__: Set[str] = { "google.api_core.gapic_v1.client_info", + "google.api_core.gapic_v1.routing", "google.api_core.gapic_v1.routing_header", } -__all__ = ["client_info", "routing_header"] +__all__ = ["client_info", "routing", "routing_header"] if _has_grpc: __lazy_modules__.update( @@ -41,6 +42,7 @@ from google.api_core.gapic_v1 import ( # noqa: E402 client_info, + routing, routing_header, ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/routing.py b/packages/google-api-core/google/api_core/gapic_v1/routing.py new file mode 100644 index 000000000000..f2e98acc8a1c --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/routing.py @@ -0,0 +1,101 @@ +# -*- 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. +# + +"""Helpers for routing and endpoint resolution.""" + +import re +from typing import Any, Optional + +from google.auth.exceptions import MutualTLSChannelError # type: ignore + +_MTLS_ENDPOINT_RE = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?" + r"(?P\.googleapis\.com)?" +) + + +def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> 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 + + m = _MTLS_ENDPOINT_RE.match(api_endpoint) + if m is None: + # Could not parse api_endpoint; return as-is. + return api_endpoint + + name, mtls_group, sandbox, googledomain = m.groups() + if mtls_group 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[Any], + universe_domain: str, + use_mtls_endpoint: str, + default_universe: str, + default_mtls_endpoint: Optional[str], + default_endpoint_template: Optional[str], +) -> Optional[str]: + """Return the API endpoint used by the client.""" + if api_override is not None: + return api_override + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + if universe_domain.lower() != default_universe.lower(): + raise MutualTLSChannelError( + f"mTLS is not supported in any universe other than {default_universe}." + ) + return default_mtls_endpoint + else: + return ( + default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) + if default_endpoint_template + else None + ) + + +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.strip() + elif universe_domain_env is not None: + universe_domain = universe_domain_env.strip() + if not universe_domain: + raise ValueError("Universe Domain cannot be an empty string.") + return universe_domain diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index 0bad668a80dd..3cbebbaa84d2 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -350,7 +350,7 @@ def prerelease_deps(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def core_deps_from_source(session): """Run the test suite installing dependencies from source.""" - default(session, prerelease=True) + default(session, install_deps_from_source=True) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py new file mode 100644 index 000000000000..1872207be107 --- /dev/null +++ b/packages/google-api-core/tests/conftest.py @@ -0,0 +1,31 @@ +# -*- 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 os +from unittest import mock +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def mock_mtls_env(): + """Autouse session-scoped fixture to isolate unit tests from workstation mTLS environments.""" + with mock.patch.dict( + os.environ, + { + "GOOGLE_API_USE_CLIENT_CERTIFICATE": "false", + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE": "false", + }, + ): + yield diff --git a/packages/google-api-core/tests/unit/gapic/test_routing.py b/packages/google-api-core/tests/unit/gapic/test_routing.py new file mode 100644 index 000000000000..1200f2589133 --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_routing.py @@ -0,0 +1,175 @@ +# -*- 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. + +from unittest import mock + +import pytest + +from google.auth.exceptions import MutualTLSChannelError + +from google.api_core.gapic_v1.routing import ( + get_api_endpoint, + get_default_mtls_endpoint, + get_universe_domain, +) + + +def test_get_default_mtls_endpoint(): + # Test valid API endpoints + assert get_default_mtls_endpoint("foo.googleapis.com") == "foo.mtls.googleapis.com" + assert ( + get_default_mtls_endpoint("foo.sandbox.googleapis.com") + == "foo.mtls.sandbox.googleapis.com" + ) + + # Test endpoints that shouldn't be converted + assert ( + get_default_mtls_endpoint("foo.mtls.googleapis.com") + == "foo.mtls.googleapis.com" + ) + assert get_default_mtls_endpoint("foo.com") == "foo.com" + + # Test empty/None endpoints + assert get_default_mtls_endpoint("") == "" + assert get_default_mtls_endpoint(None) is None + + +def test_get_api_endpoint_override(): + # If api_override is provided, it should be returned + # regardless of other args + endpoint = get_api_endpoint( + api_override="custom.endpoint.com", + client_cert_source=None, + universe_domain="googleapis.com", + use_mtls_endpoint="auto", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + assert endpoint == "custom.endpoint.com" + + +def test_get_api_endpoint_mtls_always(): + # use_mtls_endpoint == "always" should use the default mtls endpoint + endpoint = get_api_endpoint( + api_override=None, + client_cert_source=None, + universe_domain="googleapis.com", + use_mtls_endpoint="always", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + assert endpoint == "foo.mtls.googleapis.com" + + +def test_get_api_endpoint_mtls_auto_with_cert(): + # "auto" with client_cert_source should use mtls + endpoint = get_api_endpoint( + api_override=None, + client_cert_source=mock.Mock(), + universe_domain="googleapis.com", + use_mtls_endpoint="auto", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + assert endpoint == "foo.mtls.googleapis.com" + + +def test_get_api_endpoint_mtls_auto_no_cert(): + # "auto" without client_cert_source should use the default template + endpoint = get_api_endpoint( + api_override=None, + client_cert_source=None, + universe_domain="googleapis.com", + use_mtls_endpoint="auto", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + assert endpoint == "foo.googleapis.com" + + +def test_get_api_endpoint_mtls_universe_mismatch(): + # mTLS is only supported in the default universe + with pytest.raises(MutualTLSChannelError, match="mTLS is not supported"): + get_api_endpoint( + api_override=None, + client_cert_source=mock.Mock(), + universe_domain="custom-universe.com", + use_mtls_endpoint="auto", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + + +def test_get_api_endpoint_mtls_case_insensitive(): + # mTLS universe check should be case insensitive + endpoint = get_api_endpoint( + api_override=None, + client_cert_source=mock.Mock(), + universe_domain="GOOGLEAPIS.COM", + use_mtls_endpoint="auto", + default_universe="googleapis.com", + default_mtls_endpoint="foo.mtls.googleapis.com", + default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + ) + assert endpoint == "foo.mtls.googleapis.com" + + +def test_get_universe_domain(): + # client_universe_domain takes precedence + assert ( + get_universe_domain("client.com", "env.com", "default.com") # noqa: E501 + == "client.com" + ) + + # env takes precedence over default + assert ( + get_universe_domain(None, "env.com", "default.com") == "env.com" # noqa: E501 + ) + + # fallback to default + assert get_universe_domain(None, None, "default.com") == "default.com" # noqa: E501 + + +def test_get_universe_domain_strip(): + # check that whitespace is stripped + assert ( + get_universe_domain(" client.com ", "env.com", "default.com") == "client.com" + ) + assert get_universe_domain(None, " env.com ", "default.com") == "env.com" + + +def test_get_universe_domain_empty(): + with pytest.raises(ValueError, match="cannot be an empty string"): + get_universe_domain("", None, "default.com") + with pytest.raises(ValueError, match="cannot be an empty string"): + get_universe_domain(" ", None, "default.com") + + +def test_get_api_endpoint_none_template(): + endpoint = get_api_endpoint( + api_override=None, + client_cert_source=None, + universe_domain="googleapis.com", + use_mtls_endpoint="never", + default_universe="googleapis.com", + default_mtls_endpoint=None, + default_endpoint_template=None, + ) + assert endpoint is None diff --git a/packages/google-api-core/tests/unit/test_bidi.py b/packages/google-api-core/tests/unit/test_bidi.py index 4a8eb74fac94..0f4810cb0a12 100644 --- a/packages/google-api-core/tests/unit/test_bidi.py +++ b/packages/google-api-core/tests/unit/test_bidi.py @@ -31,8 +31,7 @@ except ImportError: # pragma: NO COVER pytest.skip("No GRPC", allow_module_level=True) -from google.api_core import bidi -from google.api_core import exceptions +from google.api_core import bidi, exceptions class Test_RequestQueueGenerator(object): @@ -195,7 +194,7 @@ def test_delays_entry_attempts_above_threshold(self): # (NOTE: not using assert all(...), b/c the coverage check would complain) for i, entry in enumerate(entries): if i != 3: - assert entry["reported_wait"] == 0.0 + assert entry["reported_wait"] < 0.01 # The delayed entry is expected to have been delayed for a significant # chunk of the full second, and the actual and reported delay times From 3c47f56582f7e3c8780ee5fad9d8ac06d1626d57 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 19:20:20 +0000 Subject: [PATCH 02/18] refactor(api-core): revert unrelated changes in noxfile and test_bidi --- packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/tests/unit/test_bidi.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index 3cbebbaa84d2..0bad668a80dd 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -350,7 +350,7 @@ def prerelease_deps(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def core_deps_from_source(session): """Run the test suite installing dependencies from source.""" - default(session, install_deps_from_source=True) + default(session, prerelease=True) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/packages/google-api-core/tests/unit/test_bidi.py b/packages/google-api-core/tests/unit/test_bidi.py index 0f4810cb0a12..4a8eb74fac94 100644 --- a/packages/google-api-core/tests/unit/test_bidi.py +++ b/packages/google-api-core/tests/unit/test_bidi.py @@ -31,7 +31,8 @@ except ImportError: # pragma: NO COVER pytest.skip("No GRPC", allow_module_level=True) -from google.api_core import bidi, exceptions +from google.api_core import bidi +from google.api_core import exceptions class Test_RequestQueueGenerator(object): @@ -194,7 +195,7 @@ def test_delays_entry_attempts_above_threshold(self): # (NOTE: not using assert all(...), b/c the coverage check would complain) for i, entry in enumerate(entries): if i != 3: - assert entry["reported_wait"] < 0.01 + assert entry["reported_wait"] == 0.0 # The delayed entry is expected to have been delayed for a significant # chunk of the full second, and the actual and reported delay times From f953c809e54d78c7ae01671d80f4d742353bc2e6 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 19:28:08 +0000 Subject: [PATCH 03/18] refactor(api-core): update routing tests to match generator's test template precisely --- .../tests/unit/gapic/test_routing.py | 232 ++++++++++-------- 1 file changed, 123 insertions(+), 109 deletions(-) diff --git a/packages/google-api-core/tests/unit/gapic/test_routing.py b/packages/google-api-core/tests/unit/gapic/test_routing.py index 1200f2589133..76fb5e2d6c8d 100644 --- a/packages/google-api-core/tests/unit/gapic/test_routing.py +++ b/packages/google-api-core/tests/unit/gapic/test_routing.py @@ -26,6 +26,12 @@ ) +class MockClient: + _DEFAULT_UNIVERSE = "googleapis.com" + DEFAULT_MTLS_ENDPOINT = "foo.mtls.googleapis.com" + _DEFAULT_ENDPOINT_TEMPLATE = "foo.{UNIVERSE_DOMAIN}" + + def test_get_default_mtls_endpoint(): # Test valid API endpoints assert get_default_mtls_endpoint("foo.googleapis.com") == "foo.mtls.googleapis.com" @@ -46,130 +52,138 @@ def test_get_default_mtls_endpoint(): assert get_default_mtls_endpoint(None) is None -def test_get_api_endpoint_override(): - # If api_override is provided, it should be returned - # regardless of other args - endpoint = get_api_endpoint( - api_override="custom.endpoint.com", - client_cert_source=None, - universe_domain="googleapis.com", - use_mtls_endpoint="auto", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", +def test__get_api_endpoint(): + api_override = "foo.com" + mock_client_cert_source = mock.Mock() + default_universe = MockClient._DEFAULT_UNIVERSE + default_endpoint = MockClient._DEFAULT_ENDPOINT_TEMPLATE.format( + UNIVERSE_DOMAIN=default_universe ) - assert endpoint == "custom.endpoint.com" - - -def test_get_api_endpoint_mtls_always(): - # use_mtls_endpoint == "always" should use the default mtls endpoint - endpoint = get_api_endpoint( - api_override=None, - client_cert_source=None, - universe_domain="googleapis.com", - use_mtls_endpoint="always", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + mock_universe = "bar.com" + mock_endpoint = MockClient._DEFAULT_ENDPOINT_TEMPLATE.format( + UNIVERSE_DOMAIN=mock_universe ) - assert endpoint == "foo.mtls.googleapis.com" - - -def test_get_api_endpoint_mtls_auto_with_cert(): - # "auto" with client_cert_source should use mtls - endpoint = get_api_endpoint( - api_override=None, - client_cert_source=mock.Mock(), - universe_domain="googleapis.com", - use_mtls_endpoint="auto", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + + assert ( + get_api_endpoint( + api_override, + mock_client_cert_source, + default_universe, + "always", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == api_override ) - assert endpoint == "foo.mtls.googleapis.com" - - -def test_get_api_endpoint_mtls_auto_no_cert(): - # "auto" without client_cert_source should use the default template - endpoint = get_api_endpoint( - api_override=None, - client_cert_source=None, - universe_domain="googleapis.com", - use_mtls_endpoint="auto", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + assert ( + get_api_endpoint( + None, + mock_client_cert_source, + default_universe, + "auto", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == MockClient.DEFAULT_MTLS_ENDPOINT ) - assert endpoint == "foo.googleapis.com" - - -def test_get_api_endpoint_mtls_universe_mismatch(): - # mTLS is only supported in the default universe - with pytest.raises(MutualTLSChannelError, match="mTLS is not supported"): + assert ( get_api_endpoint( - api_override=None, - client_cert_source=mock.Mock(), - universe_domain="custom-universe.com", - use_mtls_endpoint="auto", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + None, + None, + default_universe, + "auto", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, ) - - -def test_get_api_endpoint_mtls_case_insensitive(): - # mTLS universe check should be case insensitive - endpoint = get_api_endpoint( - api_override=None, - client_cert_source=mock.Mock(), - universe_domain="GOOGLEAPIS.COM", - use_mtls_endpoint="auto", - default_universe="googleapis.com", - default_mtls_endpoint="foo.mtls.googleapis.com", - default_endpoint_template="foo.{UNIVERSE_DOMAIN}", + == default_endpoint ) - assert endpoint == "foo.mtls.googleapis.com" - - -def test_get_universe_domain(): - # client_universe_domain takes precedence assert ( - get_universe_domain("client.com", "env.com", "default.com") # noqa: E501 - == "client.com" + get_api_endpoint( + None, + None, + default_universe, + "always", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == MockClient.DEFAULT_MTLS_ENDPOINT ) - - # env takes precedence over default assert ( - get_universe_domain(None, "env.com", "default.com") == "env.com" # noqa: E501 + get_api_endpoint( + None, + mock_client_cert_source, + default_universe, + "always", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == MockClient.DEFAULT_MTLS_ENDPOINT + ) + assert ( + get_api_endpoint( + None, + None, + mock_universe, + "never", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == mock_endpoint ) - - # fallback to default - assert get_universe_domain(None, None, "default.com") == "default.com" # noqa: E501 - - -def test_get_universe_domain_strip(): - # check that whitespace is stripped assert ( - get_universe_domain(" client.com ", "env.com", "default.com") == "client.com" + get_api_endpoint( + None, + None, + default_universe, + "never", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + == default_endpoint ) - assert get_universe_domain(None, " env.com ", "default.com") == "env.com" + with pytest.raises(MutualTLSChannelError) as excinfo: + get_api_endpoint( + None, + mock_client_cert_source, + mock_universe, + "auto", + MockClient._DEFAULT_UNIVERSE, + MockClient.DEFAULT_MTLS_ENDPOINT, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + assert ( + str(excinfo.value) + == "mTLS is not supported in any universe other than googleapis.com." + ) -def test_get_universe_domain_empty(): - with pytest.raises(ValueError, match="cannot be an empty string"): - get_universe_domain("", None, "default.com") - with pytest.raises(ValueError, match="cannot be an empty string"): - get_universe_domain(" ", None, "default.com") +def test__get_universe_domain(): + client_universe_domain = "foo.com" + universe_domain_env = "bar.com" -def test_get_api_endpoint_none_template(): - endpoint = get_api_endpoint( - api_override=None, - client_cert_source=None, - universe_domain="googleapis.com", - use_mtls_endpoint="never", - default_universe="googleapis.com", - default_mtls_endpoint=None, - default_endpoint_template=None, + assert ( + get_universe_domain( + client_universe_domain, universe_domain_env, MockClient._DEFAULT_UNIVERSE + ) + == client_universe_domain ) - assert endpoint is None + assert ( + get_universe_domain(None, universe_domain_env, MockClient._DEFAULT_UNIVERSE) + == universe_domain_env + ) + assert ( + get_universe_domain(None, None, MockClient._DEFAULT_UNIVERSE) + == MockClient._DEFAULT_UNIVERSE + ) + + with pytest.raises(ValueError) as excinfo: + get_universe_domain("", None, MockClient._DEFAULT_UNIVERSE) + assert str(excinfo.value) == "Universe Domain cannot be an empty string." From 0fe63b534dd5633be4623382824b3890a36356be Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 19:38:58 +0000 Subject: [PATCH 04/18] fix(api-core): update get_default_mtls_endpoint to use robust string slicing --- .../google/api_core/gapic_v1/routing.py | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/routing.py b/packages/google-api-core/google/api_core/gapic_v1/routing.py index f2e98acc8a1c..be094a7d455e 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/routing.py +++ b/packages/google-api-core/google/api_core/gapic_v1/routing.py @@ -16,16 +16,10 @@ """Helpers for routing and endpoint resolution.""" -import re from typing import Any, Optional from google.auth.exceptions import MutualTLSChannelError # type: ignore -_MTLS_ENDPOINT_RE = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?" - r"(?P\.googleapis\.com)?" -) - def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: """Converts api endpoint to mTLS endpoint. @@ -37,24 +31,18 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: Returns: Optional[str]: converted mTLS api endpoint. """ - if not api_endpoint: + if not api_endpoint or ".mtls." in api_endpoint: return api_endpoint - m = _MTLS_ENDPOINT_RE.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint + if api_endpoint.endswith(".sandbox.googleapis.com"): + # len(".sandbox.googleapis.com") == 23 + return api_endpoint[:-23] + ".mtls.sandbox.googleapis.com" - name, mtls_group, sandbox, googledomain = m.groups() - if mtls_group or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) + if api_endpoint.endswith(".googleapis.com"): + # len(".googleapis.com") == 15 + return api_endpoint[:-15] + ".mtls.googleapis.com" - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + return api_endpoint def get_api_endpoint( From 2d0808b205a9b3f4bc97c6ae94364df29ef9eb34 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 23:11:31 +0000 Subject: [PATCH 05/18] docs(api-core): address routing review feedback on docstrings, types, and variables --- .../google/api_core/gapic_v1/routing.py | 57 +++++++++++++++---- .../tests/unit/gapic/test_routing.py | 12 ++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/routing.py b/packages/google-api-core/google/api_core/gapic_v1/routing.py index be094a7d455e..2ba3af412cd6 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/routing.py +++ b/packages/google-api-core/google/api_core/gapic_v1/routing.py @@ -26,8 +26,10 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: 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. """ @@ -52,9 +54,30 @@ def get_api_endpoint( use_mtls_endpoint: str, default_universe: str, default_mtls_endpoint: Optional[str], - default_endpoint_template: Optional[str], -) -> Optional[str]: - """Return the API endpoint used by the client.""" + default_endpoint_template: str, +) -> str: + """Return the API endpoint used by the client. + + Args: + api_override (Optional[str]): The API endpoint override. If specified, + this is always returned. + client_cert_source (Optional[Any]): 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. Possible values + are "always", "auto", or "never". + default_universe (str): The default universe domain. + default_mtls_endpoint (Optional[str]): The default mTLS endpoint. + default_endpoint_template (str): The default endpoint template containing + a placeholder `{UNIVERSE_DOMAIN}`. + + Returns: + str: The API endpoint to be used by the client. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but + not supported in the configured universe domain. + ValueError: If mTLS is requested but no mTLS endpoint is available. + """ if api_override is not None: return api_override elif use_mtls_endpoint == "always" or ( @@ -64,13 +87,11 @@ def get_api_endpoint( raise MutualTLSChannelError( f"mTLS is not supported in any universe other than {default_universe}." ) + if not default_mtls_endpoint: + raise ValueError("mTLS endpoint is not available.") return default_mtls_endpoint else: - return ( - default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) - if default_endpoint_template - else None - ) + return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain) def get_universe_domain( @@ -78,12 +99,28 @@ def get_universe_domain( universe_domain_env: Optional[str], default_universe: str, ) -> str: - """Return the universe domain used by the client.""" - universe_domain = default_universe + """Return the universe domain used by the client. + + Args: + client_universe_domain (Optional[str]): The universe domain configured + via client options. + universe_domain_env (Optional[str]): The universe domain configured + via environment variable. + default_universe (str): The default universe domain. + + Returns: + str: The universe domain to be used by the client. + + Raises: + ValueError: If the resolved universe domain is an empty string. + """ if client_universe_domain is not None: universe_domain = client_universe_domain.strip() elif universe_domain_env is not None: universe_domain = universe_domain_env.strip() + else: + universe_domain = default_universe + if not universe_domain: raise ValueError("Universe Domain cannot be an empty string.") return universe_domain diff --git a/packages/google-api-core/tests/unit/gapic/test_routing.py b/packages/google-api-core/tests/unit/gapic/test_routing.py index 76fb5e2d6c8d..0fcf3131fa69 100644 --- a/packages/google-api-core/tests/unit/gapic/test_routing.py +++ b/packages/google-api-core/tests/unit/gapic/test_routing.py @@ -164,6 +164,18 @@ def test__get_api_endpoint(): == "mTLS is not supported in any universe other than googleapis.com." ) + with pytest.raises(ValueError) as excinfo: + get_api_endpoint( + None, + mock_client_cert_source, + default_universe, + "always", + MockClient._DEFAULT_UNIVERSE, + None, + MockClient._DEFAULT_ENDPOINT_TEMPLATE, + ) + assert str(excinfo.value) == "mTLS endpoint is not available." + def test__get_universe_domain(): client_universe_domain = "foo.com" From 6596ac571354ecec8e632ef7100c4e4f8d59c558 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 13:57:47 +0000 Subject: [PATCH 06/18] refactor(api-core): rename routing.py to client_utils.py as consolidated client helper --- .../google-api-core/google/api_core/gapic_v1/__init__.py | 6 +++--- .../api_core/gapic_v1/{routing.py => client_utils.py} | 2 +- .../unit/gapic/{test_routing.py => test_client_utils.py} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/google-api-core/google/api_core/gapic_v1/{routing.py => client_utils.py} (98%) rename packages/google-api-core/tests/unit/gapic/{test_routing.py => test_client_utils.py} (99%) diff --git a/packages/google-api-core/google/api_core/gapic_v1/__init__.py b/packages/google-api-core/google/api_core/gapic_v1/__init__.py index dd17bfd55975..1f6d80d916a6 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/__init__.py +++ b/packages/google-api-core/google/api_core/gapic_v1/__init__.py @@ -25,10 +25,10 @@ # Older Python versions safely ignore this variable. __lazy_modules__: Set[str] = { "google.api_core.gapic_v1.client_info", - "google.api_core.gapic_v1.routing", + "google.api_core.gapic_v1.client_utils", "google.api_core.gapic_v1.routing_header", } -__all__ = ["client_info", "routing", "routing_header"] +__all__ = ["client_info", "client_utils", "routing_header"] if _has_grpc: __lazy_modules__.update( @@ -42,7 +42,7 @@ from google.api_core.gapic_v1 import ( # noqa: E402 client_info, - routing, + client_utils, routing_header, ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/routing.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py similarity index 98% rename from packages/google-api-core/google/api_core/gapic_v1/routing.py rename to packages/google-api-core/google/api_core/gapic_v1/client_utils.py index 2ba3af412cd6..083b4eb29499 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/routing.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -14,7 +14,7 @@ # limitations under the License. # -"""Helpers for routing and endpoint resolution.""" +"""Helpers for client setup and configuration.""" from typing import Any, Optional diff --git a/packages/google-api-core/tests/unit/gapic/test_routing.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py similarity index 99% rename from packages/google-api-core/tests/unit/gapic/test_routing.py rename to packages/google-api-core/tests/unit/gapic/test_client_utils.py index 0fcf3131fa69..6c6d6f945c5c 100644 --- a/packages/google-api-core/tests/unit/gapic/test_routing.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -19,7 +19,7 @@ from google.auth.exceptions import MutualTLSChannelError -from google.api_core.gapic_v1.routing import ( +from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, get_default_mtls_endpoint, get_universe_domain, From 093ac26fd4904c4203b4498633b716248a878aa6 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 14:09:13 +0000 Subject: [PATCH 07/18] fix(api-core): make mTLS endpoint conversion case-insensitive --- .../google/api_core/gapic_v1/client_utils.py | 7 ++++--- .../tests/unit/gapic/test_client_utils.py | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index 083b4eb29499..b91d873d2dfb 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -33,14 +33,15 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: Returns: Optional[str]: converted mTLS api endpoint. """ - if not api_endpoint or ".mtls." in api_endpoint: + if not api_endpoint or ".mtls." in api_endpoint.lower(): return api_endpoint - if api_endpoint.endswith(".sandbox.googleapis.com"): + lowered_endpoint = api_endpoint.lower() + if lowered_endpoint.endswith(".sandbox.googleapis.com"): # len(".sandbox.googleapis.com") == 23 return api_endpoint[:-23] + ".mtls.sandbox.googleapis.com" - if api_endpoint.endswith(".googleapis.com"): + if lowered_endpoint.endswith(".googleapis.com"): # len(".googleapis.com") == 15 return api_endpoint[:-15] + ".mtls.googleapis.com" diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index 6c6d6f945c5c..26d29612a8e7 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -39,6 +39,15 @@ def test_get_default_mtls_endpoint(): get_default_mtls_endpoint("foo.sandbox.googleapis.com") == "foo.mtls.sandbox.googleapis.com" ) + # Test case-insensitivity + assert ( + get_default_mtls_endpoint("foo.GoogleAPIs.com") + == "foo.mtls.googleapis.com" + ) + assert ( + get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") + == "foo.mtls.sandbox.googleapis.com" + ) # Test endpoints that shouldn't be converted assert ( From 91d39e9be6aa149ef58dd68e63e3215638852506 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 14:16:01 +0000 Subject: [PATCH 08/18] fix(api-core): satisfy lint and formatting for conftest and test_client_utils in routing branch --- packages/google-api-core/tests/conftest.py | 1 + .../google-api-core/tests/unit/gapic/test_client_utils.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py index 1872207be107..148896ddcaf4 100644 --- a/packages/google-api-core/tests/conftest.py +++ b/packages/google-api-core/tests/conftest.py @@ -15,6 +15,7 @@ import os from unittest import mock + import pytest diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index 26d29612a8e7..35cede6688b0 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -17,13 +17,12 @@ import pytest -from google.auth.exceptions import MutualTLSChannelError - from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, get_default_mtls_endpoint, get_universe_domain, ) +from google.auth.exceptions import MutualTLSChannelError class MockClient: @@ -40,10 +39,7 @@ def test_get_default_mtls_endpoint(): == "foo.mtls.sandbox.googleapis.com" ) # Test case-insensitivity - assert ( - get_default_mtls_endpoint("foo.GoogleAPIs.com") - == "foo.mtls.googleapis.com" - ) + assert get_default_mtls_endpoint("foo.GoogleAPIs.com") == "foo.mtls.googleapis.com" assert ( get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com") == "foo.mtls.sandbox.googleapis.com" From 96733829d9f6a5b082bb9775fb0aab5608a9e732 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 19:24:58 +0000 Subject: [PATCH 09/18] address PR comments on client_utils.py and test_client_utils.py --- .../google/api_core/gapic_v1/client_utils.py | 7 +- .../tests/unit/gapic/test_client_utils.py | 207 +++++++++--------- 2 files changed, 111 insertions(+), 103 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index b91d873d2dfb..67d7368011dd 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -16,7 +16,7 @@ """Helpers for client setup and configuration.""" -from typing import Any, Optional +from typing import Callable, Optional, Tuple from google.auth.exceptions import MutualTLSChannelError # type: ignore @@ -50,7 +50,7 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: def get_api_endpoint( api_override: Optional[str], - client_cert_source: Optional[Any], + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], universe_domain: str, use_mtls_endpoint: str, default_universe: str, @@ -62,7 +62,8 @@ def get_api_endpoint( Args: api_override (Optional[str]): The API endpoint override. If specified, this is always returned. - client_cert_source (Optional[Any]): The client certificate source used by the client. + client_cert_source (Optional[Callable[[], Tuple[bytes, 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. Possible values are "always", "auto", or "never". diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index 35cede6688b0..2fa080f6f330 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -57,129 +57,136 @@ def test_get_default_mtls_endpoint(): assert get_default_mtls_endpoint(None) is None -def test__get_api_endpoint(): - api_override = "foo.com" - mock_client_cert_source = mock.Mock() - default_universe = MockClient._DEFAULT_UNIVERSE - default_endpoint = MockClient._DEFAULT_ENDPOINT_TEMPLATE.format( - UNIVERSE_DOMAIN=default_universe - ) - mock_universe = "bar.com" - mock_endpoint = MockClient._DEFAULT_ENDPOINT_TEMPLATE.format( - UNIVERSE_DOMAIN=mock_universe - ) - - assert ( - get_api_endpoint( - api_override, - mock_client_cert_source, - default_universe, +@pytest.mark.parametrize( + "api_override,client_cert_source,universe_domain,use_mtls_endpoint,default_universe,default_mtls_endpoint,default_endpoint_template,expected", + [ + ( + "foo.com", + mock.Mock(), + "googleapis.com", "always", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == api_override - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.com", + ), + ( None, - mock_client_cert_source, - default_universe, + mock.Mock(), + "googleapis.com", "auto", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == MockClient.DEFAULT_MTLS_ENDPOINT - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.mtls.googleapis.com", + ), + ( None, None, - default_universe, + "googleapis.com", "auto", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == default_endpoint - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.googleapis.com", + ), + ( None, None, - default_universe, + "googleapis.com", "always", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == MockClient.DEFAULT_MTLS_ENDPOINT - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.mtls.googleapis.com", + ), + ( None, - mock_client_cert_source, - default_universe, + mock.Mock(), + "googleapis.com", "always", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == MockClient.DEFAULT_MTLS_ENDPOINT - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.mtls.googleapis.com", + ), + ( None, None, - mock_universe, + "bar.com", "never", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == mock_endpoint - ) - assert ( - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.bar.com", + ), + ( None, None, - default_universe, + "googleapis.com", "never", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - == default_endpoint - ) - - with pytest.raises(MutualTLSChannelError) as excinfo: - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + "foo.googleapis.com", + ), + ( None, - mock_client_cert_source, - mock_universe, + mock.Mock(), + "bar.com", "auto", - MockClient._DEFAULT_UNIVERSE, - MockClient.DEFAULT_MTLS_ENDPOINT, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, - ) - assert ( - str(excinfo.value) - == "mTLS is not supported in any universe other than googleapis.com." - ) - - with pytest.raises(ValueError) as excinfo: - get_api_endpoint( + "googleapis.com", + "foo.mtls.googleapis.com", + "foo.{UNIVERSE_DOMAIN}", + MutualTLSChannelError, + ), + ( None, - mock_client_cert_source, - default_universe, + mock.Mock(), + "googleapis.com", "always", - MockClient._DEFAULT_UNIVERSE, + "googleapis.com", None, - MockClient._DEFAULT_ENDPOINT_TEMPLATE, + "foo.{UNIVERSE_DOMAIN}", + ValueError, + ), + ], +) +def test_get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + expected, +): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + ) + else: + assert ( + get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + use_mtls_endpoint, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + ) + == expected ) - assert str(excinfo.value) == "mTLS endpoint is not available." + def test__get_universe_domain(): From e2227a185f4343c5154ec3e0cc873a1e29cfd8db Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 19:33:26 +0000 Subject: [PATCH 10/18] fix lint/formatting issues and resolve stray conflict markers in gapic_v1/__init__.py --- .../google-api-core/google/api_core/gapic_v1/__init__.py | 8 +------- .../google-api-core/google/api_core/gapic_v1/requests.py | 2 +- .../google-api-core/tests/unit/gapic/test_client_utils.py | 3 +-- .../google-api-core/tests/unit/gapic/test_requests.py | 1 - 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/__init__.py b/packages/google-api-core/google/api_core/gapic_v1/__init__.py index 654233aaa8ff..aaebf2ba9e8e 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/__init__.py +++ b/packages/google-api-core/google/api_core/gapic_v1/__init__.py @@ -25,16 +25,11 @@ # Older Python versions safely ignore this variable. __lazy_modules__: Set[str] = { "google.api_core.gapic_v1.client_info", - "google.api_core.gapic_v1.client_utils", - "google.api_core.gapic_v1.routing_header", -} -__all__ = ["client_info", "client_utils", "routing_header"] -======= "google.api_core.gapic_v1.requests", "google.api_core.gapic_v1.routing_header", } -__all__ = ["client_info", "requests", "routing_header"] +__all__ = ["client_info", "client_utils", "requests", "routing_header"] if _has_grpc: @@ -51,7 +46,6 @@ client_info, client_utils, requests, - routing_header, ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/requests.py b/packages/google-api-core/google/api_core/gapic_v1/requests.py index f440ac69126c..8ce97c9cffa7 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/requests.py +++ b/packages/google-api-core/google/api_core/gapic_v1/requests.py @@ -21,8 +21,8 @@ if they are not already set. """ -from typing import Union import uuid +from typing import Union import google.protobuf.message diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index 2fa080f6f330..a5f017c27f6e 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -16,13 +16,13 @@ from unittest import mock import pytest +from google.auth.exceptions import MutualTLSChannelError from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, get_default_mtls_endpoint, get_universe_domain, ) -from google.auth.exceptions import MutualTLSChannelError class MockClient: @@ -188,7 +188,6 @@ def test_get_api_endpoint( ) - def test__get_universe_domain(): client_universe_domain = "foo.com" universe_domain_env = "bar.com" diff --git a/packages/google-api-core/tests/unit/gapic/test_requests.py b/packages/google-api-core/tests/unit/gapic/test_requests.py index 1e921955d043..e046f31b828b 100644 --- a/packages/google-api-core/tests/unit/gapic/test_requests.py +++ b/packages/google-api-core/tests/unit/gapic/test_requests.py @@ -19,7 +19,6 @@ from google.api_core.gapic_v1.requests import setup_request_id - # --- Mock Request Helper Classes --- From 2aeabc785662b46f11fa9e2f3713a79c45d2ef75 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 19:38:46 +0000 Subject: [PATCH 11/18] docs: clarify get_default_mtls_endpoint pass-through, fix: support port suffixes in mTLS conversion --- .../google/api_core/gapic_v1/client_utils.py | 17 +++++++++++----- .../tests/unit/gapic/test_client_utils.py | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index 67d7368011dd..215834bcc9f9 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -26,6 +26,8 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Other URLs (including those that do not match these domain suffixes or + already contain '.mtls.') are passed through as-is. Args: api_endpoint (Optional[str]): the api endpoint to convert. @@ -36,14 +38,19 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: if not api_endpoint or ".mtls." in api_endpoint.lower(): return api_endpoint - lowered_endpoint = api_endpoint.lower() - if lowered_endpoint.endswith(".sandbox.googleapis.com"): + # Handle optional port suffix (e.g. ":443") + parts = api_endpoint.split(":") + host = parts[0] + port = ":" + parts[1] if len(parts) > 1 else "" + + lowered_host = host.lower() + if lowered_host.endswith(".sandbox.googleapis.com"): # len(".sandbox.googleapis.com") == 23 - return api_endpoint[:-23] + ".mtls.sandbox.googleapis.com" + return host[:-23] + ".mtls.sandbox.googleapis.com" + port - if lowered_endpoint.endswith(".googleapis.com"): + if lowered_host.endswith(".googleapis.com"): # len(".googleapis.com") == 15 - return api_endpoint[:-15] + ".mtls.googleapis.com" + return host[:-15] + ".mtls.googleapis.com" + port return api_endpoint diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index a5f017c27f6e..0ce6b917db76 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -45,12 +45,32 @@ def test_get_default_mtls_endpoint(): == "foo.mtls.sandbox.googleapis.com" ) + # Test valid API endpoints with ports + assert ( + get_default_mtls_endpoint("foo.googleapis.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + get_default_mtls_endpoint("foo.sandbox.googleapis.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test case-insensitivity with ports + assert ( + get_default_mtls_endpoint("foo.GoogleAPIs.com:443") + == "foo.mtls.googleapis.com:443" + ) + assert ( + get_default_mtls_endpoint("foo.Sandbox.GoogleAPIs.com:443") + == "foo.mtls.sandbox.googleapis.com:443" + ) + # Test endpoints that shouldn't be converted assert ( get_default_mtls_endpoint("foo.mtls.googleapis.com") == "foo.mtls.googleapis.com" ) assert get_default_mtls_endpoint("foo.com") == "foo.com" + assert get_default_mtls_endpoint("foo.com:8080") == "foo.com:8080" # Test empty/None endpoints assert get_default_mtls_endpoint("") == "" From 1abba92bdca7372d874f33c924177afe7b7e52cf Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:22:43 +0000 Subject: [PATCH 12/18] refactor(api-core): parse ports using urlparse and utilize should_use_mtls_endpoint --- .../google/api_core/gapic_v1/client_utils.py | 67 ++++++++++++++----- .../tests/unit/gapic/test_client_utils.py | 56 +++++++++------- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index 215834bcc9f9..9e9ed96f0af9 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -16,8 +16,11 @@ """Helpers for client setup and configuration.""" +import os from typing import Callable, Optional, Tuple +from urllib.parse import urlparse, urlunparse +import google.auth.transport.mtls # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore @@ -38,28 +41,62 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: if not api_endpoint or ".mtls." in api_endpoint.lower(): return api_endpoint - # Handle optional port suffix (e.g. ":443") - parts = api_endpoint.split(":") - host = parts[0] - port = ":" + parts[1] if len(parts) > 1 else "" + has_scheme = "://" in api_endpoint + if not has_scheme: + parsed = urlparse("//" + api_endpoint) + else: + parsed = urlparse(api_endpoint) + + host = parsed.hostname + if not host: + return api_endpoint + + port = f":{parsed.port}" if parsed.port else "" lowered_host = host.lower() if lowered_host.endswith(".sandbox.googleapis.com"): - # len(".sandbox.googleapis.com") == 23 - return host[:-23] + ".mtls.sandbox.googleapis.com" + port + new_host = host[:-23] + ".mtls.sandbox.googleapis.com" + elif lowered_host.endswith(".googleapis.com"): + new_host = host[:-15] + ".mtls.googleapis.com" + else: + return api_endpoint - if lowered_host.endswith(".googleapis.com"): - # len(".googleapis.com") == 15 - return host[:-15] + ".mtls.googleapis.com" + port + netloc = new_host + port + new_parsed = parsed._replace(netloc=netloc) - return api_endpoint + if not has_scheme: + return urlunparse(new_parsed)[2:] + else: + return urlunparse(new_parsed) + + +def should_use_mtls_endpoint(client_cert_available: bool) -> bool: + """Helper to determine whether to use mTLS endpoint. + + Uses google.auth.transport.mtls.should_use_mtls_endpoint if available, + otherwise falls back to evaluation of GOOGLE_API_USE_MTLS_ENDPOINT. + """ + if hasattr(google.auth.transport.mtls, "should_use_mtls_endpoint"): + return google.auth.transport.mtls.should_use_mtls_endpoint(client_cert_available) + + # Fallback logic for older google-auth versions + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").strip().lower() + if use_mtls_endpoint == "always": + return True + elif use_mtls_endpoint == "never": + return False + elif use_mtls_endpoint == "auto": + return client_cert_available + else: + raise MutualTLSChannelError( + f"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) 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: Optional[str], default_endpoint_template: str, @@ -72,8 +109,6 @@ def get_api_endpoint( client_cert_source (Optional[Callable[[], Tuple[bytes, 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. Possible values - are "always", "auto", or "never". default_universe (str): The default universe domain. default_mtls_endpoint (Optional[str]): The default mTLS endpoint. default_endpoint_template (str): The default endpoint template containing @@ -89,9 +124,9 @@ def get_api_endpoint( """ if api_override is not None: return api_override - elif use_mtls_endpoint == "always" or ( - use_mtls_endpoint == "auto" and client_cert_source - ): + + client_cert_available = client_cert_source is not None + if should_use_mtls_endpoint(client_cert_available): if universe_domain.lower() != default_universe.lower(): raise MutualTLSChannelError( f"mTLS is not supported in any universe other than {default_universe}." diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index 0ce6b917db76..b2f0b378f5d5 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from unittest import mock import pytest @@ -45,6 +46,16 @@ def test_get_default_mtls_endpoint(): == "foo.mtls.sandbox.googleapis.com" ) + # Test valid API endpoints with schemes + assert ( + get_default_mtls_endpoint("https://foo.googleapis.com") + == "https://foo.mtls.googleapis.com" + ) + assert ( + get_default_mtls_endpoint("http://foo.googleapis.com:8080/v1") + == "http://foo.mtls.googleapis.com:8080/v1" + ) + # Test valid API endpoints with ports assert ( get_default_mtls_endpoint("foo.googleapis.com:443") @@ -182,30 +193,29 @@ def test_get_api_endpoint( default_endpoint_template, expected, ): - if isinstance(expected, type) and issubclass(expected, Exception): - with pytest.raises(expected): - get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - default_universe, - default_mtls_endpoint, - default_endpoint_template, - ) - else: - assert ( - get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - use_mtls_endpoint, - default_universe, - default_mtls_endpoint, - default_endpoint_template, + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_endpoint}): + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + ) + else: + assert ( + get_api_endpoint( + api_override, + client_cert_source, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + ) + == expected ) - == expected - ) def test__get_universe_domain(): From c0b7860ca799a87f48ba6d791cdd772fe64ad8a9 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:40:13 +0000 Subject: [PATCH 13/18] style(api-core): format code with ruff --- .../google/api_core/gapic_v1/client_utils.py | 8 ++++++-- .../google-api-core/tests/unit/gapic/test_client_utils.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index 9e9ed96f0af9..fea4c812cdb7 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -77,10 +77,14 @@ def should_use_mtls_endpoint(client_cert_available: bool) -> bool: otherwise falls back to evaluation of GOOGLE_API_USE_MTLS_ENDPOINT. """ if hasattr(google.auth.transport.mtls, "should_use_mtls_endpoint"): - return google.auth.transport.mtls.should_use_mtls_endpoint(client_cert_available) + return google.auth.transport.mtls.should_use_mtls_endpoint( + client_cert_available + ) # Fallback logic for older google-auth versions - use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").strip().lower() + use_mtls_endpoint = ( + os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").strip().lower() + ) if use_mtls_endpoint == "always": return True elif use_mtls_endpoint == "never": diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index b2f0b378f5d5..be86f6b37f3d 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -193,7 +193,9 @@ def test_get_api_endpoint( default_endpoint_template, expected, ): - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_endpoint}): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_endpoint} + ): if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected): get_api_endpoint( From fff03441b4b18c9b41855d491e677da6b6a8205f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:50:28 +0000 Subject: [PATCH 14/18] fix(api-core): resolve lint warnings in client_utils --- .../google-api-core/google/api_core/gapic_v1/client_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index fea4c812cdb7..e9b740e0301a 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -93,7 +93,7 @@ def should_use_mtls_endpoint(client_cert_available: bool) -> bool: return client_cert_available else: raise MutualTLSChannelError( - f"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) From 72f749a8792a57d833770fcb58abfc8386d927c4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:18:08 +0000 Subject: [PATCH 15/18] refactor(api-core): decouple client_utils helpers from client details and remove should_use_mtls_endpoint --- .../google/api_core/gapic_v1/client_utils.py | 64 ++------ .../tests/unit/gapic/test_client_utils.py | 144 +++++------------- 2 files changed, 52 insertions(+), 156 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index e9b740e0301a..e87343e2d68f 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -14,13 +14,9 @@ # limitations under the License. # -"""Helpers for client setup and configuration.""" - -import os -from typing import Callable, Optional, Tuple +from typing import Optional from urllib.parse import urlparse, urlunparse -import google.auth.transport.mtls # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore @@ -70,53 +66,25 @@ def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: return urlunparse(new_parsed) -def should_use_mtls_endpoint(client_cert_available: bool) -> bool: - """Helper to determine whether to use mTLS endpoint. - - Uses google.auth.transport.mtls.should_use_mtls_endpoint if available, - otherwise falls back to evaluation of GOOGLE_API_USE_MTLS_ENDPOINT. - """ - if hasattr(google.auth.transport.mtls, "should_use_mtls_endpoint"): - return google.auth.transport.mtls.should_use_mtls_endpoint( - client_cert_available - ) - - # Fallback logic for older google-auth versions - use_mtls_endpoint = ( - os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").strip().lower() - ) - if use_mtls_endpoint == "always": - return True - elif use_mtls_endpoint == "never": - return False - elif use_mtls_endpoint == "auto": - return client_cert_available - else: - raise MutualTLSChannelError( - "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" - ) - - def get_api_endpoint( api_override: Optional[str], - client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]], universe_domain: str, default_universe: str, default_mtls_endpoint: Optional[str], default_endpoint_template: str, + use_mtls: bool, ) -> str: """Return the API endpoint used by the client. Args: api_override (Optional[str]): The API endpoint override. If specified, this is always returned. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): The client - certificate source used by the client. universe_domain (str): The universe domain used by the client. default_universe (str): The default universe domain. default_mtls_endpoint (Optional[str]): The default mTLS endpoint. default_endpoint_template (str): The default endpoint template containing a placeholder `{UNIVERSE_DOMAIN}`. + use_mtls (bool): Whether to use the mTLS endpoint. Returns: str: The API endpoint to be used by the client. @@ -129,8 +97,7 @@ def get_api_endpoint( if api_override is not None: return api_override - client_cert_available = client_cert_source is not None - if should_use_mtls_endpoint(client_cert_available): + if use_mtls: if universe_domain.lower() != default_universe.lower(): raise MutualTLSChannelError( f"mTLS is not supported in any universe other than {default_universe}." @@ -143,17 +110,13 @@ def get_api_endpoint( def get_universe_domain( - client_universe_domain: Optional[str], - universe_domain_env: Optional[str], - default_universe: str, + universe_domain: Optional[str], + default_universe: str = "googleapis.com", ) -> str: """Return the universe domain used by the client. Args: - client_universe_domain (Optional[str]): The universe domain configured - via client options. - universe_domain_env (Optional[str]): The universe domain configured - via environment variable. + universe_domain (Optional[str]): The configured universe domain. default_universe (str): The default universe domain. Returns: @@ -162,13 +125,10 @@ def get_universe_domain( Raises: ValueError: If the resolved universe domain is an empty string. """ - if client_universe_domain is not None: - universe_domain = client_universe_domain.strip() - elif universe_domain_env is not None: - universe_domain = universe_domain_env.strip() - else: - universe_domain = default_universe + resolved = ( + universe_domain.strip() if universe_domain is not None else default_universe + ) - if not universe_domain: + if not resolved: raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain + return resolved diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index be86f6b37f3d..a5f7007743fd 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -13,9 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -from unittest import mock - import pytest from google.auth.exceptions import MutualTLSChannelError @@ -26,12 +23,6 @@ ) -class MockClient: - _DEFAULT_UNIVERSE = "googleapis.com" - DEFAULT_MTLS_ENDPOINT = "foo.mtls.googleapis.com" - _DEFAULT_ENDPOINT_TEMPLATE = "foo.{UNIVERSE_DOMAIN}" - - def test_get_default_mtls_endpoint(): # Test valid API endpoints assert get_default_mtls_endpoint("foo.googleapis.com") == "foo.mtls.googleapis.com" @@ -89,156 +80,101 @@ def test_get_default_mtls_endpoint(): @pytest.mark.parametrize( - "api_override,client_cert_source,universe_domain,use_mtls_endpoint,default_universe,default_mtls_endpoint,default_endpoint_template,expected", + "api_override,universe_domain,default_universe,default_mtls_endpoint,default_endpoint_template,use_mtls,expected", [ ( "foo.com", - mock.Mock(), "googleapis.com", - "always", "googleapis.com", "foo.mtls.googleapis.com", "foo.{UNIVERSE_DOMAIN}", + True, "foo.com", ), ( None, - mock.Mock(), - "googleapis.com", - "auto", - "googleapis.com", - "foo.mtls.googleapis.com", - "foo.{UNIVERSE_DOMAIN}", - "foo.mtls.googleapis.com", - ), - ( - None, - None, - "googleapis.com", - "auto", - "googleapis.com", - "foo.mtls.googleapis.com", - "foo.{UNIVERSE_DOMAIN}", - "foo.googleapis.com", - ), - ( - None, - None, - "googleapis.com", - "always", - "googleapis.com", - "foo.mtls.googleapis.com", - "foo.{UNIVERSE_DOMAIN}", - "foo.mtls.googleapis.com", - ), - ( - None, - mock.Mock(), "googleapis.com", - "always", "googleapis.com", "foo.mtls.googleapis.com", "foo.{UNIVERSE_DOMAIN}", + True, "foo.mtls.googleapis.com", ), ( - None, - None, - "bar.com", - "never", - "googleapis.com", - "foo.mtls.googleapis.com", - "foo.{UNIVERSE_DOMAIN}", - "foo.bar.com", - ), - ( - None, None, "googleapis.com", - "never", "googleapis.com", "foo.mtls.googleapis.com", "foo.{UNIVERSE_DOMAIN}", + False, "foo.googleapis.com", ), ( None, - mock.Mock(), "bar.com", - "auto", "googleapis.com", "foo.mtls.googleapis.com", "foo.{UNIVERSE_DOMAIN}", + True, MutualTLSChannelError, ), ( None, - mock.Mock(), "googleapis.com", - "always", "googleapis.com", None, "foo.{UNIVERSE_DOMAIN}", + True, ValueError, ), ], ) def test_get_api_endpoint( api_override, - client_cert_source, universe_domain, - use_mtls_endpoint, default_universe, default_mtls_endpoint, default_endpoint_template, + use_mtls, expected, ): - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_endpoint} - ): - if isinstance(expected, type) and issubclass(expected, Exception): - with pytest.raises(expected): - get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - default_universe, - default_mtls_endpoint, - default_endpoint_template, - ) - else: - assert ( - get_api_endpoint( - api_override, - client_cert_source, - universe_domain, - default_universe, - default_mtls_endpoint, - default_endpoint_template, - ) - == expected + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, ) + else: + assert ( + get_api_endpoint( + api_override, + universe_domain, + default_universe, + default_mtls_endpoint, + default_endpoint_template, + use_mtls, + ) + == expected + ) -def test__get_universe_domain(): - client_universe_domain = "foo.com" - universe_domain_env = "bar.com" +def test_get_universe_domain(): + # When universe_domain is provided + assert get_universe_domain("foo.com", "default.com") == "foo.com" + assert get_universe_domain(" foo.com ", "default.com") == "foo.com" - assert ( - get_universe_domain( - client_universe_domain, universe_domain_env, MockClient._DEFAULT_UNIVERSE - ) - == client_universe_domain - ) - assert ( - get_universe_domain(None, universe_domain_env, MockClient._DEFAULT_UNIVERSE) - == universe_domain_env - ) - assert ( - get_universe_domain(None, None, MockClient._DEFAULT_UNIVERSE) - == MockClient._DEFAULT_UNIVERSE - ) + # When universe_domain is None, falls back to default_universe + assert get_universe_domain(None, "default.com") == "default.com" + + # ValueError raised when resolved value is empty string + with pytest.raises(ValueError) as excinfo: + get_universe_domain("", "default.com") + assert str(excinfo.value) == "Universe Domain cannot be an empty string." with pytest.raises(ValueError) as excinfo: - get_universe_domain("", None, MockClient._DEFAULT_UNIVERSE) + get_universe_domain(" ", "default.com") assert str(excinfo.value) == "Universe Domain cannot be an empty string." From aa2793d5840b4f04c86a8016c5760b4eb64f9a04 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:34:05 +0000 Subject: [PATCH 16/18] style: remove obsolete utf-8 coding headers --- .../google-api-core/google/api_core/gapic_v1/client_utils.py | 1 - packages/google-api-core/tests/unit/gapic/test_client_utils.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py index e87343e2d68f..b70f8ff65529 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_utils.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/packages/google-api-core/tests/unit/gapic/test_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index a5f7007743fd..55d2619fa579 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_utils.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); From aa99c84d5fd5491920545a18fadfec81a6f20b52 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:41:27 +0000 Subject: [PATCH 17/18] style: remove obsolete utf-8 coding header from conftest.py --- packages/google-api-core/tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py index 148896ddcaf4..62a3c999f733 100644 --- a/packages/google-api-core/tests/conftest.py +++ b/packages/google-api-core/tests/conftest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); From 9bc98fc6e7638f8fddda37e6f4d4bcd2d8f41d58 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Sat, 18 Jul 2026 06:31:28 +0000 Subject: [PATCH 18/18] fix(api-core): address PR review comments for requests.py and test_requests.py --- .../google/api_core/gapic_v1/requests.py | 2 +- .../tests/unit/gapic/test_requests.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) 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 8ce97c9cffa7..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,7 +65,7 @@ def setup_request_id( setattr(request, field_name, str(uuid.uuid4())) except (AttributeError, ValueError): # Proto-plus messages or other objects - if not getattr(request, field_name, None): + if getattr(request, field_name, None) is None: setattr(request, field_name, str(uuid.uuid4())) else: if not getattr(request, field_name, None): 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 e046f31b828b..1f41c39f756b 100644 --- a/packages/google-api-core/tests/unit/gapic/test_requests.py +++ b/packages/google-api-core/tests/unit/gapic/test_requests.py @@ -27,9 +27,6 @@ def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) - def __contains__(self, key): - return hasattr(self, key) - class MockProtoRequest: def __init__(self, **kwargs): @@ -44,9 +41,6 @@ class MockValueErrorRequest: def HasField(self, key): raise ValueError("Mismatched field") - def __contains__(self, key): - return hasattr(self, key) - # --- Parameterized Test --- @@ -59,6 +53,7 @@ def __contains__(self, key): # MockRequest cases (MockRequest(), True, "uuid"), (MockRequest(request_id="already_set"), True, "already_set"), + (MockRequest(request_id=""), True, ""), (MockRequest(request_id=""), False, "uuid"), (MockRequest(request_id="already_set"), False, "already_set"), # MockProtoRequest cases @@ -70,6 +65,7 @@ def __contains__(self, key): ({}, True, "uuid"), ({"request_id": None}, True, "uuid"), ({"request_id": "already_set"}, True, "already_set"), + ({"request_id": ""}, True, ""), ({"request_id": ""}, False, "uuid"), ({"request_id": None}, False, "uuid"), ({"request_id": "already_set"}, False, "already_set"), @@ -79,6 +75,7 @@ def __contains__(self, key): ids=[ "proto3_optional_not_in_request", "proto3_optional_already_in_request", + "proto3_optional_explicit_empty", "non_proto3_optional_empty", "non_proto3_optional_already_set", "proto3_optional_not_in_request_proto", @@ -87,6 +84,7 @@ def __contains__(self, key): "dict_proto3_optional_not_in_request", "dict_proto3_optional_value_none", "dict_proto3_optional_already_in_request", + "dict_proto3_optional_explicit_empty", "dict_non_proto3_optional_empty", "dict_non_proto3_optional_value_none", "dict_non_proto3_optional_already_set", @@ -110,6 +108,6 @@ def test_setup_request_id(request_obj, is_proto3_optional, expected): ) if expected == "uuid": - assert re.match(UUID_REGEX, value) + assert re.fullmatch(UUID_REGEX, value) else: assert value == expected