Skip to content

Commit d94fcbc

Browse files
committed
feat: move BigQuery to clientless validation
Replace the `google.cloud.bigquery` / `google.api_core.client_options` / `google.auth.credentials` imports with a stdlib `urllib.request` REST probe against the emulator's `/bigquery/v2/projects/<project>/queries` endpoint, mirroring the Elasticsearch (#139), Valkey (#138), and Redis family (#136) clientless conversions. - src/pytest_databases/docker/bigquery.py: drop the three `google.*` imports; remove `credentials` and `client_options` from `BigQueryService` (keep `host`, `port`, `container`, `project`, `dataset`, and the `endpoint` property); add a `_query_bigquery_emulator(host, port, project, sql, *, timeout=2.0)` helper that POSTs `{"query": sql, "useLegacySql": false}` and parses the response; rewrite `check()` to call the helper with `SELECT 1 as one` and assert `jobComplete and rows[0].f[0].v == "1"`, swallowing `URLError`/`HTTPError`/`JSONDecodeError`/`TimeoutError`/`OSError`; delete the `bigquery_client` fixture. The emulator `--dataset=` start flag and `DATASET_NAME` env var are preserved so the default dataset is still auto-created by the emulator binary itself. - tests/test_bigquery.py: rewrite to drive the emulator through stdlib `urllib.request` only; preserve `test_service_fixture` (SELECT 1 via REST); drop `test_client_fixture`; rewrite `test_xdist` to issue `CREATE TABLE` against `bigquery_service.dataset` via the REST endpoint; add `test_plugin_imports_without_google_cloud_bigquery` regression guard that intercepts `google.cloud.bigquery`, `google.api_core.client_options`, and `google.auth.credentials` via `builtins.__import__`. - pyproject.toml: empty the `bigquery` compatibility extra so `pytest-databases[bigquery]` no longer pulls in `google-cloud-bigquery`; drop the `pytest_databases.docker.bigquery` `attr-defined` mypy override now that the module no longer touches Google client attribute surface. - docs/supported-databases/bigquery.rst: rewrite the example in the user-owned-client style; users now build their own `bigquery.Client` from `bigquery_service.endpoint`, `bigquery_service.project`, and `AnonymousCredentials()`. - uv.lock: drop `google-cloud-bigquery`, `google-crc32c`, and `google-resumable-media`.
1 parent 72db1ee commit d94fcbc

5 files changed

Lines changed: 115 additions & 177 deletions

File tree

docs/supported-databases/bigquery.rst

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
BigQuery
22
========
33

4-
Integration with `Google BigQuery <https://cloud.google.com/bigquery>`_ using the `BigQuery Emulator <https://github.com/goccy/bigquery-emulator>`_
5-
6-
This integration uses the official `Google Cloud BigQuery Python Client <https://cloud.google.com/python/docs/reference/bigquery/latest>`_ for testing against the BigQuery Emulator. The emulator is a third-party project that provides a local development environment that mimics the behavior of BigQuery, allowing you to test your application without connecting to the actual service.
4+
Integration with `Google BigQuery <https://cloud.google.com/bigquery>`_ using the
5+
`BigQuery Emulator <https://github.com/goccy/bigquery-emulator>`_.
76

87
Installation
98
------------
@@ -12,37 +11,39 @@ Installation
1211
1312
pip install pytest-databases[bigquery]
1413
14+
The ``bigquery`` extra is kept as a compatibility group. Install the BigQuery client
15+
that your application already uses.
16+
1517
Usage Example
1618
-------------
1719

1820
.. code-block:: python
1921
20-
import pytest
22+
from google.api_core.client_options import ClientOptions
23+
from google.auth.credentials import AnonymousCredentials
2124
from google.cloud import bigquery
25+
2226
from pytest_databases.docker.bigquery import BigQueryService
2327
2428
pytest_plugins = ["pytest_databases.docker.bigquery"]
2529
30+
2631
def test(bigquery_service: BigQueryService) -> None:
2732
client = bigquery.Client(
2833
project=bigquery_service.project,
29-
client_options=bigquery_service.client_options,
30-
credentials=bigquery_service.credentials,
34+
client_options=ClientOptions(api_endpoint=bigquery_service.endpoint),
35+
credentials=AnonymousCredentials(),
3136
)
3237
33-
job = client.query(query="SELECT 1 as one")
34-
resp = list(job.result())
35-
assert resp[0].one == 1
36-
37-
def test(bigquery_client: bigquery.Client) -> None:
38-
assert isinstance(bigquery_client, bigquery.Client)
38+
job = client.query(query="SELECT 1 AS one")
39+
rows = list(job.result())
40+
assert rows[0].one == 1
3941
4042
Available Fixtures
4143
------------------
4244

43-
* ``bigquery_image``: The Docker image to use for BigQuery.
44-
* ``bigquery_service``: A fixture that provides a BigQuery service.
45-
* ``bigquery_client``: A fixture that provides a BigQuery client.
45+
* ``bigquery_image``: The Docker image to use for the BigQuery emulator.
46+
* ``bigquery_service``: A fixture that provides a running BigQuery emulator service.
4647

4748
Service API
4849
-----------

pyproject.toml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Source = "https://github.com/litestar-org/pytest-databases"
6060

6161
[project.optional-dependencies]
6262
azure-storage = ["azure-storage-blob"]
63-
bigquery = ["google-cloud-bigquery"]
63+
bigquery = []
6464
cockroachdb = ["psycopg"]
6565
dragonfly = ["redis"]
6666
elasticsearch7 = ["elasticsearch7"]
@@ -195,10 +195,6 @@ warn_unused_ignores = true
195195
disable_error_code = "attr-defined"
196196
module = "pytest_databases.docker.spanner"
197197

198-
[[tool.mypy.overrides]]
199-
disable_error_code = "attr-defined"
200-
module = "pytest_databases.docker.bigquery"
201-
202198
[[tool.mypy.overrides]]
203199
disable_error_code = "attr-defined"
204200
disallow_untyped_decorators = false

src/pytest_databases/docker/bigquery.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
22

3+
import json
4+
import urllib.error
5+
import urllib.request
36
from dataclasses import dataclass
4-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, Any
58

69
import pytest
7-
from google.api_core.client_options import ClientOptions
8-
from google.auth.credentials import AnonymousCredentials, Credentials
9-
from google.cloud import bigquery
1010

1111
from pytest_databases.helpers import get_xdist_worker_num
1212
from pytest_databases.types import ServiceContainer, XdistIsolationLevel
@@ -36,15 +36,29 @@ def platform() -> str:
3636
class BigQueryService(ServiceContainer):
3737
project: str
3838
dataset: str
39-
credentials: Credentials
4039

4140
@property
4241
def endpoint(self) -> str:
4342
return f"http://{self.host}:{self.port}"
4443

45-
@property
46-
def client_options(self) -> ClientOptions:
47-
return ClientOptions(api_endpoint=self.endpoint)
44+
45+
def _query_bigquery_emulator(
46+
host: str,
47+
port: int,
48+
project: str,
49+
sql: str,
50+
*,
51+
timeout: float = 2.0,
52+
) -> dict[str, Any]:
53+
body = json.dumps({"query": sql, "useLegacySql": False}).encode("utf-8")
54+
request = urllib.request.Request(
55+
f"http://{host}:{port}/bigquery/v2/projects/{project}/queries",
56+
data=body,
57+
headers={"Content-Type": "application/json"},
58+
method="POST",
59+
)
60+
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
61+
return json.loads(response.read().decode("utf-8"))
4862

4963

5064
@pytest.fixture(scope="session")
@@ -64,17 +78,19 @@ def bigquery_service(
6478

6579
def check(_service: ServiceContainer) -> bool:
6680
try:
67-
client = bigquery.Client(
68-
project=project,
69-
client_options=ClientOptions(api_endpoint=f"http://{_service.host}:{_service.port}"),
70-
credentials=AnonymousCredentials(),
81+
payload = _query_bigquery_emulator(
82+
_service.host,
83+
_service.port,
84+
project,
85+
"SELECT 1 as one",
7186
)
72-
73-
job = client.query(query="SELECT 1 as one")
74-
75-
resp = list(job.result())
76-
return resp[0].one == 1
77-
except Exception: # noqa: BLE001
87+
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, TimeoutError, OSError):
88+
return False
89+
if payload.get("jobComplete") is not True:
90+
return False
91+
try:
92+
return payload["rows"][0]["f"][0]["v"] == "1"
93+
except (KeyError, IndexError, TypeError):
7894
return False
7995

8096
with docker_service.run(
@@ -97,14 +113,4 @@ def check(_service: ServiceContainer) -> bool:
97113
container=service.container,
98114
project=project,
99115
dataset=dataset,
100-
credentials=AnonymousCredentials(),
101116
)
102-
103-
104-
@pytest.fixture(scope="session")
105-
def bigquery_client(bigquery_service: BigQueryService) -> Generator[bigquery.Client, None, None]:
106-
yield bigquery.Client(
107-
project=bigquery_service.project,
108-
client_options=bigquery_service.client_options,
109-
credentials=bigquery_service.credentials,
110-
)

tests/test_bigquery.py

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,56 @@
66
import pytest
77

88

9-
def test_service_fixture(pytester: pytest.Pytester) -> None:
9+
def test_plugin_imports_without_google_cloud_bigquery(pytester: pytest.Pytester) -> None:
1010
pytester.makepyfile("""
11-
from google.cloud import bigquery
12-
13-
pytest_plugins = ["pytest_databases.docker.bigquery"]
14-
15-
def test(bigquery_service) -> None:
16-
client = bigquery.Client(
17-
project=bigquery_service.project,
18-
client_options=bigquery_service.client_options,
19-
credentials=bigquery_service.credentials,
20-
)
21-
22-
job = client.query(query="SELECT 1 as one")
23-
24-
resp = list(job.result())
25-
assert resp[0].one == 1
11+
import builtins
12+
13+
def test_import() -> None:
14+
original_import = builtins.__import__
15+
blocked = {
16+
"google.cloud.bigquery",
17+
"google.api_core.client_options",
18+
"google.auth.credentials",
19+
}
20+
21+
def blocked_import(name, globals=None, locals=None, fromlist=(), level=0):
22+
if name in blocked:
23+
raise ModuleNotFoundError(name)
24+
return original_import(name, globals, locals, fromlist, level)
25+
26+
builtins.__import__ = blocked_import
27+
try:
28+
import pytest_databases.docker.bigquery
29+
finally:
30+
builtins.__import__ = original_import
2631
""")
2732

28-
result = pytester.runpytest_subprocess("-p", "pytest_databases")
33+
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-vv")
2934
result.assert_outcomes(passed=1)
3035

3136

32-
def test_client_fixture(pytester: pytest.Pytester) -> None:
37+
def test_service_fixture(pytester: pytest.Pytester) -> None:
3338
pytester.makepyfile("""
34-
from google.cloud import bigquery
39+
import json
40+
import urllib.request
3541
3642
pytest_plugins = ["pytest_databases.docker.bigquery"]
3743
38-
def test(bigquery_client) -> None:
39-
assert isinstance(bigquery_client, bigquery.Client)
44+
def run_bigquery(service, sql):
45+
body = json.dumps({"query": sql, "useLegacySql": False}).encode("utf-8")
46+
request = urllib.request.Request(
47+
f"{service.endpoint}/bigquery/v2/projects/{service.project}/queries",
48+
data=body,
49+
headers={"Content-Type": "application/json"},
50+
method="POST",
51+
)
52+
with urllib.request.urlopen(request, timeout=10) as response:
53+
return json.loads(response.read().decode("utf-8"))
54+
55+
def test(bigquery_service) -> None:
56+
payload = run_bigquery(bigquery_service, "SELECT 1 as one")
57+
assert payload.get("jobComplete") is True
58+
assert payload["rows"][0]["f"][0]["v"] == "1"
4059
""")
4160

4261
result = pytester.runpytest_subprocess("-p", "pytest_databases")
@@ -45,15 +64,33 @@ def test(bigquery_client) -> None:
4564

4665
def test_xdist(pytester: pytest.Pytester) -> None:
4766
pytester.makepyfile("""
48-
from google.cloud import bigquery
67+
import json
68+
import urllib.request
4969
5070
pytest_plugins = ["pytest_databases.docker.bigquery"]
5171
52-
def test_one(bigquery_client, bigquery_service) -> None:
53-
bigquery_client.query(f"CREATE TABLE `{bigquery_service.dataset}.test` AS select 1 as the_value")
72+
def run_bigquery(service, sql):
73+
body = json.dumps({"query": sql, "useLegacySql": False}).encode("utf-8")
74+
request = urllib.request.Request(
75+
f"{service.endpoint}/bigquery/v2/projects/{service.project}/queries",
76+
data=body,
77+
headers={"Content-Type": "application/json"},
78+
method="POST",
79+
)
80+
with urllib.request.urlopen(request, timeout=30) as response:
81+
return json.loads(response.read().decode("utf-8"))
82+
83+
def test_one(bigquery_service) -> None:
84+
run_bigquery(
85+
bigquery_service,
86+
f"CREATE TABLE `{bigquery_service.dataset}.test_one` AS SELECT 1 AS the_value",
87+
)
5488
55-
def test_two(bigquery_client, bigquery_service) -> None:
56-
bigquery_client.query(f"CREATE TABLE `{bigquery_service.dataset}.test` AS select 1 as the_value")
89+
def test_two(bigquery_service) -> None:
90+
run_bigquery(
91+
bigquery_service,
92+
f"CREATE TABLE `{bigquery_service.dataset}.test_two` AS SELECT 1 AS the_value",
93+
)
5794
""")
5895

5996
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2")

0 commit comments

Comments
 (0)