Skip to content

Commit c60e977

Browse files
committed
feat: centralize service-agnostic boilerplate into api_core for GAPIC clients
1 parent 4b3ba9d commit c60e977

3 files changed

Lines changed: 148 additions & 52 deletions

File tree

packages/google-api-core/google/api_core/gapic_v1/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from google.api_core.gapic_v1 import client_helpers
1516
from google.api_core.gapic_v1 import client_info
1617
from google.api_core.gapic_v1 import config
1718
from google.api_core.gapic_v1 import config_async
@@ -20,10 +21,12 @@
2021
from google.api_core.gapic_v1 import routing_header
2122

2223
__all__ = [
24+
"client_helpers",
2325
"client_info",
2426
"config",
2527
"config_async",
2628
"method",
2729
"method_async",
2830
"routing_header",
2931
]
32+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
"""Helpers for GAPIC client initialization."""
18+
19+
import os
20+
import re
21+
from typing import Optional
22+
23+
from google.auth.exceptions import MutualTLSChannelError # type: ignore
24+
from google.auth.transport import mtls # type: ignore
25+
26+
27+
28+
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
29+
"""Converts api endpoint to mTLS endpoint.
30+
31+
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
32+
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
33+
Args:
34+
api_endpoint (Optional[str]): the api endpoint to convert.
35+
Returns:
36+
Optional[str]: converted mTLS api endpoint.
37+
"""
38+
if not api_endpoint:
39+
return api_endpoint
40+
41+
mtls_endpoint_re = re.compile(
42+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
43+
)
44+
45+
m = mtls_endpoint_re.match(api_endpoint)
46+
if m is None:
47+
# Could not parse api_endpoint; return as-is.
48+
return api_endpoint
49+
50+
name, mtls_group, sandbox, googledomain = m.groups()
51+
if mtls_group or not googledomain:
52+
return api_endpoint
53+
54+
if sandbox:
55+
return api_endpoint.replace(
56+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
57+
)
58+
59+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
60+
61+
62+
def use_client_cert_effective() -> bool:
63+
"""Returns whether client certificate should be used for mTLS if the
64+
google-auth version supports should_use_client_cert automatic mTLS enablement.
65+
66+
Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
67+
68+
Returns:
69+
bool: whether client certificate should be used for mTLS
70+
Raises:
71+
ValueError: (If using a version of google-auth without should_use_client_cert and
72+
GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.)
73+
"""
74+
# check if google-auth version supports should_use_client_cert for automatic mTLS enablement
75+
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
76+
return mtls.should_use_client_cert()
77+
else: # pragma: NO COVER
78+
# if unsupported, fallback to reading from env var
79+
use_client_cert_str = os.getenv(
80+
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
81+
).lower()
82+
if use_client_cert_str not in ("true", "false"):
83+
raise ValueError(
84+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
85+
" either `true` or `false`"
86+
)
87+
return use_client_cert_str == "true"
88+
89+
90+
def get_api_endpoint(
91+
api_override: Optional[str],
92+
client_cert_source: Optional[bytes],
93+
universe_domain: str,
94+
use_mtls_endpoint: str,
95+
default_universe: str,
96+
default_mtls_endpoint: Optional[str],
97+
default_endpoint_template: str,
98+
) -> str:
99+
"""Return the API endpoint used by the client.
100+
101+
Args:
102+
api_override (Optional[str]): The API endpoint override.
103+
client_cert_source (Optional[bytes]): The client certificate source.
104+
universe_domain (str): The universe domain.
105+
use_mtls_endpoint (str): How to use the mTLS endpoint.
106+
default_universe (str): The default universe.
107+
default_mtls_endpoint (Optional[str]): The default mTLS endpoint.
108+
default_endpoint_template (str): The default endpoint template.
109+
110+
Returns:
111+
str: The API endpoint to be used by the client.
112+
"""
113+
if api_override is not None:
114+
api_endpoint = api_override
115+
elif use_mtls_endpoint == "always" or (
116+
use_mtls_endpoint == "auto" and client_cert_source
117+
):
118+
if universe_domain != default_universe:
119+
raise MutualTLSChannelError(
120+
f"mTLS is not supported in any universe other than {default_universe}."
121+
)
122+
api_endpoint = default_mtls_endpoint
123+
else:
124+
api_endpoint = default_endpoint_template.format(
125+
UNIVERSE_DOMAIN=universe_domain
126+
)
127+
return api_endpoint
128+

packages/google-cloud-kms/google/cloud/kms_v1/services/key_management_service/client.py

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -145,28 +145,7 @@ def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]:
145145
Returns:
146146
Optional[str]: converted mTLS api endpoint.
147147
"""
148-
if not api_endpoint:
149-
return api_endpoint
150-
151-
mtls_endpoint_re = re.compile(
152-
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
153-
)
154-
155-
m = mtls_endpoint_re.match(api_endpoint)
156-
if m is None:
157-
# Could not parse api_endpoint; return as-is.
158-
return api_endpoint
159-
160-
name, mtls, sandbox, googledomain = m.groups()
161-
if mtls or not googledomain:
162-
return api_endpoint
163-
164-
if sandbox:
165-
return api_endpoint.replace(
166-
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
167-
)
168-
169-
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
148+
return gapic_v1.client_helpers.get_default_mtls_endpoint(api_endpoint)
170149

171150
# Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead.
172151
DEFAULT_ENDPOINT = "cloudkms.googleapis.com"
@@ -190,20 +169,10 @@ def _use_client_cert_effective():
190169
ValueError: (If using a version of google-auth without should_use_client_cert and
191170
GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.)
192171
"""
193-
# check if google-auth version supports should_use_client_cert for automatic mTLS enablement
194-
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
195-
return mtls.should_use_client_cert()
196-
else: # pragma: NO COVER
197-
# if unsupported, fallback to reading from env var
198-
use_client_cert_str = os.getenv(
199-
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
200-
).lower()
201-
if use_client_cert_str not in ("true", "false"):
202-
raise ValueError(
203-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
204-
" either `true` or `false`"
205-
)
206-
return use_client_cert_str == "true"
172+
return gapic_v1.client_helpers.use_client_cert_effective()
173+
174+
175+
207176

208177
@classmethod
209178
def from_service_account_info(cls, info: dict, *args, **kwargs):
@@ -600,22 +569,18 @@ def _get_api_endpoint(
600569
Returns:
601570
str: The API endpoint to be used by the client.
602571
"""
603-
if api_override is not None:
604-
api_endpoint = api_override
605-
elif use_mtls_endpoint == "always" or (
606-
use_mtls_endpoint == "auto" and client_cert_source
607-
):
608-
_default_universe = KeyManagementServiceClient._DEFAULT_UNIVERSE
609-
if universe_domain != _default_universe:
610-
raise MutualTLSChannelError(
611-
f"mTLS is not supported in any universe other than {_default_universe}."
612-
)
613-
api_endpoint = KeyManagementServiceClient.DEFAULT_MTLS_ENDPOINT
614-
else:
615-
api_endpoint = KeyManagementServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(
616-
UNIVERSE_DOMAIN=universe_domain
617-
)
618-
return api_endpoint
572+
return gapic_v1.client_helpers.get_api_endpoint(
573+
api_override,
574+
client_cert_source,
575+
universe_domain,
576+
use_mtls_endpoint,
577+
KeyManagementServiceClient._DEFAULT_UNIVERSE,
578+
KeyManagementServiceClient.DEFAULT_MTLS_ENDPOINT,
579+
KeyManagementServiceClient._DEFAULT_ENDPOINT_TEMPLATE,
580+
)
581+
582+
583+
619584

620585
@staticmethod
621586
def _get_universe_domain(

0 commit comments

Comments
 (0)