Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1902a26

Browse files
tseavercrwilcox
andauthored
feat: infer project from explicit service account creds (#51)
Closes #27 Co-authored-by: Christopher Wilcox <[email protected]>
1 parent e051170 commit 1902a26

File tree

2 files changed

+160
-8
lines changed

2 files changed

+160
-8
lines changed

packages/google-cloud-core/google/cloud/client.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
import io
1818
import json
19+
import os
1920
from pickle import PicklingError
2021

2122
import six
2223

2324
import google.api_core.client_options
2425
import google.api_core.exceptions
2526
import google.auth
27+
from google.auth import environment_vars
2628
import google.auth.credentials
2729
import google.auth.transport.requests
2830
from google.cloud._helpers import _determine_default_project
@@ -188,26 +190,50 @@ class _ClientProjectMixin(object):
188190
"""Mixin to allow setting the project on the client.
189191
190192
:type project: str
191-
:param project: the project which the client acts on behalf of. If not
192-
passed falls back to the default inferred from the
193-
environment.
193+
:param project:
194+
(Optional) the project which the client acts on behalf of. If not
195+
passed, falls back to the default inferred from the environment.
196+
197+
:type credentials: :class:`google.auth.credentials.Credentials`
198+
:param credentials:
199+
(Optional) credentials used to discover a project, if not passed.
194200
195201
:raises: :class:`EnvironmentError` if the project is neither passed in nor
196-
set in the environment. :class:`ValueError` if the project value
197-
is invalid.
202+
set on the credentials or in the environment. :class:`ValueError`
203+
if the project value is invalid.
198204
"""
199205

200-
def __init__(self, project=None):
201-
project = self._determine_default(project)
206+
def __init__(self, project=None, credentials=None):
207+
# This test duplicates the one from `google.auth.default`, but earlier,
208+
# for backward compatibility: we want the environment variable to
209+
# override any project set on the credentials. See:
210+
# https://github.com/googleapis/python-cloud-core/issues/27
211+
if project is None:
212+
project = os.getenv(
213+
environment_vars.PROJECT,
214+
os.getenv(environment_vars.LEGACY_PROJECT),
215+
)
216+
217+
# Project set on explicit credentials overrides discovery from
218+
# SDK / GAE / GCE.
219+
if project is None and credentials is not None:
220+
project = getattr(credentials, "project_id", None)
221+
222+
if project is None:
223+
project = self._determine_default(project)
224+
202225
if project is None:
203226
raise EnvironmentError(
204227
"Project was not passed and could not be "
205228
"determined from the environment."
206229
)
230+
207231
if isinstance(project, six.binary_type):
208232
project = project.decode("utf-8")
233+
209234
if not isinstance(project, six.string_types):
210235
raise ValueError("Project must be a string.")
236+
211237
self.project = project
212238

213239
@staticmethod
@@ -246,5 +272,5 @@ class ClientWithProject(Client, _ClientProjectMixin):
246272
_SET_PROJECT = True # Used by from_service_account_json()
247273

248274
def __init__(self, project=None, credentials=None, client_options=None, _http=None):
249-
_ClientProjectMixin.__init__(self, project=project)
275+
_ClientProjectMixin.__init__(self, project=project, credentials=credentials)
250276
Client.__init__(self, credentials=credentials, client_options=client_options, _http=_http)

packages/google-cloud-core/tests/unit/test_client.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,132 @@ def test_from_service_account_json_bad_args(self):
174174
)
175175

176176

177+
class Test_ClientProjectMixin(unittest.TestCase):
178+
@staticmethod
179+
def _get_target_class():
180+
from google.cloud.client import _ClientProjectMixin
181+
182+
return _ClientProjectMixin
183+
184+
def _make_one(self, *args, **kw):
185+
return self._get_target_class()(*args, **kw)
186+
187+
def test_ctor_defaults_wo_envvar(self):
188+
environ = {}
189+
patch_env = mock.patch("os.environ", new=environ)
190+
patch_default = mock.patch(
191+
"google.cloud.client._determine_default_project",
192+
return_value=None,
193+
)
194+
with patch_env:
195+
with patch_default as patched:
196+
with self.assertRaises(EnvironmentError):
197+
self._make_one()
198+
199+
patched.assert_called_once_with(None)
200+
201+
def test_ctor_defaults_w_envvar(self):
202+
from google.auth.environment_vars import PROJECT
203+
204+
project = "some-project-123"
205+
environ = {PROJECT: project}
206+
patch_env = mock.patch("os.environ", new=environ)
207+
with patch_env:
208+
client = self._make_one()
209+
210+
self.assertEqual(client.project, project)
211+
212+
def test_ctor_defaults_w_legacy_envvar(self):
213+
from google.auth.environment_vars import LEGACY_PROJECT
214+
215+
project = "some-project-123"
216+
environ = {LEGACY_PROJECT: project}
217+
patch_env = mock.patch("os.environ", new=environ)
218+
with patch_env:
219+
client = self._make_one()
220+
221+
self.assertEqual(client.project, project)
222+
223+
def test_ctor_w_explicit_project(self):
224+
explicit_project = "explicit-project-456"
225+
patch_default = mock.patch(
226+
"google.cloud.client._determine_default_project",
227+
return_value=None,
228+
)
229+
with patch_default as patched:
230+
client = self._make_one(project=explicit_project)
231+
232+
self.assertEqual(client.project, explicit_project)
233+
234+
patched.assert_not_called()
235+
236+
def test_ctor_w_explicit_project_bytes(self):
237+
explicit_project = b"explicit-project-456"
238+
patch_default = mock.patch(
239+
"google.cloud.client._determine_default_project",
240+
return_value=None,
241+
)
242+
with patch_default as patched:
243+
client = self._make_one(project=explicit_project)
244+
245+
self.assertEqual(client.project, explicit_project.decode("utf-8"))
246+
247+
patched.assert_not_called()
248+
249+
def test_ctor_w_explicit_project_invalid(self):
250+
explicit_project = object()
251+
patch_default = mock.patch(
252+
"google.cloud.client._determine_default_project",
253+
return_value=None,
254+
)
255+
with patch_default as patched:
256+
with self.assertRaises(ValueError):
257+
self._make_one(project=explicit_project)
258+
259+
patched.assert_not_called()
260+
261+
@staticmethod
262+
def _make_credentials(**kw):
263+
from google.auth.credentials import Credentials
264+
265+
class _Credentials(Credentials):
266+
def __init__(self, **kw):
267+
self.__dict__.update(kw)
268+
269+
def refresh(self): # pragma: NO COVER
270+
pass
271+
272+
return _Credentials(**kw)
273+
274+
def test_ctor_w_explicit_credentials_wo_project(self):
275+
default_project = "default-project-123"
276+
credentials = self._make_credentials()
277+
patch_default = mock.patch(
278+
"google.cloud.client._determine_default_project",
279+
return_value=default_project,
280+
)
281+
with patch_default as patched:
282+
client = self._make_one(credentials=credentials)
283+
284+
self.assertEqual(client.project, default_project)
285+
286+
patched.assert_called_once_with(None)
287+
288+
def test_ctor_w_explicit_credentials_w_project(self):
289+
project = "credentials-project-456"
290+
credentials = self._make_credentials(project_id=project)
291+
patch_default = mock.patch(
292+
"google.cloud.client._determine_default_project",
293+
return_value=None,
294+
)
295+
with patch_default as patched:
296+
client = self._make_one(credentials=credentials)
297+
298+
self.assertEqual(client.project, project)
299+
300+
patched.assert_not_called()
301+
302+
177303
class TestClientWithProject(unittest.TestCase):
178304
@staticmethod
179305
def _get_target_class():

0 commit comments

Comments
 (0)