diff --git a/bigtable/google/cloud/bigtable/client.py b/bigtable/google/cloud/bigtable/client.py index 62877371a945..e03ebe91113d 100644 --- a/bigtable/google/cloud/bigtable/client.py +++ b/bigtable/google/cloud/bigtable/client.py @@ -31,7 +31,6 @@ import os -import google.auth import google.auth.credentials from google.gax.utils import metrics from google.longrunning import operations_grpc @@ -39,8 +38,7 @@ from google.cloud._helpers import make_insecure_stub from google.cloud._helpers import make_secure_stub from google.cloud._http import DEFAULT_USER_AGENT -from google.cloud.client import _ClientFactoryMixin -from google.cloud.client import _ClientProjectMixin +from google.cloud.client import ClientWithProject from google.cloud.environment_vars import BIGTABLE_EMULATOR from google.cloud.bigtable import __version__ @@ -166,13 +164,13 @@ def _make_table_stub(client): client.emulator_host) -class Client(_ClientFactoryMixin, _ClientProjectMixin): +class Client(ClientWithProject): """Client for interacting with Google Cloud Bigtable API. .. note:: Since the Cloud Bigtable API requires the gRPC transport, no - ``http`` argument is accepted by this class. + ``_http`` argument is accepted by this class. :type project: :class:`str` or :func:`unicode ` :param project: (Optional) The ID of the project which owns the @@ -209,31 +207,17 @@ class Client(_ClientFactoryMixin, _ClientProjectMixin): def __init__(self, project=None, credentials=None, read_only=False, admin=False, user_agent=DEFAULT_USER_AGENT): - _ClientProjectMixin.__init__(self, project=project) - if credentials is None: - credentials, _ = google.auth.default() - if read_only and admin: raise ValueError('A read-only client cannot also perform' 'administrative actions.') - scopes = [] - if read_only: - scopes.append(READ_ONLY_SCOPE) - else: - scopes.append(DATA_SCOPE) - + # NOTE: This API has no use for the _http argument, but sending it + # will have no impact since the _http() @property only lazily + # creates a working HTTP object. + super(Client, self).__init__( + project=project, credentials=credentials, _http=None) self._read_only = bool(read_only) - - if admin: - scopes.append(ADMIN_SCOPE) - self._admin = bool(admin) - - credentials = google.auth.credentials.with_scopes_if_required( - credentials, scopes) - - self._credentials = credentials self.user_agent = user_agent self.emulator_host = os.getenv(BIGTABLE_EMULATOR) @@ -244,6 +228,22 @@ def __init__(self, project=None, credentials=None, self._operations_stub_internal = _make_operations_stub(self) self._table_stub_internal = _make_table_stub(self) + self._set_scopes() + + def _set_scopes(self): + """Set the scopes on the current credentials.""" + scopes = [] + if self._read_only: + scopes.append(READ_ONLY_SCOPE) + else: + scopes.append(DATA_SCOPE) + + if self._admin: + scopes.append(ADMIN_SCOPE) + + self._credentials = google.auth.credentials.with_scopes_if_required( + self._credentials, scopes) + def copy(self): """Make a copy of this client. diff --git a/core/google/cloud/client.py b/core/google/cloud/client.py index 468cf9e40a52..8a350c6814da 100644 --- a/core/google/cloud/client.py +++ b/core/google/cloud/client.py @@ -23,7 +23,7 @@ import google.auth import google.auth.credentials -from google.cloud._helpers import _determine_default_project +from google.cloud import _helpers from google.oauth2 import service_account @@ -34,54 +34,7 @@ ) -class _ClientFactoryMixin(object): - """Mixin to allow factories that create credentials. - - .. note:: - - This class is virtual. - """ - - _SET_PROJECT = False - - @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. - - :type kwargs: dict - :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') - with io.open(json_credentials_path, 'r', encoding='utf-8') as json_fi: - credentials_info = json.load(json_fi) - credentials = service_account.Credentials.from_service_account_info( - credentials_info) - if cls._SET_PROJECT: - if 'project' not in kwargs: - kwargs['project'] = credentials_info.get('project_id') - - kwargs['credentials'] = credentials - return cls(*args, **kwargs) - - -class Client(_ClientFactoryMixin): +class Client(object): """Client to bundle configuration needed for API requests. Stores ``credentials`` and an HTTP object so that subclasses @@ -123,6 +76,8 @@ class Client(_ClientFactoryMixin): change in the future. """ + _SET_PROJECT = False + SCOPE = None """The scopes required for authenticating with a service. @@ -159,38 +114,44 @@ def _http(self): self._credentials) return self._http_internal + @classmethod + def from_service_account_json(cls, json_credentials_path, *args, **kwargs): + """Factory to retrieve JSON credentials while creating client. -class _ClientProjectMixin(object): - """Mixin to allow setting the project on the 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 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 args: tuple + :param args: Remaining positional arguments to pass to constructor. - :raises: :class:`EnvironmentError` if the project is neither passed in nor - set in the environment. :class:`ValueError` if the project value - is invalid. - """ + :type kwargs: dict + :param kwargs: Remaining keyword arguments to pass to constructor. - def __init__(self, project=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 + :rtype: :class:`Client` + :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') + with io.open(json_credentials_path, 'r', encoding='utf-8') as json_fi: + credentials_info = json.load(json_fi) + credentials = service_account.Credentials.from_service_account_info( + credentials_info) + if cls._SET_PROJECT: + if 'project' not in kwargs: + kwargs['project'] = credentials_info.get('project_id') - @staticmethod - def _determine_default(project): - """Helper: use default project detection.""" - return _determine_default_project(project) + kwargs['credentials'] = credentials + return cls(*args, **kwargs) -class ClientWithProject(Client, _ClientProjectMixin): +class ClientWithProject(Client): """Client that also stores a project. :type project: str @@ -213,12 +174,23 @@ class ClientWithProject(Client, _ClientProjectMixin): 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. + :raises EnvironmentError: 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, _http=None): - _ClientProjectMixin.__init__(self, project=project) - Client.__init__(self, credentials=credentials, _http=_http) + super(ClientWithProject, self).__init__( + credentials=credentials, _http=_http) + + project = self._determine_default(project) + if project is None: + raise EnvironmentError('Project was not passed and could not be ' + 'determined from the environment.') + self.project = _helpers._bytes_to_unicode(project) + + @staticmethod + def _determine_default(project): + """Helper: use default project detection.""" + return _helpers._determine_default_project(project) diff --git a/core/tests/unit/test_client.py b/core/tests/unit/test_client.py index 25667712c69a..84c717753d26 100644 --- a/core/tests/unit/test_client.py +++ b/core/tests/unit/test_client.py @@ -25,19 +25,6 @@ def _make_credentials(): return mock.Mock(spec=google.auth.credentials.Credentials) -class Test_ClientFactoryMixin(unittest.TestCase): - - @staticmethod - def _get_target_class(): - from google.cloud.client import _ClientFactoryMixin - - return _ClientFactoryMixin - - def test_virtual(self): - klass = self._get_target_class() - self.assertFalse('__init__' in klass.__dict__) - - class TestClient(unittest.TestCase): @staticmethod diff --git a/spanner/google/cloud/spanner/client.py b/spanner/google/cloud/spanner/client.py index b701b017abb0..445d82851ccf 100644 --- a/spanner/google/cloud/spanner/client.py +++ b/spanner/google/cloud/spanner/client.py @@ -35,8 +35,7 @@ # pylint: enable=line-too-long from google.cloud._http import DEFAULT_USER_AGENT -from google.cloud.client import _ClientFactoryMixin -from google.cloud.client import _ClientProjectMixin +from google.cloud.client import ClientWithProject from google.cloud.iterator import GAXIterator from google.cloud.spanner import __version__ from google.cloud.spanner._helpers import _options_with_prefix @@ -73,13 +72,13 @@ def from_pb(cls, config_pb): return cls(config_pb.name, config_pb.display_name) -class Client(_ClientFactoryMixin, _ClientProjectMixin): +class Client(ClientWithProject): """Client for interacting with Cloud Spanner API. .. note:: Since the Cloud Spanner API requires the gRPC transport, no - ``http`` argument is accepted by this class. + ``_http`` argument is accepted by this class. :type project: :class:`str` or :func:`unicode ` :param project: (Optional) The ID of the project which owns the @@ -104,21 +103,16 @@ class Client(_ClientFactoryMixin, _ClientProjectMixin): _database_admin_api = None _SET_PROJECT = True # Used by from_service_account_json() + SCOPE = (SPANNER_ADMIN_SCOPE,) + """The scopes required for Google Cloud Spanner.""" + def __init__(self, project=None, credentials=None, user_agent=DEFAULT_USER_AGENT): - - _ClientProjectMixin.__init__(self, project=project) - if credentials is None: - credentials, _ = google.auth.default() - - scopes = [ - SPANNER_ADMIN_SCOPE, - ] - - credentials = google.auth.credentials.with_scopes_if_required( - credentials, scopes) - - self._credentials = credentials + # NOTE: This API has no use for the _http argument, but sending it + # will have no impact since the _http() @property only lazily + # creates a working HTTP object. + super(Client, self).__init__( + project=project, credentials=credentials, _http=None) self.user_agent = user_agent @property