Skip to content

Commit 7208833

Browse files
committed
feat: split client_helpers into domain-specific modules
Splits the monolithic client_helpers.py into routing.py, client_cert.py, config_helpers.py, and method_helpers.py to improve readability and maintainability. Updates gapic_v1 exports and test imports accordingly.
1 parent 2353e82 commit 7208833

7 files changed

Lines changed: 353 additions & 309 deletions

File tree

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@
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
16-
from google.api_core.gapic_v1 import client_info
17-
from google.api_core.gapic_v1 import config
18-
from google.api_core.gapic_v1 import config_async
19-
from google.api_core.gapic_v1 import method
20-
from google.api_core.gapic_v1 import method_async
21-
from google.api_core.gapic_v1 import routing_header
15+
from google.api_core.gapic_v1 import (_client_cert, _config_helpers,
16+
_method_helpers, _routing, client_info,
17+
config, config_async, method,
18+
method_async, routing_header)
2219

2320
__all__ = [
24-
"client_helpers",
21+
"_client_cert",
2522
"client_info",
2623
"config",
2724
"config_async",
25+
"_config_helpers",
2826
"method",
2927
"method_async",
28+
"_method_helpers",
29+
"_routing",
3030
"routing_header",
3131
]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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 client certificate handling and mTLS authentication."""
18+
19+
import os
20+
from typing import Any, Optional
21+
22+
from google.auth.transport import mtls # type: ignore
23+
24+
25+
def use_client_cert_effective() -> bool:
26+
"""Returns whether client certificate should be used for mTLS if the
27+
google-auth version supports should_use_client_cert automatic mTLS
28+
enablement.
29+
30+
Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
31+
32+
Returns:
33+
bool: whether client certificate should be used for mTLS
34+
Raises:
35+
ValueError: (If using a version of google-auth without
36+
should_use_client_cert and GOOGLE_API_USE_CLIENT_CERTIFICATE is
37+
set to an unexpected value.)
38+
"""
39+
# check if google-auth version supports should_use_client_cert for
40+
# automatic mTLS enablement
41+
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
42+
return mtls.should_use_client_cert()
43+
else: # pragma: NO COVER
44+
# if unsupported, fallback to reading from env var
45+
use_client_cert_str = os.getenv(
46+
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
47+
).lower()
48+
if use_client_cert_str not in ("true", "false"):
49+
raise ValueError(
50+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` "
51+
"must be either `true` or `false`"
52+
)
53+
return use_client_cert_str == "true"
54+
55+
56+
def get_client_cert_source(
57+
provided_cert_source: Optional[Any], use_cert_flag: bool
58+
) -> Optional[Any]:
59+
"""Return the client cert source to be used by the client.
60+
61+
Args:
62+
provided_cert_source (bytes): The client certificate source provided.
63+
use_cert_flag (bool): A flag indicating whether to use the
64+
client certificate.
65+
66+
Returns:
67+
bytes or None: The client cert source to be used by the client.
68+
"""
69+
client_cert_source = None
70+
if use_cert_flag:
71+
if provided_cert_source:
72+
client_cert_source = provided_cert_source
73+
elif (
74+
hasattr(mtls, "has_default_client_cert_source")
75+
and mtls.has_default_client_cert_source()
76+
):
77+
client_cert_source = mtls.default_client_cert_source()
78+
return client_cert_source
79+
80+
81+
# Backward compatibility aliases for private methods
82+
# Previously, gapic-generator-python generated clients used these methods
83+
_use_client_cert_effective = use_client_cert_effective
84+
_get_client_cert_source = get_client_cert_source
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 parsing environment variables."""
18+
19+
import os
20+
from typing import Optional, Tuple
21+
22+
from google.api_core.gapic_v1._client_cert import use_client_cert_effective
23+
from google.auth.exceptions import MutualTLSChannelError # type: ignore
24+
25+
26+
def read_environment_variables() -> Tuple[bool, str, Optional[str]]:
27+
"""Returns the environment variables used by the client.
28+
29+
Returns:
30+
Tuple[bool, str, Optional[str]]: returns the
31+
GOOGLE_API_USE_CLIENT_CERTIFICATE, GOOGLE_API_USE_MTLS_ENDPOINT,
32+
and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables.
33+
34+
Raises:
35+
ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not
36+
any of ["true", "false"].
37+
google.auth.exceptions.MutualTLSChannelError: If
38+
GOOGLE_API_USE_MTLS_ENDPOINT is not any of
39+
["auto", "never", "always"].
40+
"""
41+
use_client_cert = use_client_cert_effective()
42+
use_mtls_endpoint = os.getenv(
43+
"GOOGLE_API_USE_MTLS_ENDPOINT", "auto"
44+
).lower() # noqa: E501
45+
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
46+
if use_mtls_endpoint not in ("auto", "never", "always"):
47+
raise MutualTLSChannelError(
48+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
49+
"must be `never`, `auto` or `always`"
50+
)
51+
return use_client_cert, use_mtls_endpoint, universe_domain_env
52+
53+
54+
# Backward compatibility aliases for private methods
55+
# Previously, gapic-generator-python generated clients used these methods
56+
_read_environment_variables = read_environment_variables
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 method requests."""
18+
19+
import uuid
20+
from typing import Any
21+
22+
23+
def setup_request_id(
24+
request: Any, field_name: str, is_proto3_optional: bool
25+
) -> None: # noqa: E501
26+
"""Populate a UUID4 field in the request if it is not already set.
27+
28+
Args:
29+
request (Union[google.protobuf.message.Message, dict]): The
30+
request object.
31+
field_name (str): The name of the field to populate.
32+
is_proto3_optional (bool): Whether the field is proto3 optional.
33+
"""
34+
if isinstance(request, dict):
35+
if is_proto3_optional:
36+
if field_name not in request:
37+
request[field_name] = str(uuid.uuid4())
38+
elif not request.get(field_name):
39+
request[field_name] = str(uuid.uuid4())
40+
return
41+
42+
if is_proto3_optional:
43+
try:
44+
# Pure protobuf messages
45+
if not request.HasField(field_name):
46+
setattr(request, field_name, str(uuid.uuid4()))
47+
except (AttributeError, ValueError):
48+
# Proto-plus messages or other objects
49+
if field_name not in request:
50+
setattr(request, field_name, str(uuid.uuid4()))
51+
else:
52+
if not getattr(request, field_name):
53+
setattr(request, field_name, str(uuid.uuid4()))
54+
55+
56+
# Backward compatibility aliases for private methods
57+
# Previously, gapic-generator-python generated clients used these methods
58+
_setup_request_id = setup_request_id
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 routing and endpoint resolution."""
18+
19+
import re
20+
from typing import Any, Optional
21+
22+
from google.auth.exceptions import MutualTLSChannelError # type: ignore
23+
24+
_MTLS_ENDPOINT_RE = re.compile(
25+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?"
26+
r"(?P<googledomain>\.googleapis\.com)?"
27+
)
28+
29+
30+
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
31+
"""Converts api endpoint to mTLS endpoint.
32+
33+
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
34+
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
35+
Args:
36+
api_endpoint (Optional[str]): the api endpoint to convert.
37+
Returns:
38+
Optional[str]: converted mTLS api endpoint.
39+
"""
40+
if not api_endpoint:
41+
return api_endpoint
42+
43+
m = _MTLS_ENDPOINT_RE.match(api_endpoint)
44+
if m is None:
45+
# Could not parse api_endpoint; return as-is.
46+
return api_endpoint
47+
48+
name, mtls_group, sandbox, googledomain = m.groups()
49+
if mtls_group or not googledomain:
50+
return api_endpoint
51+
52+
if sandbox:
53+
return api_endpoint.replace(
54+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
55+
)
56+
57+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
58+
59+
60+
def get_api_endpoint(
61+
api_override: Optional[str],
62+
client_cert_source: Optional[Any],
63+
universe_domain: str,
64+
use_mtls_endpoint: str,
65+
default_universe: str,
66+
default_mtls_endpoint: Optional[str],
67+
default_endpoint_template: str,
68+
) -> Optional[str]:
69+
"""Return the API endpoint used by the client."""
70+
if api_override is not None:
71+
return api_override
72+
elif use_mtls_endpoint == "always" or (
73+
use_mtls_endpoint == "auto" and client_cert_source
74+
):
75+
if universe_domain != default_universe:
76+
raise MutualTLSChannelError(
77+
f"mTLS is not supported in any universe other than "
78+
f"{default_universe}."
79+
)
80+
return default_mtls_endpoint
81+
else:
82+
return default_endpoint_template.format(
83+
UNIVERSE_DOMAIN=universe_domain
84+
) # noqa: E501
85+
86+
87+
def get_universe_domain(
88+
client_universe_domain: Optional[str],
89+
universe_domain_env: Optional[str],
90+
default_universe: str,
91+
) -> str:
92+
"""Return the universe domain used by the client."""
93+
universe_domain = default_universe
94+
if client_universe_domain is not None:
95+
universe_domain = client_universe_domain
96+
elif universe_domain_env is not None:
97+
universe_domain = universe_domain_env
98+
if len(universe_domain.strip()) == 0:
99+
raise ValueError("Universe Domain cannot be an empty string.")
100+
return universe_domain
101+
102+
103+
# Backward compatibility aliases for private methods
104+
# Previously, gapic-generator-python generated clients used these methods
105+
_get_default_mtls_endpoint = get_default_mtls_endpoint
106+
_get_universe_domain = get_universe_domain

0 commit comments

Comments
 (0)