Skip to content

Commit 790e702

Browse files
authored
feat: add quota_project, credentials_file, and scopes support (#1022)
Add support for client options: * quota_project_id * credentials_file * scopes These are only available when default credentials are used.
1 parent 5028fe7 commit 790e702

File tree

7 files changed

+189
-51
lines changed

7 files changed

+189
-51
lines changed

googleapiclient/_auth.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,27 @@
3838
HAS_OAUTH2CLIENT = False
3939

4040

41-
def default_credentials():
41+
def credentials_from_file(filename, scopes=None, quota_project_id=None):
42+
"""Returns credentials loaded from a file."""
43+
if HAS_GOOGLE_AUTH:
44+
credentials, _ = google.auth.load_credentials_from_file(filename, scopes=scopes, quota_project_id=quota_project_id)
45+
return credentials
46+
else:
47+
raise EnvironmentError(
48+
"client_options.credentials_file is only supported in google-auth.")
49+
50+
51+
def default_credentials(scopes=None, quota_project_id=None):
4252
"""Returns Application Default Credentials."""
4353
if HAS_GOOGLE_AUTH:
44-
credentials, _ = google.auth.default()
54+
credentials, _ = google.auth.default(scopes=scopes, quota_project_id=quota_project_id)
4555
return credentials
4656
elif HAS_OAUTH2CLIENT:
57+
if scopes is not None or quota_project_id is not None:
58+
raise EnvironmentError(
59+
"client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
60+
"Please install google-auth."
61+
)
4762
return oauth2client.client.GoogleCredentials.get_application_default()
4863
else:
4964
raise EnvironmentError(

googleapiclient/discovery.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
# Standard library imports
3131
import copy
3232
from collections import OrderedDict
33+
3334
try:
3435
from email.generator import BytesGenerator
3536
except ImportError:
@@ -260,14 +261,17 @@ def build(
260261
else:
261262
discovery_http = http
262263

263-
for discovery_url in \
264-
_discovery_service_uri_options(discoveryServiceUrl, version):
264+
for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
265265
requested_url = uritemplate.expand(discovery_url, params)
266266

267267
try:
268268
content = _retrieve_discovery_doc(
269-
requested_url, discovery_http, cache_discovery, cache,
270-
developerKey, num_retries=num_retries
269+
requested_url,
270+
discovery_http,
271+
cache_discovery,
272+
cache,
273+
developerKey,
274+
num_retries=num_retries,
271275
)
272276
return build_from_document(
273277
content,
@@ -308,13 +312,15 @@ def _discovery_service_uri_options(discoveryServiceUrl, version):
308312
# V1 Discovery won't work if the requested version is None
309313
if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
310314
logger.warning(
311-
"Discovery V1 does not support empty versions. Defaulting to V2...")
315+
"Discovery V1 does not support empty versions. Defaulting to V2..."
316+
)
312317
urls.pop(0)
313318
return list(OrderedDict.fromkeys(urls))
314319

315320

316-
def _retrieve_discovery_doc(url, http, cache_discovery,
317-
cache=None, developerKey=None, num_retries=1):
321+
def _retrieve_discovery_doc(
322+
url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
323+
):
318324
"""Retrieves the discovery_doc from cache or the internet.
319325
320326
Args:
@@ -444,8 +450,20 @@ def build_from_document(
444450
setting up mutual TLS channel.
445451
"""
446452

447-
if http is not None and credentials is not None:
448-
raise ValueError("Arguments http and credentials are mutually exclusive.")
453+
if client_options is None:
454+
client_options = google.api_core.client_options.ClientOptions()
455+
if isinstance(client_options, six.moves.collections_abc.Mapping):
456+
client_options = google.api_core.client_options.from_dict(client_options)
457+
458+
if http is not None:
459+
# if http is passed, the user cannot provide credentials
460+
banned_options = [
461+
(credentials, "credentials"),
462+
(client_options.credentials_file, "client_options.credentials_file"),
463+
]
464+
for option, name in banned_options:
465+
if option is not None:
466+
raise ValueError("Arguments http and {} are mutually exclusive".format(name))
449467

450468
if isinstance(service, six.string_types):
451469
service = json.loads(service)
@@ -463,11 +481,8 @@ def build_from_document(
463481

464482
# If an API Endpoint is provided on client options, use that as the base URL
465483
base = urljoin(service["rootUrl"], service["servicePath"])
466-
if client_options:
467-
if isinstance(client_options, six.moves.collections_abc.Mapping):
468-
client_options = google.api_core.client_options.from_dict(client_options)
469-
if client_options.api_endpoint:
470-
base = client_options.api_endpoint
484+
if client_options.api_endpoint:
485+
base = client_options.api_endpoint
471486

472487
schema = Schemas(service)
473488

@@ -483,13 +498,30 @@ def build_from_document(
483498
# If so, then the we need to setup authentication if no developerKey is
484499
# specified.
485500
if scopes and not developerKey:
501+
# Make sure the user didn't pass multiple credentials
502+
if client_options.credentials_file and credentials:
503+
raise google.api_core.exceptions.DuplicateCredentialArgs(
504+
"client_options.credentials_file and credentials are mutually exclusive."
505+
)
506+
# Check for credentials file via client options
507+
if client_options.credentials_file:
508+
credentials = _auth.credentials_from_file(
509+
client_options.credentials_file,
510+
scopes=client_options.scopes,
511+
quota_project_id=client_options.quota_project_id,
512+
)
486513
# If the user didn't pass in credentials, attempt to acquire application
487514
# default credentials.
488515
if credentials is None:
489-
credentials = _auth.default_credentials()
516+
credentials = _auth.default_credentials(
517+
scopes=client_options.scopes,
518+
quota_project_id=client_options.quota_project_id,
519+
)
490520

491521
# The credentials need to be scoped.
492-
credentials = _auth.with_scopes(credentials, scopes)
522+
# If the user provided scopes via client_options don't override them
523+
if not client_options.scopes:
524+
credentials = _auth.with_scopes(credentials, scopes)
493525

494526
# If credentials are provided, create an authorized http instance;
495527
# otherwise, skip authentication.
@@ -519,7 +551,9 @@ def build_from_document(
519551
and client_options.client_encrypted_cert_source
520552
):
521553
client_cert_to_use = client_options.client_encrypted_cert_source
522-
elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
554+
elif (
555+
adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
556+
):
523557
client_cert_to_use = mtls.default_client_encrypted_cert_source(
524558
adc_cert_path, adc_key_path
525559
)

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def lint(session):
4444
)
4545

4646

47-
@nox.session(python=["2.7", "3.5", "3.6", "3.7"])
47+
@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8"])
4848
@nox.parametrize(
4949
"oauth2client",
5050
[

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"httplib2>=0.9.2,<1dev",
4242
"google-auth>=1.16.0",
4343
"google-auth-httplib2>=0.0.3",
44-
"google-api-core>=1.18.0,<2dev",
44+
"google-api-core>=1.21.0,<2dev",
4545
"six>=1.6.1,<2dev",
4646
"uritemplate>=3.0.0,<4dev",
4747
]

tests/test__auth.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,35 @@ def test_default_credentials(self):
4040

4141
self.assertEqual(credentials, mock.sentinel.credentials)
4242

43+
def test_credentials_from_file(self):
44+
with mock.patch(
45+
"google.auth.load_credentials_from_file", autospec=True
46+
) as default:
47+
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)
48+
49+
credentials = _auth.credentials_from_file("credentials.json")
50+
51+
self.assertEqual(credentials, mock.sentinel.credentials)
52+
default.assert_called_once_with(
53+
"credentials.json", scopes=None, quota_project_id=None
54+
)
55+
56+
def test_default_credentials_with_scopes(self):
57+
with mock.patch("google.auth.default", autospec=True) as default:
58+
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)
59+
credentials = _auth.default_credentials(scopes=["1", "2"])
60+
61+
default.assert_called_once_with(scopes=["1", "2"], quota_project_id=None)
62+
self.assertEqual(credentials, mock.sentinel.credentials)
63+
64+
def test_default_credentials_with_quota_project(self):
65+
with mock.patch("google.auth.default", autospec=True) as default:
66+
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)
67+
credentials = _auth.default_credentials(quota_project_id="my-project")
68+
69+
default.assert_called_once_with(scopes=None, quota_project_id="my-project")
70+
self.assertEqual(credentials, mock.sentinel.credentials)
71+
4372
def test_with_scopes_non_scoped(self):
4473
credentials = mock.Mock(spec=google.auth.credentials.Credentials)
4574

@@ -95,6 +124,16 @@ def test_default_credentials(self):
95124

96125
self.assertEqual(credentials, mock.sentinel.credentials)
97126

127+
def test_credentials_from_file(self):
128+
with self.assertRaises(EnvironmentError):
129+
credentials = _auth.credentials_from_file("credentials.json")
130+
131+
def test_default_credentials_with_scopes_and_quota_project(self):
132+
with self.assertRaises(EnvironmentError):
133+
credentials = _auth.default_credentials(
134+
scopes=["1", "2"], quota_project_id="my-project"
135+
)
136+
98137
def test_with_scopes_non_scoped(self):
99138
credentials = mock.Mock(spec=oauth2client.client.Credentials)
100139

0 commit comments

Comments
 (0)