Skip to content

Commit 85c2c6d

Browse files
author
Jon Wayne Parrott
authored
Add support for google-auth and remove Python 2.6 support (googleapis#319)
* `discovery.build` and `discovery.build_from_document` now accept both oauth2client credentials and google-auth credentials. * `discovery.build` and `discovery.build_from_document` now unambiguously use the http argument for *all* requests, including the request to get the discovery document. * The `http` and `credentials` arguments to `discovery.build` and `discovery.build_from_document` are now mutally exclusive. * If neither `http` or `credentials` is specified to `discovery.build` and `discovery.build_from_document`, then Application Default Credentials will be used. * oauth2client is still the "default" authentication library.
1 parent 94a5394 commit 85c2c6d

File tree

7 files changed

+308
-56
lines changed

7 files changed

+308
-56
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ sudo: false
44
cache: pip
55
env:
66
matrix:
7-
- TOX_ENV=py26
87
- TOX_ENV=py27
98
- TOX_ENV=py33
109
- TOX_ENV=py34

googleapiclient/_auth.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helpers for authentication using oauth2client or google-auth."""
16+
17+
import httplib2
18+
19+
try:
20+
import google.auth
21+
import google_auth_httplib2
22+
HAS_GOOGLE_AUTH = True
23+
except ImportError: # pragma: NO COVER
24+
HAS_GOOGLE_AUTH = False
25+
26+
try:
27+
import oauth2client
28+
import oauth2client.client
29+
HAS_OAUTH2CLIENT = True
30+
except ImportError: # pragma: NO COVER
31+
HAS_OAUTH2CLIENT = False
32+
33+
34+
def default_credentials():
35+
"""Returns Application Default Credentials."""
36+
if HAS_GOOGLE_AUTH:
37+
credentials, _ = google.auth.default()
38+
return credentials
39+
elif HAS_OAUTH2CLIENT:
40+
return oauth2client.client.GoogleCredentials.get_application_default()
41+
else:
42+
raise EnvironmentError(
43+
'No authentication library is available. Please install either '
44+
'google-auth or oauth2client.')
45+
46+
47+
def with_scopes(credentials, scopes):
48+
"""Scopes the credentials if necessary.
49+
50+
Args:
51+
credentials (Union[
52+
google.auth.credentials.Credentials,
53+
oauth2client.client.Credentials]): The credentials to scope.
54+
scopes (Sequence[str]): The list of scopes.
55+
56+
Returns:
57+
Union[google.auth.credentials.Credentials,
58+
oauth2client.client.Credentials]: The scoped credentials.
59+
"""
60+
if HAS_GOOGLE_AUTH and isinstance(
61+
credentials, google.auth.credentials.Credentials):
62+
return google.auth.credentials.with_scopes_if_required(
63+
credentials, scopes)
64+
else:
65+
try:
66+
if credentials.create_scoped_required():
67+
return credentials.create_scoped(scopes)
68+
else:
69+
return credentials
70+
except AttributeError:
71+
return credentials
72+
73+
74+
def authorized_http(credentials):
75+
"""Returns an http client that is authorized with the given credentials.
76+
77+
Args:
78+
credentials (Union[
79+
google.auth.credentials.Credentials,
80+
oauth2client.client.Credentials]): The credentials to use.
81+
82+
Returns:
83+
Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
84+
authorized http client.
85+
"""
86+
if HAS_GOOGLE_AUTH and isinstance(
87+
credentials, google.auth.credentials.Credentials):
88+
return google_auth_httplib2.AuthorizedHttp(credentials)
89+
else:
90+
return credentials.authorize(httplib2.Http())

googleapiclient/discovery.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import uritemplate
5454

5555
# Local imports
56+
from googleapiclient import _auth
5657
from googleapiclient import mimeparse
5758
from googleapiclient.errors import HttpError
5859
from googleapiclient.errors import InvalidJsonError
@@ -197,7 +198,8 @@ def build(serviceName,
197198
model: googleapiclient.Model, converts to and from the wire format.
198199
requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
199200
request.
200-
credentials: oauth2client.Credentials, credentials to be used for
201+
credentials: oauth2client.Credentials or
202+
google.auth.credentials.Credentials, credentials to be used for
201203
authentication.
202204
cache_discovery: Boolean, whether or not to cache the discovery doc.
203205
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
@@ -211,15 +213,14 @@ def build(serviceName,
211213
'apiVersion': version
212214
}
213215

214-
if http is None:
215-
http = httplib2.Http()
216+
discovery_http = http if http is not None else httplib2.Http()
216217

217218
for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI,):
218219
requested_url = uritemplate.expand(discovery_url, params)
219220

220221
try:
221-
content = _retrieve_discovery_doc(requested_url, http, cache_discovery,
222-
cache)
222+
content = _retrieve_discovery_doc(
223+
requested_url, discovery_http, cache_discovery, cache)
223224
return build_from_document(content, base=discovery_url, http=http,
224225
developerKey=developerKey, model=model, requestBuilder=requestBuilder,
225226
credentials=credentials)
@@ -316,17 +317,16 @@ def build_from_document(
316317
model: Model class instance that serializes and de-serializes requests and
317318
responses.
318319
requestBuilder: Takes an http request and packages it up to be executed.
319-
credentials: object, credentials to be used for authentication.
320+
credentials: oauth2client.Credentials or
321+
google.auth.credentials.Credentials, credentials to be used for
322+
authentication.
320323
321324
Returns:
322325
A Resource object with methods for interacting with the service.
323326
"""
324327

325-
if http is None:
326-
http = httplib2.Http()
327-
328-
# future is no longer used.
329-
future = {}
328+
if http is not None and credentials is not None:
329+
raise ValueError('Arguments http and credentials are mutually exclusive.')
330330

331331
if isinstance(service, six.string_types):
332332
service = json.loads(service)
@@ -342,31 +342,36 @@ def build_from_document(
342342
base = urljoin(service['rootUrl'], service['servicePath'])
343343
schema = Schemas(service)
344344

345-
if credentials:
346-
# If credentials were passed in, we could have two cases:
347-
# 1. the scopes were specified, in which case the given credentials
348-
# are used for authorizing the http;
349-
# 2. the scopes were not provided (meaning the Application Default
350-
# Credentials are to be used). In this case, the Application Default
351-
# Credentials are built and used instead of the original credentials.
352-
# If there are no scopes found (meaning the given service requires no
353-
# authentication), there is no authorization of the http.
354-
if (isinstance(credentials, GoogleCredentials) and
355-
credentials.create_scoped_required()):
356-
scopes = service.get('auth', {}).get('oauth2', {}).get('scopes', {})
357-
if scopes:
358-
credentials = credentials.create_scoped(list(scopes.keys()))
359-
else:
360-
# No need to authorize the http object
361-
# if the service does not require authentication.
362-
credentials = None
345+
# If the http client is not specified, then we must construct an http client
346+
# to make requests. If the service has scopes, then we also need to setup
347+
# authentication.
348+
if http is None:
349+
# Does the service require scopes?
350+
scopes = list(
351+
service.get('auth', {}).get('oauth2', {}).get('scopes', {}).keys())
363352

364-
if credentials:
365-
http = credentials.authorize(http)
353+
# If so, then the we need to setup authentication.
354+
if scopes:
355+
# If the user didn't pass in credentials, attempt to acquire application
356+
# default credentials.
357+
if credentials is None:
358+
credentials = _auth.default_credentials()
359+
360+
# The credentials need to be scoped.
361+
credentials = _auth.with_scopes(credentials, scopes)
362+
363+
# Create an authorized http instance
364+
http = _auth.authorized_http(credentials)
365+
366+
# If the service doesn't require scopes then there is no need for
367+
# authentication.
368+
else:
369+
http = httplib2.Http()
366370

367371
if model is None:
368372
features = service.get('features', [])
369373
model = JsonModel('dataWrapper' in features)
374+
370375
return Resource(http=http, baseUrl=base, model=model,
371376
developerKey=developerKey, requestBuilder=requestBuilder,
372377
resourceDesc=service, rootDesc=service, schema=schema)

setup.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121

2222
import sys
2323

24-
if sys.version_info < (2, 6):
25-
print('google-api-python-client requires python version >= 2.6.',
24+
if sys.version_info < (2, 7):
25+
print('google-api-python-client requires python version >= 2.7.',
2626
file=sys.stderr)
2727
sys.exit(1)
2828
if (3, 1) <= sys.version_info < (3, 3):
@@ -69,9 +69,6 @@ def _DetectBadness():
6969
'uritemplate>=3.0.0,<4dev',
7070
]
7171

72-
if sys.version_info < (2, 7):
73-
install_requires.append('argparse')
74-
7572
long_desc = """The Google API Client for Python is a client library for
7673
accessing the Plus, Moderator, and many other Google APIs."""
7774

@@ -92,7 +89,6 @@ def _DetectBadness():
9289
keywords="google api client",
9390
classifiers=[
9491
'Programming Language :: Python :: 2',
95-
'Programming Language :: Python :: 2.6',
9692
'Programming Language :: Python :: 2.7',
9793
'Programming Language :: Python :: 3',
9894
'Programming Language :: Python :: 3.3',

tests/test__auth.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import mock
16+
17+
import google.auth.credentials
18+
import google_auth_httplib2
19+
import httplib2
20+
import oauth2client.client
21+
import unittest2
22+
23+
from googleapiclient import _auth
24+
25+
26+
class TestAuthWithGoogleAuth(unittest2.TestCase):
27+
def setUp(self):
28+
_auth.HAS_GOOGLE_AUTH = True
29+
_auth.HAS_OAUTH2CLIENT = False
30+
31+
def tearDown(self):
32+
_auth.HAS_GOOGLE_AUTH = True
33+
_auth.HAS_OAUTH2CLIENT = True
34+
35+
def test_default_credentials(self):
36+
with mock.patch('google.auth.default', autospec=True) as default:
37+
default.return_value = (
38+
mock.sentinel.credentials, mock.sentinel.project)
39+
40+
credentials = _auth.default_credentials()
41+
42+
self.assertEqual(credentials, mock.sentinel.credentials)
43+
44+
def test_with_scopes_non_scoped(self):
45+
credentials = mock.Mock(spec=google.auth.credentials.Credentials)
46+
47+
returned = _auth.with_scopes(credentials, mock.sentinel.scopes)
48+
49+
self.assertEqual(credentials, returned)
50+
51+
def test_with_scopes_scoped(self):
52+
class CredentialsWithScopes(
53+
google.auth.credentials.Credentials,
54+
google.auth.credentials.Scoped):
55+
pass
56+
57+
credentials = mock.Mock(spec=CredentialsWithScopes)
58+
credentials.requires_scopes = True
59+
60+
returned = _auth.with_scopes(credentials, mock.sentinel.scopes)
61+
62+
self.assertNotEqual(credentials, returned)
63+
self.assertEqual(returned, credentials.with_scopes.return_value)
64+
credentials.with_scopes.assert_called_once_with(mock.sentinel.scopes)
65+
66+
def test_authorized_http(self):
67+
credentials = mock.Mock(spec=google.auth.credentials.Credentials)
68+
69+
http = _auth.authorized_http(credentials)
70+
71+
self.assertIsInstance(http, google_auth_httplib2.AuthorizedHttp)
72+
self.assertEqual(http.credentials, credentials)
73+
74+
75+
class TestAuthWithOAuth2Client(unittest2.TestCase):
76+
def setUp(self):
77+
_auth.HAS_GOOGLE_AUTH = False
78+
_auth.HAS_OAUTH2CLIENT = True
79+
80+
def tearDown(self):
81+
_auth.HAS_GOOGLE_AUTH = True
82+
_auth.HAS_OAUTH2CLIENT = True
83+
84+
def test_default_credentials(self):
85+
default_patch = mock.patch(
86+
'oauth2client.client.GoogleCredentials.get_application_default')
87+
88+
with default_patch as default:
89+
default.return_value = mock.sentinel.credentials
90+
91+
credentials = _auth.default_credentials()
92+
93+
self.assertEqual(credentials, mock.sentinel.credentials)
94+
95+
def test_with_scopes_non_scoped(self):
96+
credentials = mock.Mock(spec=oauth2client.client.Credentials)
97+
98+
returned = _auth.with_scopes(credentials, mock.sentinel.scopes)
99+
100+
self.assertEqual(credentials, returned)
101+
102+
def test_with_scopes_scoped(self):
103+
credentials = mock.Mock(spec=oauth2client.client.GoogleCredentials)
104+
credentials.create_scoped_required.return_value = True
105+
106+
returned = _auth.with_scopes(credentials, mock.sentinel.scopes)
107+
108+
self.assertNotEqual(credentials, returned)
109+
self.assertEqual(returned, credentials.create_scoped.return_value)
110+
credentials.create_scoped.assert_called_once_with(mock.sentinel.scopes)
111+
112+
def test_authorized_http(self):
113+
credentials = mock.Mock(spec=oauth2client.client.Credentials)
114+
115+
http = _auth.authorized_http(credentials)
116+
117+
self.assertEqual(http, credentials.authorize.return_value)
118+
self.assertIsInstance(
119+
credentials.authorize.call_args[0][0], httplib2.Http)
120+
121+
122+
class TestAuthWithoutAuth(unittest2.TestCase):
123+
124+
def setUp(self):
125+
_auth.HAS_GOOGLE_AUTH = False
126+
_auth.HAS_OAUTH2CLIENT = False
127+
128+
def tearDown(self):
129+
_auth.HAS_GOOGLE_AUTH = True
130+
_auth.HAS_OAUTH2CLIENT = True
131+
132+
def test_default_credentials(self):
133+
with self.assertRaises(EnvironmentError):
134+
print(_auth.default_credentials())

0 commit comments

Comments
 (0)