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