# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Base classes for client used to interact with Google Cloud APIs.""" import io import json import os from pickle import PicklingError import six import google.api_core.client_options import google.api_core.exceptions import google.auth from google.auth import environment_vars import google.auth.credentials import google.auth.transport.requests from google.cloud._helpers import _determine_default_project from google.oauth2 import service_account _GOOGLE_AUTH_CREDENTIALS_HELP = ( "This library only supports credentials from google-auth-library-python. " "See https://google-auth.readthedocs.io/en/latest/ " "for help on authentication with this library." ) # Default timeout for auth requests. _CREDENTIALS_REFRESH_TIMEOUT = 300 class _ClientFactoryMixin(object): """Mixin to allow factories that create credentials. .. note:: This class is virtual. """ _SET_PROJECT = False @classmethod def from_service_account_info(cls, info, *args, **kwargs): """Factory to retrieve JSON credentials while creating client. :type info: str :param info: The JSON object with a private key and other credentials information (downloaded from the Google APIs console). :type args: tuple :param args: Remaining positional arguments to pass to constructor. :param kwargs: Remaining keyword arguments to pass to constructor. :rtype: :class:`_ClientFactoryMixin` :returns: The client created with the retrieved JSON credentials. :raises TypeError: if there is a conflict with the kwargs and the credentials created by the factory. """ if "credentials" in kwargs: raise TypeError("credentials must not be in keyword arguments") credentials = service_account.Credentials.from_service_account_info(info) if cls._SET_PROJECT: if "project" not in kwargs: kwargs["project"] = info.get("project_id") kwargs["credentials"] = credentials return cls(*args, **kwargs) @classmethod def from_service_account_json(cls, json_credentials_path, *args, **kwargs): """Factory to retrieve JSON credentials while creating client. :type json_credentials_path: str :param json_credentials_path: The path to a private key file (this file was given to you when you created the service account). This file must contain a JSON object with a private key and other credentials information (downloaded from the Google APIs console). :type args: tuple :param args: Remaining positional arguments to pass to constructor. :param kwargs: Remaining keyword arguments to pass to constructor. :rtype: :class:`_ClientFactoryMixin` :returns: The client created with the retrieved JSON credentials. :raises TypeError: if there is a conflict with the kwargs and the credentials created by the factory. """ with io.open(json_credentials_path, "r", encoding="utf-8") as json_fi: credentials_info = json.load(json_fi) return cls.from_service_account_info(credentials_info, *args, **kwargs) class Client(_ClientFactoryMixin): """Client to bundle configuration needed for API requests. Stores ``credentials`` and an HTTP object so that subclasses can pass them along to a connection class. If no value is passed in for ``_http``, a :class:`requests.Session` object will be created and authorized with the ``credentials``. If not, the ``credentials`` and ``_http`` need not be related. Callers and subclasses may seek to use the private key from ``credentials`` to sign data. Args: credentials (google.auth.credentials.Credentials): (Optional) The OAuth2 Credentials to use for this client. If not passed (and if no ``_http`` object is passed), falls back to the default inferred from the environment. client_options (google.api_core.client_options.ClientOptions): (Optional) Custom options for the client. _http (requests.Session): (Optional) HTTP object to make requests. Can be any object that defines ``request()`` with the same interface as :meth:`requests.Session.request`. If not passed, an ``_http`` object is created that is bound to the ``credentials`` for the current object. This parameter should be considered private, and could change in the future. Raises: google.auth.exceptions.DefaultCredentialsError: Raised if ``credentials`` is not specified and the library fails to acquire default credentials. """ SCOPE = None """The scopes required for authenticating with a service. Needs to be set by subclasses. """ def __init__(self, credentials=None, _http=None, client_options=None): if isinstance(client_options, dict): client_options = google.api_core.client_options.from_dict(client_options) if client_options is None: client_options = google.api_core.client_options.ClientOptions() if credentials and client_options.credentials_file: raise google.api_core.exceptions.DuplicateCredentialArgs( "'credentials' and 'client_options.credentials_file' are mutually exclusive." ) if credentials and not isinstance( credentials, google.auth.credentials.Credentials ): raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP) scopes = client_options.scopes or self.SCOPE # if no http is provided, credentials must exist if not _http and credentials is None: if client_options.credentials_file: credentials, _ = google.auth.load_credentials_from_file( client_options.credentials_file, scopes=scopes ) else: credentials, _ = google.auth.default(scopes=scopes) self._credentials = google.auth.credentials.with_scopes_if_required( credentials, scopes=scopes ) if client_options.quota_project_id: self._credentials = self._credentials.with_quota_project( client_options.quota_project_id ) self._http_internal = _http self._client_cert_source = client_options.client_cert_source def __getstate__(self): """Explicitly state that clients are not pickleable.""" raise PicklingError( "\n".join( [ "Pickling client objects is explicitly not supported.", "Clients have non-trivial state that is local and unpickleable.", ] ) ) @property def _http(self): """Getter for object used for HTTP transport. :rtype: :class:`~requests.Session` :returns: An HTTP object. """ if self._http_internal is None: self._http_internal = google.auth.transport.requests.AuthorizedSession( self._credentials, refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT, ) self._http_internal.configure_mtls_channel(self._client_cert_source) return self._http_internal def close(self): """Clean up transport, if set. Suggested use: .. code-block:: python import contextlib with contextlib.closing(client): # closes on exit do_something_with(client) """ if self._http_internal is not None: self._http_internal.close() class _ClientProjectMixin(object): """Mixin to allow setting the project on the client. :type project: str :param project: (Optional) the project which the client acts on behalf of. If not passed, falls back to the default inferred from the environment. :type credentials: :class:`google.auth.credentials.Credentials` :param credentials: (Optional) credentials used to discover a project, if not passed. :raises: :class:`EnvironmentError` if the project is neither passed in nor set on the credentials or in the environment. :class:`ValueError` if the project value is invalid. """ def __init__(self, project=None, credentials=None): # This test duplicates the one from `google.auth.default`, but earlier, # for backward compatibility: we want the environment variable to # override any project set on the credentials. See: # https://github.com/googleapis/python-cloud-core/issues/27 if project is None: project = os.getenv( environment_vars.PROJECT, os.getenv(environment_vars.LEGACY_PROJECT), ) # Project set on explicit credentials overrides discovery from # SDK / GAE / GCE. if project is None and credentials is not None: project = getattr(credentials, "project_id", None) if project is None: project = self._determine_default(project) if project is None: raise EnvironmentError( "Project was not passed and could not be " "determined from the environment." ) if isinstance(project, six.binary_type): project = project.decode("utf-8") if not isinstance(project, six.string_types): raise ValueError("Project must be a string.") self.project = project @staticmethod def _determine_default(project): """Helper: use default project detection.""" return _determine_default_project(project) class ClientWithProject(Client, _ClientProjectMixin): """Client that also stores a project. :type project: str :param project: the project which the client acts on behalf of. If not passed falls back to the default inferred from the environment. :type credentials: :class:`~google.auth.credentials.Credentials` :param credentials: (Optional) The OAuth2 Credentials to use for this client. If not passed (and if no ``_http`` object is passed), falls back to the default inferred from the environment. :type _http: :class:`~requests.Session` :param _http: (Optional) HTTP object to make requests. Can be any object that defines ``request()`` with the same interface as :meth:`~requests.Session.request`. If not passed, an ``_http`` object is created that is bound to the ``credentials`` for the current object. This parameter should be considered private, and could change in the future. :raises: :class:`ValueError` if the project is neither passed in nor set in the environment. """ _SET_PROJECT = True # Used by from_service_account_json() def __init__(self, project=None, credentials=None, client_options=None, _http=None): _ClientProjectMixin.__init__(self, project=project, credentials=credentials) Client.__init__( self, credentials=credentials, client_options=client_options, _http=_http )