Skip to content

Commit 7711bb0

Browse files
max-sixtytswast
authored andcommitted
ENH: project_id optional for to_gbq and read_gbq (#127)
* project_id is optional * don't skip if no project * docstring & import order * project not required only if default creds available * Use tuple for credentials & project for default project detection. * Update bad_project_id test to query actual data. I think BigQuery stopped checking for valid project on queries with no data access. * Skip credentials tests if key not present.
1 parent 187a57f commit 7711bb0

File tree

5 files changed

+95
-90
lines changed

5 files changed

+95
-90
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
.ipynb_checkpoints
2121
.tags
2222
.pytest_cache
23-
.testmondata
23+
.testmon*
24+
.vscode/
2425

2526
# Docs #
2627
########

docs/source/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Changelog
44
0.5.0 / TBD
55
-----------
66

7+
- Project ID parameter is optional in ``read_gbq`` and ``to_gbq`` when it can
8+
inferred from the environment. Note: you must still pass in a project ID when
9+
using user-based authentication. (:issue:`103`)
10+
11+
Internal changes
12+
~~~~~~~~~~~~~~~~
13+
714
- Tests now use `nox` to run in multiple Python environments. (:issue:`52`)
815
- Renamed internal modules. (:issue:`154`)
916

pandas_gbq/gbq.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,15 @@ def __init__(self, project_id, reauth=False,
186186
self.auth_local_webserver = auth_local_webserver
187187
self.dialect = dialect
188188
self.credentials_path = _get_credentials_file()
189-
self.credentials = self.get_credentials()
189+
self.credentials, default_project = self.get_credentials()
190+
191+
if self.project_id is None:
192+
self.project_id = default_project
193+
194+
if self.project_id is None:
195+
raise ValueError(
196+
'Could not determine project ID and one was not supplied.')
197+
190198
self.client = self.get_client()
191199

192200
# BQ Queries costs $5 per TB. First 1 TB per month is free
@@ -196,12 +204,14 @@ def __init__(self, project_id, reauth=False,
196204
def get_credentials(self):
197205
if self.private_key:
198206
return self.get_service_account_credentials()
199-
else:
200-
# Try to retrieve Application Default Credentials
201-
credentials = self.get_application_default_credentials()
202-
if not credentials:
203-
credentials = self.get_user_account_credentials()
204-
return credentials
207+
208+
# Try to retrieve Application Default Credentials
209+
credentials, default_project = (
210+
self.get_application_default_credentials())
211+
if credentials:
212+
return credentials, default_project
213+
214+
return self.get_user_account_credentials(), None
205215

206216
def get_application_default_credentials(self):
207217
"""
@@ -227,11 +237,13 @@ def get_application_default_credentials(self):
227237
from google.auth.exceptions import DefaultCredentialsError
228238

229239
try:
230-
credentials, _ = google.auth.default(scopes=[self.scope])
240+
credentials, default_project = google.auth.default(
241+
scopes=[self.scope])
231242
except (DefaultCredentialsError, IOError):
232-
return None
243+
return None, None
233244

234-
return _try_credentials(self.project_id, credentials)
245+
billing_project = self.project_id or default_project
246+
return _try_credentials(billing_project, credentials), default_project
235247

236248
def load_user_account_credentials(self):
237249
"""
@@ -412,7 +424,7 @@ def get_service_account_credentials(self):
412424
request = google.auth.transport.requests.Request()
413425
credentials.refresh(request)
414426

415-
return credentials
427+
return credentials, json_key.get('project_id')
416428
except (KeyError, ValueError, TypeError, AttributeError):
417429
raise InvalidPrivateKeyFormat(
418430
"Private key is missing or invalid. It should be service "
@@ -750,7 +762,7 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None,
750762
----------
751763
query : str
752764
SQL-Like Query to return data values
753-
project_id : str
765+
project_id : str (optional when available in environment)
754766
Google BigQuery Account project ID.
755767
index_col : str (optional)
756768
Name of result column to use for index in results DataFrame
@@ -809,9 +821,6 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None,
809821
"a future version. Set logging level in order to vary "
810822
"verbosity", FutureWarning, stacklevel=1)
811823

812-
if not project_id:
813-
raise TypeError("Missing required parameter: project_id")
814-
815824
if dialect not in ('legacy', 'standard'):
816825
raise ValueError("'{0}' is not valid for dialect".format(dialect))
817826

@@ -859,7 +868,7 @@ def read_gbq(query, project_id=None, index_col=None, col_order=None,
859868
return final_df
860869

861870

862-
def to_gbq(dataframe, destination_table, project_id, chunksize=None,
871+
def to_gbq(dataframe, destination_table, project_id=None, chunksize=None,
863872
verbose=None, reauth=False, if_exists='fail', private_key=None,
864873
auth_local_webserver=False, table_schema=None):
865874
"""Write a DataFrame to a Google BigQuery table.
@@ -891,7 +900,7 @@ def to_gbq(dataframe, destination_table, project_id, chunksize=None,
891900
DataFrame to be written
892901
destination_table : string
893902
Name of table to be written, in the form 'dataset.tablename'
894-
project_id : str
903+
project_id : str (optional when available in environment)
895904
Google BigQuery Account project ID.
896905
chunksize : int (default None)
897906
Number of rows to be inserted in each chunk from the dataframe. Use

tests/system.py

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,8 @@ def _get_dataset_prefix_random():
5050

5151

5252
def _get_project_id():
53-
54-
project = os.environ.get('GBQ_PROJECT_ID')
55-
if not project:
56-
pytest.skip(
57-
"Cannot run integration tests without a project id")
58-
return project
53+
return (os.environ.get('GBQ_PROJECT_ID')
54+
or os.environ.get('GOOGLE_CLOUD_PROJECT')) # noqa
5955

6056

6157
def _get_private_key_path():
@@ -85,9 +81,12 @@ def _test_imports():
8581
gbq._test_google_api_imports()
8682

8783

88-
@pytest.fixture
89-
def project():
90-
return _get_project_id()
84+
@pytest.fixture(params=['env'])
85+
def project(request):
86+
if request.param == 'env':
87+
return _get_project_id()
88+
elif request.param == 'none':
89+
return None
9190

9291

9392
def _check_if_can_get_correct_default_credentials():
@@ -99,11 +98,13 @@ def _check_if_can_get_correct_default_credentials():
9998
from google.auth.exceptions import DefaultCredentialsError
10099

101100
try:
102-
credentials, _ = google.auth.default(scopes=[gbq.GbqConnector.scope])
101+
credentials, project = google.auth.default(
102+
scopes=[gbq.GbqConnector.scope])
103103
except (DefaultCredentialsError, IOError):
104104
return False
105105

106-
return gbq._try_credentials(_get_project_id(), credentials) is not None
106+
return gbq._try_credentials(
107+
project or _get_project_id(), credentials) is not None
107108

108109

109110
def clean_gbq_environment(dataset_prefix, private_key=None):
@@ -171,46 +172,14 @@ def test_generate_bq_schema_deprecated():
171172
gbq.generate_bq_schema(df)
172173

173174

174-
@pytest.fixture(params=['local', 'service_path', 'service_creds'])
175-
def auth_type(request):
176-
177-
auth = request.param
178-
179-
if auth == 'local':
180-
181-
if _in_travis_environment():
182-
pytest.skip("Cannot run local auth in travis environment")
183-
184-
elif auth == 'service_path':
185-
186-
if _in_travis_environment():
187-
pytest.skip("Only run one auth type in Travis to save time")
188-
189-
_skip_if_no_private_key_path()
190-
elif auth == 'service_creds':
191-
_skip_if_no_private_key_contents()
192-
else:
193-
raise ValueError
194-
return auth
195-
196-
197175
@pytest.fixture()
198-
def credentials(auth_type):
199-
200-
if auth_type == 'local':
201-
return None
202-
203-
elif auth_type == 'service_path':
204-
return _get_private_key_path()
205-
elif auth_type == 'service_creds':
206-
return _get_private_key_contents()
207-
else:
208-
raise ValueError
176+
def credentials():
177+
_skip_if_no_private_key_contents()
178+
return _get_private_key_contents()
209179

210180

211181
@pytest.fixture()
212182
def gbq_connector(project, credentials):
213-
214183
return gbq.GbqConnector(project, private_key=credentials)
215184

216185

@@ -220,7 +189,7 @@ def test_should_be_able_to_make_a_connector(self, gbq_connector):
220189
assert gbq_connector is not None, 'Could not create a GbqConnector'
221190

222191
def test_should_be_able_to_get_valid_credentials(self, gbq_connector):
223-
credentials = gbq_connector.get_credentials()
192+
credentials, _ = gbq_connector.get_credentials()
224193
assert credentials.valid
225194

226195
def test_should_be_able_to_get_a_bigquery_client(self, gbq_connector):
@@ -236,14 +205,12 @@ def test_should_be_able_to_get_results_from_query(self, gbq_connector):
236205
assert pages is not None
237206

238207

239-
class TestGBQConnectorIntegrationWithLocalUserAccountAuth(object):
208+
class TestAuth(object):
240209

241210
@pytest.fixture(autouse=True)
242-
def setup(self, project):
243-
244-
_skip_local_auth_if_in_travis_env()
245-
246-
self.sut = gbq.GbqConnector(project, auth_local_webserver=True)
211+
def setup(self, gbq_connector):
212+
self.sut = gbq_connector
213+
self.sut.auth_local_webserver = True
247214

248215
def test_get_application_default_credentials_does_not_throw_error(self):
249216
if _check_if_can_get_correct_default_credentials():
@@ -252,27 +219,33 @@ def test_get_application_default_credentials_does_not_throw_error(self):
252219
from google.auth.exceptions import DefaultCredentialsError
253220
with mock.patch('google.auth.default',
254221
side_effect=DefaultCredentialsError()):
255-
credentials = self.sut.get_application_default_credentials()
222+
credentials, _ = self.sut.get_application_default_credentials()
256223
else:
257-
credentials = self.sut.get_application_default_credentials()
224+
credentials, _ = self.sut.get_application_default_credentials()
258225
assert credentials is None
259226

260227
def test_get_application_default_credentials_returns_credentials(self):
261228
if not _check_if_can_get_correct_default_credentials():
262229
pytest.skip("Cannot get default_credentials "
263230
"from the environment!")
264231
from google.auth.credentials import Credentials
265-
credentials = self.sut.get_application_default_credentials()
232+
credentials, default_project = (
233+
self.sut.get_application_default_credentials())
234+
266235
assert isinstance(credentials, Credentials)
236+
assert default_project is not None
267237

268238
def test_get_user_account_credentials_bad_file_returns_credentials(self):
239+
_skip_local_auth_if_in_travis_env()
269240

270241
from google.auth.credentials import Credentials
271242
with mock.patch('__main__.open', side_effect=IOError()):
272243
credentials = self.sut.get_user_account_credentials()
273244
assert isinstance(credentials, Credentials)
274245

275246
def test_get_user_account_credentials_returns_credentials(self):
247+
_skip_local_auth_if_in_travis_env()
248+
276249
from google.auth.credentials import Credentials
277250
credentials = self.sut.get_user_account_credentials()
278251
assert isinstance(credentials, Credentials)
@@ -515,7 +488,8 @@ def test_malformed_query(self):
515488

516489
def test_bad_project_id(self):
517490
with pytest.raises(gbq.GenericGBQException):
518-
gbq.read_gbq("SELECT 1", project_id='001',
491+
gbq.read_gbq('SELCET * FROM [publicdata:samples.shakespeare]',
492+
project_id='not-my-project',
519493
private_key=self.credentials)
520494

521495
def test_bad_table_name(self):

0 commit comments

Comments
 (0)