Skip to content

Commit 4f72d4e

Browse files
waltaskewtswast
andauthored
feat: Allow Users to Supply Their Own BigQuery Client (#474)
- Add a flag 'user_supplied_client' which prevents the library from attempting to create a BigQuery client. - Document the use of `connect_args` for suppling their own BigQuery client to the dbapi Co-authored-by: Tim Swast <swast@google.com>
1 parent f566371 commit 4f72d4e

File tree

5 files changed

+69
-11
lines changed

5 files changed

+69
-11
lines changed

README.rst

+19
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ To create the base64 encoded string you can use the command line tool ``base64``
234234

235235
Alternatively, you can use an online generator like `www.base64encode.org <https://www.base64encode.org>_` to paste your credentials JSON file to be encoded.
236236

237+
238+
Supplying Your Own BigQuery Client
239+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
240+
241+
The above connection string parameters allow you to influence how the BigQuery client used to execute your queries will be instantiated.
242+
If you need additional control, you can supply a BigQuery client of your own:
243+
244+
.. code-block:: python
245+
246+
from google.cloud import bigquery
247+
248+
custom_bq_client = bigquery.Client(...)
249+
250+
engine = create_engine(
251+
'bigquery://some-project/some-dataset?user_supplied_client=True',
252+
connect_args={'client': custom_bq_client},
253+
)
254+
255+
237256
Creating tables
238257
^^^^^^^^^^^^^^^
239258

sqlalchemy_bigquery/base.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ def create_connect_args(self, url):
801801
credentials_base64,
802802
default_query_job_config,
803803
list_tables_page_size,
804+
user_supplied_client,
804805
) = parse_url(url)
805806

806807
self.arraysize = arraysize or self.arraysize
@@ -812,15 +813,21 @@ def create_connect_args(self, url):
812813
self._add_default_dataset_to_job_config(
813814
default_query_job_config, project_id, dataset_id
814815
)
815-
client = _helpers.create_bigquery_client(
816-
credentials_path=self.credentials_path,
817-
credentials_info=self.credentials_info,
818-
credentials_base64=self.credentials_base64,
819-
project_id=project_id,
820-
location=self.location,
821-
default_query_job_config=default_query_job_config,
822-
)
823-
return ([client], {})
816+
817+
if user_supplied_client:
818+
# The user is expected to supply a client with
819+
# create_engine('...', connect_args={'client': bq_client})
820+
return ([], {})
821+
else:
822+
client = _helpers.create_bigquery_client(
823+
credentials_path=self.credentials_path,
824+
credentials_info=self.credentials_info,
825+
credentials_base64=self.credentials_base64,
826+
project_id=project_id,
827+
location=self.location,
828+
default_query_job_config=default_query_job_config,
829+
)
830+
return ([], {"client": client})
824831

825832
def _get_table_or_view_names(self, connection, item_types, schema=None):
826833
current_schema = schema or self.dataset_id

sqlalchemy_bigquery/parse_url.py

+8
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def parse_url(url): # noqa: C901
7070
credentials_path = None
7171
credentials_base64 = None
7272
list_tables_page_size = None
73+
user_supplied_client = False
7374

7475
# location
7576
if "location" in query:
@@ -101,6 +102,10 @@ def parse_url(url): # noqa: C901
101102
+ str_list_tables_page_size
102103
)
103104

105+
# user_supplied_client
106+
if "user_supplied_client" in query:
107+
user_supplied_client = query.pop("user_supplied_client").lower() == "true"
108+
104109
# if only these "non-config" values were present, the dict will now be empty
105110
if not query:
106111
# if a dataset_id exists, we need to return a job_config that isn't None
@@ -115,6 +120,7 @@ def parse_url(url): # noqa: C901
115120
credentials_base64,
116121
QueryJobConfig(),
117122
list_tables_page_size,
123+
user_supplied_client,
118124
)
119125
else:
120126
return (
@@ -126,6 +132,7 @@ def parse_url(url): # noqa: C901
126132
credentials_base64,
127133
None,
128134
list_tables_page_size,
135+
user_supplied_client,
129136
)
130137

131138
job_config = QueryJobConfig()
@@ -275,4 +282,5 @@ def parse_url(url): # noqa: C901
275282
credentials_base64,
276283
job_config,
277284
list_tables_page_size,
285+
user_supplied_client,
278286
)

tests/unit/test_parse_url.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def url_with_everything():
6363
"&schema_update_options=ALLOW_FIELD_ADDITION,ALLOW_FIELD_RELAXATION"
6464
"&use_query_cache=true"
6565
"&write_disposition=WRITE_APPEND"
66+
"&user_supplied_client=true"
6667
)
6768

6869

@@ -76,6 +77,7 @@ def test_basic(url_with_everything):
7677
credentials_base64,
7778
job_config,
7879
list_tables_page_size,
80+
user_supplied_client,
7981
) = parse_url(url_with_everything)
8082

8183
assert project_id == "some-project"
@@ -86,6 +88,7 @@ def test_basic(url_with_everything):
8688
assert credentials_path == "/some/path/to.json"
8789
assert credentials_base64 == "eyJrZXkiOiJ2YWx1ZSJ9Cg=="
8890
assert isinstance(job_config, QueryJobConfig)
91+
assert user_supplied_client
8992

9093

9194
@pytest.mark.parametrize(
@@ -161,11 +164,15 @@ def test_bad_values(param, value):
161164

162165

163166
def test_empty_url():
164-
for value in parse_url(make_url("bigquery://")):
167+
values = parse_url(make_url("bigquery://"))
168+
for value in values[:-1]:
165169
assert value is None
170+
assert not values[-1]
166171

167-
for value in parse_url(make_url("bigquery:///")):
172+
values = parse_url(make_url("bigquery:///"))
173+
for value in values[:-1]:
168174
assert value is None
175+
assert not values[-1]
169176

170177

171178
def test_empty_with_non_config():
@@ -183,6 +190,7 @@ def test_empty_with_non_config():
183190
credentials_base64,
184191
job_config,
185192
list_tables_page_size,
193+
user_supplied_credentials,
186194
) = url
187195

188196
assert project_id is None
@@ -193,6 +201,7 @@ def test_empty_with_non_config():
193201
assert credentials_base64 is None
194202
assert job_config is None
195203
assert list_tables_page_size is None
204+
assert not user_supplied_credentials
196205

197206

198207
def test_only_dataset():
@@ -206,6 +215,7 @@ def test_only_dataset():
206215
credentials_base64,
207216
job_config,
208217
list_tables_page_size,
218+
user_supplied_credentials,
209219
) = url
210220

211221
assert project_id is None
@@ -216,6 +226,7 @@ def test_only_dataset():
216226
assert credentials_base64 is None
217227
assert list_tables_page_size is None
218228
assert isinstance(job_config, QueryJobConfig)
229+
assert not user_supplied_credentials
219230
# we can't actually test that the dataset is on the job_config,
220231
# since we take care of that afterwards, when we have a client to fill in the project
221232

tests/unit/test_sqlalchemy_bigquery.py

+13
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,16 @@ def test_unnest_function(args, kw):
233233
assert isinstance(
234234
sqlalchemy.select([f]).subquery().c.unnest.type, sqlalchemy.String
235235
)
236+
237+
238+
@mock.patch("sqlalchemy_bigquery._helpers.create_bigquery_client")
239+
def test_setting_user_supplied_client_skips_creating_client(
240+
mock_create_bigquery_client,
241+
):
242+
import sqlalchemy_bigquery # noqa
243+
244+
result = sqlalchemy_bigquery.BigQueryDialect().create_connect_args(
245+
mock.MagicMock(database=None, query={"user_supplied_client": "true"})
246+
)
247+
assert result == ([], {})
248+
assert not mock_create_bigquery_client.called

0 commit comments

Comments
 (0)