Skip to content

Commit 4f6baaa

Browse files
committed
feat(templates): generate _compat.py for all integration test goldens
1 parent 0aa48cd commit 4f6baaa

7 files changed

Lines changed: 2051 additions & 0 deletions

File tree

  • packages/gapic-generator/tests/integration/goldens
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# # Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
"""A compatibility module for older versions of google-api-core."""
17+
18+
import functools
19+
import json
20+
import operator
21+
import os
22+
import re
23+
import uuid
24+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
25+
from google.auth.exceptions import MutualTLSChannelError
26+
import google.protobuf.message
27+
28+
29+
try:
30+
from google.api_core.universe import (
31+
get_default_mtls_endpoint,
32+
get_api_endpoint,
33+
get_universe_domain,
34+
)
35+
except ImportError:
36+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Universe domain support was introduced in google-api-core 2.18.0.
37+
# Remove these fallback definitions when google-api-core >= 2.18.0 becomes the minimum required version in generated client setup dependencies.
38+
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
39+
"""Converts api endpoint to mTLS endpoint."""
40+
if not api_endpoint:
41+
return api_endpoint
42+
43+
mtls_endpoint_re = re.compile(
44+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
45+
)
46+
47+
m = mtls_endpoint_re.match(api_endpoint)
48+
if m is None:
49+
# Could not parse api_endpoint; return as-is.
50+
return api_endpoint
51+
52+
name, mtls, sandbox, googledomain = m.groups()
53+
if mtls or not googledomain:
54+
return api_endpoint
55+
56+
if sandbox:
57+
return api_endpoint.replace(
58+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
59+
)
60+
61+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
62+
63+
def get_api_endpoint(
64+
api_override: Optional[str],
65+
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
66+
universe_domain: str,
67+
use_mtls_endpoint: str,
68+
default_universe: str,
69+
default_mtls_endpoint: Optional[str],
70+
default_endpoint_template: str,
71+
) -> Optional[str]:
72+
"""Return the API endpoint used by the client."""
73+
if api_override is not None:
74+
api_endpoint = api_override
75+
elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source):
76+
if universe_domain != default_universe:
77+
raise MutualTLSChannelError(
78+
f"mTLS is not supported in any universe other than {default_universe}."
79+
)
80+
api_endpoint = default_mtls_endpoint
81+
else:
82+
api_endpoint = default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
83+
return api_endpoint
84+
85+
def get_universe_domain(
86+
client_universe_domain: Optional[str],
87+
universe_domain_env: Optional[str],
88+
default_universe: str,
89+
) -> str:
90+
"""Return the universe domain used by the client."""
91+
universe_domain = default_universe
92+
if client_universe_domain is not None:
93+
universe_domain = client_universe_domain
94+
elif universe_domain_env is not None:
95+
universe_domain = universe_domain_env
96+
if len(universe_domain.strip()) == 0:
97+
raise ValueError("Universe Domain cannot be an empty string.")
98+
return universe_domain
99+
100+
101+
try:
102+
from google.api_core.gapic_v1.config import (
103+
use_client_cert_effective,
104+
get_client_cert_source,
105+
read_environment_variables,
106+
)
107+
except ImportError:
108+
from google.auth.transport import mtls # type: ignore
109+
110+
# 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.
111+
112+
def use_client_cert_effective() -> bool:
113+
"""Returns whether client certificate should be used for mTLS."""
114+
if hasattr(mtls, "should_use_client_cert"):
115+
return mtls.should_use_client_cert()
116+
else:
117+
use_client_cert_str = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower()
118+
if use_client_cert_str not in ("true", "false"):
119+
raise ValueError(
120+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
121+
" either `true` or `false`"
122+
)
123+
return use_client_cert_str == "true"
124+
125+
def get_client_cert_source(
126+
provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
127+
use_cert_flag: bool,
128+
) -> Optional[Callable[[], Tuple[bytes, bytes]]]:
129+
"""Return the client cert source to be used by the client."""
130+
client_cert_source = None
131+
if use_cert_flag:
132+
if provided_cert_source:
133+
client_cert_source = provided_cert_source
134+
elif (
135+
hasattr(mtls, "has_default_client_cert_source")
136+
and mtls.has_default_client_cert_source()
137+
):
138+
client_cert_source = mtls.default_client_cert_source()
139+
else:
140+
raise ValueError(
141+
"Client certificate is required for mTLS, but no client certificate source was provided or found."
142+
)
143+
return client_cert_source
144+
145+
def read_environment_variables() -> Tuple[bool, str, Optional[str]]:
146+
"""Returns the environment variables used by the client."""
147+
use_client_cert = use_client_cert_effective()
148+
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
149+
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
150+
if use_mtls_endpoint not in ("auto", "never", "always"):
151+
raise MutualTLSChannelError(
152+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
153+
"must be `never`, `auto` or `always`"
154+
)
155+
return use_client_cert, use_mtls_endpoint, universe_domain_env
156+
157+
158+
try:
159+
from google.api_core.gapic_v1.requests import setup_request_id # type: ignore
160+
except ImportError:
161+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Request ID setup helper was introduced in google-api-core 2.26.0.
162+
# Remove this fallback definition when google-api-core >= 2.26.0 becomes the minimum required version in generated client setup dependencies.
163+
def setup_request_id(request, field_name: str, is_proto3_optional: bool):
164+
"""Populate a UUID4 field in the request if it is not already set.
165+
166+
Args:
167+
request (Union[google.protobuf.message.Message, dict]): The request object.
168+
field_name (str): The name of the field to populate.
169+
is_proto3_optional (bool): Whether the field is proto3 optional.
170+
"""
171+
if request is None:
172+
return
173+
174+
request_id_val = str(uuid.uuid4())
175+
176+
if isinstance(request, dict):
177+
if is_proto3_optional:
178+
if field_name not in request or request[field_name] is None:
179+
request[field_name] = request_id_val
180+
elif not request.get(field_name):
181+
request[field_name] = request_id_val
182+
return
183+
184+
if is_proto3_optional:
185+
try:
186+
# Pure protobuf messages
187+
if not request.HasField(field_name):
188+
setattr(request, field_name, request_id_val)
189+
except (AttributeError, ValueError):
190+
# Proto-plus messages or other objects
191+
if hasattr(request, "_pb"):
192+
try:
193+
if not request._pb.HasField(field_name):
194+
setattr(request, field_name, request_id_val)
195+
return
196+
except (AttributeError, ValueError):
197+
pass
198+
if getattr(request, field_name, None) is None:
199+
setattr(request, field_name, request_id_val)
200+
else:
201+
if not getattr(request, field_name, None):
202+
setattr(request, field_name, request_id_val)
203+
204+
205+
try:
206+
from google.api_core.rest_helpers import (
207+
flatten_query_params,
208+
transcode_request,
209+
)
210+
except ImportError: # pragma: NO COVER
211+
# TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
212+
from google.protobuf import json_format # type: ignore
213+
from google.api_core import path_template # type: ignore
214+
215+
def flatten_query_params(obj, strict=False): # pragma: NO COVER
216+
if obj is not None and not isinstance(obj, dict):
217+
raise TypeError("flatten_query_params must be called with dict object")
218+
return _flatten(obj, key_path=[], strict=strict)
219+
220+
def _flatten(obj, key_path, strict=False): # pragma: NO COVER
221+
if obj is None:
222+
return []
223+
if isinstance(obj, dict):
224+
return _flatten_dict(obj, key_path=key_path, strict=strict)
225+
if isinstance(obj, list):
226+
return _flatten_list(obj, key_path=key_path, strict=strict)
227+
return _flatten_value(obj, key_path=key_path, strict=strict)
228+
229+
def _is_primitive_value(obj): # pragma: NO COVER
230+
if obj is None:
231+
return False
232+
if isinstance(obj, (list, dict)):
233+
raise ValueError("query params may not contain repeated dicts or lists")
234+
return True
235+
236+
def _flatten_value(obj, key_path, strict=False): # pragma: NO COVER
237+
return [(".".join(key_path), _canonicalize(obj, strict=strict))]
238+
239+
def _flatten_dict(obj, key_path, strict=False): # pragma: NO COVER
240+
items = (
241+
_flatten(value, key_path=key_path + [key], strict=strict)
242+
for key, value in obj.items()
243+
)
244+
return functools.reduce(operator.concat, items, [])
245+
246+
def _flatten_list(elems, key_path, strict=False): # pragma: NO COVER
247+
items = (
248+
_flatten_value(elem, key_path=key_path, strict=strict)
249+
for elem in elems
250+
if _is_primitive_value(elem)
251+
)
252+
return functools.reduce(operator.concat, items, [])
253+
254+
def _canonicalize(obj, strict=False): # pragma: NO COVER
255+
if strict:
256+
value = str(obj)
257+
if isinstance(obj, bool):
258+
value = value.lower()
259+
return value
260+
return obj
261+
262+
def transcode_request( # pragma: NO COVER
263+
http_options: List[Dict[str, str]],
264+
request: Any,
265+
required_fields_default_values: Optional[Dict[str, Any]] = None,
266+
rest_numeric_enums: bool = False,
267+
) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
268+
pb_request = getattr(request, "_pb", request)
269+
transcoded_request = path_template.transcode(http_options, pb_request)
270+
271+
body_json = None
272+
if transcoded_request.get("body") is not None:
273+
body_json = json_format.MessageToJson(
274+
transcoded_request["body"],
275+
use_integers_for_enums=rest_numeric_enums,
276+
)
277+
278+
query_params_json = {}
279+
if transcoded_request.get("query_params") is not None:
280+
query_params_json = json.loads(json_format.MessageToJson(
281+
transcoded_request["query_params"],
282+
use_integers_for_enums=rest_numeric_enums,
283+
))
284+
285+
if required_fields_default_values:
286+
for k, v in required_fields_default_values.items():
287+
if k not in query_params_json:
288+
query_params_json[k] = v
289+
290+
if rest_numeric_enums:
291+
query_params_json["$alt"] = "json;enum-encoding=int"
292+
293+
return transcoded_request, body_json, query_params_json

0 commit comments

Comments
 (0)