Skip to content

Commit 5abeb85

Browse files
committed
feat: centralize additional client helpers to api_core
1 parent 2168bc0 commit 5abeb85

2 files changed

Lines changed: 243 additions & 10 deletions

File tree

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

Lines changed: 134 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
import os
2020
import re
21-
from typing import Any, Optional
21+
import uuid
22+
from typing import Any, Optional, Tuple
2223

2324
from google.auth.exceptions import MutualTLSChannelError # type: ignore
2425
from google.auth.transport import mtls # type: ignore
2526

2627
_MTLS_ENDPOINT_RE = re.compile(
27-
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
28+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?"
29+
r"(?P<googledomain>\.googleapis\.com)?"
2830
)
2931

3032

@@ -60,17 +62,20 @@ def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
6062

6163
def _use_client_cert_effective() -> bool:
6264
"""Returns whether client certificate should be used for mTLS if the
63-
google-auth version supports should_use_client_cert automatic mTLS enablement.
65+
google-auth version supports should_use_client_cert automatic mTLS
66+
enablement.
6467
6568
Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
6669
6770
Returns:
6871
bool: whether client certificate should be used for mTLS
6972
Raises:
70-
ValueError: (If using a version of google-auth without should_use_client_cert and
71-
GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.)
73+
ValueError: (If using a version of google-auth without
74+
should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is
75+
set to an unexpected value.)
7276
"""
73-
# check if google-auth version supports should_use_client_cert for automatic mTLS enablement
77+
# check if google-auth version supports should_use_client_cert for
78+
# automatic mTLS enablement
7479
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
7580
return mtls.should_use_client_cert()
7681
else: # pragma: NO COVER
@@ -80,8 +85,8 @@ def _use_client_cert_effective() -> bool:
8085
).lower()
8186
if use_client_cert_str not in ("true", "false"):
8287
raise ValueError(
83-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
84-
" either `true` or `false`"
88+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` "
89+
"must be either `true` or `false`"
8590
)
8691
return use_client_cert_str == "true"
8792

@@ -116,8 +121,127 @@ def _get_api_endpoint(
116121
):
117122
if universe_domain != default_universe:
118123
raise MutualTLSChannelError(
119-
f"mTLS is not supported in any universe other than {default_universe}."
124+
f"mTLS is not supported in any universe other than "
125+
f"{default_universe}."
120126
)
121127
return default_mtls_endpoint
122128
else:
123-
return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
129+
return default_endpoint_template.format(
130+
UNIVERSE_DOMAIN=universe_domain
131+
)
132+
133+
134+
def _read_environment_variables() -> Tuple[bool, str, Optional[str]]:
135+
"""Returns the environment variables used by the client.
136+
137+
Returns:
138+
Tuple[bool, str, Optional[str]]: returns the
139+
GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT,
140+
and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables.
141+
142+
Raises:
143+
ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not
144+
any of ["true", "false"].
145+
google.auth.exceptions.MutualTLSChannelError: If
146+
GOOGLE_API_USE_MTLS_ENDPOINT is not any of
147+
["auto", "never", "always"].
148+
"""
149+
use_client_cert = _use_client_cert_effective()
150+
use_mtls_endpoint = os.getenv(
151+
"GOOGLE_API_USE_MTLS_ENDPOINT", "auto"
152+
).lower()
153+
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
154+
if use_mtls_endpoint not in ("auto", "never", "always"):
155+
raise MutualTLSChannelError(
156+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
157+
"must be `never`, `auto` or `always`"
158+
)
159+
return use_client_cert, use_mtls_endpoint, universe_domain_env
160+
161+
162+
def _get_client_cert_source(
163+
provided_cert_source: Optional[Any], use_cert_flag: bool
164+
) -> Optional[Any]:
165+
"""Return the client cert source to be used by the client.
166+
167+
Args:
168+
provided_cert_source (bytes): The client certificate source provided.
169+
use_cert_flag (bool): A flag indicating whether to use the
170+
client certificate.
171+
172+
Returns:
173+
bytes or None: The client cert source to be used by the client.
174+
"""
175+
client_cert_source = None
176+
if use_cert_flag:
177+
if provided_cert_source:
178+
client_cert_source = provided_cert_source
179+
elif (
180+
hasattr(mtls, "has_default_client_cert_source")
181+
and mtls.has_default_client_cert_source()
182+
):
183+
client_cert_source = mtls.default_client_cert_source()
184+
return client_cert_source
185+
186+
187+
def _get_universe_domain(
188+
client_universe_domain: Optional[str],
189+
universe_domain_env: Optional[str],
190+
default_universe: str,
191+
) -> str:
192+
"""Return the universe domain used by the client.
193+
194+
Args:
195+
client_universe_domain (Optional[str]): The universe domain
196+
configured via the client options.
197+
universe_domain_env (Optional[str]): The universe domain
198+
configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" env var.
199+
default_universe (str): The default universe domain.
200+
201+
Returns:
202+
str: The universe domain to be used by the client.
203+
204+
Raises:
205+
ValueError: If the universe domain is an empty string.
206+
"""
207+
universe_domain = default_universe
208+
if client_universe_domain is not None:
209+
universe_domain = client_universe_domain
210+
elif universe_domain_env is not None:
211+
universe_domain = universe_domain_env
212+
if len(universe_domain.strip()) == 0:
213+
raise ValueError("Universe Domain cannot be an empty string.")
214+
return universe_domain
215+
216+
217+
def _setup_request_id(
218+
request: Any, field_name: str, is_proto3_optional: bool
219+
) -> None:
220+
"""Populate a UUID4 field in the request if it is not already set.
221+
222+
Args:
223+
request (Union[google.protobuf.message.Message, dict]): The
224+
request object.
225+
field_name (str): The name of the field to populate.
226+
is_proto3_optional (bool): Whether the field is proto3 optional.
227+
"""
228+
if isinstance(request, dict):
229+
if is_proto3_optional:
230+
if field_name not in request:
231+
request[field_name] = str(uuid.uuid4())
232+
elif not request.get(field_name):
233+
request[field_name] = str(uuid.uuid4())
234+
return
235+
236+
if is_proto3_optional:
237+
try:
238+
# Pure protobuf messages
239+
if not request.HasField(field_name):
240+
setattr(request, field_name, str(uuid.uuid4()))
241+
except (AttributeError, ValueError):
242+
# Proto-plus messages or other objects
243+
if field_name not in request:
244+
setattr(request, field_name, str(uuid.uuid4()))
245+
else:
246+
if not getattr(request, field_name):
247+
setattr(request, field_name, str(uuid.uuid4()))

packages/google-api-core/tests/unit/gapic/test_client_helpers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,112 @@ def test__get_api_endpoint_mtls_universe_mismatch():
149149
default_mtls_endpoint="foo.mtls.googleapis.com",
150150
default_endpoint_template="foo.{UNIVERSE_DOMAIN}",
151151
)
152+
153+
154+
@mock.patch(
155+
"google.api_core.gapic_v1.client_helpers._use_client_cert_effective"
156+
)
157+
@mock.patch.dict(os.environ, clear=True)
158+
def test__read_environment_variables(mock_effective):
159+
mock_effective.return_value = True
160+
os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "always"
161+
os.environ["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = "custom.com"
162+
163+
cert, mtls, domain = client_helpers._read_environment_variables()
164+
assert cert is True
165+
assert mtls == "always"
166+
assert domain == "custom.com"
167+
168+
169+
@mock.patch.dict(os.environ, clear=True)
170+
def test__read_environment_variables_invalid_mtls():
171+
os.environ["GOOGLE_API_USE_MTLS_ENDPOINT"] = "invalid"
172+
with pytest.raises(
173+
MutualTLSChannelError, match="must be `never`, `auto` or `always`"
174+
):
175+
client_helpers._read_environment_variables()
176+
177+
178+
@mock.patch("google.auth.transport.mtls.has_default_client_cert_source")
179+
@mock.patch("google.auth.transport.mtls.default_client_cert_source")
180+
def test__get_client_cert_source(mock_default, mock_has_default):
181+
mock_default.return_value = b"default_cert"
182+
mock_has_default.return_value = True
183+
184+
# When use_cert_flag is False, return None
185+
assert client_helpers._get_client_cert_source(b"provided", False) is None
186+
187+
# When provided_cert_source is given, return provided
188+
assert (
189+
client_helpers._get_client_cert_source(b"provided", True)
190+
== b"provided"
191+
)
192+
193+
# When no provided cert but default is available
194+
assert (
195+
client_helpers._get_client_cert_source(None, True) == b"default_cert"
196+
)
197+
198+
199+
def test__get_universe_domain():
200+
# client_universe_domain takes precedence
201+
assert (
202+
client_helpers._get_universe_domain(
203+
"client.com", "env.com", "default.com"
204+
)
205+
== "client.com"
206+
)
207+
208+
# env takes precedence over default
209+
assert (
210+
client_helpers._get_universe_domain(None, "env.com", "default.com")
211+
== "env.com"
212+
)
213+
214+
# fallback to default
215+
assert (
216+
client_helpers._get_universe_domain(None, None, "default.com")
217+
== "default.com"
218+
)
219+
220+
221+
def test__get_universe_domain_empty():
222+
with pytest.raises(ValueError, match="cannot be an empty string"):
223+
client_helpers._get_universe_domain("", None, "default.com")
224+
225+
226+
def test__setup_request_id():
227+
import uuid
228+
229+
# test dict request
230+
req = {}
231+
client_helpers._setup_request_id(req, "request_id", True)
232+
assert "request_id" in req
233+
uuid_str = req["request_id"]
234+
uuid.UUID(uuid_str) # verify it is a valid UUID
235+
236+
# test dict request when already set
237+
req = {"request_id": "existing"}
238+
client_helpers._setup_request_id(req, "request_id", True)
239+
assert req["request_id"] == "existing"
240+
241+
class DummyRequest:
242+
def __init__(self):
243+
self.request_id = ""
244+
245+
def HasField(self, field_name):
246+
if not hasattr(self, field_name):
247+
raise ValueError()
248+
return bool(getattr(self, field_name))
249+
250+
# test object request proto3 optional true
251+
req_obj = DummyRequest()
252+
client_helpers._setup_request_id(req_obj, "request_id", True)
253+
assert req_obj.request_id != ""
254+
uuid.UUID(req_obj.request_id)
255+
256+
# test object request proto3 optional false
257+
req_obj2 = DummyRequest()
258+
client_helpers._setup_request_id(req_obj2, "request_id", False)
259+
assert req_obj2.request_id != ""
260+
uuid.UUID(req_obj2.request_id)

0 commit comments

Comments
 (0)