Skip to content

Commit

Permalink
feat: add quota_project, credentials file, and scopes options (#15)
Browse files Browse the repository at this point in the history
Co-authored-by: Tres Seaver <tseaver@palladion.com>
  • Loading branch information
busunkim96 and tseaver committed Aug 4, 2020
1 parent f727aba commit a1e11e1
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 43 deletions.
43 changes: 33 additions & 10 deletions google/cloud/client.py
Expand Up @@ -20,6 +20,8 @@

import six

import google.api_core.client_options
import google.api_core.exceptions
import google.auth
import google.auth.credentials
import google.auth.transport.requests
Expand Down Expand Up @@ -102,6 +104,8 @@ class Client(_ClientFactoryMixin):
(Optional) The OAuth2 Credentials to use for this client. If not
passed (and if no ``_http`` object is passed), falls back to the
default inferred from the environment.
client_options (google.api_core.client_options.ClientOptions):
(Optional) Custom options for the client.
_http (requests.Session):
(Optional) HTTP object to make requests. Can be any object that
defines ``request()`` with the same interface as
Expand All @@ -123,16 +127,35 @@ class Client(_ClientFactoryMixin):
Needs to be set by subclasses.
"""

def __init__(self, credentials=None, _http=None):
if credentials is not None and not isinstance(
credentials, google.auth.credentials.Credentials
):
def __init__(self, credentials=None, _http=None, client_options=None):
if isinstance(client_options, dict):
client_options = google.api_core.client_options.from_dict(client_options)
if client_options is None:
client_options = google.api_core.client_options.ClientOptions()

if credentials and client_options.credentials_file:
raise google.api_core.exceptions.DuplicateCredentialArgs(
"'credentials' and 'client_options.credentials_file' are mutually exclusive.")

if credentials and not isinstance(credentials, google.auth.credentials.Credentials):
raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP)
if credentials is None and _http is None:
credentials, _ = google.auth.default()

scopes = client_options.scopes or self.SCOPE

# if no http is provided, credentials must exist
if not _http and credentials is None:
if client_options.credentials_file:
credentials, _ = google.auth.load_credentials_from_file(
client_options.credentials_file, scopes=scopes)
else:
credentials, _ = google.auth.default(scopes=scopes)

self._credentials = google.auth.credentials.with_scopes_if_required(
credentials, self.SCOPE
)
credentials, scopes=scopes)

if client_options.quota_project_id:
self._credentials = self._credentials.with_quota_project(client_options.quota_project_id)

self._http_internal = _http

def __getstate__(self):
Expand Down Expand Up @@ -222,6 +245,6 @@ class ClientWithProject(Client, _ClientProjectMixin):

_SET_PROJECT = True # Used by from_service_account_json()

def __init__(self, project=None, credentials=None, _http=None):
def __init__(self, project=None, credentials=None, client_options=None, _http=None):
_ClientProjectMixin.__init__(self, project=project)
Client.__init__(self, credentials=credentials, _http=_http)
Client.__init__(self, credentials=credentials, client_options=client_options, _http=_http)
99 changes: 66 additions & 33 deletions tests/unit/test_client.py
Expand Up @@ -50,14 +50,14 @@ def _make_one(self, *args, **kw):
def test_unpickleable(self):
import pickle

CREDENTIALS = _make_credentials()
credentials = _make_credentials()
HTTP = object()

client_obj = self._make_one(credentials=CREDENTIALS, _http=HTTP)
client_obj = self._make_one(credentials=credentials, _http=HTTP)
with self.assertRaises(pickle.PicklingError):
pickle.dumps(client_obj)

def test_constructor_defaults(self):
def test_ctor_defaults(self):
credentials = _make_credentials()

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

self.assertIs(client_obj._credentials, credentials)
self.assertIsNone(client_obj._http_internal)
default.assert_called_once_with()
default.assert_called_once_with(scopes=None)

def test_constructor_explicit(self):
def test_ctor_explicit(self):
credentials = _make_credentials()
http = mock.sentinel.http
client_obj = self._make_one(credentials=credentials, _http=http)

self.assertIs(client_obj._credentials, credentials)
self.assertIs(client_obj._http_internal, http)

def test_constructor_bad_credentials(self):
def test_ctor_client_options_w_conflicting_creds(self):
from google.api_core.exceptions import DuplicateCredentialArgs

credentials = _make_credentials()
client_options = {'credentials_file': '/path/to/creds.json'}
with self.assertRaises(DuplicateCredentialArgs):
self._make_one(credentials=credentials, client_options=client_options)

def test_ctor_bad_credentials(self):
credentials = mock.sentinel.credentials

with self.assertRaises(ValueError):
self._make_one(credentials=credentials)

def test_ctor_client_options_w_creds_file_scopes(self):
credentials = _make_credentials()
credentials_file = '/path/to/creds.json'
scopes = ['SCOPE1', 'SCOPE2']
client_options = {'credentials_file': credentials_file, 'scopes': scopes}

patch = mock.patch("google.auth.load_credentials_from_file", return_value=(credentials, None))
with patch as load_credentials_from_file:
client_obj = self._make_one(client_options=client_options)

self.assertIs(client_obj._credentials, credentials)
self.assertIsNone(client_obj._http_internal)
load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)

def test_ctor_client_options_w_quota_project(self):
credentials = _make_credentials()
quota_project_id = 'quota-project-123'
client_options = {'quota_project_id': quota_project_id}

client_obj = self._make_one(credentials=credentials, client_options=client_options)

self.assertIs(client_obj._credentials, credentials.with_quota_project.return_value)
credentials.with_quota_project.assert_called_once_with(quota_project_id)

def test_ctor__http_property_existing(self):
credentials = _make_credentials()
http = object()
client = self._make_one(credentials=credentials, _http=http)
self.assertIs(client._http_internal, http)
self.assertIs(client._http, http)

def test_ctor__http_property_new(self):
from google.cloud.client import _CREDENTIALS_REFRESH_TIMEOUT

credentials = _make_credentials()
client = self._make_one(credentials=credentials)
self.assertIsNone(client._http_internal)

authorized_session_patch = mock.patch(
"google.auth.transport.requests.AuthorizedSession",
return_value=mock.sentinel.http,
)
with authorized_session_patch as AuthorizedSession:
self.assertIs(client._http, mock.sentinel.http)
# Check the mock.
AuthorizedSession.assert_called_once_with(credentials, refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT)
# Make sure the cached value is used on subsequent access.
self.assertIs(client._http_internal, mock.sentinel.http)
self.assertIs(client._http, mock.sentinel.http)
self.assertEqual(AuthorizedSession.call_count, 1)

def test_from_service_account_json(self):
from google.cloud import _helpers

Expand Down Expand Up @@ -114,32 +173,6 @@ def test_from_service_account_json_bad_args(self):
mock.sentinel.filename, credentials=mock.sentinel.credentials
)

def test__http_property_existing(self):
credentials = _make_credentials()
http = object()
client = self._make_one(credentials=credentials, _http=http)
self.assertIs(client._http_internal, http)
self.assertIs(client._http, http)

def test__http_property_new(self):
from google.cloud.client import _CREDENTIALS_REFRESH_TIMEOUT
credentials = _make_credentials()
client = self._make_one(credentials=credentials)
self.assertIsNone(client._http_internal)

authorized_session_patch = mock.patch(
"google.auth.transport.requests.AuthorizedSession",
return_value=mock.sentinel.http,
)
with authorized_session_patch as AuthorizedSession:
self.assertIs(client._http, mock.sentinel.http)
# Check the mock.
AuthorizedSession.assert_called_once_with(credentials, refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT)
# Make sure the cached value is used on subsequent access.
self.assertIs(client._http_internal, mock.sentinel.http)
self.assertIs(client._http, mock.sentinel.http)
self.assertEqual(AuthorizedSession.call_count, 1)


class TestClientWithProject(unittest.TestCase):
@staticmethod
Expand Down Expand Up @@ -167,7 +200,7 @@ def test_constructor_defaults(self):
self.assertEqual(client_obj.project, project)
self.assertIs(client_obj._credentials, credentials)
self.assertIsNone(client_obj._http_internal)
default.assert_called_once_with()
default.assert_called_once_with(scopes=None)
_determine_default_project.assert_called_once_with(None)

def test_constructor_missing_project(self):
Expand Down

0 comments on commit a1e11e1

Please sign in to comment.