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 
    23from urllib.parse import urlencode 
    24import warnings 
    25 
    26from google.api_core.client_info import ClientInfo 
    27from google.cloud import exceptions 
    28from google.cloud import version 
    29 
    30 
    31API_BASE_URL = "https://www.googleapis.com" 
    32"""The base of the API call URL.""" 
    33 
    34DEFAULT_USER_AGENT = "gcloud-python/{0}".format(version.__version__) 
    35"""The user agent for google-cloud-python requests.""" 
    36 
    37CLIENT_INFO_HEADER = "X-Goog-API-Client" 
    38CLIENT_INFO_TEMPLATE = "gl-python/" + platform.python_version() + " gccl/{}" 
    39 
    40_USER_AGENT_ALL_CAPS_DEPRECATED = """\ 
    41The 'USER_AGENT' class-level attribute is deprecated.  Please use 
    42'user_agent' instead. 
    43""" 
    44 
    45_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\ 
    46The '_EXTRA_HEADERS' class-level attribute is deprecated.  Please use 
    47'extra_headers' instead. 
    48""" 
    49 
    50_DEFAULT_TIMEOUT = 60  # in seconds 
    51 
    52 
    53class Connection(object): 
    54    """A generic connection to Google Cloud Platform. 
    55 
    56    :type client: :class:`~google.cloud.client.Client` 
    57    :param client: The client that owns the current connection. 
    58 
    59    :type client_info: :class:`~google.api_core.client_info.ClientInfo` 
    60    :param client_info: (Optional) instance used to generate user agent. 
    61    """ 
    62 
    63    _user_agent = DEFAULT_USER_AGENT 
    64 
    65    def __init__(self, client, client_info=None): 
    66        self._client = client 
    67 
    68        if client_info is None: 
    69            client_info = ClientInfo() 
    70 
    71        self._client_info = client_info 
    72        self._extra_headers = {} 
    73 
    74    @property 
    75    def USER_AGENT(self): 
    76        """Deprecated:  get / set user agent sent by connection. 
    77 
    78        :rtype: str 
    79        :returns: user agent 
    80        """ 
    81        warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) 
    82        return self.user_agent 
    83 
    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 
    88 
    89    @property 
    90    def user_agent(self): 
    91        """Get / set user agent sent by connection. 
    92 
    93        :rtype: str 
    94        :returns: user agent 
    95        """ 
    96        return self._client_info.to_user_agent() 
    97 
    98    @user_agent.setter 
    99    def user_agent(self, value): 
    100        self._client_info.user_agent = value 
    101 
    102    @property 
    103    def _EXTRA_HEADERS(self): 
    104        """Deprecated:  get / set extra headers sent by connection. 
    105 
    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 
    113 
    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 
    120 
    121    @property 
    122    def extra_headers(self): 
    123        """Get / set extra headers sent by connection. 
    124 
    125        :rtype: dict 
    126        :returns: header keys / values 
    127        """ 
    128        return self._extra_headers 
    129 
    130    @extra_headers.setter 
    131    def extra_headers(self, value): 
    132        self._extra_headers = value 
    133 
    134    @property 
    135    def credentials(self): 
    136        """Getter for current credentials. 
    137 
    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 
    143 
    144    @property 
    145    def http(self): 
    146        """A getter for the HTTP transport used in talking to the API. 
    147 
    148        Returns: 
    149            google.auth.transport.requests.AuthorizedSession: 
    150                A :class:`requests.Session` instance. 
    151        """ 
    152        return self._client._http 
    153 
    154 
    155class JSONConnection(Connection): 
    156    """A connection to a Google JSON-based API. 
    157 
    158    These APIs are discovery based. For reference: 
    159 
    160        https://developers.google.com/discovery/ 
    161 
    162    This defines :meth:`api_request` for making a generic JSON 
    163    API request and API requests are created elsewhere. 
    164 
    165    * :attr:`API_BASE_URL` 
    166    * :attr:`API_VERSION` 
    167    * :attr:`API_URL_TEMPLATE` 
    168 
    169    must be updated by subclasses. 
    170    """ 
    171 
    172    API_BASE_URL: Optional[str] = None 
    173    """The base of the API call URL.""" 
    174 
    175    API_BASE_MTLS_URL: Optional[str] = None 
    176    """The base of the API call URL for mutual TLS.""" 
    177 
    178    ALLOW_AUTO_SWITCH_TO_MTLS_URL = False 
    179    """Indicates if auto switch to mTLS url is allowed.""" 
    180 
    181    API_VERSION: Optional[str] = None 
    182    """The version of the API, used in building the API call's URL.""" 
    183 
    184    API_URL_TEMPLATE: Optional[str] = None 
    185    """A template for the URL of a particular API call.""" 
    186 
    187    def get_api_base_url_for_mtls(self, api_base_url=None): 
    188        """Return the api base url for mutual TLS. 
    189 
    190        Typically, you shouldn't need to use this method. 
    191 
    192        The logic is as follows: 
    193 
    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. 
    197 
    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`. 
    202 
    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`. 
    206 
    207        :rtype: str 
    208        :returns: The api base url used for mTLS. 
    209        """ 
    210        if api_base_url: 
    211            return api_base_url 
    212 
    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 
    226 
    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. 
    231 
    232        Typically, you shouldn't need to use this method. 
    233 
    234        :type path: str 
    235        :param path: The path to the resource (ie, ``'/b/bucket-name'``). 
    236 
    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. 
    241 
    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. 
    245 
    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. 
    250 
    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        ) 
    259 
    260        query_params = query_params or {} 
    261 
    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 
    269 
    270        query_params.setdefault("prettyPrint", "false") 
    271 
    272        url += "?" + urlencode(query_params, doseq=True) 
    273 
    274        return url 
    275 
    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. 
    288 
    289        Typically, you shouldn't need to use this method. 
    290 
    291        :type method: str 
    292        :param method: The HTTP method to use in the request. 
    293 
    294        :type url: str 
    295        :param url: The URL to send the request to. 
    296 
    297        :type data: str 
    298        :param data: The data to send as the body of the request. 
    299 
    300        :type content_type: str 
    301        :param content_type: The proper MIME type of the data provided. 
    302 
    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. 
    307 
    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. 
    313 
    314        :type timeout: float or tuple 
    315        :param timeout: (optional) The amount of time, in seconds, to wait 
    316            for the server response. 
    317 
    318            Can also be passed as a tuple (connect_timeout, read_timeout). 
    319            See :meth:`requests.Session.request` documentation for details. 
    320 
    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 
    324 
    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" 
    331 
    332        if content_type: 
    333            headers["Content-Type"] = content_type 
    334 
    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 
    340 
    341        return self._do_request( 
    342            method, url, headers, data, target_object, timeout=timeout 
    343        ) 
    344 
    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. 
    349 
    350        Allows batch context managers to override and defer a request. 
    351 
    352        :type method: str 
    353        :param method: The HTTP method to use in the request. 
    354 
    355        :type url: str 
    356        :param url: The URL to send the request to. 
    357 
    358        :type headers: dict 
    359        :param headers: A dictionary of HTTP headers to send with the request. 
    360 
    361        :type data: str 
    362        :param data: The data to send as the body of the request. 
    363 
    364        :type target_object: object 
    365        :param target_object: 
    366            (Optional) Unused ``target_object`` here but may be used by a 
    367            superclass. 
    368 
    369        :type timeout: float or tuple 
    370        :param timeout: (optional) The amount of time, in seconds, to wait 
    371            for the server response. 
    372 
    373            Can also be passed as a tuple (connect_timeout, read_timeout). 
    374            See :meth:`requests.Session.request` documentation for details. 
    375 
    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        ) 
    382 
    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. 
    399 
    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. 
    403 
    404        :type method: str 
    405        :param method: The HTTP method name (ie, ``GET``, ``POST``, etc). 
    406                       Required. 
    407 
    408        :type path: str 
    409        :param path: The path to the resource (ie, ``'/b/bucket-name'``). 
    410                     Required. 
    411 
    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. 
    416 
    417        :type data: str 
    418        :param data: The data to send as the body of the request. Default is 
    419                     the empty string. 
    420 
    421        :type content_type: str 
    422        :param content_type: The proper MIME type of the data provided. Default 
    423                             is None. 
    424 
    425        :type headers: dict 
    426        :param headers: extra HTTP headers to be sent with the request. 
    427 
    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. 
    432 
    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. 
    439 
    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. 
    444 
    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. 
    450 
    451        :type timeout: float or tuple 
    452        :param timeout: (optional) The amount of time, in seconds, to wait 
    453            for the server response. 
    454 
    455            Can also be passed as a tuple (connect_timeout, read_timeout). 
    456            See :meth:`requests.Session.request` documentation for details. 
    457 
    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 
    461 
    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        ) 
    475 
    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" 
    481 
    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        ) 
    492 
    493        if not 200 <= response.status_code < 300: 
    494            raise exceptions.from_http_response(response) 
    495 
    496        if expect_json and response.content: 
    497            return response.json() 
    498        else: 
    499            return response.content