Skip to content

Commit 7ce7f3f

Browse files
authored
Trim pydata-google-auth package and add tests (#3)
Trim pydata-google-auth package and add tests This is the initial version of the proposed pydata-google-auth package (to be used by pandas-gbq and ibis). It includes two methods: * `pydata_google_auth.default()` * A function that does the same as pandas-gbq does auth currently. Tries `google.auth.default()` and then falls back to user credentials. * `pydata_google_auth.get_user_credentials()` * A public `get_user_credentials()` function, as proposed in googleapis/python-bigquery-pandas#161. Missing in this implementation is a more configurable way to adjust credentials caching. I currently use the `reauth` logic from pandas-gbq. I drop `try_credentials()`, as it makes less sense when this module might be used for other APIs besides BigQuery. Plus there were problems with `try_credentials()` even for pandas-gbq (googleapis/python-bigquery-pandas#202, googleapis/python-bigquery-pandas#198).
1 parent cfa1290 commit 7ce7f3f

File tree

6 files changed

+151
-266
lines changed

6 files changed

+151
-266
lines changed

pydata_google_auth/__init__.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
from .auth import default # noqa
21

2+
from .auth import default
3+
from .auth import get_user_credentials
34
from ._version import get_versions
5+
46
versions = get_versions()
57
__version__ = versions.get('closest-tag', versions['version'])
68
__git_revision__ = versions['full-revisionid']
7-
del get_versions, versions
9+
10+
"""pydata-google-auth
11+
12+
This package provides helpers for fetching Google API credentials.
13+
"""
14+
15+
__all__ = [
16+
'__version__',
17+
'__git_revision__',
18+
'default',
19+
'get_user_credentials',
20+
]

pydata_google_auth/auth.py

+86-106
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,62 @@
1-
"""Private module for fetching Google BigQuery credentials."""
1+
"""Private module for fetching Google API credentials."""
22

33
import json
44
import logging
55
import os
66
import os.path
77

8-
import pydata_google_auth.exceptions
8+
import google.auth
9+
import google.auth.exceptions
10+
import google.oauth2.credentials
11+
from google_auth_oauthlib import flow
12+
import oauthlib.oauth2.rfc6749.errors
13+
import google.auth.transport.requests
914

15+
from pydata_google_auth import exceptions
1016

11-
logger = logging.getLogger(__name__)
1217

18+
logger = logging.getLogger(__name__)
1319

14-
def default(
15-
scopes,
16-
client_id,
17-
client_secret,
18-
credentials_dirname,
19-
credentials_filename,
20-
project_id=None,
21-
auth_local_webserver=False,
22-
try_credentials=None):
23-
if try_credentials is None:
24-
def try_credentials(credentials, project_id):
25-
return credentials, project_id
26-
27-
return get_credentials(
28-
scopes,
29-
client_id,
30-
client_secret,
31-
credentials_dirname,
32-
credentials_filename,
33-
project_id=project_id,
34-
auth_local_webserver=auth_local_webserver,
35-
try_credentials=try_credentials)
20+
CLIENT_ID = (
21+
'262006177488-3425ks60hkk80fssi9vpohv88g6q1iqd'
22+
'.apps.googleusercontent.com'
23+
)
24+
CLIENT_SECRET = 'JSF-iczmzEgbTR-XK-2xaWAc'
25+
CREDENTIALS_DIRNAME = 'pydata'
26+
CREDENTIALS_FILENAME = 'pydata_google_credentials.json'
3627

3728

38-
def get_credentials(
29+
def default(
3930
scopes,
40-
client_id,
41-
client_secret,
42-
credentials_dirname,
43-
credentials_filename,
44-
try_credentials,
45-
project_id=None, reauth=False,
31+
client_id=CLIENT_ID,
32+
client_secret=CLIENT_SECRET,
33+
credentials_dirname=CREDENTIALS_DIRNAME,
34+
credentials_filename=CREDENTIALS_FILENAME,
35+
reauth=False,
4636
auth_local_webserver=False):
4737
# Try to retrieve Application Default Credentials
48-
credentials, default_project = get_application_default_credentials(
49-
scopes, project_id=project_id, try_credentials=try_credentials)
38+
credentials, default_project = get_application_default_credentials(scopes)
5039

51-
if credentials:
40+
if credentials and credentials.valid:
5241
return credentials, default_project
5342

54-
credentials = get_user_account_credentials(
43+
credentials = get_user_credentials(
5544
scopes,
56-
client_id,
57-
client_secret,
58-
credentials_dirname,
59-
credentials_filename,
60-
project_id=project_id,
45+
client_id=client_id,
46+
client_secret=client_secret,
47+
credentials_dirname=credentials_dirname,
48+
credentials_filename=credentials_filename,
6149
reauth=reauth,
62-
auth_local_webserver=auth_local_webserver,
63-
try_credentials=try_credentials)
64-
return credentials, project_id
50+
auth_local_webserver=auth_local_webserver)
6551

52+
if not credentials or not credentials.valid:
53+
raise exceptions.PyDataCredentialsError(
54+
'Could not get any valid credentials.')
6655

67-
def get_application_default_credentials(
68-
scopes, try_credentials, project_id=None):
56+
return credentials, None
57+
58+
59+
def get_application_default_credentials(scopes):
6960
"""
7061
This method tries to retrieve the "default application credentials".
7162
This could be useful for running code on Google Cloud Platform.
@@ -85,29 +76,28 @@ def get_application_default_credentials(
8576
from the environment. Or, the retrieved credentials do not
8677
have access to the project (project_id) on BigQuery.
8778
"""
88-
import google.auth
89-
from google.auth.exceptions import DefaultCredentialsError
9079

9180
try:
92-
credentials, default_project = google.auth.default(scopes=scopes)
93-
except (DefaultCredentialsError, IOError):
81+
credentials, project = google.auth.default(scopes=scopes)
82+
except (google.auth.exceptions.DefaultCredentialsError, IOError) as exc:
83+
logger.debug('Error getting default credentials: {}'.format(str(exc)))
9484
return None, None
9585

96-
# Even though we now have credentials, check that the credentials can be
97-
# used with BigQuery. For example, we could be running on a GCE instance
98-
# that does not allow the BigQuery scopes.
99-
billing_project = project_id or default_project
100-
return try_credentials(credentials, billing_project)
86+
if credentials and not credentials.valid:
87+
request = google.auth.transport.requests.Request()
88+
credentials.refresh(request)
89+
90+
return credentials, project
10191

10292

103-
def get_user_account_credentials(
93+
def get_user_credentials(
10494
scopes,
105-
client_id,
106-
client_secret,
107-
credentials_dirname,
108-
credentials_filename,
109-
project_id=None, reauth=False, auth_local_webserver=False,
110-
credentials_path=None):
95+
client_id=CLIENT_ID,
96+
client_secret=CLIENT_SECRET,
97+
credentials_dirname=CREDENTIALS_DIRNAME,
98+
credentials_filename=CREDENTIALS_FILENAME,
99+
reauth=False,
100+
auth_local_webserver=False):
111101
"""Gets user account credentials.
112102
113103
This method authenticates using user credentials, either loading saved
@@ -122,26 +112,16 @@ def get_user_account_credentials(
122112
GoogleCredentials : credentials
123113
Credentials for the user with BigQuery access.
124114
"""
125-
from google_auth_oauthlib.flow import InstalledAppFlow
126-
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
127-
128115
# Use the default credentials location under ~/.config and the
129116
# equivalent directory on windows if the user has not specified a
130117
# credentials path.
131-
if not credentials_path:
132-
credentials_path = get_default_credentials_path(
133-
credentials_dirname,
134-
credentials_filename)
135-
136-
# Previously, pandas-gbq saved user account credentials in the
137-
# current working directory. If the bigquery_credentials.dat file
138-
# exists in the current working directory, move the credentials to
139-
# the new default location.
140-
if os.path.isfile('bigquery_credentials.dat'):
141-
os.rename(credentials_filename, credentials_path)
118+
credentials_path = get_default_credentials_path(
119+
credentials_dirname,
120+
credentials_filename)
142121

143-
credentials = load_user_account_credentials(
144-
project_id=project_id, credentials_path=credentials_path)
122+
credentials = None
123+
if not reauth:
124+
credentials = load_user_credentials_from_file(credentials_path)
145125

146126
client_config = {
147127
'installed': {
@@ -153,26 +133,40 @@ def get_user_account_credentials(
153133
}
154134
}
155135

156-
if credentials is None or reauth:
157-
app_flow = InstalledAppFlow.from_client_config(
136+
if credentials is None:
137+
app_flow = flow.InstalledAppFlow.from_client_config(
158138
client_config, scopes=scopes)
159139

160140
try:
161141
if auth_local_webserver:
162142
credentials = app_flow.run_local_server()
163143
else:
164144
credentials = app_flow.run_console()
165-
except OAuth2Error as ex:
166-
raise pydata_google_auth.exceptions.AccessDenied(
167-
"Unable to get valid credentials: {0}".format(ex))
145+
except oauthlib.oauth2.rfc6749.errors.OAuth2Error as exc:
146+
raise exceptions.PyDataCredentialsError(
147+
"Unable to get valid credentials: {0}".format(exc))
168148

169149
save_user_account_credentials(credentials, credentials_path)
170150

151+
if credentials and not credentials.valid:
152+
request = google.auth.transport.requests.Request()
153+
credentials.refresh(request)
154+
171155
return credentials
172156

173157

174-
def load_user_account_credentials(
175-
try_credentials, project_id=None, credentials_path=None):
158+
def load_user_credentials_from_info(credentials_json):
159+
return google.oauth2.credentials.Credentials(
160+
token=credentials_json.get('access_token'),
161+
refresh_token=credentials_json.get('refresh_token'),
162+
id_token=credentials_json.get('id_token'),
163+
token_uri=credentials_json.get('token_uri'),
164+
client_id=credentials_json.get('client_id'),
165+
client_secret=credentials_json.get('client_secret'),
166+
scopes=credentials_json.get('scopes'))
167+
168+
169+
def load_user_credentials_from_file(credentials_path):
176170
"""
177171
Loads user account credentials from a local file.
178172
@@ -192,40 +186,26 @@ def load_user_account_credentials(
192186
credentials do not have access to the project (project_id)
193187
on BigQuery.
194188
"""
195-
import google.auth.transport.requests
196-
from google.oauth2.credentials import Credentials
197-
198189
try:
199190
with open(credentials_path) as credentials_file:
200191
credentials_json = json.load(credentials_file)
201-
except (IOError, ValueError):
192+
except (IOError, ValueError) as exc:
193+
logger.debug('Error loading credentials from {}: {}'.format(
194+
credentials_path, str(exc)))
202195
return None
203196

204-
credentials = Credentials(
205-
token=credentials_json.get('access_token'),
206-
refresh_token=credentials_json.get('refresh_token'),
207-
id_token=credentials_json.get('id_token'),
208-
token_uri=credentials_json.get('token_uri'),
209-
client_id=credentials_json.get('client_id'),
210-
client_secret=credentials_json.get('client_secret'),
211-
scopes=credentials_json.get('scopes'))
212-
213-
# Refresh the token before trying to use it.
214-
request = google.auth.transport.requests.Request()
215-
credentials.refresh(request)
216-
217-
return try_credentials(credentials, project_id)
197+
return load_user_credentials_from_info(credentials_json)
218198

219199

220200
def get_default_credentials_path(credentials_dirname, credentials_filename):
221201
"""
222-
Gets the default path to the BigQuery credentials
202+
Gets the default path to the Google user credentials
223203
224204
.. versionadded 0.3.0
225205
226206
Returns
227207
-------
228-
Path to the BigQuery credentials
208+
Path to the Google user credentials
229209
"""
230210
if os.name == 'nt':
231211
config_path = os.environ['APPDATA']
@@ -234,7 +214,7 @@ def get_default_credentials_path(credentials_dirname, credentials_filename):
234214

235215
config_path = os.path.join(config_path, credentials_dirname)
236216

237-
# Create a pandas_gbq directory in an application-specific hidden
217+
# Create a pydata directory in an application-specific hidden
238218
# user folder on the operating system.
239219
if not os.path.exists(config_path):
240220
os.makedirs(config_path)

pydata_google_auth/exceptions.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11

2-
3-
class AccessDenied(ValueError):
2+
class PyDataCredentialsError(ValueError):
43
"""
54
Raised when invalid credentials are provided, or tokens have expired.
65
"""
76
pass
8-
9-
10-
class InvalidPrivateKeyFormat(ValueError):
11-
"""
12-
Raised when provided private key has invalid format.
13-
"""
14-
pass

tests/system/conftest.py

-10
This file was deleted.

0 commit comments

Comments
 (0)