Skip to content

Commit 3de277b

Browse files
committed
Use tuple for credentials & project for default project detection.
1 parent 2d50519 commit 3de277b

File tree

3 files changed

+69
-66
lines changed

3 files changed

+69
-66
lines changed

pandas_gbq/gbq.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,15 @@ def __init__(self, project_id, reauth=False,
189189
self.auth_local_webserver = auth_local_webserver
190190
self.dialect = dialect
191191
self.credentials_path = _get_credentials_file()
192-
self.credentials = self.get_credentials()
192+
self.credentials, default_project = self.get_credentials()
193+
194+
if self.project_id is None:
195+
self.project_id = default_project
196+
197+
if self.project_id is None:
198+
raise ValueError(
199+
'Could not determine project ID and one was not supplied.')
200+
193201
self.client = self.get_client()
194202

195203
# BQ Queries costs $5 per TB. First 1 TB per month is free
@@ -199,12 +207,13 @@ def __init__(self, project_id, reauth=False,
199207
def get_credentials(self):
200208
if self.private_key:
201209
return self.get_service_account_credentials()
202-
else:
203-
# Try to retrieve Application Default Credentials
204-
credentials = self.get_application_default_credentials()
205-
if not credentials:
206-
credentials = self.get_user_account_credentials()
207-
return credentials
210+
211+
# Try to retrieve Application Default Credentials
212+
credentials, default_project = self.get_application_default_credentials()
213+
if credentials:
214+
return credentials, default_project
215+
216+
return self.get_user_account_credentials(), None
208217

209218
def get_application_default_credentials(self):
210219
"""
@@ -230,11 +239,13 @@ def get_application_default_credentials(self):
230239
from google.auth.exceptions import DefaultCredentialsError
231240

232241
try:
233-
credentials, _ = google.auth.default(scopes=[self.scope])
242+
credentials, default_project = google.auth.default(
243+
scopes=[self.scope])
234244
except (DefaultCredentialsError, IOError):
235-
return None
245+
return None, None
236246

237-
return _try_credentials(self.project_id, credentials)
247+
billing_project = self.project_id or default_project
248+
return _try_credentials(billing_project, credentials), default_project
238249

239250
def load_user_account_credentials(self):
240251
"""
@@ -415,7 +426,7 @@ def get_service_account_credentials(self):
415426
request = google.auth.transport.requests.Request()
416427
credentials.refresh(request)
417428

418-
return credentials
429+
return credentials, json_key.get('project_id')
419430
except (KeyError, ValueError, TypeError, AttributeError):
420431
raise InvalidPrivateKeyFormat(
421432
"Private key is missing or invalid. It should be service "

tests/system.py

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222

2323
TABLE_ID = 'new_test'
2424

25+
def _skip_local_auth_if_in_travis_env():
26+
if _in_travis_environment():
27+
pytest.skip("Cannot run local auth in travis environment")
2528

2629
def _skip_if_no_private_key_path():
2730
if not _get_private_key_path():
@@ -167,43 +170,13 @@ def test_generate_bq_schema_deprecated():
167170
gbq.generate_bq_schema(df)
168171

169172

170-
@pytest.fixture(params=[
171-
pytest.param('local', marks=pytest.mark.local_auth),
172-
pytest.param('service_path', marks=pytest.mark.s_path_auth),
173-
pytest.param('service_creds', marks=pytest.mark.s_cred_auth),
174-
])
175-
def auth_type(request):
176-
177-
auth = request.param
178-
179-
if auth == 'local':
180-
pass
181-
elif auth == 'service_path':
182-
_skip_if_no_private_key_path()
183-
elif auth == 'service_creds':
184-
_skip_if_no_private_key_contents()
185-
else:
186-
raise ValueError
187-
return auth
188-
189-
190173
@pytest.fixture()
191-
def credentials(auth_type):
192-
193-
if auth_type == 'local':
194-
return None
195-
196-
elif auth_type == 'service_path':
197-
return _get_private_key_path()
198-
elif auth_type == 'service_creds':
199-
return _get_private_key_contents()
200-
else:
201-
raise ValueError
174+
def credentials():
175+
return _get_private_key_contents()
202176

203177

204178
@pytest.fixture()
205179
def gbq_connector(project, credentials):
206-
207180
return gbq.GbqConnector(project, private_key=credentials)
208181

209182

@@ -229,13 +202,12 @@ def test_should_be_able_to_get_results_from_query(self, gbq_connector):
229202
assert pages is not None
230203

231204

232-
@pytest.mark.local_auth
233-
class TestGBQConnectorIntegrationWithLocalUserAccountAuth(object):
205+
class TestAuth(object):
234206

235207
@pytest.fixture(autouse=True)
236-
def setup(self, project):
237-
238-
self.sut = gbq.GbqConnector(project, auth_local_webserver=True)
208+
def setup(self, gbq_connector):
209+
self.sut = gbq_connector
210+
self.sut.auth_local_webserver = True
239211

240212
def test_get_application_default_credentials_does_not_throw_error(self):
241213
if _check_if_can_get_correct_default_credentials():
@@ -244,27 +216,33 @@ def test_get_application_default_credentials_does_not_throw_error(self):
244216
from google.auth.exceptions import DefaultCredentialsError
245217
with mock.patch('google.auth.default',
246218
side_effect=DefaultCredentialsError()):
247-
credentials = self.sut.get_application_default_credentials()
219+
credentials, _ = self.sut.get_application_default_credentials()
248220
else:
249-
credentials = self.sut.get_application_default_credentials()
221+
credentials, _ = self.sut.get_application_default_credentials()
250222
assert credentials is None
251223

252224
def test_get_application_default_credentials_returns_credentials(self):
253225
if not _check_if_can_get_correct_default_credentials():
254226
pytest.skip("Cannot get default_credentials "
255227
"from the environment!")
256228
from google.auth.credentials import Credentials
257-
credentials = self.sut.get_application_default_credentials()
229+
credentials, default_project = (
230+
self.sut.get_application_default_credentials())
231+
258232
assert isinstance(credentials, Credentials)
233+
assert default_project is not None
259234

260235
def test_get_user_account_credentials_bad_file_returns_credentials(self):
236+
_skip_local_auth_if_in_travis_env()
261237

262238
from google.auth.credentials import Credentials
263239
with mock.patch('__main__.open', side_effect=IOError()):
264240
credentials = self.sut.get_user_account_credentials()
265241
assert isinstance(credentials, Credentials)
266242

267243
def test_get_user_account_credentials_returns_credentials(self):
244+
_skip_local_auth_if_in_travis_env()
245+
268246
from google.auth.credentials import Credentials
269247
credentials = self.sut.get_user_account_credentials()
270248
assert isinstance(credentials, Credentials)

tests/unit/test_gbq.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,26 @@
1515

1616
@pytest.fixture(autouse=True)
1717
def mock_bigquery_client(monkeypatch):
18+
from google.api_core.exceptions import NotFound
1819
import google.cloud.bigquery
1920
import google.cloud.bigquery.table
2021
mock_client = mock.create_autospec(google.cloud.bigquery.Client)
22+
mock_schema = [
23+
google.cloud.bigquery.SchemaField('_f0', 'INTEGER')
24+
]
2125
# Mock out SELECT 1 query results.
2226
mock_query = mock.create_autospec(google.cloud.bigquery.QueryJob)
2327
mock_query.job_id = 'some-random-id'
2428
mock_query.state = 'DONE'
2529
mock_rows = mock.create_autospec(
2630
google.cloud.bigquery.table.RowIterator)
2731
mock_rows.total_rows = 1
28-
mock_rows.schema = [
29-
google.cloud.bigquery.SchemaField('_f0', 'INTEGER')]
32+
mock_rows.schema = mock_schema
3033
mock_rows.__iter__.return_value = [(1,)]
3134
mock_query.result.return_value = mock_rows
3235
mock_client.query.return_value = mock_query
36+
# Mock table creation.
37+
mock_client.get_table.side_effect = NotFound('nope')
3338
monkeypatch.setattr(
3439
gbq.GbqConnector, 'get_client', lambda _: mock_client)
3540

@@ -42,11 +47,7 @@ def no_auth(monkeypatch):
4247
monkeypatch.setattr(
4348
gbq.GbqConnector,
4449
'get_application_default_credentials',
45-
lambda _: mock_credentials)
46-
monkeypatch.setattr(
47-
gbq.GbqConnector,
48-
'get_user_account_credentials',
49-
lambda _: mock_credentials)
50+
lambda _: (mock_credentials, 'default-project'))
5051

5152

5253
def test_should_return_credentials_path_set_by_env_var():
@@ -76,12 +77,16 @@ def test_should_return_bigquery_correctly_typed(
7677

7778
def test_to_gbq_should_fail_if_invalid_table_name_passed():
7879
with pytest.raises(gbq.NotFoundException):
79-
gbq.to_gbq(DataFrame(), 'invalid_table_name', project_id="1234")
80+
gbq.to_gbq(DataFrame([[1]]), 'invalid_table_name', project_id="1234")
8081

8182

82-
def test_to_gbq_with_no_project_id_given_should_fail():
83+
def test_to_gbq_with_no_project_id_given_should_fail(monkeypatch):
84+
monkeypatch.setattr(
85+
gbq.GbqConnector,
86+
'get_application_default_credentials',
87+
lambda _: None)
8388
with pytest.raises(TypeError):
84-
gbq.to_gbq(DataFrame(), 'dataset.tablename')
89+
gbq.to_gbq(DataFrame([[1]]), 'dataset.tablename')
8590

8691

8792
def test_to_gbq_with_verbose_new_pandas_warns_deprecation():
@@ -95,7 +100,7 @@ def test_to_gbq_with_verbose_new_pandas_warns_deprecation():
95100
mock_version.side_effect = [min_bq_version, pandas_version]
96101
try:
97102
gbq.to_gbq(
98-
DataFrame(),
103+
DataFrame([[1]]),
99104
'dataset.tablename',
100105
project_id='my-project',
101106
verbose=True)
@@ -114,7 +119,7 @@ def test_to_gbq_with_not_verbose_new_pandas_warns_deprecation():
114119
mock_version.side_effect = [min_bq_version, pandas_version]
115120
try:
116121
gbq.to_gbq(
117-
DataFrame(),
122+
DataFrame([[1]]),
118123
'dataset.tablename',
119124
project_id='my-project',
120125
verbose=False)
@@ -132,7 +137,7 @@ def test_to_gbq_wo_verbose_w_new_pandas_no_warnings(recwarn):
132137
mock_version.side_effect = [min_bq_version, pandas_version]
133138
try:
134139
gbq.to_gbq(
135-
DataFrame(), 'dataset.tablename', project_id='my-project')
140+
DataFrame([[1]]), 'dataset.tablename', project_id='my-project')
136141
except gbq.TableCreationError:
137142
pass
138143
assert len(recwarn) == 0
@@ -148,7 +153,7 @@ def test_to_gbq_with_verbose_old_pandas_no_warnings(recwarn):
148153
mock_version.side_effect = [min_bq_version, pandas_version]
149154
try:
150155
gbq.to_gbq(
151-
DataFrame(),
156+
DataFrame([[1]]),
152157
'dataset.tablename',
153158
project_id='my-project',
154159
verbose=True)
@@ -157,11 +162,20 @@ def test_to_gbq_with_verbose_old_pandas_no_warnings(recwarn):
157162
assert len(recwarn) == 0
158163

159164

160-
def test_read_gbq_with_no_project_id_given_should_fail():
165+
def test_read_gbq_with_no_project_id_given_should_fail(monkeypatch):
166+
monkeypatch.setattr(
167+
gbq.GbqConnector,
168+
'get_application_default_credentials',
169+
lambda _: None)
161170
with pytest.raises(TypeError):
162171
gbq.read_gbq('SELECT 1')
163172

164173

174+
def test_read_gbq_with_inferred_project_id(monkeypatch):
175+
df = gbq.read_gbq('SELECT 1')
176+
assert df is not None
177+
178+
165179
def test_that_parse_data_works_properly():
166180
from google.cloud.bigquery.table import Row
167181
test_schema = {'fields': [

0 commit comments

Comments
 (0)