Skip to content

Commit

Permalink
feat: add 'Client.from_service_account_info' factory (#54)
Browse files Browse the repository at this point in the history
Closes #8.
  • Loading branch information
tseaver committed Jun 14, 2021
1 parent 00cdb4d commit 7e59360
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 21 deletions.
41 changes: 31 additions & 10 deletions google/cloud/client.py
Expand Up @@ -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.
Expand All @@ -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):
Expand Down
72 changes: 61 additions & 11 deletions tests/unit/test_client.py
Expand Up @@ -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(),
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 7e59360

Please sign in to comment.