diff --git a/google/cloud/client.py b/google/cloud/client.py index fdb81a5..0cfffdb 100644 --- a/google/cloud/client.py +++ b/google/cloud/client.py @@ -51,6 +51,36 @@ class _ClientFactoryMixin(object): _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. @@ -73,19 +103,10 @@ def from_service_account_json(cls, json_credentials_path, *args, **kwargs): :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) + return cls.from_service_account_info(credentials_info) class Client(_ClientFactoryMixin): diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 8137826..0e31348 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -143,15 +143,39 @@ def test_ctor__http_property_new(self): self.assertIs(client._http, session) self.assertEqual(AuthorizedSession.call_count, 1) + def test_from_service_account_info(self): + klass = self._get_target_class() + + info = {"dummy": "value", "valid": "json"} + constructor_patch = mock.patch( + "google.oauth2.service_account.Credentials.from_service_account_info", + return_value=_make_credentials(), + ) + + with constructor_patch as constructor: + client_obj = klass.from_service_account_info(info) + + self.assertIs(client_obj._credentials, constructor.return_value) + self.assertIsNone(client_obj._http_internal) + constructor.assert_called_once_with(info) + + def test_from_service_account_info_w_explicit_credentials(self): + KLASS = self._get_target_class() + + info = {"dummy": "value", "valid": "json"} + + with self.assertRaises(TypeError): + KLASS.from_service_account_info(info, credentials=mock.sentinel.credentials) + def test_from_service_account_json(self): from google.cloud import _helpers klass = self._get_target_class() - # Mock both the file opening and the credentials constructor. info = {"dummy": "value", "valid": "json"} - json_fi = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info))) - file_open_patch = mock.patch("io.open", return_value=json_fi) + json_file = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info))) + + file_open_patch = mock.patch("io.open", return_value=json_file) constructor_patch = mock.patch( "google.oauth2.service_account.Credentials." "from_service_account_info", return_value=_make_credentials(), @@ -167,14 +191,6 @@ def test_from_service_account_json(self): file_open.assert_called_once_with(mock.sentinel.filename, "r", encoding="utf-8") constructor.assert_called_once_with(info) - def test_from_service_account_json_bad_args(self): - KLASS = self._get_target_class() - - with self.assertRaises(TypeError): - KLASS.from_service_account_json( - mock.sentinel.filename, credentials=mock.sentinel.credentials - ) - class Test_ClientProjectMixin(unittest.TestCase): @staticmethod @@ -377,6 +393,40 @@ def test_constructor_explicit_unicode(self): PROJECT = u"PROJECT" self._explicit_ctor_helper(PROJECT) + def _from_service_account_info_helper(self, project=None): + klass = self._get_target_class() + + info = {"dummy": "value", "valid": "json"} + kwargs = {} + + if project is None: + expected_project = "eye-d-of-project" + else: + expected_project = project + kwargs["project"] = project + + info["project_id"] = expected_project + + constructor_patch = mock.patch( + "google.oauth2.service_account.Credentials.from_service_account_info", + return_value=_make_credentials(), + ) + + with constructor_patch as constructor: + client_obj = klass.from_service_account_info(info, **kwargs) + + self.assertIs(client_obj._credentials, constructor.return_value) + self.assertIsNone(client_obj._http_internal) + self.assertEqual(client_obj.project, expected_project) + + constructor.assert_called_once_with(info) + + def test_from_service_account_info(self): + self._from_service_account_info_helper() + + def test_from_service_account_info_with_project(self): + self._from_service_account_info_helper(project="prah-jekt") + def _from_service_account_json_helper(self, project=None): from google.cloud import _helpers