Visitar URL original
feat(generator): add fallback compatibility imports for google-api-co… · googleapis/google-cloud-python@9c13ae2 · GitHub
Skip to content

Commit 9c13ae2

Browse files
authored
feat(generator): add fallback compatibility imports for google-api-core helpers (#17999)
## Description Updates `_compat.py.j2` to import helper functions (`get_default_mtls_endpoint`, `get_api_endpoint`, `get_universe_domain`, `setup_request_id` and `transcode_request`) from `google-api-core`, with fallbacks to local implementations for older versions. ### Summary of Changes - **`_compat.py.j2`**: Added `try...except ImportError` fallback imports for `google-api-core` helper functions. ### Note on `setup_request_id` `google-api-core`. The implementation in `google-api-core==2.33.0` differs slightly from what the generator expects and will be updated in an upcoming release. Keeping it local avoids dependency mismatches and preserves 100% test coverage.
1 parent b87776d commit 9c13ae2

22 files changed

Lines changed: 1706 additions & 1586 deletions

File tree

‎packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2‎

Lines changed: 248 additions & 224 deletions
Large diffs are not rendered by default.

‎packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
442442

443443
self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = read_environment_variables()
444444
self._client_cert_source = {{ service.client_name }}._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert)
445-
self._universe_domain = get_universe_domain(universe_domain_opt, self._universe_domain_env)
445+
self._universe_domain = get_universe_domain(universe_domain_opt, self._universe_domain_env, default_universe={{ service.client_name }}._DEFAULT_UNIVERSE)
446446
self._api_endpoint: str = "" # updated below, depending on `transport`
447447

448448
# Initialize the universe domain validation.

‎packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py‎

Lines changed: 172 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -69,175 +69,186 @@ def read_environment_variables():
6969

7070
DEFAULT_UNIVERSE = "googleapis.com"
7171

72+
try:
73+
from google.api_core.universe import get_default_mtls_endpoint
74+
except ImportError: # pragma: NO COVER
75+
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
76+
"""Converts api endpoint to mTLS endpoint.
77+
78+
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
79+
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
80+
Other URLs (including those that do not match these domain suffixes or
81+
already contain '.mtls.') are passed through as-is.
82+
83+
Args:
84+
api_endpoint (Optional[str]): the api endpoint to convert.
85+
86+
Returns:
87+
Optional[str]: converted mTLS api endpoint.
88+
"""
89+
if not api_endpoint or ".mtls." in api_endpoint.lower():
90+
return api_endpoint
91+
92+
has_scheme = "://" in api_endpoint
93+
if not has_scheme:
94+
parsed = urlparse("//" + api_endpoint)
95+
else:
96+
parsed = urlparse(api_endpoint)
97+
98+
host = parsed.hostname
99+
if not host:
100+
return api_endpoint
101+
102+
port = f":{parsed.port}" if parsed.port else ""
103+
104+
lowered_host = host.lower()
105+
suffix_sandbox = ".sandbox.googleapis.com"
106+
suffix_google = ".googleapis.com"
107+
if lowered_host.endswith(suffix_sandbox):
108+
new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com"
109+
elif lowered_host.endswith(suffix_google):
110+
new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com"
111+
else:
112+
return api_endpoint
113+
114+
netloc = new_host + port
115+
new_parsed = parsed._replace(netloc=netloc)
116+
117+
if not has_scheme:
118+
return urlunparse(new_parsed)[2:]
119+
else:
120+
return urlunparse(new_parsed)
72121

73-
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
74-
"""Converts api endpoint to mTLS endpoint.
75-
76-
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
77-
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
78-
Other URLs (including those that do not match these domain suffixes or
79-
already contain '.mtls.') are passed through as-is.
80-
81-
Args:
82-
api_endpoint (Optional[str]): the api endpoint to convert.
122+
try:
123+
from google.api_core.universe import get_api_endpoint
124+
except ImportError: # pragma: NO COVER
125+
def get_api_endpoint(
126+
api_override: Optional[str],
127+
universe_domain: str,
128+
default_universe: str,
129+
default_mtls_endpoint: Optional[str],
130+
default_endpoint_template: str,
131+
use_mtls: bool,
132+
) -> str:
133+
"""Return the API endpoint used by the client.
134+
135+
Args:
136+
api_override (Optional[str]): The API endpoint override. If specified,
137+
this is always returned.
138+
universe_domain (str): The universe domain used by the client.
139+
default_universe (str): The default universe domain.
140+
default_mtls_endpoint (Optional[str]): The default mTLS endpoint.
141+
default_endpoint_template (str): The default endpoint template containing
142+
a placeholder `{UNIVERSE_DOMAIN}`.
143+
use_mtls (bool): Whether to use the mTLS endpoint.
144+
145+
Returns:
146+
str: The API endpoint to be used by the client.
147+
148+
Raises:
149+
google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but
150+
not supported in the configured universe domain.
151+
ValueError: If mTLS is requested but no mTLS endpoint is available.
152+
"""
153+
if api_override is not None:
154+
return api_override
155+
156+
if use_mtls:
157+
if universe_domain.lower() != default_universe.lower():
158+
raise MutualTLSChannelError(
159+
f"mTLS is not supported in any universe other than {default_universe}."
160+
)
161+
if not default_mtls_endpoint:
162+
raise ValueError("mTLS endpoint is not available.")
163+
return default_mtls_endpoint
164+
else:
165+
return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
83166

84-
Returns:
85-
Optional[str]: converted mTLS api endpoint.
86-
"""
87-
if not api_endpoint or ".mtls." in api_endpoint.lower():
88-
return api_endpoint
89-
90-
has_scheme = "://" in api_endpoint
91-
if not has_scheme:
92-
parsed = urlparse("//" + api_endpoint)
93-
else:
94-
parsed = urlparse(api_endpoint)
95-
96-
host = parsed.hostname
97-
if not host:
98-
return api_endpoint
99-
100-
port = f":{parsed.port}" if parsed.port else ""
101-
102-
lowered_host = host.lower()
103-
suffix_sandbox = ".sandbox.googleapis.com"
104-
suffix_google = ".googleapis.com"
105-
if lowered_host.endswith(suffix_sandbox):
106-
new_host = host[: -len(suffix_sandbox)] + ".mtls.sandbox.googleapis.com"
107-
elif lowered_host.endswith(suffix_google):
108-
new_host = host[: -len(suffix_google)] + ".mtls.googleapis.com"
109-
else:
110-
return api_endpoint
111-
112-
netloc = new_host + port
113-
new_parsed = parsed._replace(netloc=netloc)
114-
115-
if not has_scheme:
116-
return urlunparse(new_parsed)[2:]
117-
else:
118-
return urlunparse(new_parsed)
119-
120-
def get_api_endpoint(
121-
api_override: Optional[str],
122-
universe_domain: str,
123-
default_universe: str,
124-
default_mtls_endpoint: Optional[str],
125-
default_endpoint_template: str,
126-
use_mtls: bool,
127-
) -> str:
128-
"""Return the API endpoint used by the client.
129-
130-
Args:
131-
api_override (Optional[str]): The API endpoint override. If specified,
132-
this is always returned.
133-
universe_domain (str): The universe domain used by the client.
134-
default_universe (str): The default universe domain.
135-
default_mtls_endpoint (Optional[str]): The default mTLS endpoint.
136-
default_endpoint_template (str): The default endpoint template containing
137-
a placeholder `{UNIVERSE_DOMAIN}`.
138-
use_mtls (bool): Whether to use the mTLS endpoint.
167+
try:
168+
from google.api_core.universe import get_universe_domain
169+
except ImportError: # pragma: NO COVER
170+
def get_universe_domain(
171+
*potential_universes: Optional[str],
172+
default_universe: str,
173+
) -> str:
174+
"""Return the universe domain used by the client.
175+
176+
Args:
177+
*potential_universes (Optional[str]): Potential universe domains in order of preference.
178+
default_universe (str): The default universe domain.
179+
180+
Returns:
181+
str: The universe domain to be used by the client.
182+
183+
Raises:
184+
EmptyUniverseError: If the resolved universe domain is an empty string.
185+
"""
186+
resolved = next(
187+
(x.strip() for x in potential_universes if x is not None),
188+
default_universe,
189+
)
139190

140-
Returns:
141-
str: The API endpoint to be used by the client.
191+
if not resolved:
192+
raise EmptyUniverseError()
193+
return resolved
142194

143-
Raises:
144-
google.auth.exceptions.MutualTLSChannelError: If mTLS is requested but
145-
not supported in the configured universe domain.
146-
ValueError: If mTLS is requested but no mTLS endpoint is available.
147-
"""
148-
if api_override is not None:
149-
return api_override
150195

151-
if use_mtls:
152-
if universe_domain.lower() != default_universe.lower():
153-
raise MutualTLSChannelError(
154-
f"mTLS is not supported in any universe other than {default_universe}."
196+
try:
197+
from google.api_core.rest_helpers import transcode_request # type: ignore
198+
except ImportError: # pragma: NO COVER
199+
def transcode_request(
200+
http_options: List[Dict[str, str]],
201+
request: Any,
202+
required_fields_default_values: Optional[Dict[str, Any]] = None,
203+
rest_numeric_enums: bool = False,
204+
) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
205+
"""Transcodes a request into HTTP method, URI, body, and query parameters.
206+
207+
Args:
208+
http_options (List[Dict[str, str]]): List of HTTP transcoding rules.
209+
request (Any): The protobuf or proto-plus request message.
210+
required_fields_default_values (Optional[Dict[str, Any]]): Dictionary
211+
of required fields default values to merge into query parameters if missing.
212+
rest_numeric_enums (bool): Whether to encode enums as integers.
213+
214+
Returns:
215+
Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: A tuple containing:
216+
- The raw transcoded request dictionary (containing keys like 'uri', 'method').
217+
- The serialized request body JSON string, or None if no body.
218+
- The query parameters dictionary.
219+
"""
220+
if request is None:
221+
raise TypeError("request cannot be None")
222+
223+
# Convert proto-plus message to its underlying protobuf message if needed
224+
pb_request = getattr(request, "_pb", request)
225+
226+
transcoded_request = path_template.transcode(http_options, pb_request)
227+
228+
body_json = None
229+
if transcoded_request.get("body") is not None:
230+
body_json = json_format.MessageToJson(
231+
transcoded_request["body"],
232+
use_integers_for_enums=rest_numeric_enums,
155233
)
156-
if not default_mtls_endpoint:
157-
raise ValueError("mTLS endpoint is not available.")
158-
return default_mtls_endpoint
159-
else:
160-
return default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
161-
162-
def get_universe_domain(
163-
*potential_universes: Optional[str],
164-
default_universe: str = DEFAULT_UNIVERSE,
165-
) -> str:
166-
"""Return the universe domain used by the client.
167-
168-
Args:
169-
*potential_universes (Optional[str]): Potential universe domains in order of preference.
170-
default_universe (str): The default universe domain.
171-
172-
Returns:
173-
str: The universe domain to be used by the client.
174-
175-
Raises:
176-
EmptyUniverseError: If the resolved universe domain is an empty string.
177-
"""
178-
resolved = next(
179-
(x.strip() for x in potential_universes if x is not None),
180-
default_universe,
181-
)
182-
183-
if not resolved:
184-
raise EmptyUniverseError()
185-
return resolved
186-
187-
188-
def transcode_request(
189-
http_options: List[Dict[str, str]],
190-
request: Any,
191-
required_fields_default_values: Optional[Dict[str, Any]] = None,
192-
rest_numeric_enums: bool = False,
193-
) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
194-
"""Transcodes a request into HTTP method, URI, body, and query parameters.
195-
196-
Args:
197-
http_options (List[Dict[str, str]]): List of HTTP transcoding rules.
198-
request (Any): The protobuf or proto-plus request message.
199-
required_fields_default_values (Optional[Dict[str, Any]]): Dictionary
200-
of required fields default values to merge into query parameters if missing.
201-
rest_numeric_enums (bool): Whether to encode enums as integers.
202234

203-
Returns:
204-
Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: A tuple containing:
205-
- The raw transcoded request dictionary (containing keys like 'uri', 'method').
206-
- The serialized request body JSON string, or None if no body.
207-
- The query parameters dictionary.
208-
"""
209-
if request is None:
210-
raise TypeError("request cannot be None")
211-
212-
# Convert proto-plus message to its underlying protobuf message if needed
213-
pb_request = getattr(request, "_pb", request)
214-
215-
transcoded_request = path_template.transcode(http_options, pb_request)
216-
217-
body_json = None
218-
if transcoded_request.get("body") is not None:
219-
body_json = json_format.MessageToJson(
220-
transcoded_request["body"],
221-
use_integers_for_enums=rest_numeric_enums,
222-
)
223-
224-
query_params_json = {}
225-
if transcoded_request.get("query_params") is not None:
226-
query_params_json = json.loads(
227-
json_format.MessageToJson(
228-
transcoded_request["query_params"],
229-
use_integers_for_enums=rest_numeric_enums,
235+
query_params_json = {}
236+
if transcoded_request.get("query_params") is not None:
237+
query_params_json = json.loads(
238+
json_format.MessageToJson(
239+
transcoded_request["query_params"],
240+
use_integers_for_enums=rest_numeric_enums,
241+
)
230242
)
231-
)
232243

233-
# If required_fields_default_values is provided, we merge default values for missing
234-
# required fields into the query parameters.
235-
if required_fields_default_values:
236-
for k, v in required_fields_default_values.items():
237-
if k not in query_params_json:
238-
query_params_json[k] = v
244+
# If required_fields_default_values is provided, we merge default values for missing
245+
# required fields into the query parameters.
246+
if required_fields_default_values:
247+
for k, v in required_fields_default_values.items():
248+
if k not in query_params_json:
249+
query_params_json[k] = v
239250

240-
if rest_numeric_enums:
241-
query_params_json["$alt"] = "json;enum-encoding=int"
251+
if rest_numeric_enums:
252+
query_params_json["$alt"] = "json;enum-encoding=int"
242253

243-
return transcoded_request, body_json, query_params_json
254+
return transcoded_request, body_json, query_params_json

‎packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ def __init__(self, *,
491491

492492
self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env = read_environment_variables()
493493
self._client_cert_source = AssetServiceClient._get_client_cert_source(self._client_options.client_cert_source, self._use_client_cert)
494-
self._universe_domain = get_universe_domain(universe_domain_opt, self._universe_domain_env)
494+
self._universe_domain = get_universe_domain(universe_domain_opt, self._universe_domain_env, default_universe=AssetServiceClient._DEFAULT_UNIVERSE)
495495
self._api_endpoint: str = "" # updated below, depending on `transport`
496496

497497
# Initialize the universe domain validation.

0 commit comments

Comments
 (0)