Skip to content
This repository was archived by the owner on Feb 13, 2026. It is now read-only.

Commit 7e59360

Browse files
authored
feat: add 'Client.from_service_account_info' factory (#54)
Closes #8.
1 parent 00cdb4d commit 7e59360

File tree

2 files changed

+92
-21
lines changed

2 files changed

+92
-21
lines changed

google/cloud/client.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@ class _ClientFactoryMixin(object):
5151

5252
_SET_PROJECT = False
5353

54+
@classmethod
55+
def from_service_account_info(cls, info, *args, **kwargs):
56+
"""Factory to retrieve JSON credentials while creating client.
57+
58+
:type info: str
59+
:param info:
60+
The JSON object with a private key and other credentials
61+
information (downloaded from the Google APIs console).
62+
63+
:type args: tuple
64+
:param args: Remaining positional arguments to pass to constructor.
65+
66+
:param kwargs: Remaining keyword arguments to pass to constructor.
67+
68+
:rtype: :class:`_ClientFactoryMixin`
69+
:returns: The client created with the retrieved JSON credentials.
70+
:raises TypeError: if there is a conflict with the kwargs
71+
and the credentials created by the factory.
72+
"""
73+
if "credentials" in kwargs:
74+
raise TypeError("credentials must not be in keyword arguments")
75+
76+
credentials = service_account.Credentials.from_service_account_info(info)
77+
if cls._SET_PROJECT:
78+
if "project" not in kwargs:
79+
kwargs["project"] = info.get("project_id")
80+
81+
kwargs["credentials"] = credentials
82+
return cls(*args, **kwargs)
83+
5484
@classmethod
5585
def from_service_account_json(cls, json_credentials_path, *args, **kwargs):
5686
"""Factory to retrieve JSON credentials while creating client.
@@ -73,19 +103,10 @@ def from_service_account_json(cls, json_credentials_path, *args, **kwargs):
73103
:raises TypeError: if there is a conflict with the kwargs
74104
and the credentials created by the factory.
75105
"""
76-
if "credentials" in kwargs:
77-
raise TypeError("credentials must not be in keyword arguments")
78106
with io.open(json_credentials_path, "r", encoding="utf-8") as json_fi:
79107
credentials_info = json.load(json_fi)
80-
credentials = service_account.Credentials.from_service_account_info(
81-
credentials_info
82-
)
83-
if cls._SET_PROJECT:
84-
if "project" not in kwargs:
85-
kwargs["project"] = credentials_info.get("project_id")
86108

87-
kwargs["credentials"] = credentials
88-
return cls(*args, **kwargs)
109+
return cls.from_service_account_info(credentials_info)
89110

90111

91112
class Client(_ClientFactoryMixin):

tests/unit/test_client.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,39 @@ def test_ctor__http_property_new(self):
143143
self.assertIs(client._http, session)
144144
self.assertEqual(AuthorizedSession.call_count, 1)
145145

146+
def test_from_service_account_info(self):
147+
klass = self._get_target_class()
148+
149+
info = {"dummy": "value", "valid": "json"}
150+
constructor_patch = mock.patch(
151+
"google.oauth2.service_account.Credentials.from_service_account_info",
152+
return_value=_make_credentials(),
153+
)
154+
155+
with constructor_patch as constructor:
156+
client_obj = klass.from_service_account_info(info)
157+
158+
self.assertIs(client_obj._credentials, constructor.return_value)
159+
self.assertIsNone(client_obj._http_internal)
160+
constructor.assert_called_once_with(info)
161+
162+
def test_from_service_account_info_w_explicit_credentials(self):
163+
KLASS = self._get_target_class()
164+
165+
info = {"dummy": "value", "valid": "json"}
166+
167+
with self.assertRaises(TypeError):
168+
KLASS.from_service_account_info(info, credentials=mock.sentinel.credentials)
169+
146170
def test_from_service_account_json(self):
147171
from google.cloud import _helpers
148172

149173
klass = self._get_target_class()
150174

151-
# Mock both the file opening and the credentials constructor.
152175
info = {"dummy": "value", "valid": "json"}
153-
json_fi = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info)))
154-
file_open_patch = mock.patch("io.open", return_value=json_fi)
176+
json_file = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info)))
177+
178+
file_open_patch = mock.patch("io.open", return_value=json_file)
155179
constructor_patch = mock.patch(
156180
"google.oauth2.service_account.Credentials." "from_service_account_info",
157181
return_value=_make_credentials(),
@@ -167,14 +191,6 @@ def test_from_service_account_json(self):
167191
file_open.assert_called_once_with(mock.sentinel.filename, "r", encoding="utf-8")
168192
constructor.assert_called_once_with(info)
169193

170-
def test_from_service_account_json_bad_args(self):
171-
KLASS = self._get_target_class()
172-
173-
with self.assertRaises(TypeError):
174-
KLASS.from_service_account_json(
175-
mock.sentinel.filename, credentials=mock.sentinel.credentials
176-
)
177-
178194

179195
class Test_ClientProjectMixin(unittest.TestCase):
180196
@staticmethod
@@ -377,6 +393,40 @@ def test_constructor_explicit_unicode(self):
377393
PROJECT = u"PROJECT"
378394
self._explicit_ctor_helper(PROJECT)
379395

396+
def _from_service_account_info_helper(self, project=None):
397+
klass = self._get_target_class()
398+
399+
info = {"dummy": "value", "valid": "json"}
400+
kwargs = {}
401+
402+
if project is None:
403+
expected_project = "eye-d-of-project"
404+
else:
405+
expected_project = project
406+
kwargs["project"] = project
407+
408+
info["project_id"] = expected_project
409+
410+
constructor_patch = mock.patch(
411+
"google.oauth2.service_account.Credentials.from_service_account_info",
412+
return_value=_make_credentials(),
413+
)
414+
415+
with constructor_patch as constructor:
416+
client_obj = klass.from_service_account_info(info, **kwargs)
417+
418+
self.assertIs(client_obj._credentials, constructor.return_value)
419+
self.assertIsNone(client_obj._http_internal)
420+
self.assertEqual(client_obj.project, expected_project)
421+
422+
constructor.assert_called_once_with(info)
423+
424+
def test_from_service_account_info(self):
425+
self._from_service_account_info_helper()
426+
427+
def test_from_service_account_info_with_project(self):
428+
self._from_service_account_info_helper(project="prah-jekt")
429+
380430
def _from_service_account_json_helper(self, project=None):
381431
from google.cloud import _helpers
382432

0 commit comments

Comments
 (0)