Skip to content

Commit 1919e26

Browse files
mhaseebtariqjreback
authored andcommitted
ENH: GbqConnector should be able to fetch default credentials on Google Compute Engine
closes #13577 Author: Muhammad Haseeb Tariq <[email protected]> Closes #13608 from mhaseebtariq/master and squashes the following commits: a65b3f1 [Muhammad Haseeb Tariq] newline in documentation 6bbdcdc [Muhammad Haseeb Tariq] fixed documentation 6fe392a [Muhammad Haseeb Tariq] API error on google-api-python-client==1.2 4cf26aa [Muhammad Haseeb Tariq] handling GoogleCredentials import error ac48104 [Muhammad Haseeb Tariq] Updated whatsnew entry cf41e76 [Muhammad Haseeb Tariq] modified documentation + restructured tests 62815e1 [Muhammad Haseeb Tariq] Import errors 5fd1775 [Muhammad Haseeb Tariq] added more documentation 64134ac [Muhammad Haseeb Tariq] apiclient import fix 5a2dd15 [Muhammad Haseeb Tariq] feedback changes 17dd814 [Muhammad Haseeb Tariq] splitting integration tests for 3479ed8 [Muhammad Haseeb Tariq] Merge branch 'master' into master d4c7e3d [Muhammad Haseeb Tariq] Issue #13577 - Feedback incorporation 7f1fd26 [Muhammad Haseeb Tariq] Coverage was giving wrong percentage fca8003 [Muhammad Haseeb Tariq] lint errors a02b620 [Muhammad Haseeb Tariq] unit test fix - if libraries can not be imported 9740938 [Muhammad Haseeb Tariq] concav unable to capture coverage 17f4740 [Muhammad Haseeb Tariq] Issue #13577 - flake8 fixes 9ec2a68 [Muhammad Haseeb Tariq] Issue #13577
1 parent c3e24a1 commit 1919e26

File tree

4 files changed

+124
-9
lines changed

4 files changed

+124
-9
lines changed

doc/source/io.rst

+9
Original file line numberDiff line numberDiff line change
@@ -4478,6 +4478,15 @@ Additional information on service accounts can be found
44784478

44794479
You will need to install an additional dependency: `oauth2client <https://github.com/google/oauth2client>`__.
44804480

4481+
Authentication via ``application default credentials`` is also possible. This is only valid
4482+
if the parameter ``private_key`` is not provided. This method also requires that
4483+
the credentials can be fetched from the environment the code is running in.
4484+
Otherwise, the OAuth2 client-side authentication is used.
4485+
Additional information on
4486+
`application default credentials <https://developers.google.com/identity/protocols/application-default-credentials>`__.
4487+
4488+
.. versionadded:: 0.19.0
4489+
44814490
.. note::
44824491

44834492
The `'private_key'` parameter can be set to either the file path of the service account key

doc/source/whatsnew/v0.19.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ Google BigQuery Enhancements
362362
Other enhancements
363363
^^^^^^^^^^^^^^^^^^
364364

365+
- The ``.get_credentials()`` method of ``GbqConnector`` can now first try to fetch [the application default credentials](https://developers.google.com/identity/protocols/application-default-credentials). See the :ref:`docs <io.bigquery_authentication>` for more details (:issue:`13577`).
366+
365367
- The ``.tz_localize()`` method of ``DatetimeIndex`` and ``Timestamp`` has gained the ``errors`` keyword, so you can potentially coerce nonexistent timestamps to ``NaT``. The default behaviour remains to raising a ``NonExistentTimeError`` (:issue:`13057`)
366368
- ``pd.to_numeric()`` now accepts a ``downcast`` parameter, which will downcast the data if possible to smallest specified numerical dtype (:issue:`13352`)
367369

pandas/io/gbq.py

+74-9
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,60 @@ def get_credentials(self):
160160
if self.private_key:
161161
return self.get_service_account_credentials()
162162
else:
163-
return self.get_user_account_credentials()
163+
# Try to retrieve Application Default Credentials
164+
credentials = self.get_application_default_credentials()
165+
if not credentials:
166+
credentials = self.get_user_account_credentials()
167+
return credentials
168+
169+
def get_application_default_credentials(self):
170+
"""
171+
This method tries to retrieve the "default application credentials".
172+
This could be useful for running code on Google Cloud Platform.
173+
174+
.. versionadded:: 0.19.0
175+
176+
Parameters
177+
----------
178+
None
179+
180+
Returns
181+
-------
182+
- GoogleCredentials,
183+
If the default application credentials can be retrieved
184+
from the environment. The retrieved credentials should also
185+
have access to the project (self.project_id) on BigQuery.
186+
- OR None,
187+
If default application credentials can not be retrieved
188+
from the environment. Or, the retrieved credentials do not
189+
have access to the project (self.project_id) on BigQuery.
190+
"""
191+
import httplib2
192+
try:
193+
from googleapiclient.discovery import build
194+
except ImportError:
195+
from apiclient.discovery import build
196+
try:
197+
from oauth2client.client import GoogleCredentials
198+
except ImportError:
199+
return None
200+
201+
try:
202+
credentials = GoogleCredentials.get_application_default()
203+
except:
204+
return None
205+
206+
http = httplib2.Http()
207+
try:
208+
http = credentials.authorize(http)
209+
bigquery_service = build('bigquery', 'v2', http=http)
210+
# Check if the application has rights to the BigQuery project
211+
jobs = bigquery_service.jobs()
212+
job_data = {'configuration': {'query': {'query': 'SELECT 1'}}}
213+
jobs.insert(projectId=self.project_id, body=job_data).execute()
214+
return credentials
215+
except:
216+
return None
164217

165218
def get_user_account_credentials(self):
166219
from oauth2client.client import OAuth2WebServerFlow
@@ -577,10 +630,16 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None,
577630
https://developers.google.com/api-client-library/python/apis/bigquery/v2
578631
579632
Authentication to the Google BigQuery service is via OAuth 2.0.
580-
By default user account credentials are used. You will be asked to
581-
grant permissions for product name 'pandas GBQ'. It is also posible
582-
to authenticate via service account credentials by using
583-
private_key parameter.
633+
- If "private_key" is not provided:
634+
By default "application default credentials" are used.
635+
636+
.. versionadded:: 0.19.0
637+
638+
If default application credentials are not found or are restrictive,
639+
user account credentials are used. In this case, you will be asked to
640+
grant permissions for product name 'pandas GBQ'.
641+
- If "private_key" is provided:
642+
Service account credentials will be used to authenticate.
584643
585644
Parameters
586645
----------
@@ -688,10 +747,16 @@ def to_gbq(dataframe, destination_table, project_id, chunksize=10000,
688747
https://developers.google.com/api-client-library/python/apis/bigquery/v2
689748
690749
Authentication to the Google BigQuery service is via OAuth 2.0.
691-
By default user account credentials are used. You will be asked to
692-
grant permissions for product name 'pandas GBQ'. It is also posible
693-
to authenticate via service account credentials by using
694-
private_key parameter.
750+
- If "private_key" is not provided:
751+
By default "application default credentials" are used.
752+
753+
.. versionadded:: 0.19.0
754+
755+
If default application credentials are not found or are restrictive,
756+
user account credentials are used. In this case, you will be asked to
757+
grant permissions for product name 'pandas GBQ'.
758+
- If "private_key" is provided:
759+
Service account credentials will be used to authenticate.
695760
696761
Parameters
697762
----------

pandas/io/tests/test_gbq.py

+39
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,30 @@ def test_requirements():
151151
raise nose.SkipTest(import_exception)
152152

153153

154+
def _check_if_can_get_correct_default_credentials():
155+
# Checks if "Application Default Credentials" can be fetched
156+
# from the environment the tests are running in.
157+
# See Issue #13577
158+
test_requirements()
159+
import httplib2
160+
try:
161+
from googleapiclient.discovery import build
162+
except ImportError:
163+
from apiclient.discovery import build
164+
try:
165+
from oauth2client.client import GoogleCredentials
166+
credentials = GoogleCredentials.get_application_default()
167+
http = httplib2.Http()
168+
http = credentials.authorize(http)
169+
bigquery_service = build('bigquery', 'v2', http=http)
170+
jobs = bigquery_service.jobs()
171+
job_data = {'configuration': {'query': {'query': 'SELECT 1'}}}
172+
jobs.insert(projectId=PROJECT_ID, body=job_data).execute()
173+
return True
174+
except:
175+
return False
176+
177+
154178
def clean_gbq_environment(private_key=None):
155179
dataset = gbq._Dataset(PROJECT_ID, private_key=private_key)
156180

@@ -217,6 +241,21 @@ def test_should_be_able_to_get_results_from_query(self):
217241
schema, pages = self.sut.run_query('SELECT 1')
218242
self.assertTrue(pages is not None)
219243

244+
def test_get_application_default_credentials_does_not_throw_error(self):
245+
if _check_if_can_get_correct_default_credentials():
246+
raise nose.SkipTest("Can get default_credentials "
247+
"from the environment!")
248+
credentials = self.sut.get_application_default_credentials()
249+
self.assertIsNone(credentials)
250+
251+
def test_get_application_default_credentials_returns_credentials(self):
252+
if not _check_if_can_get_correct_default_credentials():
253+
raise nose.SkipTest("Cannot get default_credentials "
254+
"from the environment!")
255+
from oauth2client.client import GoogleCredentials
256+
credentials = self.sut.get_application_default_credentials()
257+
self.assertTrue(isinstance(credentials, GoogleCredentials))
258+
220259

221260
class TestGBQConnectorServiceAccountKeyPathIntegration(tm.TestCase):
222261
def setUp(self):

0 commit comments

Comments
 (0)