Skip to content

Commit b7054c2

Browse files
committed
fix(generator): use unified _compat.py.j2 and remove downstream tests
1 parent e6a35db commit b7054c2

26 files changed

Lines changed: 1560 additions & 343 deletions

File tree

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

Lines changed: 197 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
# {% include '_license.j2' %}
22

3+
"""A compatibility module for older versions of google-api-core."""
4+
5+
import functools
6+
import json
7+
import operator
8+
import os
39
import re
4-
from typing import Optional, Callable, Tuple, Union
10+
import uuid
11+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
512
from google.auth.exceptions import MutualTLSChannelError
13+
import google.protobuf.message
14+
615

716
try:
817
from google.api_core.universe import (
@@ -11,8 +20,7 @@ try:
1120
get_universe_domain,
1221
)
1322
except ImportError:
14-
# TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
15-
23+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
1624
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
1725
"""Converts api endpoint to mTLS endpoint."""
1826
if not api_endpoint:
@@ -74,3 +82,189 @@ except ImportError:
7482
if len(universe_domain.strip()) == 0:
7583
raise ValueError("Universe Domain cannot be an empty string.")
7684
return universe_domain
85+
86+
87+
try:
88+
from google.api_core.gapic_v1.config import (
89+
use_client_cert_effective,
90+
get_client_cert_source,
91+
read_environment_variables,
92+
)
93+
except ImportError:
94+
from google.auth.transport import mtls # type: ignore
95+
96+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
97+
98+
def use_client_cert_effective() -> bool:
99+
"""Returns whether client certificate should be used for mTLS."""
100+
if hasattr(mtls, "should_use_client_cert"):
101+
return mtls.should_use_client_cert()
102+
else:
103+
use_client_cert_str = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower()
104+
if use_client_cert_str not in ("true", "false"):
105+
raise ValueError(
106+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
107+
" either `true` or `false`"
108+
)
109+
return use_client_cert_str == "true"
110+
111+
def get_client_cert_source(
112+
provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
113+
use_cert_flag: bool,
114+
) -> Optional[Callable[[], Tuple[bytes, bytes]]]:
115+
"""Return the client cert source to be used by the client."""
116+
client_cert_source = None
117+
if use_cert_flag:
118+
if provided_cert_source:
119+
client_cert_source = provided_cert_source
120+
elif (
121+
hasattr(mtls, "has_default_client_cert_source")
122+
and mtls.has_default_client_cert_source()
123+
):
124+
client_cert_source = mtls.default_client_cert_source()
125+
else:
126+
raise ValueError(
127+
"Client certificate is required for mTLS, but no client certificate source was provided or found."
128+
)
129+
return client_cert_source
130+
131+
def read_environment_variables() -> Tuple[bool, str, Optional[str]]:
132+
"""Returns the environment variables used by the client."""
133+
use_client_cert = use_client_cert_effective()
134+
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
135+
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
136+
if use_mtls_endpoint not in ("auto", "never", "always"):
137+
raise MutualTLSChannelError(
138+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
139+
"must be `never`, `auto` or `always`"
140+
)
141+
return use_client_cert, use_mtls_endpoint, universe_domain_env
142+
143+
144+
try:
145+
from google.api_core.gapic_v1.request import setup_request_id # type: ignore
146+
except ImportError:
147+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove this fallback when google-api-core >= 2.26.0 is the minimum required version.
148+
def setup_request_id(request, field_name: str, is_proto3_optional: bool):
149+
"""Populate a UUID4 field in the request if it is not already set.
150+
151+
Args:
152+
request (Union[google.protobuf.message.Message, dict]): The request object.
153+
field_name (str): The name of the field to populate.
154+
is_proto3_optional (bool): Whether the field is proto3 optional.
155+
"""
156+
request_id_val = str(uuid.uuid4())
157+
if request is None:
158+
return
159+
160+
if isinstance(request, dict):
161+
if is_proto3_optional:
162+
if field_name not in request or request[field_name] is None:
163+
request[field_name] = request_id_val
164+
elif not request.get(field_name):
165+
request[field_name] = request_id_val
166+
return
167+
168+
if is_proto3_optional:
169+
try:
170+
# Pure protobuf messages
171+
if not request.HasField(field_name):
172+
setattr(request, field_name, request_id_val)
173+
except (AttributeError, ValueError):
174+
# Proto-plus messages or other objects
175+
if getattr(request, field_name, None) is None:
176+
setattr(request, field_name, request_id_val)
177+
else:
178+
if not getattr(request, field_name, None):
179+
setattr(request, field_name, request_id_val)
180+
181+
182+
try:
183+
from google.api_core.rest_helpers import (
184+
flatten_query_params,
185+
transcode_request,
186+
)
187+
except ImportError: # pragma: NO COVER
188+
# TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
189+
from google.protobuf import json_format # type: ignore
190+
from google.api_core import path_template # type: ignore
191+
192+
def flatten_query_params(obj, strict=False): # pragma: NO COVER
193+
if obj is not None and not isinstance(obj, dict):
194+
raise TypeError("flatten_query_params must be called with dict object")
195+
return _flatten(obj, key_path=[], strict=strict)
196+
197+
def _flatten(obj, key_path, strict=False): # pragma: NO COVER
198+
if obj is None:
199+
return []
200+
if isinstance(obj, dict):
201+
return _flatten_dict(obj, key_path=key_path, strict=strict)
202+
if isinstance(obj, list):
203+
return _flatten_list(obj, key_path=key_path, strict=strict)
204+
return _flatten_value(obj, key_path=key_path, strict=strict)
205+
206+
def _is_primitive_value(obj): # pragma: NO COVER
207+
if obj is None:
208+
return False
209+
if isinstance(obj, (list, dict)):
210+
raise ValueError("query params may not contain repeated dicts or lists")
211+
return True
212+
213+
def _flatten_value(obj, key_path, strict=False): # pragma: NO COVER
214+
return [(".".join(key_path), _canonicalize(obj, strict=strict))]
215+
216+
def _flatten_dict(obj, key_path, strict=False): # pragma: NO COVER
217+
items = (
218+
_flatten(value, key_path=key_path + [key], strict=strict)
219+
for key, value in obj.items()
220+
)
221+
return functools.reduce(operator.concat, items, [])
222+
223+
def _flatten_list(elems, key_path, strict=False): # pragma: NO COVER
224+
items = (
225+
_flatten_value(elem, key_path=key_path, strict=strict)
226+
for elem in elems
227+
if _is_primitive_value(elem)
228+
)
229+
return functools.reduce(operator.concat, items, [])
230+
231+
def _canonicalize(obj, strict=False): # pragma: NO COVER
232+
if strict:
233+
value = str(obj)
234+
if isinstance(obj, bool):
235+
value = value.lower()
236+
return value
237+
return obj
238+
239+
def transcode_request( # pragma: NO COVER
240+
http_options: List[Dict[str, str]],
241+
request: Any,
242+
required_fields_default_values: Optional[Dict[str, Any]] = None,
243+
rest_numeric_enums: bool = False,
244+
) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
245+
pb_request = getattr(request, "_pb", request)
246+
transcoded_request = path_template.transcode(http_options, pb_request)
247+
248+
body_json = None
249+
if transcoded_request.get("body") is not None:
250+
body_json = json_format.MessageToJson(
251+
transcoded_request["body"],
252+
use_integers_for_enums=rest_numeric_enums,
253+
)
254+
255+
query_params_json = {}
256+
if transcoded_request.get("query_params") is not None:
257+
query_params_json = json.loads(json_format.MessageToJson(
258+
transcoded_request["query_params"],
259+
use_integers_for_enums=rest_numeric_enums,
260+
))
261+
262+
if required_fields_default_values:
263+
for k, v in required_fields_default_values.items():
264+
if k not in query_params_json:
265+
query_params_json[k] = v
266+
267+
if rest_numeric_enums:
268+
query_params_json["$alt"] = "json;enum-encoding=int"
269+
270+
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: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from google.api_core import client_options as client_options_lib
2828
from google.api_core import exceptions as core_exceptions
2929
from google.api_core import gapic_v1
30-
from google.api_core.gapic_v1 import client_utils
3130
from google.api_core import retry as retries
3231
from google.auth import credentials as ga_credentials # type: ignore
3332
from google.auth.transport import mtls # type: ignore
@@ -102,9 +101,38 @@ class AssetServiceClient(metaclass=AssetServiceClientMeta):
102101
"""Asset service definition."""
103102

104103
@staticmethod
105-
def _get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
106-
"""Converts api endpoint to mTLS endpoint."""
107-
return client_utils.get_default_mtls_endpoint(api_endpoint)
104+
def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]:
105+
"""Converts api endpoint to mTLS endpoint.
106+
107+
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
108+
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
109+
Args:
110+
api_endpoint (Optional[str]): the api endpoint to convert.
111+
Returns:
112+
Optional[str]: converted mTLS api endpoint.
113+
"""
114+
if not api_endpoint:
115+
return api_endpoint
116+
117+
mtls_endpoint_re = re.compile(
118+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
119+
)
120+
121+
m = mtls_endpoint_re.match(api_endpoint)
122+
if m is None:
123+
# Could not parse api_endpoint; return as-is.
124+
return api_endpoint
125+
126+
name, mtls, sandbox, googledomain = m.groups()
127+
if mtls or not googledomain:
128+
return api_endpoint
129+
130+
if sandbox:
131+
return api_endpoint.replace(
132+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
133+
)
134+
135+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
108136

109137
# Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead.
110138
DEFAULT_ENDPOINT = "cloudasset.googleapis.com"
@@ -422,31 +450,53 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag):
422450
return client_cert_source
423451

424452
@staticmethod
425-
def _get_api_endpoint(
426-
api_override: Optional[str],
427-
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
428-
universe_domain: str,
429-
use_mtls_endpoint: str,
430-
) -> str:
431-
"""Return the API endpoint used by the client."""
432-
return client_utils.get_api_endpoint(
433-
api_override,
434-
client_cert_source,
435-
universe_domain,
436-
use_mtls_endpoint,
437-
AssetServiceClient._DEFAULT_UNIVERSE,
438-
AssetServiceClient.DEFAULT_MTLS_ENDPOINT,
439-
AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE,
440-
)
453+
def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str:
454+
"""Return the API endpoint used by the client.
455+
456+
Args:
457+
api_override (str): The API endpoint override. If specified, this is always
458+
the return value of this function and the other arguments are not used.
459+
client_cert_source (bytes): The client certificate source used by the client.
460+
universe_domain (str): The universe domain used by the client.
461+
use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters.
462+
Possible values are "always", "auto", or "never".
463+
464+
Returns:
465+
str: The API endpoint to be used by the client.
466+
"""
467+
if api_override is not None:
468+
api_endpoint = api_override
469+
elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source):
470+
_default_universe = AssetServiceClient._DEFAULT_UNIVERSE
471+
if universe_domain != _default_universe:
472+
raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.")
473+
api_endpoint = AssetServiceClient.DEFAULT_MTLS_ENDPOINT
474+
else:
475+
api_endpoint = AssetServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain)
476+
return api_endpoint
441477

442478
@staticmethod
443479
def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str:
444-
"""Return the universe domain used by the client."""
445-
return client_utils.get_universe_domain(
446-
client_universe_domain,
447-
universe_domain_env,
448-
AssetServiceClient._DEFAULT_UNIVERSE,
449-
)
480+
"""Return the universe domain used by the client.
481+
482+
Args:
483+
client_universe_domain (Optional[str]): The universe domain configured via the client options.
484+
universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable.
485+
486+
Returns:
487+
str: The universe domain to be used by the client.
488+
489+
Raises:
490+
ValueError: If the universe domain is an empty string.
491+
"""
492+
universe_domain = AssetServiceClient._DEFAULT_UNIVERSE
493+
if client_universe_domain is not None:
494+
universe_domain = client_universe_domain
495+
elif universe_domain_env is not None:
496+
universe_domain = universe_domain_env
497+
if len(universe_domain.strip()) == 0:
498+
raise ValueError("Universe Domain cannot be an empty string.")
499+
return universe_domain
450500

451501
def _validate_universe_domain(self):
452502
"""Validates client's and credentials' universe domains are consistent.

0 commit comments

Comments
 (0)