Skip to content

Commit 9ceb8f7

Browse files
committed
WIP: cache credentials.
1 parent 3f3192f commit 9ceb8f7

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

pandas_gbq/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .gbq import to_gbq, read_gbq # noqa
1+
from .gbq import to_gbq, read_gbq, Context, context # noqa
22

33
from ._version import get_versions
44

pandas_gbq/gbq.py

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,65 @@ class TableCreationError(ValueError):
162162
pass
163163

164164

165+
class Context(object):
166+
"""Storage for objects to be used throughout a session.
167+
168+
A Context object is initialized when the ``pandas_gbq`` module is
169+
imported, and can be found at :attr:`pandas_gbq.context`.
170+
"""
171+
172+
def __init__(self):
173+
self._credentials = None
174+
self._project = None
175+
176+
@property
177+
def credentials(self):
178+
"""google.auth.credentials.Credentials: Credentials to use for Google
179+
APIs.
180+
181+
Note:
182+
These credentials are automatically cached in memory by calls to
183+
:func:`pandas_gbq.read_gbq` and :func:`pandas_gbq.to_gbq`. To
184+
manually set the credentials, construct an
185+
:class:`google.auth.credentials.Credentials` object and set it as
186+
the context credentials as demonstrated in the example below. See
187+
`auth docs`_ for more information on obtaining credentials.
188+
Example:
189+
Manually setting the context credentials:
190+
>>> import pandas_gbq
191+
>>> from google.oauth2 import service_account
192+
>>> credentials = (service_account
193+
... .Credentials.from_service_account_file(
194+
... '/path/to/key.json'))
195+
>>> pandas_gbq.context.credentials = credentials
196+
.. _auth docs: http://google-auth.readthedocs.io
197+
/en/latest/user-guide.html#obtaining-credentials
198+
"""
199+
return self._credentials
200+
201+
@credentials.setter
202+
def credentials(self, value):
203+
self._credentials = value
204+
205+
@property
206+
def project(self):
207+
"""str: Default project to use for calls to Google APIs.
208+
Example:
209+
Manually setting the context project:
210+
>>> import pandas_gbq
211+
>>> pandas_gbq.context.project = 'my-project'
212+
"""
213+
return self._project
214+
215+
@project.setter
216+
def project(self, value):
217+
self._project = value
218+
219+
220+
# Create an empty context, used to cache credentials.
221+
context = Context()
222+
223+
165224
class GbqConnector(object):
166225
def __init__(
167226
self,
@@ -173,6 +232,7 @@ def __init__(
173232
location=None,
174233
try_credentials=None,
175234
):
235+
global context
176236
from google.api_core.exceptions import GoogleAPIError
177237
from google.api_core.exceptions import ClientError
178238
from pandas_gbq import auth
@@ -185,13 +245,20 @@ def __init__(
185245
self.auth_local_webserver = auth_local_webserver
186246
self.dialect = dialect
187247
self.credentials_path = _get_credentials_file()
188-
self.credentials, default_project = auth.get_credentials(
189-
private_key=private_key,
190-
project_id=project_id,
191-
reauth=reauth,
192-
auth_local_webserver=auth_local_webserver,
193-
try_credentials=try_credentials,
194-
)
248+
249+
# Load credentials from cache.
250+
self.credentials = context.credentials
251+
default_project = context.project
252+
253+
# Credentials were explicitly asked for, so don't use the cache.
254+
if private_key or reauth or not self.credentials:
255+
self.credentials, default_project = auth.get_credentials(
256+
private_key=private_key,
257+
project_id=project_id,
258+
reauth=reauth,
259+
auth_local_webserver=auth_local_webserver,
260+
try_credentials=try_credentials,
261+
)
195262

196263
if self.project_id is None:
197264
self.project_id = default_project
@@ -201,6 +268,12 @@ def __init__(
201268
"Could not determine project ID and one was not supplied."
202269
)
203270

271+
# Cache the credentials if they haven't been set yet.
272+
if context.credentials is None:
273+
context.credentials = self.credentials
274+
if context.project is None:
275+
context.project = self.project_id
276+
204277
self.client = self.get_client()
205278

206279
# BQ Queries costs $5 per TB. First 1 TB per month is free

tests/unit/test_context.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
3+
try:
4+
from unittest import mock
5+
except ImportError: # pragma: NO COVER
6+
import mock
7+
8+
import pytest
9+
10+
import pandas_gbq
11+
12+
13+
@pytest.fixture(autouse=True)
14+
def mock_get_credentials(monkeypatch):
15+
from pandas_gbq import auth
16+
17+
mock_get_credentials = mock.Mock()
18+
mock_get_credentials.return_value = (mock_credentials, 'my-project')
19+
20+
monkeypatch.setattr(
21+
auth, "get_credentials", mock_get_credentials
22+
)
23+
return mock_get_credentials
24+
25+
26+
def test_read_gbq_should_save_credentials(mock_get_credentials):
27+
assert pandas_gbq.context.credentials is not None
28+
assert pandas_gbq.context.project is not None
29+
30+
# run read_gbq
31+
32+
mock_get_credentials.assert_called_once()
33+
mock_get_credentials.reset_mock()
34+
assert pandas_gbq.context.credentials is not None
35+
assert pandas_gbq.context.project is not None
36+
37+
# run read_gbq
38+
mock_get_credentials.assert_never_called() # Not called again.

0 commit comments

Comments
 (0)