Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/_http/__init__.py: 24%
115 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
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.
15"""Shared implementation of connections to API servers."""
17import collections
18import collections.abc
19import json
20import os
21import platform
22from typing import Optional
23from urllib.parse import urlencode
24import warnings
26from google.api_core.client_info import ClientInfo
27from google.cloud import exceptions
28from google.cloud import version
31API_BASE_URL = "https://www.googleapis.com"
32"""The base of the API call URL."""
34DEFAULT_USER_AGENT = "gcloud-python/{0}".format(version.__version__)
35"""The user agent for google-cloud-python requests."""
37CLIENT_INFO_HEADER = "X-Goog-API-Client"
38CLIENT_INFO_TEMPLATE = "gl-python/" + platform.python_version() + " gccl/{}"
40_USER_AGENT_ALL_CAPS_DEPRECATED = """\
41The 'USER_AGENT' class-level attribute is deprecated. Please use
42'user_agent' instead.
43"""
45_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\
46The '_EXTRA_HEADERS' class-level attribute is deprecated. Please use
47'extra_headers' instead.
48"""
50_DEFAULT_TIMEOUT = 60 # in seconds
53class Connection(object):
54 """A generic connection to Google Cloud Platform.
56 :type client: :class:`~google.cloud.client.Client`
57 :param client: The client that owns the current connection.
59 :type client_info: :class:`~google.api_core.client_info.ClientInfo`
60 :param client_info: (Optional) instance used to generate user agent.
61 """
63 _user_agent = DEFAULT_USER_AGENT
65 def __init__(self, client, client_info=None):
66 self._client = client
68 if client_info is None:
69 client_info = ClientInfo()
71 self._client_info = client_info
72 self._extra_headers = {}
74 @property
75 def USER_AGENT(self):
76 """Deprecated: get / set user agent sent by connection.
78 :rtype: str
79 :returns: user agent
80 """
81 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2)
82 return self.user_agent
84 @USER_AGENT.setter
85 def USER_AGENT(self, value):
86 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2)
87 self.user_agent = value
89 @property
90 def user_agent(self):
91 """Get / set user agent sent by connection.
93 :rtype: str
94 :returns: user agent
95 """
96 return self._client_info.to_user_agent()
98 @user_agent.setter
99 def user_agent(self, value):
100 self._client_info.user_agent = value
102 @property
103 def _EXTRA_HEADERS(self):
104 """Deprecated: get / set extra headers sent by connection.
106 :rtype: dict
107 :returns: header keys / values
108 """
109 warnings.warn(
110 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2
111 )
112 return self.extra_headers
114 @_EXTRA_HEADERS.setter
115 def _EXTRA_HEADERS(self, value):
116 warnings.warn(
117 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2
118 )
119 self.extra_headers = value
121 @property
122 def extra_headers(self):
123 """Get / set extra headers sent by connection.
125 :rtype: dict
126 :returns: header keys / values
127 """
128 return self._extra_headers
130 @extra_headers.setter
131 def extra_headers(self, value):
132 self._extra_headers = value
134 @property
135 def credentials(self):
136 """Getter for current credentials.
138 :rtype: :class:`google.auth.credentials.Credentials` or
139 :class:`NoneType`
140 :returns: The credentials object associated with this connection.
141 """
142 return self._client._credentials
144 @property
145 def http(self):
146 """A getter for the HTTP transport used in talking to the API.
148 Returns:
149 google.auth.transport.requests.AuthorizedSession:
150 A :class:`requests.Session` instance.
151 """
152 return self._client._http
155class JSONConnection(Connection):
156 """A connection to a Google JSON-based API.
158 These APIs are discovery based. For reference:
160 https://developers.google.com/discovery/
162 This defines :meth:`api_request` for making a generic JSON
163 API request and API requests are created elsewhere.
165 * :attr:`API_BASE_URL`
166 * :attr:`API_VERSION`
167 * :attr:`API_URL_TEMPLATE`
169 must be updated by subclasses.
170 """
172 API_BASE_URL: Optional[str] = None
173 """The base of the API call URL."""
175 API_BASE_MTLS_URL: Optional[str] = None
176 """The base of the API call URL for mutual TLS."""
178 ALLOW_AUTO_SWITCH_TO_MTLS_URL = False
179 """Indicates if auto switch to mTLS url is allowed."""
181 API_VERSION: Optional[str] = None
182 """The version of the API, used in building the API call's URL."""
184 API_URL_TEMPLATE: Optional[str] = None
185 """A template for the URL of a particular API call."""
187 def get_api_base_url_for_mtls(self, api_base_url=None):
188 """Return the api base url for mutual TLS.
190 Typically, you shouldn't need to use this method.
192 The logic is as follows:
194 If `api_base_url` is provided, just return this value; otherwise, the
195 return value depends `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable
196 value.
198 If the environment variable value is "always", return `API_BASE_MTLS_URL`.
199 If the environment variable value is "never", return `API_BASE_URL`.
200 Otherwise, if `ALLOW_AUTO_SWITCH_TO_MTLS_URL` is True and the underlying
201 http is mTLS, then return `API_BASE_MTLS_URL`; otherwise return `API_BASE_URL`.
203 :type api_base_url: str
204 :param api_base_url: User provided api base url. It takes precedence over
205 `API_BASE_URL` and `API_BASE_MTLS_URL`.
207 :rtype: str
208 :returns: The api base url used for mTLS.
209 """
210 if api_base_url:
211 return api_base_url
213 env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
214 if env == "always":
215 url_to_use = self.API_BASE_MTLS_URL
216 elif env == "never":
217 url_to_use = self.API_BASE_URL
218 else:
219 if self.ALLOW_AUTO_SWITCH_TO_MTLS_URL:
220 url_to_use = (
221 self.API_BASE_MTLS_URL if self.http.is_mtls else self.API_BASE_URL
222 )
223 else:
224 url_to_use = self.API_BASE_URL
225 return url_to_use
227 def build_api_url(
228 self, path, query_params=None, api_base_url=None, api_version=None
229 ):
230 """Construct an API url given a few components, some optional.
232 Typically, you shouldn't need to use this method.
234 :type path: str
235 :param path: The path to the resource (ie, ``'/b/bucket-name'``).
237 :type query_params: dict or list
238 :param query_params: A dictionary of keys and values (or list of
239 key-value pairs) to insert into the query
240 string of the URL.
242 :type api_base_url: str
243 :param api_base_url: The base URL for the API endpoint.
244 Typically you won't have to provide this.
246 :type api_version: str
247 :param api_version: The version of the API to call.
248 Typically you shouldn't provide this and instead
249 use the default for the library.
251 :rtype: str
252 :returns: The URL assembled from the pieces provided.
253 """
254 url = self.API_URL_TEMPLATE.format(
255 api_base_url=self.get_api_base_url_for_mtls(api_base_url),
256 api_version=(api_version or self.API_VERSION),
257 path=path,
258 )
260 query_params = query_params or {}
262 if isinstance(query_params, collections.abc.Mapping):
263 query_params = query_params.copy()
264 else:
265 query_params_dict = collections.defaultdict(list)
266 for key, value in query_params:
267 query_params_dict[key].append(value)
268 query_params = query_params_dict
270 query_params.setdefault("prettyPrint", "false")
272 url += "?" + urlencode(query_params, doseq=True)
274 return url
276 def _make_request(
277 self,
278 method,
279 url,
280 data=None,
281 content_type=None,
282 headers=None,
283 target_object=None,
284 timeout=_DEFAULT_TIMEOUT,
285 extra_api_info=None,
286 ):
287 """A low level method to send a request to the API.
289 Typically, you shouldn't need to use this method.
291 :type method: str
292 :param method: The HTTP method to use in the request.
294 :type url: str
295 :param url: The URL to send the request to.
297 :type data: str
298 :param data: The data to send as the body of the request.
300 :type content_type: str
301 :param content_type: The proper MIME type of the data provided.
303 :type headers: dict
304 :param headers: (Optional) A dictionary of HTTP headers to send with
305 the request. If passed, will be modified directly
306 here with added headers.
308 :type target_object: object
309 :param target_object:
310 (Optional) Argument to be used by library callers. This can allow
311 custom behavior, for example, to defer an HTTP request and complete
312 initialization of the object at a later time.
314 :type timeout: float or tuple
315 :param timeout: (optional) The amount of time, in seconds, to wait
316 for the server response.
318 Can also be passed as a tuple (connect_timeout, read_timeout).
319 See :meth:`requests.Session.request` documentation for details.
321 :type extra_api_info: string
322 :param extra_api_info: (optional) Extra api info to be appended to
323 the X-Goog-API-Client header
325 :rtype: :class:`requests.Response`
326 :returns: The HTTP response.
327 """
328 headers = headers or {}
329 headers.update(self.extra_headers)
330 headers["Accept-Encoding"] = "gzip"
332 if content_type:
333 headers["Content-Type"] = content_type
335 if extra_api_info:
336 headers[CLIENT_INFO_HEADER] = f"{self.user_agent} {extra_api_info}"
337 else:
338 headers[CLIENT_INFO_HEADER] = self.user_agent
339 headers["User-Agent"] = self.user_agent
341 return self._do_request(
342 method, url, headers, data, target_object, timeout=timeout
343 )
345 def _do_request(
346 self, method, url, headers, data, target_object, timeout=_DEFAULT_TIMEOUT
347 ): # pylint: disable=unused-argument
348 """Low-level helper: perform the actual API request over HTTP.
350 Allows batch context managers to override and defer a request.
352 :type method: str
353 :param method: The HTTP method to use in the request.
355 :type url: str
356 :param url: The URL to send the request to.
358 :type headers: dict
359 :param headers: A dictionary of HTTP headers to send with the request.
361 :type data: str
362 :param data: The data to send as the body of the request.
364 :type target_object: object
365 :param target_object:
366 (Optional) Unused ``target_object`` here but may be used by a
367 superclass.
369 :type timeout: float or tuple
370 :param timeout: (optional) The amount of time, in seconds, to wait
371 for the server response.
373 Can also be passed as a tuple (connect_timeout, read_timeout).
374 See :meth:`requests.Session.request` documentation for details.
376 :rtype: :class:`requests.Response`
377 :returns: The HTTP response.
378 """
379 return self.http.request(
380 url=url, method=method, headers=headers, data=data, timeout=timeout
381 )
383 def api_request(
384 self,
385 method,
386 path,
387 query_params=None,
388 data=None,
389 content_type=None,
390 headers=None,
391 api_base_url=None,
392 api_version=None,
393 expect_json=True,
394 _target_object=None,
395 timeout=_DEFAULT_TIMEOUT,
396 extra_api_info=None,
397 ):
398 """Make a request over the HTTP transport to the API.
400 You shouldn't need to use this method, but if you plan to
401 interact with the API using these primitives, this is the
402 correct one to use.
404 :type method: str
405 :param method: The HTTP method name (ie, ``GET``, ``POST``, etc).
406 Required.
408 :type path: str
409 :param path: The path to the resource (ie, ``'/b/bucket-name'``).
410 Required.
412 :type query_params: dict or list
413 :param query_params: A dictionary of keys and values (or list of
414 key-value pairs) to insert into the query
415 string of the URL.
417 :type data: str
418 :param data: The data to send as the body of the request. Default is
419 the empty string.
421 :type content_type: str
422 :param content_type: The proper MIME type of the data provided. Default
423 is None.
425 :type headers: dict
426 :param headers: extra HTTP headers to be sent with the request.
428 :type api_base_url: str
429 :param api_base_url: The base URL for the API endpoint.
430 Typically you won't have to provide this.
431 Default is the standard API base URL.
433 :type api_version: str
434 :param api_version: The version of the API to call. Typically
435 you shouldn't provide this and instead use
436 the default for the library. Default is the
437 latest API version supported by
438 google-cloud-python.
440 :type expect_json: bool
441 :param expect_json: If True, this method will try to parse the
442 response as JSON and raise an exception if
443 that cannot be done. Default is True.
445 :type _target_object: :class:`object`
446 :param _target_object:
447 (Optional) Protected argument to be used by library callers. This
448 can allow custom behavior, for example, to defer an HTTP request
449 and complete initialization of the object at a later time.
451 :type timeout: float or tuple
452 :param timeout: (optional) The amount of time, in seconds, to wait
453 for the server response.
455 Can also be passed as a tuple (connect_timeout, read_timeout).
456 See :meth:`requests.Session.request` documentation for details.
458 :type extra_api_info: string
459 :param extra_api_info: (optional) Extra api info to be appended to
460 the X-Goog-API-Client header
462 :raises ~google.cloud.exceptions.GoogleCloudError: if the response code
463 is not 200 OK.
464 :raises ValueError: if the response content type is not JSON.
465 :rtype: dict or str
466 :returns: The API response payload, either as a raw string or
467 a dictionary if the response is valid JSON.
468 """
469 url = self.build_api_url(
470 path=path,
471 query_params=query_params,
472 api_base_url=api_base_url,
473 api_version=api_version,
474 )
476 # Making the executive decision that any dictionary
477 # data will be sent properly as JSON.
478 if data and isinstance(data, dict):
479 data = json.dumps(data)
480 content_type = "application/json"
482 response = self._make_request(
483 method=method,
484 url=url,
485 data=data,
486 content_type=content_type,
487 headers=headers,
488 target_object=_target_object,
489 timeout=timeout,
490 extra_api_info=extra_api_info,
491 )
493 if not 200 <= response.status_code < 300:
494 raise exceptions.from_http_response(response)
496 if expect_json and response.content:
497 return response.json()
498 else:
499 return response.content