From 93c52700ad22db90213ba44f33903581b6afbed4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 14:56:01 +0000 Subject: [PATCH 01/26] feat(api-core): move mtls and client configuration logic to gapic_v1 public helpers Introduces client_cert and config_helpers modules containing use_client_cert_effective, get_client_cert_source, and read_environment_variables helpers. Exposes these modules as public in gapic_v1. --- .../google/api_core/gapic_v1/__init__.py | 6 +- .../google/api_core/gapic_v1/client_cert.py | 79 +++++++++++++++++++ .../api_core/gapic_v1/config_helpers.py | 50 ++++++++++++ packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/tests/conftest.py | 31 ++++++++ .../tests/unit/gapic/test_client_cert.py | 79 +++++++++++++++++++ .../tests/unit/gapic/test_config_helpers.py | 47 +++++++++++ .../google-api-core/tests/unit/test_bidi.py | 5 +- 8 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 packages/google-api-core/google/api_core/gapic_v1/client_cert.py create mode 100644 packages/google-api-core/google/api_core/gapic_v1/config_helpers.py create mode 100644 packages/google-api-core/tests/conftest.py create mode 100644 packages/google-api-core/tests/unit/gapic/test_client_cert.py create mode 100644 packages/google-api-core/tests/unit/gapic/test_config_helpers.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..0230f43ace0d 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,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_cert", + "google.api_core.gapic_v1.config_helpers", "google.api_core.gapic_v1.routing_header", } -__all__ = ["client_info", "routing_header"] +__all__ = ["client_info", "client_cert", "config_helpers", "routing_header"] if _has_grpc: __lazy_modules__.update( @@ -41,6 +43,8 @@ from google.api_core.gapic_v1 import ( # noqa: E402 client_info, + client_cert, + config_helpers, routing_header, ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py new file mode 100644 index 000000000000..2f4a038778db --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py @@ -0,0 +1,79 @@ +# -*- 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 client certificate handling and mTLS authentication.""" + +import os +from typing import Callable, Optional, Tuple + +from google.auth.transport import mtls # type: ignore + + +def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS + enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without + should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is + set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for + # automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + 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. + + Args: + provided_cert_source (Callable[[], Tuple[bytes, bytes]]): The client certificate source provided. + use_cert_flag (bool): A flag indicating whether to use the + client certificate. + + Returns: + Callable[[], Tuple[bytes, bytes]] or None: 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() + return client_cert_source diff --git a/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py b/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py new file mode 100644 index 000000000000..c5cc708e006e --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py @@ -0,0 +1,50 @@ +# -*- 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 parsing environment variables.""" + +import os +from typing import Optional, Tuple + +from google.auth.exceptions import MutualTLSChannelError # type: ignore + +from google.api_core.gapic_v1.client_cert import use_client_cert_effective + + +def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client. + + Returns: + Tuple[bool, str, Optional[str]]: returns the + GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT, + and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. + + Raises: + ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not + any of ["true", "false"]. + google.auth.exceptions.MutualTLSChannelError: If + GOOGLE_API_USE_MTLS_ENDPOINT is not any of + ["auto", "never", "always"]. + """ + 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 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_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py new file mode 100644 index 000000000000..aa7ba9eb8f02 --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -0,0 +1,79 @@ +# -*- 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 + + +from google.api_core.gapic_v1.client_cert import ( + get_client_cert_source, + use_client_cert_effective, +) + + +@mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True) +def test_use_client_cert_effective_with_google_auth(mock_method): + # Test when google-auth supports the method + mock_method.return_value = True + assert use_client_cert_effective() is True + + mock_method.return_value = False + assert use_client_cert_effective() is False + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_use_client_cert_effective_fallback(): + # We must patch hasattr to simulate google-auth lacking the method + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", return_value=False): + # Default is false + assert use_client_cert_effective() is False + + env_true = {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} + with mock.patch.dict(os.environ, env_true): + assert use_client_cert_effective() is True + + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert use_client_cert_effective() is False + + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} + ): + match_str = "must be either `true` or `false`" + with pytest.raises(ValueError, match=match_str): + use_client_cert_effective() + + +@mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", create=True +) # noqa: E501 +@mock.patch( + "google.auth.transport.mtls.default_client_cert_source", create=True +) # noqa: E501 +def test_get_client_cert_source(mock_default, mock_has_default): + mock_default.return_value = b"default_cert" + mock_has_default.return_value = True + + # When use_cert_flag is False, return None + assert get_client_cert_source(b"provided", False) is None + + # When provided_cert_source is given, return provided + assert get_client_cert_source(b"provided", True) == b"provided" # noqa: E501 + + # When no provided cert but default is available + assert get_client_cert_source(None, True) == b"default_cert" diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py new file mode 100644 index 000000000000..e0d634a3b119 --- /dev/null +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -0,0 +1,47 @@ +# -*- 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 + +from google.auth.exceptions import MutualTLSChannelError + +from google.api_core.gapic_v1.config_helpers import read_environment_variables + + +@mock.patch( + "google.api_core.gapic_v1.config_helpers.use_client_cert_effective" +) # noqa: E501 +@mock.patch.dict(os.environ, clear=True) +def test_read_environment_variables(mock_effective): + mock_effective.return_value = True + os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "always" + os.environ["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = "custom.com" + + cert, mtls, domain = read_environment_variables() + assert cert is True + assert mtls == "always" + assert domain == "custom.com" + + +@mock.patch.dict(os.environ, clear=True) +def test_read_environment_variables_invalid_mtls(): + os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "invalid" + with pytest.raises( + MutualTLSChannelError, match="must be `never`, `auto` or `always`" + ): + read_environment_variables() 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 8808918355959b3dafb8b7dad365dc1288d34f9f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 14:54:01 +0000 Subject: [PATCH 02/26] 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 03/26] 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 04/26] 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 05/26] 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 d90149ae8eed63493c085dfef1ba62708062bbf4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:38:23 +0000 Subject: [PATCH 06/26] test(api-core): align mtls and config helper tests with generator templates --- .../tests/unit/gapic/test_client_cert.py | 134 +++++++++++------- .../tests/unit/gapic/test_config_helpers.py | 52 ++++--- 2 files changed, 110 insertions(+), 76 deletions(-) diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index aa7ba9eb8f02..1abc8eb22a14 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -18,62 +18,90 @@ import pytest - +from google.auth.transport import mtls from google.api_core.gapic_v1.client_cert import ( get_client_cert_source, use_client_cert_effective, ) -@mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True) -def test_use_client_cert_effective_with_google_auth(mock_method): - # Test when google-auth supports the method - mock_method.return_value = True - assert use_client_cert_effective() is True - - mock_method.return_value = False - assert use_client_cert_effective() is False - - -@mock.patch.dict(os.environ, {}, clear=True) -def test_use_client_cert_effective_fallback(): - # We must patch hasattr to simulate google-auth lacking the method - with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", return_value=False): - # Default is false - assert use_client_cert_effective() is False - - env_true = {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"} - with mock.patch.dict(os.environ, env_true): - assert use_client_cert_effective() is True - - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} - ): - assert use_client_cert_effective() is False - - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"} - ): - match_str = "must be either `true` or `false`" - with pytest.raises(ValueError, match=match_str): - use_client_cert_effective() - - -@mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", create=True -) # noqa: E501 -@mock.patch( - "google.auth.transport.mtls.default_client_cert_source", create=True -) # noqa: E501 -def test_get_client_cert_source(mock_default, mock_has_default): - mock_default.return_value = b"default_cert" - mock_has_default.return_value = True - - # When use_cert_flag is False, return None - assert get_client_cert_source(b"provided", False) is None - - # When provided_cert_source is given, return provided - assert get_client_cert_source(b"provided", True) == b"provided" # noqa: E501 - - # When no provided cert but default is available - assert get_client_cert_source(None, True) == b"default_cert" +@pytest.mark.parametrize( + "has_method, method_val, env_val, expected", + [ + # should_use_client_cert is available cases + (True, True, None, "true"), + (True, False, None, "false"), + (True, False, "unsupported", "false"), + # should_use_client_cert is unavailable cases + (False, None, "true", "true"), + (False, None, "false", "false"), + (False, None, "True", "true"), + (False, None, "False", "false"), + (False, None, "TRUE", "true"), + (False, None, "FALSE", "false"), + (False, None, None, "false"), + (False, None, "unsupported", "value_error"), + ], + ids=[ + "google_auth_true", + "google_auth_false", + "google_auth_false_env_unsupported", + "fallback_env_true_lowercase", + "fallback_env_false_lowercase", + "fallback_env_true_titlecase", + "fallback_env_false_titlecase", + "fallback_env_true_uppercase", + "fallback_env_false_uppercase", + "fallback_env_unset", + "fallback_env_unsupported", + ] +) +def test_use_client_cert_effective(has_method, method_val, env_val, expected): + # Mock hasattr to control whether should_use_client_cert exists + original_hasattr = hasattr + def custom_hasattr(obj, name): + if obj is mtls and name == "should_use_client_cert": + return has_method + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): + with mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True, return_value=method_val): + env = {} + if env_val is not None: + env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val + with mock.patch.dict(os.environ, env, clear=True): + if expected == "value_error": + with pytest.raises(ValueError, match="must be either `true` or `false`"): + use_client_cert_effective() + else: + assert use_client_cert_effective() is (expected == "true") + + +@pytest.mark.parametrize( + "provided, use_cert, has_default_avail, default_val, expected", + [ + (None, False, True, b"default", None), + (b"provided", False, True, b"default", None), + (b"provided", True, True, b"default", b"provided"), + (None, True, True, b"default", b"default"), + (None, True, False, b"default", None), + ], + ids=[ + "use_cert_false_no_provided", + "use_cert_false_with_provided", + "use_cert_true_with_provided", + "use_cert_true_no_provided_default_avail", + "use_cert_true_no_provided_default_unavail", + ] +) +def test_get_client_cert_source(provided, use_cert, has_default_avail, default_val, expected): + original_hasattr = hasattr + def custom_hasattr(obj, name): + if obj is mtls and name == "has_default_client_cert_source": + return has_default_avail + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): + with mock.patch("google.auth.transport.mtls.has_default_client_cert_source", create=True, return_value=has_default_avail): + with mock.patch("google.auth.transport.mtls.default_client_cert_source", create=True, return_value=default_val): + assert get_client_cert_source(provided, use_cert) == expected diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py index e0d634a3b119..dcc785ee6cbd 100644 --- a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -19,29 +19,35 @@ import pytest from google.auth.exceptions import MutualTLSChannelError - from google.api_core.gapic_v1.config_helpers import read_environment_variables -@mock.patch( - "google.api_core.gapic_v1.config_helpers.use_client_cert_effective" -) # noqa: E501 -@mock.patch.dict(os.environ, clear=True) -def test_read_environment_variables(mock_effective): - mock_effective.return_value = True - os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "always" - os.environ["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = "custom.com" - - cert, mtls, domain = read_environment_variables() - assert cert is True - assert mtls == "always" - assert domain == "custom.com" - - -@mock.patch.dict(os.environ, clear=True) -def test_read_environment_variables_invalid_mtls(): - os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "invalid" - with pytest.raises( - MutualTLSChannelError, match="must be `never`, `auto` or `always`" - ): - read_environment_variables() +@pytest.mark.parametrize( + "env, mock_cert_val, expected", + [ + ({}, False, (False, "auto", None)), + ({}, True, (True, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, False, (False, "never", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), + ({"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, False, (False, "auto", "foo.com")), + ], + ids=[ + "default_env", + "client_cert_true", + "mtls_never", + "mtls_always", + "mtls_auto", + "mtls_invalid", + "universe_domain", + ] +) +def test_read_environment_variables(env, mock_cert_val, expected): + with mock.patch("google.api_core.gapic_v1.config_helpers.use_client_cert_effective", return_value=mock_cert_val): + with mock.patch.dict(os.environ, env, clear=True): + if expected == "mutual_tls_error": + with pytest.raises(MutualTLSChannelError, match="must be `never`, `auto` or `always`"): + read_environment_variables() + else: + assert read_environment_variables() == expected From daf631e0e3ea4a90f834a5dd25cf1e766067e75f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:43:47 +0000 Subject: [PATCH 07/26] style(api-core): format client cert and config helper tests with black --- .../tests/unit/gapic/test_client_cert.py | 32 +++++++++++++++---- .../tests/unit/gapic/test_config_helpers.py | 17 +++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index 1abc8eb22a14..e3314bbb02f7 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -54,24 +54,31 @@ "fallback_env_false_uppercase", "fallback_env_unset", "fallback_env_unsupported", - ] + ], ) def test_use_client_cert_effective(has_method, method_val, env_val, expected): # Mock hasattr to control whether should_use_client_cert exists original_hasattr = hasattr + def custom_hasattr(obj, name): if obj is mtls and name == "should_use_client_cert": return has_method return original_hasattr(obj, name) with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch("google.auth.transport.mtls.should_use_client_cert", create=True, return_value=method_val): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + create=True, + return_value=method_val, + ): env = {} if env_val is not None: env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val with mock.patch.dict(os.environ, env, clear=True): if expected == "value_error": - with pytest.raises(ValueError, match="must be either `true` or `false`"): + with pytest.raises( + ValueError, match="must be either `true` or `false`" + ): use_client_cert_effective() else: assert use_client_cert_effective() is (expected == "true") @@ -92,16 +99,27 @@ def custom_hasattr(obj, name): "use_cert_true_with_provided", "use_cert_true_no_provided_default_avail", "use_cert_true_no_provided_default_unavail", - ] + ], ) -def test_get_client_cert_source(provided, use_cert, has_default_avail, default_val, expected): +def test_get_client_cert_source( + provided, use_cert, has_default_avail, default_val, expected +): original_hasattr = hasattr + def custom_hasattr(obj, name): if obj is mtls and name == "has_default_client_cert_source": return has_default_avail return original_hasattr(obj, name) with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch("google.auth.transport.mtls.has_default_client_cert_source", create=True, return_value=has_default_avail): - with mock.patch("google.auth.transport.mtls.default_client_cert_source", create=True, return_value=default_val): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + create=True, + return_value=has_default_avail, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + create=True, + return_value=default_val, + ): assert get_client_cert_source(provided, use_cert) == expected diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py index dcc785ee6cbd..b9e27ed627a3 100644 --- a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py +++ b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py @@ -31,7 +31,11 @@ ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), - ({"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, False, (False, "auto", "foo.com")), + ( + {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, + False, + (False, "auto", "foo.com"), + ), ], ids=[ "default_env", @@ -41,13 +45,18 @@ "mtls_auto", "mtls_invalid", "universe_domain", - ] + ], ) def test_read_environment_variables(env, mock_cert_val, expected): - with mock.patch("google.api_core.gapic_v1.config_helpers.use_client_cert_effective", return_value=mock_cert_val): + with mock.patch( + "google.api_core.gapic_v1.config_helpers.use_client_cert_effective", + return_value=mock_cert_val, + ): with mock.patch.dict(os.environ, env, clear=True): if expected == "mutual_tls_error": - with pytest.raises(MutualTLSChannelError, match="must be `never`, `auto` or `always`"): + with pytest.raises( + MutualTLSChannelError, match="must be `never`, `auto` or `always`" + ): read_environment_variables() else: assert read_environment_variables() == expected From b5042220a98eebf57249c16db7d6dffca057a296 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:48:10 +0000 Subject: [PATCH 08/26] fix(api-core): raise ValueError on missing cert source in get_client_cert_source --- .../google/api_core/gapic_v1/client_cert.py | 11 +++++++---- .../tests/unit/gapic/test_client_cert.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py index 2f4a038778db..c62d95f6a5ea 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py +++ b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py @@ -67,13 +67,16 @@ def get_client_cert_source( Returns: Callable[[], Tuple[bytes, bytes]] or None: 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 + return 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() - return client_cert_source + return mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return None diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py index e3314bbb02f7..3c0878816478 100644 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ b/packages/google-api-core/tests/unit/gapic/test_client_cert.py @@ -91,7 +91,7 @@ def custom_hasattr(obj, name): (b"provided", False, True, b"default", None), (b"provided", True, True, b"default", b"provided"), (None, True, True, b"default", b"default"), - (None, True, False, b"default", None), + (None, True, False, b"default", "value_error"), ], ids=[ "use_cert_false_no_provided", @@ -122,4 +122,10 @@ def custom_hasattr(obj, name): create=True, return_value=default_val, ): - assert get_client_cert_source(provided, use_cert) == expected + if expected == "value_error": + with pytest.raises( + ValueError, match="Client certificate is required for mTLS" + ): + get_client_cert_source(provided, use_cert) + else: + assert get_client_cert_source(provided, use_cert) == expected From 2e0174c95485f5dfe3bd2073871bc225ae0667ed Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 21:51:47 +0000 Subject: [PATCH 09/26] revert(api-core): revert unrelated changes to noxfile.py and test_bidi.py --- 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 2d0808b205a9b3f4bc97c6ae94364df29ef9eb34 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Thu, 16 Jul 2026 23:11:31 +0000 Subject: [PATCH 10/26] 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 11/26] 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 5462a362748628715f978c8aaa9fa42c920740ea Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 14:00:17 +0000 Subject: [PATCH 12/26] refactor(api-core): consolidate client_cert and config_helpers into client_utils.py --- .../google/api_core/gapic_v1/client_cert.py | 82 ---------- .../google/api_core/gapic_v1/client_utils.py | 90 ++++++++++- .../api_core/gapic_v1/config_helpers.py | 50 ------ .../tests/unit/gapic/test_client_cert.py | 131 --------------- .../tests/unit/gapic/test_client_utils.py | 152 +++++++++++++++++- .../tests/unit/gapic/test_config_helpers.py | 62 ------- 6 files changed, 240 insertions(+), 327 deletions(-) delete mode 100644 packages/google-api-core/google/api_core/gapic_v1/client_cert.py delete mode 100644 packages/google-api-core/google/api_core/gapic_v1/config_helpers.py delete mode 100644 packages/google-api-core/tests/unit/gapic/test_client_cert.py delete mode 100644 packages/google-api-core/tests/unit/gapic/test_config_helpers.py diff --git a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py b/packages/google-api-core/google/api_core/gapic_v1/client_cert.py deleted file mode 100644 index c62d95f6a5ea..000000000000 --- a/packages/google-api-core/google/api_core/gapic_v1/client_cert.py +++ /dev/null @@ -1,82 +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. -# - -"""Helpers for client certificate handling and mTLS authentication.""" - -import os -from typing import Callable, Optional, Tuple - -from google.auth.transport import mtls # type: ignore - - -def use_client_cert_effective() -> bool: - """Returns whether client certificate should be used for mTLS if the - google-auth version supports should_use_client_cert automatic mTLS - enablement. - - Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. - - Returns: - bool: whether client certificate should be used for mTLS - Raises: - ValueError: (If using a version of google-auth without - should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is - set to an unexpected value.) - """ - # check if google-auth version supports should_use_client_cert for - # automatic mTLS enablement - if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER - return mtls.should_use_client_cert() - else: # pragma: NO COVER - # if unsupported, fallback to reading from env var - 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. - - Args: - provided_cert_source (Callable[[], Tuple[bytes, bytes]]): The client certificate source provided. - use_cert_flag (bool): A flag indicating whether to use the - client certificate. - - Returns: - Callable[[], Tuple[bytes, bytes]] or None: The client cert source to be used by the client. - """ - if use_cert_flag: - if provided_cert_source: - return provided_cert_source - elif ( - hasattr(mtls, "has_default_client_cert_source") - and mtls.has_default_client_cert_source() - ): - return mtls.default_client_cert_source() - else: - raise ValueError( - "Client certificate is required for mTLS, but no client certificate source was provided or found." - ) - return None 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..c87cdbc06dcc 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,9 +16,11 @@ """Helpers for client setup and configuration.""" -from typing import Any, Optional +import os +from typing import Any, Callable, Optional, Tuple from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.auth.transport import mtls # type: ignore def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]: @@ -124,3 +126,89 @@ def get_universe_domain( if not universe_domain: raise ValueError("Universe Domain cannot be an empty string.") return universe_domain + + +def use_client_cert_effective() -> bool: + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS + enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without + should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is + set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for + # automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + 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. + + Args: + provided_cert_source (Callable[[], Tuple[bytes, bytes]]): The client certificate source provided. + use_cert_flag (bool): A flag indicating whether to use the + client certificate. + + Returns: + Callable[[], Tuple[bytes, bytes]] or None: The client cert source to be used by the client. + """ + if use_cert_flag: + if provided_cert_source: + return provided_cert_source + elif ( + hasattr(mtls, "has_default_client_cert_source") + and mtls.has_default_client_cert_source() + ): + return mtls.default_client_cert_source() + else: + raise ValueError( + "Client certificate is required for mTLS, but no client certificate source was provided or found." + ) + return None + + +def read_environment_variables() -> Tuple[bool, str, Optional[str]]: + """Returns the environment variables used by the client. + + Returns: + Tuple[bool, str, Optional[str]]: returns the + GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT, + and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. + + Raises: + ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not + any of ["true", "false"]. + google.auth.exceptions.MutualTLSChannelError: If + GOOGLE_API_USE_MTLS_ENDPOINT is not any of + ["auto", "never", "always"]. + """ + 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 diff --git a/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py b/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py deleted file mode 100644 index c5cc708e006e..000000000000 --- a/packages/google-api-core/google/api_core/gapic_v1/config_helpers.py +++ /dev/null @@ -1,50 +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. -# - -"""Helpers for parsing environment variables.""" - -import os -from typing import Optional, Tuple - -from google.auth.exceptions import MutualTLSChannelError # type: ignore - -from google.api_core.gapic_v1.client_cert import use_client_cert_effective - - -def read_environment_variables() -> Tuple[bool, str, Optional[str]]: - """Returns the environment variables used by the client. - - Returns: - Tuple[bool, str, Optional[str]]: returns the - GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT, - and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. - - Raises: - ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not - any of ["true", "false"]. - google.auth.exceptions.MutualTLSChannelError: If - GOOGLE_API_USE_MTLS_ENDPOINT is not any of - ["auto", "never", "always"]. - """ - 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 diff --git a/packages/google-api-core/tests/unit/gapic/test_client_cert.py b/packages/google-api-core/tests/unit/gapic/test_client_cert.py deleted file mode 100644 index 3c0878816478..000000000000 --- a/packages/google-api-core/tests/unit/gapic/test_client_cert.py +++ /dev/null @@ -1,131 +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 os -from unittest import mock - -import pytest - -from google.auth.transport import mtls -from google.api_core.gapic_v1.client_cert import ( - get_client_cert_source, - use_client_cert_effective, -) - - -@pytest.mark.parametrize( - "has_method, method_val, env_val, expected", - [ - # should_use_client_cert is available cases - (True, True, None, "true"), - (True, False, None, "false"), - (True, False, "unsupported", "false"), - # should_use_client_cert is unavailable cases - (False, None, "true", "true"), - (False, None, "false", "false"), - (False, None, "True", "true"), - (False, None, "False", "false"), - (False, None, "TRUE", "true"), - (False, None, "FALSE", "false"), - (False, None, None, "false"), - (False, None, "unsupported", "value_error"), - ], - ids=[ - "google_auth_true", - "google_auth_false", - "google_auth_false_env_unsupported", - "fallback_env_true_lowercase", - "fallback_env_false_lowercase", - "fallback_env_true_titlecase", - "fallback_env_false_titlecase", - "fallback_env_true_uppercase", - "fallback_env_false_uppercase", - "fallback_env_unset", - "fallback_env_unsupported", - ], -) -def test_use_client_cert_effective(has_method, method_val, env_val, expected): - # Mock hasattr to control whether should_use_client_cert exists - original_hasattr = hasattr - - def custom_hasattr(obj, name): - if obj is mtls and name == "should_use_client_cert": - return has_method - return original_hasattr(obj, name) - - with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch( - "google.auth.transport.mtls.should_use_client_cert", - create=True, - return_value=method_val, - ): - env = {} - if env_val is not None: - env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val - with mock.patch.dict(os.environ, env, clear=True): - if expected == "value_error": - with pytest.raises( - ValueError, match="must be either `true` or `false`" - ): - use_client_cert_effective() - else: - assert use_client_cert_effective() is (expected == "true") - - -@pytest.mark.parametrize( - "provided, use_cert, has_default_avail, default_val, expected", - [ - (None, False, True, b"default", None), - (b"provided", False, True, b"default", None), - (b"provided", True, True, b"default", b"provided"), - (None, True, True, b"default", b"default"), - (None, True, False, b"default", "value_error"), - ], - ids=[ - "use_cert_false_no_provided", - "use_cert_false_with_provided", - "use_cert_true_with_provided", - "use_cert_true_no_provided_default_avail", - "use_cert_true_no_provided_default_unavail", - ], -) -def test_get_client_cert_source( - provided, use_cert, has_default_avail, default_val, expected -): - original_hasattr = hasattr - - def custom_hasattr(obj, name): - if obj is mtls and name == "has_default_client_cert_source": - return has_default_avail - return original_hasattr(obj, name) - - with mock.patch("google.api_core.gapic_v1.client_cert.hasattr", custom_hasattr): - with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - create=True, - return_value=has_default_avail, - ): - with mock.patch( - "google.auth.transport.mtls.default_client_cert_source", - create=True, - return_value=default_val, - ): - if expected == "value_error": - with pytest.raises( - ValueError, match="Client certificate is required for mTLS" - ): - get_client_cert_source(provided, use_cert) - else: - assert get_client_cert_source(provided, use_cert) == expected 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..9bacd506ca9e 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,16 +13,20 @@ # 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 - +from google.auth.transport import mtls from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, get_default_mtls_endpoint, get_universe_domain, + get_client_cert_source, + use_client_cert_effective, + read_environment_variables, ) @@ -199,3 +203,149 @@ def test__get_universe_domain(): with pytest.raises(ValueError) as excinfo: get_universe_domain("", None, MockClient._DEFAULT_UNIVERSE) assert str(excinfo.value) == "Universe Domain cannot be an empty string." + + +@pytest.mark.parametrize( + "has_method, method_val, env_val, expected", + [ + # should_use_client_cert is available cases + (True, True, None, "true"), + (True, False, None, "false"), + (True, False, "unsupported", "false"), + # should_use_client_cert is unavailable cases + (False, None, "true", "true"), + (False, None, "false", "false"), + (False, None, "True", "true"), + (False, None, "False", "false"), + (False, None, "TRUE", "true"), + (False, None, "FALSE", "false"), + (False, None, None, "false"), + (False, None, "unsupported", "value_error"), + ], + ids=[ + "google_auth_true", + "google_auth_false", + "google_auth_false_env_unsupported", + "fallback_env_true_lowercase", + "fallback_env_false_lowercase", + "fallback_env_true_titlecase", + "fallback_env_false_titlecase", + "fallback_env_true_uppercase", + "fallback_env_false_uppercase", + "fallback_env_unset", + "fallback_env_unsupported", + ], +) +def test_use_client_cert_effective(has_method, method_val, env_val, expected): + # Mock hasattr to control whether should_use_client_cert exists + original_hasattr = hasattr + + def custom_hasattr(obj, name): + if obj is mtls and name == "should_use_client_cert": + return has_method + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_utils.hasattr", custom_hasattr): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", + create=True, + return_value=method_val, + ): + env = {} + if env_val is not None: + env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val + with mock.patch.dict(os.environ, env, clear=True): + if expected == "value_error": + with pytest.raises( + ValueError, match="must be either `true` or `false`" + ): + use_client_cert_effective() + else: + assert use_client_cert_effective() is (expected == "true") + + +@pytest.mark.parametrize( + "provided, use_cert, has_default_avail, default_val, expected", + [ + (None, False, True, b"default", None), + (b"provided", False, True, b"default", None), + (b"provided", True, True, b"default", b"provided"), + (None, True, True, b"default", b"default"), + (None, True, False, b"default", "value_error"), + ], + ids=[ + "use_cert_false_no_provided", + "use_cert_false_with_provided", + "use_cert_true_with_provided", + "use_cert_true_no_provided_default_avail", + "use_cert_true_no_provided_default_unavail", + ], +) +def test_get_client_cert_source( + provided, use_cert, has_default_avail, default_val, expected +): + original_hasattr = hasattr + + def custom_hasattr(obj, name): + if obj is mtls and name == "has_default_client_cert_source": + return has_default_avail + return original_hasattr(obj, name) + + with mock.patch("google.api_core.gapic_v1.client_utils.hasattr", custom_hasattr): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + create=True, + return_value=has_default_avail, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + create=True, + return_value=default_val, + ): + if expected == "value_error": + with pytest.raises( + ValueError, match="Client certificate is required for mTLS" + ): + get_client_cert_source(provided, use_cert) + else: + assert get_client_cert_source(provided, use_cert) == expected + + +@pytest.mark.parametrize( + "env, mock_cert_val, expected", + [ + ({}, False, (False, "auto", None)), + ({}, True, (True, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, False, (False, "never", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), + ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), + ( + {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, + False, + (False, "auto", "foo.com"), + ), + ], + ids=[ + "default_env", + "client_cert_true", + "mtls_never", + "mtls_always", + "mtls_auto", + "mtls_invalid", + "universe_domain", + ], +) +def test_read_environment_variables(env, mock_cert_val, expected): + with mock.patch( + "google.api_core.gapic_v1.client_utils.use_client_cert_effective", + return_value=mock_cert_val, + ): + with mock.patch.dict(os.environ, env, clear=True): + if expected == "mutual_tls_error": + with pytest.raises( + MutualTLSChannelError, match="must be `never`, `auto` or `always`" + ): + read_environment_variables() + else: + assert read_environment_variables() == expected diff --git a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py b/packages/google-api-core/tests/unit/gapic/test_config_helpers.py deleted file mode 100644 index b9e27ed627a3..000000000000 --- a/packages/google-api-core/tests/unit/gapic/test_config_helpers.py +++ /dev/null @@ -1,62 +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 os -from unittest import mock - -import pytest - -from google.auth.exceptions import MutualTLSChannelError -from google.api_core.gapic_v1.config_helpers import read_environment_variables - - -@pytest.mark.parametrize( - "env, mock_cert_val, expected", - [ - ({}, False, (False, "auto", None)), - ({}, True, (True, "auto", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, False, (False, "never", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), - ( - {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, - False, - (False, "auto", "foo.com"), - ), - ], - ids=[ - "default_env", - "client_cert_true", - "mtls_never", - "mtls_always", - "mtls_auto", - "mtls_invalid", - "universe_domain", - ], -) -def test_read_environment_variables(env, mock_cert_val, expected): - with mock.patch( - "google.api_core.gapic_v1.config_helpers.use_client_cert_effective", - return_value=mock_cert_val, - ): - with mock.patch.dict(os.environ, env, clear=True): - if expected == "mutual_tls_error": - with pytest.raises( - MutualTLSChannelError, match="must be `never`, `auto` or `always`" - ): - read_environment_variables() - else: - assert read_environment_variables() == expected From 093ac26fd4904c4203b4498633b716248a878aa6 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 14:09:13 +0000 Subject: [PATCH 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 c0c5a6738e915f2b8e130e2077d7ea9b73e4486c Mon Sep 17 00:00:00 2001 From: nbayati <99771966+nbayati@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:36:05 -0700 Subject: [PATCH 18/26] fix(transport): propagate mTLS adapter to auth session and fix connection leaks (#17689) This PR is fixing two issues found in the auth library: 1. Adapter Connection Leaks Whenever the `configure_mtls_channel()` method was called, the code would create a new mTLS `HTTPAdapter` and mount it to `"https://"`. However, it never closed the *old* adapter it was replacing. Because of how the underlying `requests` and `urllib3` libraries work, the old adapter's connection pool was left open, creating dangling sockets and eventually causing file descriptor exhaustion. **The Fix:** I updated the method to retrieve the existing `"https://"` adapter (via `self.get_adapter()`) before replacing it. If an old adapter is found, we now explicitly call `old_adapter.close()` to shut down its connection pool and cleanly release the sockets back to the OS. 2. Missing Auth Request Session Update (Issue #17680) The `AuthorizedSession` class maintains an internal session called `_auth_request_session`. This hidden session is specifically responsible for doing background work, like fetching new OAuth tokens or signing IAM requests. When mTLS was enabled, the new secure adapter was *only* applied to the main user-facing session, completely missing this internal session. As a result, critical token refreshes were bypassing mTLS and failing when they hit strict Certificate-Based Access (CBA) endpoints. **The Fix:** I added logic to check if `self._auth_request_session` exists during the mTLS configuration. If it does, we now apply the exact same cleanup (closing its old adapter) and then explicitly mount the new mTLS adapter to it. This guarantees that all background token refreshes are routed securely over the mTLS channel. Also added a documentation warning to clarify that dynamically reconfiguring the channel mutates the underlying session and is not natively thread-safe. --- .../google/auth/transport/requests.py | 74 +++++++++- .../google/auth/transport/urllib3.py | 13 +- .../tests/transport/test_requests.py | 135 ++++++++++++++++++ .../tests/transport/test_urllib3.py | 57 ++++++++ 4 files changed, 274 insertions(+), 5 deletions(-) diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index eef0384652a6..12f4dda8e084 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -208,7 +208,7 @@ class _MutualTlsAdapter(requests.adapters.HTTPAdapter): google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid. """ - def __init__(self, cert, key): + def __init__(self, cert, key, **kwargs): import certifi import ssl @@ -250,7 +250,7 @@ def __init__(self, cert, key): self._ctx_poolmanager = ctx_poolmanager self._ctx_proxymanager = ctx_proxymanager - super(_MutualTlsAdapter, self).__init__() + super(_MutualTlsAdapter, self).__init__(**kwargs) def init_poolmanager(self, *args, **kwargs): kwargs["ssl_context"] = self._ctx_poolmanager @@ -457,6 +457,11 @@ def configure_mtls_channel(self, client_cert_callback=None): If the callback is None, application default SSL credentials will be used. + .. warning:: + Calling this method mutates the underlying `requests.Session` adapter + dictionary. It is not thread-safe to call this explicitly while other + threads are making requests. + Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel creation failed for any reason. The existing session state (such @@ -475,10 +480,58 @@ def configure_mtls_channel(self, client_cert_callback=None): client_cert_callback ) + old_adapter = self.adapters.get("https://") + + kwargs = {} + if old_adapter is not None: + kwargs["max_retries"] = getattr(old_adapter, "max_retries", 0) + kwargs["pool_connections"] = getattr( + old_adapter, "_pool_connections", requests.adapters.DEFAULT_POOLSIZE + ) + kwargs["pool_maxsize"] = getattr( + old_adapter, "_pool_maxsize", requests.adapters.DEFAULT_POOLSIZE + ) + kwargs["pool_block"] = getattr( + old_adapter, "_pool_block", requests.adapters.DEFAULT_POOLBLOCK + ) + + old_auth_adapter = None + auth_kwargs = {} + if self._auth_request_session is not None: + old_auth_adapter = self._auth_request_session.adapters.get("https://") + + if old_auth_adapter is not None: + auth_kwargs["max_retries"] = getattr( + old_auth_adapter, "max_retries", 0 + ) + auth_kwargs["pool_connections"] = getattr( + old_auth_adapter, + "_pool_connections", + requests.adapters.DEFAULT_POOLSIZE, + ) + auth_kwargs["pool_maxsize"] = getattr( + old_auth_adapter, + "_pool_maxsize", + requests.adapters.DEFAULT_POOLSIZE, + ) + auth_kwargs["pool_block"] = getattr( + old_auth_adapter, + "_pool_block", + requests.adapters.DEFAULT_POOLBLOCK, + ) + if is_mtls: - new_adapter = _MutualTlsAdapter(cert, key) + new_adapter = _MutualTlsAdapter(cert, key, **kwargs) + if self._auth_request_session is not None: + new_auth_adapter = _MutualTlsAdapter(cert, key, **auth_kwargs) + else: + new_auth_adapter = None else: - new_adapter = requests.adapters.HTTPAdapter() + new_adapter = requests.adapters.HTTPAdapter(**kwargs) + if self._auth_request_session is not None: + new_auth_adapter = requests.adapters.HTTPAdapter(**auth_kwargs) + else: + new_auth_adapter = None except ( exceptions.ClientCertError, ImportError, @@ -489,6 +542,19 @@ def configure_mtls_channel(self, client_cert_callback=None): raise new_exc from caught_exc self.mount("https://", new_adapter) + + if old_adapter is not None and old_adapter is not new_adapter: + old_adapter.close() + + if self._auth_request_session is not None and new_auth_adapter is not None: + self._auth_request_session.mount("https://", new_auth_adapter) + + if ( + old_auth_adapter is not None + and old_auth_adapter is not new_auth_adapter + ): + old_auth_adapter.close() + self._is_mtls = is_mtls if is_mtls: self._cached_cert = cert diff --git a/packages/google-auth/google/auth/transport/urllib3.py b/packages/google-auth/google/auth/transport/urllib3.py index 1b0fac7c342f..18e6128e03bd 100644 --- a/packages/google-auth/google/auth/transport/urllib3.py +++ b/packages/google-auth/google/auth/transport/urllib3.py @@ -335,6 +335,11 @@ def configure_mtls_channel(self, client_cert_callback=None): If the callback is None, application default SSL credentials will be used. + .. warning:: + Calling this method mutates the underlying `urllib3.PoolManager`. + It is not thread-safe to call this explicitly while other + threads are making requests. + Returns: True if the channel is mutual TLS and False otherwise. @@ -367,9 +372,15 @@ def configure_mtls_channel(self, client_cert_callback=None): new_exc = exceptions.MutualTLSChannelError(caught_exc) raise new_exc from caught_exc + old_http = self.http + self.http = new_http self._is_mtls = new_is_mtls self._request.http = new_http + + if old_http is not None and old_http is not new_http: + getattr(old_http, "clear", getattr(old_http, "close", lambda: None))() + if new_is_mtls: self._cached_cert = cert else: @@ -491,7 +502,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def __del__(self): if hasattr(self, "http") and self.http is not None: - self.http.clear() + getattr(self.http, "clear", getattr(self.http, "close", lambda: None))() @property def headers(self): diff --git a/packages/google-auth/tests/transport/test_requests.py b/packages/google-auth/tests/transport/test_requests.py index f14ccea58465..5aba3772132e 100644 --- a/packages/google-auth/tests/transport/test_requests.py +++ b/packages/google-auth/tests/transport/test_requests.py @@ -453,6 +453,141 @@ def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key google.auth.transport.requests._MutualTlsAdapter, ) + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_closes_old_adapters( + self, mock_get_client_cert_and_key + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock() + ) + old_main_adapter = mock.Mock(spec=requests.adapters.HTTPAdapter) + old_auth_adapter = mock.Mock(spec=requests.adapters.HTTPAdapter) + + auth_session.mount("https://", old_main_adapter) + auth_session._auth_request_session.mount("https://", old_auth_adapter) + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() + + old_main_adapter.close.assert_called_once() + old_auth_adapter.close.assert_called_once() + + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_mounts_adapter_to_auth_request_session( + self, mock_get_client_cert_and_key + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock() + ) + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() + + assert auth_session.is_mtls + # Main session gets the mTLS adapter + assert isinstance( + auth_session.adapters["https://"], + google.auth.transport.requests._MutualTlsAdapter, + ) + # _auth_request_session gets a separate adapter instance + assert isinstance( + auth_session._auth_request_session.adapters["https://"], + google.auth.transport.requests._MutualTlsAdapter, + ) + assert ( + auth_session.adapters["https://"] + is not auth_session._auth_request_session.adapters["https://"] + ) + + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_without_https_adapter( + self, mock_get_client_cert_and_key + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock() + ) + + # Remove the 'https://' adapter to trigger InvalidSchema + auth_session.adapters.pop("https://", None) + auth_session._auth_request_session.adapters.pop("https://", None) + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() + + assert auth_session.is_mtls + # Main session gets the mTLS adapter + assert isinstance( + auth_session.adapters["https://"], + google.auth.transport.requests._MutualTlsAdapter, + ) + # _auth_request_session gets the exact same adapter + assert isinstance( + auth_session._auth_request_session.adapters["https://"], + google.auth.transport.requests._MutualTlsAdapter, + ) + + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_without_auth_request_session( + self, mock_get_client_cert_and_key + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock(), auth_request=mock.Mock() + ) + assert auth_session._auth_request_session is None + + old_main_adapter = mock.Mock(spec=requests.adapters.HTTPAdapter) + auth_session.mount("https://", old_main_adapter) + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() + + old_main_adapter.close.assert_called_once() + assert auth_session.is_mtls + assert isinstance( + auth_session.adapters["https://"], + google.auth.transport.requests._MutualTlsAdapter, + ) + @mock.patch.object(google.auth.transport.requests._MutualTlsAdapter, "__init__") @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True diff --git a/packages/google-auth/tests/transport/test_urllib3.py b/packages/google-auth/tests/transport/test_urllib3.py index 33674030aa8d..e1c92dbebc2c 100644 --- a/packages/google-auth/tests/transport/test_urllib3.py +++ b/packages/google-auth/tests/transport/test_urllib3.py @@ -243,6 +243,63 @@ def test_configure_mtls_channel_with_metadata( cert=pytest.public_cert_bytes, key=pytest.private_key_bytes ) + @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_closes_old_poolmanager( + self, mock_get_client_cert_and_key, mock_make_mutual_tls_http + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + old_http = mock.create_autospec(urllib3.PoolManager) + authed_http = google.auth.transport.urllib3.AuthorizedHttp( + credentials=mock.Mock(), http=old_http + ) + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + is_mtls = authed_http.configure_mtls_channel() + + assert is_mtls + old_http.clear.assert_called_once() + mock_make_mutual_tls_http.assert_called_once_with( + cert=pytest.public_cert_bytes, key=pytest.private_key_bytes + ) + + @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_with_none_http( + self, mock_get_client_cert_and_key, mock_make_mutual_tls_http + ): + mock_get_client_cert_and_key.return_value = ( + True, + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + authed_http = google.auth.transport.urllib3.AuthorizedHttp( + credentials=mock.Mock() + ) + authed_http.http = None # Force old_http to be None + + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + is_mtls = authed_http.configure_mtls_channel() + + assert is_mtls + mock_make_mutual_tls_http.assert_called_once_with( + cert=pytest.public_cert_bytes, key=pytest.private_key_bytes + ) + @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True From a52129a56baa4b2d74224f021a40825bbb048b29 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 19:48:27 +0000 Subject: [PATCH 19/26] fix syntax/conflict residue and formatting issues in gapic_v1 --- .../google-api-core/google/api_core/gapic_v1/__init__.py | 7 +------ .../google-api-core/google/api_core/gapic_v1/requests.py | 2 +- packages/google-api-core/tests/conftest.py | 1 + .../google-api-core/tests/unit/gapic/test_client_utils.py | 6 +++--- packages/google-api-core/tests/unit/gapic/test_requests.py | 1 - 5 files changed, 6 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 4c97bc5bfc0f..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: 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/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 9bacd506ca9e..c03f0964cb46 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,16 +17,16 @@ from unittest import mock import pytest - from google.auth.exceptions import MutualTLSChannelError from google.auth.transport import mtls + from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, + get_client_cert_source, get_default_mtls_endpoint, get_universe_domain, - get_client_cert_source, - use_client_cert_effective, read_environment_variables, + use_client_cert_effective, ) 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 3158f863a5d08c9338afe81d35fdc38067c1a376 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:11:38 +0000 Subject: [PATCH 20/26] refactor(api-core): remove deprecated fallback client cert helpers and environment parsing --- .../google/api_core/gapic_v1/client_utils.py | 85 ---------- .../tests/unit/gapic/test_client_utils.py | 146 ------------------ 2 files changed, 231 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 c87cdbc06dcc..7c810416e0f4 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 @@ -127,88 +127,3 @@ def get_universe_domain( raise ValueError("Universe Domain cannot be an empty string.") return universe_domain - -def use_client_cert_effective() -> bool: - """Returns whether client certificate should be used for mTLS if the - google-auth version supports should_use_client_cert automatic mTLS - enablement. - - Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. - - Returns: - bool: whether client certificate should be used for mTLS - Raises: - ValueError: (If using a version of google-auth without - should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is - set to an unexpected value.) - """ - # check if google-auth version supports should_use_client_cert for - # automatic mTLS enablement - if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER - return mtls.should_use_client_cert() - else: # pragma: NO COVER - # if unsupported, fallback to reading from env var - 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. - - Args: - provided_cert_source (Callable[[], Tuple[bytes, bytes]]): The client certificate source provided. - use_cert_flag (bool): A flag indicating whether to use the - client certificate. - - Returns: - Callable[[], Tuple[bytes, bytes]] or None: The client cert source to be used by the client. - """ - if use_cert_flag: - if provided_cert_source: - return provided_cert_source - elif ( - hasattr(mtls, "has_default_client_cert_source") - and mtls.has_default_client_cert_source() - ): - return mtls.default_client_cert_source() - else: - raise ValueError( - "Client certificate is required for mTLS, but no client certificate source was provided or found." - ) - return None - - -def read_environment_variables() -> Tuple[bool, str, Optional[str]]: - """Returns the environment variables used by the client. - - Returns: - Tuple[bool, str, Optional[str]]: returns the - GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT, - and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. - - Raises: - ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not - any of ["true", "false"]. - google.auth.exceptions.MutualTLSChannelError: If - GOOGLE_API_USE_MTLS_ENDPOINT is not any of - ["auto", "never", "always"]. - """ - 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 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 c03f0964cb46..21fee4cc7567 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 @@ -22,11 +22,8 @@ from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, - get_client_cert_source, get_default_mtls_endpoint, get_universe_domain, - read_environment_variables, - use_client_cert_effective, ) @@ -205,147 +202,4 @@ def test__get_universe_domain(): assert str(excinfo.value) == "Universe Domain cannot be an empty string." -@pytest.mark.parametrize( - "has_method, method_val, env_val, expected", - [ - # should_use_client_cert is available cases - (True, True, None, "true"), - (True, False, None, "false"), - (True, False, "unsupported", "false"), - # should_use_client_cert is unavailable cases - (False, None, "true", "true"), - (False, None, "false", "false"), - (False, None, "True", "true"), - (False, None, "False", "false"), - (False, None, "TRUE", "true"), - (False, None, "FALSE", "false"), - (False, None, None, "false"), - (False, None, "unsupported", "value_error"), - ], - ids=[ - "google_auth_true", - "google_auth_false", - "google_auth_false_env_unsupported", - "fallback_env_true_lowercase", - "fallback_env_false_lowercase", - "fallback_env_true_titlecase", - "fallback_env_false_titlecase", - "fallback_env_true_uppercase", - "fallback_env_false_uppercase", - "fallback_env_unset", - "fallback_env_unsupported", - ], -) -def test_use_client_cert_effective(has_method, method_val, env_val, expected): - # Mock hasattr to control whether should_use_client_cert exists - original_hasattr = hasattr - - def custom_hasattr(obj, name): - if obj is mtls and name == "should_use_client_cert": - return has_method - return original_hasattr(obj, name) - - with mock.patch("google.api_core.gapic_v1.client_utils.hasattr", custom_hasattr): - with mock.patch( - "google.auth.transport.mtls.should_use_client_cert", - create=True, - return_value=method_val, - ): - env = {} - if env_val is not None: - env["GOOGLE_API_USE_CLIENT_CERTIFICATE"] = env_val - with mock.patch.dict(os.environ, env, clear=True): - if expected == "value_error": - with pytest.raises( - ValueError, match="must be either `true` or `false`" - ): - use_client_cert_effective() - else: - assert use_client_cert_effective() is (expected == "true") - - -@pytest.mark.parametrize( - "provided, use_cert, has_default_avail, default_val, expected", - [ - (None, False, True, b"default", None), - (b"provided", False, True, b"default", None), - (b"provided", True, True, b"default", b"provided"), - (None, True, True, b"default", b"default"), - (None, True, False, b"default", "value_error"), - ], - ids=[ - "use_cert_false_no_provided", - "use_cert_false_with_provided", - "use_cert_true_with_provided", - "use_cert_true_no_provided_default_avail", - "use_cert_true_no_provided_default_unavail", - ], -) -def test_get_client_cert_source( - provided, use_cert, has_default_avail, default_val, expected -): - original_hasattr = hasattr - - def custom_hasattr(obj, name): - if obj is mtls and name == "has_default_client_cert_source": - return has_default_avail - return original_hasattr(obj, name) - - with mock.patch("google.api_core.gapic_v1.client_utils.hasattr", custom_hasattr): - with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - create=True, - return_value=has_default_avail, - ): - with mock.patch( - "google.auth.transport.mtls.default_client_cert_source", - create=True, - return_value=default_val, - ): - if expected == "value_error": - with pytest.raises( - ValueError, match="Client certificate is required for mTLS" - ): - get_client_cert_source(provided, use_cert) - else: - assert get_client_cert_source(provided, use_cert) == expected - -@pytest.mark.parametrize( - "env, mock_cert_val, expected", - [ - ({}, False, (False, "auto", None)), - ({}, True, (True, "auto", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}, False, (False, "never", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}, False, (False, "always", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}, False, (False, "auto", None)), - ({"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}, False, "mutual_tls_error"), - ( - {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"}, - False, - (False, "auto", "foo.com"), - ), - ], - ids=[ - "default_env", - "client_cert_true", - "mtls_never", - "mtls_always", - "mtls_auto", - "mtls_invalid", - "universe_domain", - ], -) -def test_read_environment_variables(env, mock_cert_val, expected): - with mock.patch( - "google.api_core.gapic_v1.client_utils.use_client_cert_effective", - return_value=mock_cert_val, - ): - with mock.patch.dict(os.environ, env, clear=True): - if expected == "mutual_tls_error": - with pytest.raises( - MutualTLSChannelError, match="must be `never`, `auto` or `always`" - ): - read_environment_variables() - else: - assert read_environment_variables() == expected From 1abba92bdca7372d874f33c924177afe7b7e52cf Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:22:43 +0000 Subject: [PATCH 21/26] 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 22/26] 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 c8620119d8e52ed2fefebda01192d3a5211935fc Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 20:49:20 +0000 Subject: [PATCH 23/26] fix(api-core): resolve lint warnings in client_utils and tests --- .../google-api-core/google/api_core/gapic_v1/client_utils.py | 3 +-- .../google-api-core/tests/unit/gapic/test_client_utils.py | 4 ---- 2 files changed, 1 insertion(+), 6 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 6ff59617226c..2c114baa7e80 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 @@ -91,7 +91,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`" ) @@ -170,4 +170,3 @@ def get_universe_domain( 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_client_utils.py b/packages/google-api-core/tests/unit/gapic/test_client_utils.py index c6783610bd7e..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 @@ -18,7 +18,6 @@ import pytest from google.auth.exceptions import MutualTLSChannelError -from google.auth.transport import mtls from google.api_core.gapic_v1.client_utils import ( get_api_endpoint, @@ -243,6 +242,3 @@ def test__get_universe_domain(): 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 ddcaee12a53c5451c47c1402287b424d28cd3712 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:17:44 +0000 Subject: [PATCH 24/26] refactor(api-core): decouple client_utils helpers from client details and remove should_use_mtls_endpoint --- .../google/api_core/gapic_v1/client_utils.py | 62 ++------ .../tests/unit/gapic/test_client_utils.py | 144 +++++------------- 2 files changed, 52 insertions(+), 154 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 2c114baa7e80..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,11 +14,9 @@ # limitations under the License. # -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 @@ -68,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. @@ -127,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}." @@ -141,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: @@ -160,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 46e4347e1dca887ea267589e22f5b9d9c4b92181 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:34:15 +0000 Subject: [PATCH 25/26] 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 0314206692224a3473cc04bfa38f9eafc97dfb1c Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Fri, 17 Jul 2026 21:41:40 +0000 Subject: [PATCH 26/26] 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");