1# Copyright 2014 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"""Shared implementation of connections to API servers."""
16
17import collections
18import collections.abc
19import json
20import os
21import platform
22from typing import Optional, Set
23from urllib.parse import urlencode
24import warnings
25
26# PEP 0810: Explicit Lazy Imports
27# Python 3.15+ natively intercepts and defers these imports.
28# Developers can disable this behavior and force eager imports.
29# For more information, see:
30# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
31# Older Python versions safely ignore this variable.
32# NOTE: We statically define all modules here to ensure static analysis tools
33# (mypy, pyright, Ruff) can easily parse them. If support is not present, the
34# imports are ignored, making their presence safe.
35__lazy_modules__: Set[str] = {
36 "google.api_core.client_info",
37 "google.cloud.exceptions",
38 "google.cloud.version",
39}
40
41from google.api_core.client_info import ClientInfo
42from google.cloud import exceptions
43from google.cloud import version
44
45
46API_BASE_URL = "https://www.googleapis.com"
47"""The base of the API call URL."""
48
49DEFAULT_USER_AGENT = "gcloud-python/{0}".format(version.__version__)
50"""The user agent for google-cloud-python requests."""
51
52CLIENT_INFO_HEADER = "x-goog-api-client"
53CLIENT_INFO_TEMPLATE = "gl-python/" + platform.python_version() + " gccl/{}"
54
55_USER_AGENT_ALL_CAPS_DEPRECATED = """\
56The 'USER_AGENT' class-level attribute is deprecated. Please use
57'user_agent' instead.
58"""
59
60_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\
61The '_EXTRA_HEADERS' class-level attribute is deprecated. Please use
62'extra_headers' instead.
63"""
64
65_DEFAULT_TIMEOUT = 60 # in seconds
66
67
68class Connection(object):
69 """A generic connection to Google Cloud Platform.
70
71 :type client: :class:`~google.cloud.client.Client`
72 :param client: The client that owns the current connection.
73
74 :type client_info: :class:`~google.api_core.client_info.ClientInfo`
75 :param client_info: (Optional) instance used to generate user agent.
76 """
77
78 _user_agent = DEFAULT_USER_AGENT
79
80 def __init__(self, client, client_info=None):
81 self._client = client
82
83 if client_info is None:
84 client_info = ClientInfo()
85
86 self._client_info = client_info
87 self._extra_headers = {}
88
89 @property
90 def USER_AGENT(self):
91 """Deprecated: get / set user agent sent by connection.
92
93 :rtype: str
94 :returns: user agent
95 """
96 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2)
97 return self.user_agent
98
99 @USER_AGENT.setter
100 def USER_AGENT(self, value):
101 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2)
102 self.user_agent = value
103
104 @property
105 def user_agent(self):
106 """Get / set user agent sent by connection.
107
108 :rtype: str
109 :returns: user agent
110 """
111 return self._client_info.to_user_agent()
112
113 @user_agent.setter
114 def user_agent(self, value):
115 self._client_info.user_agent = value
116
117 @property
118 def _EXTRA_HEADERS(self):
119 """Deprecated: get / set extra headers sent by connection.
120
121 :rtype: dict
122 :returns: header keys / values
123 """
124 warnings.warn(
125 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2
126 )
127 return self.extra_headers
128
129 @_EXTRA_HEADERS.setter
130 def _EXTRA_HEADERS(self, value):
131 warnings.warn(
132 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2
133 )
134 self.extra_headers = value
135
136 @property
137 def extra_headers(self):
138 """Get / set extra headers sent by connection.
139
140 :rtype: dict
141 :returns: header keys / values
142 """
143 return self._extra_headers
144
145 @extra_headers.setter
146 def extra_headers(self, value):
147 self._extra_headers = value
148
149 @property
150 def credentials(self):
151 """Getter for current credentials.
152
153 :rtype: :class:`google.auth.credentials.Credentials` or
154 :class:`NoneType`
155 :returns: The credentials object associated with this connection.
156 """
157 return self._client._credentials
158
159 @property
160 def http(self):
161 """A getter for the HTTP transport used in talking to the API.
162
163 Returns:
164 google.auth.transport.requests.AuthorizedSession:
165 A :class:`requests.Session` instance.
166 """
167 return self._client._http
168
169
170class JSONConnection(Connection):
171 """A connection to a Google JSON-based API.
172
173 These APIs are discovery based. For reference:
174
175 https://developers.google.com/discovery/
176
177 This defines :meth:`api_request` for making a generic JSON
178 API request and API requests are created elsewhere.
179
180 * :attr:`API_BASE_URL`
181 * :attr:`API_VERSION`
182 * :attr:`API_URL_TEMPLATE`
183
184 must be updated by subclasses.
185 """
186
187 API_BASE_URL: Optional[str] = None
188 """The base of the API call URL."""
189
190 API_BASE_MTLS_URL: Optional[str] = None
191 """The base of the API call URL for mutual TLS."""
192
193 ALLOW_AUTO_SWITCH_TO_MTLS_URL = False
194 """Indicates if auto switch to mTLS url is allowed."""
195
196 API_VERSION: Optional[str] = None
197 """The version of the API, used in building the API call's URL."""
198
199 API_URL_TEMPLATE: Optional[str] = None
200 """A template for the URL of a particular API call."""
201
202 def get_api_base_url_for_mtls(self, api_base_url=None):
203 """Return the api base url for mutual TLS.
204
205 Typically, you shouldn't need to use this method.
206
207 The logic is as follows:
208
209 If `api_base_url` is provided, just return this value; otherwise, the
210 return value depends `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable
211 value.
212
213 If the environment variable value is "always", return `API_BASE_MTLS_URL`.
214 If the environment variable value is "never", return `API_BASE_URL`.
215 Otherwise, if `ALLOW_AUTO_SWITCH_TO_MTLS_URL` is True and the underlying
216 http is mTLS, then return `API_BASE_MTLS_URL`; otherwise return `API_BASE_URL`.
217
218 :type api_base_url: str
219 :param api_base_url: User provided api base url. It takes precedence over
220 `API_BASE_URL` and `API_BASE_MTLS_URL`.
221
222 :rtype: str
223 :returns: The api base url used for mTLS.
224 """
225 if api_base_url:
226 return api_base_url
227
228 env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
229 if env == "always":
230 url_to_use = self.API_BASE_MTLS_URL
231 elif env == "never":
232 url_to_use = self.API_BASE_URL
233 else:
234 if self.ALLOW_AUTO_SWITCH_TO_MTLS_URL:
235 url_to_use = (
236 self.API_BASE_MTLS_URL if self.http.is_mtls else self.API_BASE_URL
237 )
238 else:
239 url_to_use = self.API_BASE_URL
240 return url_to_use
241
242 def build_api_url(
243 self, path, query_params=None, api_base_url=None, api_version=None
244 ):
245 """Construct an API url given a few components, some optional.
246
247 Typically, you shouldn't need to use this method.
248
249 :type path: str
250 :param path: The path to the resource (ie, ``'/b/bucket-name'``).
251
252 :type query_params: dict or list
253 :param query_params: A dictionary of keys and values (or list of
254 key-value pairs) to insert into the query
255 string of the URL.
256
257 :type api_base_url: str
258 :param api_base_url: The base URL for the API endpoint.
259 Typically you won't have to provide this.
260
261 :type api_version: str
262 :param api_version: The version of the API to call.
263 Typically you shouldn't provide this and instead
264 use the default for the library.
265
266 :rtype: str
267 :returns: The URL assembled from the pieces provided.
268 """
269 url = self.API_URL_TEMPLATE.format(
270 api_base_url=self.get_api_base_url_for_mtls(api_base_url),
271 api_version=(api_version or self.API_VERSION),
272 path=path,
273 )
274
275 query_params = query_params or {}
276
277 if isinstance(query_params, collections.abc.Mapping):
278 query_params = query_params.copy()
279 else:
280 query_params_dict = collections.defaultdict(list)
281 for key, value in query_params:
282 query_params_dict[key].append(value)
283 query_params = query_params_dict
284
285 query_params.setdefault("prettyPrint", "false")
286
287 url += "?" + urlencode(query_params, doseq=True)
288
289 return url
290
291 def _make_request(
292 self,
293 method,
294 url,
295 data=None,
296 content_type=None,
297 headers=None,
298 target_object=None,
299 timeout=_DEFAULT_TIMEOUT,
300 extra_api_info=None,
301 ):
302 """A low level method to send a request to the API.
303
304 Typically, you shouldn't need to use this method.
305
306 :type method: str
307 :param method: The HTTP method to use in the request.
308
309 :type url: str
310 :param url: The URL to send the request to.
311
312 :type data: str
313 :param data: The data to send as the body of the request.
314
315 :type content_type: str
316 :param content_type: The proper MIME type of the data provided.
317
318 :type headers: dict
319 :param headers: (Optional) A dictionary of HTTP headers to send with
320 the request. If passed, will be modified directly
321 here with added headers.
322
323 :type target_object: object
324 :param target_object:
325 (Optional) Argument to be used by library callers. This can allow
326 custom behavior, for example, to defer an HTTP request and complete
327 initialization of the object at a later time.
328
329 :type timeout: float or tuple
330 :param timeout: (optional) The amount of time, in seconds, to wait
331 for the server response.
332
333 Can also be passed as a tuple (connect_timeout, read_timeout).
334 See :meth:`requests.Session.request` documentation for details.
335
336 :type extra_api_info: string
337 :param extra_api_info: (optional) Extra api info to be appended to
338 the X-Goog-API-Client header
339
340 :rtype: :class:`requests.Response`
341 :returns: The HTTP response.
342 """
343 headers = headers or {}
344 headers.update(self.extra_headers)
345 headers["Accept-Encoding"] = "gzip"
346
347 if content_type:
348 headers["Content-Type"] = content_type
349
350 if extra_api_info:
351 headers[CLIENT_INFO_HEADER] = f"{self.user_agent} {extra_api_info}"
352 else:
353 headers[CLIENT_INFO_HEADER] = self.user_agent
354 headers["User-Agent"] = self.user_agent
355
356 return self._do_request(
357 method, url, headers, data, target_object, timeout=timeout
358 )
359
360 def _do_request(
361 self, method, url, headers, data, target_object, timeout=_DEFAULT_TIMEOUT
362 ): # pylint: disable=unused-argument
363 """Low-level helper: perform the actual API request over HTTP.
364
365 Allows batch context managers to override and defer a request.
366
367 :type method: str
368 :param method: The HTTP method to use in the request.
369
370 :type url: str
371 :param url: The URL to send the request to.
372
373 :type headers: dict
374 :param headers: A dictionary of HTTP headers to send with the request.
375
376 :type data: str
377 :param data: The data to send as the body of the request.
378
379 :type target_object: object
380 :param target_object:
381 (Optional) Unused ``target_object`` here but may be used by a
382 superclass.
383
384 :type timeout: float or tuple
385 :param timeout: (optional) The amount of time, in seconds, to wait
386 for the server response.
387
388 Can also be passed as a tuple (connect_timeout, read_timeout).
389 See :meth:`requests.Session.request` documentation for details.
390
391 :rtype: :class:`requests.Response`
392 :returns: The HTTP response.
393 """
394 return self.http.request(
395 url=url, method=method, headers=headers, data=data, timeout=timeout
396 )
397
398 def api_request(
399 self,
400 method,
401 path,
402 query_params=None,
403 data=None,
404 content_type=None,
405 headers=None,
406 api_base_url=None,
407 api_version=None,
408 expect_json=True,
409 _target_object=None,
410 timeout=_DEFAULT_TIMEOUT,
411 extra_api_info=None,
412 ):
413 """Make a request over the HTTP transport to the API.
414
415 You shouldn't need to use this method, but if you plan to
416 interact with the API using these primitives, this is the
417 correct one to use.
418
419 :type method: str
420 :param method: The HTTP method name (ie, ``GET``, ``POST``, etc).
421 Required.
422
423 :type path: str
424 :param path: The path to the resource (ie, ``'/b/bucket-name'``).
425 Required.
426
427 :type query_params: dict or list
428 :param query_params: A dictionary of keys and values (or list of
429 key-value pairs) to insert into the query
430 string of the URL.
431
432 :type data: str
433 :param data: The data to send as the body of the request. Default is
434 the empty string.
435
436 :type content_type: str
437 :param content_type: The proper MIME type of the data provided. Default
438 is None.
439
440 :type headers: dict
441 :param headers: extra HTTP headers to be sent with the request.
442
443 :type api_base_url: str
444 :param api_base_url: The base URL for the API endpoint.
445 Typically you won't have to provide this.
446 Default is the standard API base URL.
447
448 :type api_version: str
449 :param api_version: The version of the API to call. Typically
450 you shouldn't provide this and instead use
451 the default for the library. Default is the
452 latest API version supported by
453 google-cloud-python.
454
455 :type expect_json: bool
456 :param expect_json: If True, this method will try to parse the
457 response as JSON and raise an exception if
458 that cannot be done. Default is True.
459
460 :type _target_object: :class:`object`
461 :param _target_object:
462 (Optional) Protected argument to be used by library callers. This
463 can allow custom behavior, for example, to defer an HTTP request
464 and complete initialization of the object at a later time.
465
466 :type timeout: float or tuple
467 :param timeout: (optional) The amount of time, in seconds, to wait
468 for the server response.
469
470 Can also be passed as a tuple (connect_timeout, read_timeout).
471 See :meth:`requests.Session.request` documentation for details.
472
473 :type extra_api_info: string
474 :param extra_api_info: (optional) Extra api info to be appended to
475 the X-Goog-API-Client header
476
477 :raises ~google.cloud.exceptions.GoogleCloudError: if the response code
478 is not 200 OK.
479 :raises ValueError: if the response content type is not JSON.
480 :rtype: dict or str
481 :returns: The API response payload, either as a raw string or
482 a dictionary if the response is valid JSON.
483 """
484 url = self.build_api_url(
485 path=path,
486 query_params=query_params,
487 api_base_url=api_base_url,
488 api_version=api_version,
489 )
490
491 # Making the executive decision that any dictionary
492 # data will be sent properly as JSON.
493 if data and isinstance(data, dict):
494 data = json.dumps(data)
495 content_type = "application/json"
496
497 response = self._make_request(
498 method=method,
499 url=url,
500 data=data,
501 content_type=content_type,
502 headers=headers,
503 target_object=_target_object,
504 timeout=timeout,
505 extra_api_info=extra_api_info,
506 )
507
508 if not 200 <= response.status_code < 300:
509 raise exceptions.from_http_response(response)
510
511 if expect_json and response.content:
512 return response.json()
513 else:
514 return response.content