1# Copyright 2015 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"""Base classes for client used to interact with Google Cloud APIs.""" 
    16 
    17import io 
    18import json 
    19import os 
    20from pickle import PicklingError 
    21from typing import Tuple 
    22from typing import Union 
    23 
    24import google.api_core.client_options 
    25import google.api_core.exceptions 
    26import google.auth 
    27from google.auth import environment_vars 
    28import google.auth.credentials 
    29import google.auth.transport.requests 
    30from google.cloud._helpers import _determine_default_project 
    31from google.oauth2 import service_account 
    32 
    33try: 
    34    import google.auth.api_key 
    35 
    36    HAS_GOOGLE_AUTH_API_KEY = True 
    37except ImportError: 
    38    HAS_GOOGLE_AUTH_API_KEY = False 
    39 
    40 
    41_GOOGLE_AUTH_CREDENTIALS_HELP = ( 
    42    "This library only supports credentials from google-auth-library-python. " 
    43    "See https://google-auth.readthedocs.io/en/latest/ " 
    44    "for help on authentication with this library." 
    45) 
    46 
    47# Default timeout for auth requests. 
    48_CREDENTIALS_REFRESH_TIMEOUT = 300 
    49 
    50 
    51class _ClientFactoryMixin(object): 
    52    """Mixin to allow factories that create credentials. 
    53 
    54    .. note:: 
    55 
    56        This class is virtual. 
    57    """ 
    58 
    59    _SET_PROJECT = False 
    60 
    61    @classmethod 
    62    def from_service_account_info(cls, info, *args, **kwargs): 
    63        """Factory to retrieve JSON credentials while creating client. 
    64 
    65        :type info: dict 
    66        :param info: 
    67            The JSON object with a private key and other credentials 
    68            information (downloaded from the Google APIs console). 
    69 
    70        :type args: tuple 
    71        :param args: Remaining positional arguments to pass to constructor. 
    72 
    73        :param kwargs: Remaining keyword arguments to pass to constructor. 
    74 
    75        :rtype: :class:`_ClientFactoryMixin` 
    76        :returns: The client created with the retrieved JSON credentials. 
    77        :raises TypeError: if there is a conflict with the kwargs 
    78                 and the credentials created by the factory. 
    79        """ 
    80        if "credentials" in kwargs: 
    81            raise TypeError("credentials must not be in keyword arguments") 
    82 
    83        credentials = service_account.Credentials.from_service_account_info(info) 
    84        if cls._SET_PROJECT: 
    85            if "project" not in kwargs: 
    86                kwargs["project"] = info.get("project_id") 
    87 
    88        kwargs["credentials"] = credentials 
    89        return cls(*args, **kwargs) 
    90 
    91    @classmethod 
    92    def from_service_account_json(cls, json_credentials_path, *args, **kwargs): 
    93        """Factory to retrieve JSON credentials while creating client. 
    94 
    95        :type json_credentials_path: str 
    96        :param json_credentials_path: The path to a private key file (this file 
    97                                      was given to you when you created the 
    98                                      service account). This file must contain 
    99                                      a JSON object with a private key and 
    100                                      other credentials information (downloaded 
    101                                      from the Google APIs console). 
    102 
    103        :type args: tuple 
    104        :param args: Remaining positional arguments to pass to constructor. 
    105 
    106        :param kwargs: Remaining keyword arguments to pass to constructor. 
    107 
    108        :rtype: :class:`_ClientFactoryMixin` 
    109        :returns: The client created with the retrieved JSON credentials. 
    110        :raises TypeError: if there is a conflict with the kwargs 
    111                 and the credentials created by the factory. 
    112        """ 
    113        with io.open(json_credentials_path, "r", encoding="utf-8") as json_fi: 
    114            credentials_info = json.load(json_fi) 
    115 
    116        return cls.from_service_account_info(credentials_info, *args, **kwargs) 
    117 
    118 
    119class Client(_ClientFactoryMixin): 
    120    """Client to bundle configuration needed for API requests. 
    121 
    122    Stores ``credentials`` and an HTTP object so that subclasses 
    123    can pass them along to a connection class. 
    124 
    125    If no value is passed in for ``_http``, a :class:`requests.Session` object 
    126    will be created and authorized with the ``credentials``. If not, the 
    127    ``credentials`` and ``_http`` need not be related. 
    128 
    129    Callers and subclasses may seek to use the private key from 
    130    ``credentials`` to sign data. 
    131 
    132    Args: 
    133        credentials (google.auth.credentials.Credentials): 
    134            (Optional) The OAuth2 Credentials to use for this client. If not 
    135            passed (and if no ``_http`` object is passed), falls back to the 
    136            default inferred from the environment. 
    137        client_options (google.api_core.client_options.ClientOptions): 
    138            (Optional) Custom options for the client. 
    139        _http (requests.Session): 
    140            (Optional) HTTP object to make requests. Can be any object that 
    141            defines ``request()`` with the same interface as 
    142            :meth:`requests.Session.request`. If not passed, an ``_http`` 
    143            object is created that is bound to the ``credentials`` for the 
    144            current object. 
    145            This parameter should be considered private, and could change in 
    146            the future. 
    147 
    148    Raises: 
    149        google.auth.exceptions.DefaultCredentialsError: 
    150            Raised if ``credentials`` is not specified and the library fails 
    151            to acquire default credentials. 
    152    """ 
    153 
    154    SCOPE: Union[Tuple[str, ...], None] = None 
    155    """The scopes required for authenticating with a service. 
    156 
    157    Needs to be set by subclasses. 
    158    """ 
    159 
    160    def __init__(self, credentials=None, _http=None, client_options=None): 
    161        if isinstance(client_options, dict): 
    162            client_options = google.api_core.client_options.from_dict(client_options) 
    163        if client_options is None: 
    164            client_options = google.api_core.client_options.ClientOptions() 
    165 
    166        if credentials and client_options.credentials_file: 
    167            raise google.api_core.exceptions.DuplicateCredentialArgs( 
    168                "'credentials' and 'client_options.credentials_file' are mutually exclusive." 
    169            ) 
    170 
    171        if ( 
    172            HAS_GOOGLE_AUTH_API_KEY 
    173            and client_options.api_key 
    174            and (credentials or client_options.credentials_file) 
    175        ): 
    176            raise google.api_core.exceptions.DuplicateCredentialArgs( 
    177                "'client_options.api_key' is mutually exclusive with 'credentials' and 'client_options.credentials_file'." 
    178            ) 
    179 
    180        if credentials and not isinstance( 
    181            credentials, google.auth.credentials.Credentials 
    182        ): 
    183            raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP) 
    184 
    185        scopes = client_options.scopes or self.SCOPE 
    186 
    187        # if no http is provided, credentials must exist 
    188        if not _http and credentials is None: 
    189            if client_options.credentials_file: 
    190                credentials, _ = google.auth.load_credentials_from_file( 
    191                    client_options.credentials_file, scopes=scopes 
    192                ) 
    193            elif HAS_GOOGLE_AUTH_API_KEY and client_options.api_key is not None: 
    194                credentials = google.auth.api_key.Credentials(client_options.api_key) 
    195            else: 
    196                credentials, _ = google.auth.default(scopes=scopes) 
    197 
    198        self._credentials = google.auth.credentials.with_scopes_if_required( 
    199            credentials, scopes=scopes 
    200        ) 
    201 
    202        if client_options.quota_project_id: 
    203            self._credentials = self._credentials.with_quota_project( 
    204                client_options.quota_project_id 
    205            ) 
    206 
    207        self._http_internal = _http 
    208        self._client_cert_source = client_options.client_cert_source 
    209 
    210    def __getstate__(self): 
    211        """Explicitly state that clients are not pickleable.""" 
    212        raise PicklingError( 
    213            "\n".join( 
    214                [ 
    215                    "Pickling client objects is explicitly not supported.", 
    216                    "Clients have non-trivial state that is local and unpickleable.", 
    217                ] 
    218            ) 
    219        ) 
    220 
    221    @property 
    222    def _http(self): 
    223        """Getter for object used for HTTP transport. 
    224 
    225        :rtype: :class:`~requests.Session` 
    226        :returns: An HTTP object. 
    227        """ 
    228        if self._http_internal is None: 
    229            self._http_internal = google.auth.transport.requests.AuthorizedSession( 
    230                self._credentials, 
    231                refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT, 
    232            ) 
    233            self._http_internal.configure_mtls_channel(self._client_cert_source) 
    234        return self._http_internal 
    235 
    236    def close(self): 
    237        """Clean up transport, if set. 
    238 
    239        Suggested use: 
    240 
    241        .. code-block:: python 
    242 
    243           import contextlib 
    244 
    245           with contextlib.closing(client):  # closes on exit 
    246               do_something_with(client) 
    247        """ 
    248        if self._http_internal is not None: 
    249            self._http_internal.close() 
    250 
    251 
    252class _ClientProjectMixin(object): 
    253    """Mixin to allow setting the project on the client. 
    254 
    255    :type project: str 
    256    :param project: 
    257        (Optional) the project which the client acts on behalf of. If not 
    258        passed, falls back to the default inferred from the environment. 
    259 
    260    :type credentials: :class:`google.auth.credentials.Credentials` 
    261    :param credentials: 
    262        (Optional) credentials used to discover a project, if not passed. 
    263 
    264    :raises: :class:`EnvironmentError` if the project is neither passed in nor 
    265             set on the credentials or in the environment. :class:`ValueError` 
    266             if the project value is invalid. 
    267    """ 
    268 
    269    def __init__(self, project=None, credentials=None): 
    270        # This test duplicates the one from `google.auth.default`, but earlier, 
    271        # for backward compatibility:  we want the environment variable to 
    272        # override any project set on the credentials.  See: 
    273        # https://github.com/googleapis/python-cloud-core/issues/27 
    274        if project is None: 
    275            project = os.getenv( 
    276                environment_vars.PROJECT, 
    277                os.getenv(environment_vars.LEGACY_PROJECT), 
    278            ) 
    279 
    280        # Project set on explicit credentials overrides discovery from 
    281        # SDK / GAE / GCE. 
    282        if project is None and credentials is not None: 
    283            project = getattr(credentials, "project_id", None) 
    284 
    285        if project is None: 
    286            project = self._determine_default(project) 
    287 
    288        if project is None: 
    289            raise EnvironmentError( 
    290                "Project was not passed and could not be " 
    291                "determined from the environment." 
    292            ) 
    293 
    294        if isinstance(project, bytes): 
    295            project = project.decode("utf-8") 
    296 
    297        if not isinstance(project, str): 
    298            raise ValueError("Project must be a string.") 
    299 
    300        self.project = project 
    301 
    302    @staticmethod 
    303    def _determine_default(project): 
    304        """Helper:  use default project detection.""" 
    305        return _determine_default_project(project) 
    306 
    307 
    308class ClientWithProject(Client, _ClientProjectMixin): 
    309    """Client that also stores a project. 
    310 
    311    :type project: str 
    312    :param project: the project which the client acts on behalf of. If not 
    313                    passed falls back to the default inferred from the 
    314                    environment. 
    315 
    316    :type credentials: :class:`~google.auth.credentials.Credentials` 
    317    :param credentials: (Optional) The OAuth2 Credentials to use for this 
    318                        client. If not passed (and if no ``_http`` object is 
    319                        passed), falls back to the default inferred from the 
    320                        environment. 
    321 
    322    :type _http: :class:`~requests.Session` 
    323    :param _http: (Optional) HTTP object to make requests. Can be any object 
    324                  that defines ``request()`` with the same interface as 
    325                  :meth:`~requests.Session.request`. If not passed, an 
    326                  ``_http`` object is created that is bound to the 
    327                  ``credentials`` for the current object. 
    328                  This parameter should be considered private, and could 
    329                  change in the future. 
    330 
    331    :raises: :class:`ValueError` if the project is neither passed in nor 
    332             set in the environment. 
    333    """ 
    334 
    335    _SET_PROJECT = True  # Used by from_service_account_json() 
    336 
    337    def __init__(self, project=None, credentials=None, client_options=None, _http=None): 
    338        _ClientProjectMixin.__init__(self, project=project, credentials=credentials) 
    339        Client.__init__( 
    340            self, credentials=credentials, client_options=client_options, _http=_http 
    341        )