Skip to content

Commit a1e11e1

Browse files
busunkim96tseaver
andauthored
feat: add quota_project, credentials file, and scopes options (#15)
Co-authored-by: Tres Seaver <[email protected]>
1 parent f727aba commit a1e11e1

File tree

2 files changed

+99
-43
lines changed

2 files changed

+99
-43
lines changed

google/cloud/client.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import six
2222

23+
import google.api_core.client_options
24+
import google.api_core.exceptions
2325
import google.auth
2426
import google.auth.credentials
2527
import google.auth.transport.requests
@@ -102,6 +104,8 @@ class Client(_ClientFactoryMixin):
102104
(Optional) The OAuth2 Credentials to use for this client. If not
103105
passed (and if no ``_http`` object is passed), falls back to the
104106
default inferred from the environment.
107+
client_options (google.api_core.client_options.ClientOptions):
108+
(Optional) Custom options for the client.
105109
_http (requests.Session):
106110
(Optional) HTTP object to make requests. Can be any object that
107111
defines ``request()`` with the same interface as
@@ -123,16 +127,35 @@ class Client(_ClientFactoryMixin):
123127
Needs to be set by subclasses.
124128
"""
125129

126-
def __init__(self, credentials=None, _http=None):
127-
if credentials is not None and not isinstance(
128-
credentials, google.auth.credentials.Credentials
129-
):
130+
def __init__(self, credentials=None, _http=None, client_options=None):
131+
if isinstance(client_options, dict):
132+
client_options = google.api_core.client_options.from_dict(client_options)
133+
if client_options is None:
134+
client_options = google.api_core.client_options.ClientOptions()
135+
136+
if credentials and client_options.credentials_file:
137+
raise google.api_core.exceptions.DuplicateCredentialArgs(
138+
"'credentials' and 'client_options.credentials_file' are mutually exclusive.")
139+
140+
if credentials and not isinstance(credentials, google.auth.credentials.Credentials):
130141
raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP)
131-
if credentials is None and _http is None:
132-
credentials, _ = google.auth.default()
142+
143+
scopes = client_options.scopes or self.SCOPE
144+
145+
# if no http is provided, credentials must exist
146+
if not _http and credentials is None:
147+
if client_options.credentials_file:
148+
credentials, _ = google.auth.load_credentials_from_file(
149+
client_options.credentials_file, scopes=scopes)
150+
else:
151+
credentials, _ = google.auth.default(scopes=scopes)
152+
133153
self._credentials = google.auth.credentials.with_scopes_if_required(
134-
credentials, self.SCOPE
135-
)
154+
credentials, scopes=scopes)
155+
156+
if client_options.quota_project_id:
157+
self._credentials = self._credentials.with_quota_project(client_options.quota_project_id)
158+
136159
self._http_internal = _http
137160

138161
def __getstate__(self):
@@ -222,6 +245,6 @@ class ClientWithProject(Client, _ClientProjectMixin):
222245

223246
_SET_PROJECT = True # Used by from_service_account_json()
224247

225-
def __init__(self, project=None, credentials=None, _http=None):
248+
def __init__(self, project=None, credentials=None, client_options=None, _http=None):
226249
_ClientProjectMixin.__init__(self, project=project)
227-
Client.__init__(self, credentials=credentials, _http=_http)
250+
Client.__init__(self, credentials=credentials, client_options=client_options, _http=_http)

tests/unit/test_client.py

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ def _make_one(self, *args, **kw):
5050
def test_unpickleable(self):
5151
import pickle
5252

53-
CREDENTIALS = _make_credentials()
53+
credentials = _make_credentials()
5454
HTTP = object()
5555

56-
client_obj = self._make_one(credentials=CREDENTIALS, _http=HTTP)
56+
client_obj = self._make_one(credentials=credentials, _http=HTTP)
5757
with self.assertRaises(pickle.PicklingError):
5858
pickle.dumps(client_obj)
5959

60-
def test_constructor_defaults(self):
60+
def test_ctor_defaults(self):
6161
credentials = _make_credentials()
6262

6363
patch = mock.patch("google.auth.default", return_value=(credentials, None))
@@ -66,22 +66,81 @@ def test_constructor_defaults(self):
6666

6767
self.assertIs(client_obj._credentials, credentials)
6868
self.assertIsNone(client_obj._http_internal)
69-
default.assert_called_once_with()
69+
default.assert_called_once_with(scopes=None)
7070

71-
def test_constructor_explicit(self):
71+
def test_ctor_explicit(self):
7272
credentials = _make_credentials()
7373
http = mock.sentinel.http
7474
client_obj = self._make_one(credentials=credentials, _http=http)
7575

7676
self.assertIs(client_obj._credentials, credentials)
7777
self.assertIs(client_obj._http_internal, http)
7878

79-
def test_constructor_bad_credentials(self):
79+
def test_ctor_client_options_w_conflicting_creds(self):
80+
from google.api_core.exceptions import DuplicateCredentialArgs
81+
82+
credentials = _make_credentials()
83+
client_options = {'credentials_file': '/path/to/creds.json'}
84+
with self.assertRaises(DuplicateCredentialArgs):
85+
self._make_one(credentials=credentials, client_options=client_options)
86+
87+
def test_ctor_bad_credentials(self):
8088
credentials = mock.sentinel.credentials
8189

8290
with self.assertRaises(ValueError):
8391
self._make_one(credentials=credentials)
8492

93+
def test_ctor_client_options_w_creds_file_scopes(self):
94+
credentials = _make_credentials()
95+
credentials_file = '/path/to/creds.json'
96+
scopes = ['SCOPE1', 'SCOPE2']
97+
client_options = {'credentials_file': credentials_file, 'scopes': scopes}
98+
99+
patch = mock.patch("google.auth.load_credentials_from_file", return_value=(credentials, None))
100+
with patch as load_credentials_from_file:
101+
client_obj = self._make_one(client_options=client_options)
102+
103+
self.assertIs(client_obj._credentials, credentials)
104+
self.assertIsNone(client_obj._http_internal)
105+
load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
106+
107+
def test_ctor_client_options_w_quota_project(self):
108+
credentials = _make_credentials()
109+
quota_project_id = 'quota-project-123'
110+
client_options = {'quota_project_id': quota_project_id}
111+
112+
client_obj = self._make_one(credentials=credentials, client_options=client_options)
113+
114+
self.assertIs(client_obj._credentials, credentials.with_quota_project.return_value)
115+
credentials.with_quota_project.assert_called_once_with(quota_project_id)
116+
117+
def test_ctor__http_property_existing(self):
118+
credentials = _make_credentials()
119+
http = object()
120+
client = self._make_one(credentials=credentials, _http=http)
121+
self.assertIs(client._http_internal, http)
122+
self.assertIs(client._http, http)
123+
124+
def test_ctor__http_property_new(self):
125+
from google.cloud.client import _CREDENTIALS_REFRESH_TIMEOUT
126+
127+
credentials = _make_credentials()
128+
client = self._make_one(credentials=credentials)
129+
self.assertIsNone(client._http_internal)
130+
131+
authorized_session_patch = mock.patch(
132+
"google.auth.transport.requests.AuthorizedSession",
133+
return_value=mock.sentinel.http,
134+
)
135+
with authorized_session_patch as AuthorizedSession:
136+
self.assertIs(client._http, mock.sentinel.http)
137+
# Check the mock.
138+
AuthorizedSession.assert_called_once_with(credentials, refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT)
139+
# Make sure the cached value is used on subsequent access.
140+
self.assertIs(client._http_internal, mock.sentinel.http)
141+
self.assertIs(client._http, mock.sentinel.http)
142+
self.assertEqual(AuthorizedSession.call_count, 1)
143+
85144
def test_from_service_account_json(self):
86145
from google.cloud import _helpers
87146

@@ -114,32 +173,6 @@ def test_from_service_account_json_bad_args(self):
114173
mock.sentinel.filename, credentials=mock.sentinel.credentials
115174
)
116175

117-
def test__http_property_existing(self):
118-
credentials = _make_credentials()
119-
http = object()
120-
client = self._make_one(credentials=credentials, _http=http)
121-
self.assertIs(client._http_internal, http)
122-
self.assertIs(client._http, http)
123-
124-
def test__http_property_new(self):
125-
from google.cloud.client import _CREDENTIALS_REFRESH_TIMEOUT
126-
credentials = _make_credentials()
127-
client = self._make_one(credentials=credentials)
128-
self.assertIsNone(client._http_internal)
129-
130-
authorized_session_patch = mock.patch(
131-
"google.auth.transport.requests.AuthorizedSession",
132-
return_value=mock.sentinel.http,
133-
)
134-
with authorized_session_patch as AuthorizedSession:
135-
self.assertIs(client._http, mock.sentinel.http)
136-
# Check the mock.
137-
AuthorizedSession.assert_called_once_with(credentials, refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT)
138-
# Make sure the cached value is used on subsequent access.
139-
self.assertIs(client._http_internal, mock.sentinel.http)
140-
self.assertIs(client._http, mock.sentinel.http)
141-
self.assertEqual(AuthorizedSession.call_count, 1)
142-
143176

144177
class TestClientWithProject(unittest.TestCase):
145178
@staticmethod
@@ -167,7 +200,7 @@ def test_constructor_defaults(self):
167200
self.assertEqual(client_obj.project, project)
168201
self.assertIs(client_obj._credentials, credentials)
169202
self.assertIsNone(client_obj._http_internal)
170-
default.assert_called_once_with()
203+
default.assert_called_once_with(scopes=None)
171204
_determine_default_project.assert_called_once_with(None)
172205

173206
def test_constructor_missing_project(self):

0 commit comments

Comments
 (0)