diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2adbf6b..c65d08b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,8 +5,8 @@ # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax # Note: This file is autogenerated. To make changes to the codeowner team, please update .repo-metadata.json. -# @googleapis/yoshi-python @googleapis/python-core-client-libraries @googleapis/api-bigquery-dataframe are the default owners for changes in this repo -* @googleapis/yoshi-python @googleapis/python-core-client-libraries @googleapis/api-bigquery-dataframe +# @googleapis/cloud-sdk-python-team @googleapis/bigquery-dataframe-team are the default owners for changes in this repo +* @googleapis/cloud-sdk-python-team @googleapis/bigquery-dataframe-team -# @googleapis/python-samples-reviewers @googleapis/python-core-client-libraries @googleapis/api-bigquery-dataframe are the default owners for samples changes -/samples/ @googleapis/python-samples-reviewers @googleapis/python-core-client-libraries @googleapis/api-bigquery-dataframe +# @googleapis/python-samples-reviewers @googleapis/cloud-sdk-python-team @googleapis/bigquery-dataframe-team are the default owners for samples changes +/samples/ @googleapis/python-samples-reviewers @googleapis/cloud-sdk-python-team @googleapis/bigquery-dataframe-team diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 1aff524..f5d9c90 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1,20 +1,16 @@ # Blunderbuss config -# -# This file controls who is assigned for pull requests and issues. -# Note: This file is autogenerated. To make changes to the assignee -# team, please update `codeowner_team` in `.repo-metadata.json`. assign_issues: - - googleapis/python-core-client-libraries - - googleapis/api-bigquery-dataframe + - googleapis/cloud-sdk-python-team + - googleapis/bigquery-dataframe-team assign_issues_by: - labels: - "samples" to: - googleapis/python-samples-reviewers - - googleapis/python-core-client-libraries - - googleapis/api-bigquery-dataframe + - googleapis/cloud-sdk-python-team + - googleapis/bigquery-dataframe-team assign_prs: - - googleapis/python-core-client-libraries - - googleapis/api-bigquery-dataframe + - googleapis/cloud-sdk-python-team + - googleapis/bigquery-dataframe-team diff --git a/.librarian/state.yaml b/.librarian/state.yaml index ae9b32d..060d3f7 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,7 +1,7 @@ -image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:1a2a85ab507aea26d787c06cc7979decb117164c81dd78a745982dfda80d4f68 +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:160860d189ff1c2f7515638478823712fa5b243e27ccc33a2728669fa1e2ed0c libraries: - id: bigquery-magics - version: 0.12.0 + version: 0.12.1 last_generated_commit: "" apis: [] source_roots: diff --git a/.repo-metadata.json b/.repo-metadata.json index ad6c940..e9c372c 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -12,5 +12,5 @@ "api_id": "bigquery.googleapis.com", "requires_billing": false, "default_version": "", - "codeowner_team": "@googleapis/python-core-client-libraries @googleapis/api-bigquery-dataframe" + "codeowner_team": "@googleapis/cloud-sdk-python-team @googleapis/bigquery-dataframe-team" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 226c932..4617cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/bigquery-magics/#history +## [0.12.1](https://github.com/googleapis/python-bigquery-magics/compare/v0.12.0...v0.12.1) (2026-02-25) + + +### Bug Fixes + +* usability improvement to the graph visualizer (#215) ([7655ada777d16f6514040747855fd1f0e9d77b1b](https://github.com/googleapis/python-bigquery-magics/commit/7655ada777d16f6514040747855fd1f0e9d77b1b)) + ## [0.12.0](https://github.com/googleapis/python-bigquery-magics/compare/v0.11.0...v0.12.0) (2026-02-10) diff --git a/bigquery_magics/bigquery.py b/bigquery_magics/bigquery.py index a10de40..28f9e60 100644 --- a/bigquery_magics/bigquery.py +++ b/bigquery_magics/bigquery.py @@ -648,7 +648,10 @@ def _get_graph_name(query_text: str): """ match = re.match(r"\s*GRAPH\s+(\S+)\.(\S+)", query_text, re.IGNORECASE) if match: - return (match.group(1), match.group(2)) + (dataset_id, graph_id) = (match.group(1)), match.group(2) + if "`" in dataset_id or "`" in graph_id: + return None # Backticks in graph name not support for schema view + return (dataset_id, graph_id) return None @@ -668,10 +671,15 @@ def _get_graph_schema( job_config = bigquery.QueryJobConfig( query_parameters=[bigquery.ScalarQueryParameter("graph_id", "STRING", graph_id)] ) - info_schema_results = bq_client.query( - info_schema_query, job_config=job_config - ).to_dataframe() - + job_config.use_legacy_sql = False + try: + info_schema_results = bq_client.query( + info_schema_query, job_config=job_config + ).to_dataframe() + except Exception: + # If the INFORMATION_SCHEMA query fails for some reason, disable only schema + # view, not the entire visualizer. + return None if info_schema_results.shape == (1, 1): return graph_server._convert_schema(info_schema_results.iloc[0, 0]) return None @@ -733,7 +741,7 @@ def _add_graph_widget( "Error: The query result is too large for graph visualization." ) ) - return + return False schema = _get_graph_schema(bq_client, query_text, query_job) @@ -764,6 +772,7 @@ def _add_graph_widget( '"bigquery.graph_visualization.NodeExpansion"', ) IPython.display.display(IPython.core.display.HTML(html_content)) + return True def _is_valid_json(s: str): @@ -870,7 +879,12 @@ def _make_bq_query( result = result.to_dataframe(**dataframe_kwargs) if args.graph and _supports_graph_widget(result): - _add_graph_widget(bq_client, result, query, query_job, args) + if _add_graph_widget(bq_client, result, query, query_job, args): + # Invoke _handle_result() in case the result is saved to a variable, + # but return None to suppress the default table view, which is redundant + # with the table view in the graph visualizer. + _handle_result(result, args) + return None return _handle_result(result, args) diff --git a/bigquery_magics/graph_server.py b/bigquery_magics/graph_server.py index 0089e97..fac2bb2 100644 --- a/bigquery_magics/graph_server.py +++ b/bigquery_magics/graph_server.py @@ -315,7 +315,7 @@ class ThreadedTCPServer(socketserver.TCPServer): # Daemon threads automatically terminate when the main program exits daemon_threads = True - with ThreadedTCPServer(("", self.port), GraphServerHandler) as httpd: + with ThreadedTCPServer(("127.0.0.1", self.port), GraphServerHandler) as httpd: self._server = httpd self._server.serve_forever() diff --git a/bigquery_magics/version.py b/bigquery_magics/version.py index 259421b..7673815 100644 --- a/bigquery_magics/version.py +++ b/bigquery_magics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.0" +__version__ = "0.12.1" diff --git a/tests/unit/bigquery/test_bigquery.py b/tests/unit/bigquery/test_bigquery.py index d101616..efb2c2f 100644 --- a/tests/unit/bigquery/test_bigquery.py +++ b/tests/unit/bigquery/test_bigquery.py @@ -241,6 +241,23 @@ def test__run_query_dry_run_without_errors_is_silent(): assert len(captured.stdout) == 0 +def test__get_graph_name(): + assert magics._get_graph_name("GRAPH foo.bar") == ("foo", "bar") + assert magics._get_graph_name("GRAPH `foo.bar`") is None + assert magics._get_graph_name("GRAPH `foo`.bar") is None + assert magics._get_graph_name("SELECT 1") is None + + +def test__get_graph_schema_exception(): + bq_client = mock.create_autospec(bigquery.Client, instance=True) + bq_client.query.side_effect = Exception("error") + query_text = "GRAPH foo.bar" + query_job = mock.Mock() + query_job.configuration.destination.project = "my-project" + + assert magics._get_graph_schema(bq_client, query_text, query_job) is None + + @pytest.mark.skipif( bigquery_storage is None, reason="Requires `google-cloud-bigquery-storage`" ) @@ -614,9 +631,7 @@ def test_bigquery_graph_json_json_result(monkeypatch): display_mock.assert_called() assert bqstorage_mock.called # BQ storage client was used - assert isinstance(return_value, pandas.DataFrame) - assert len(return_value) == len(result) # verify row count - assert list(return_value) == list(result) # verify column names + assert return_value is None @pytest.mark.skipif( @@ -735,9 +750,7 @@ def test_bigquery_graph_json_result(monkeypatch): assert '\\"location\\": null' in html_content assert bqstorage_mock.called # BQ storage client was used - assert isinstance(return_value, pandas.DataFrame) - assert len(return_value) == len(result) # verify row count - assert list(return_value) == list(result) # verify column names + assert return_value is None @pytest.mark.skipif( @@ -1015,9 +1028,7 @@ def test_bigquery_graph_colab(monkeypatch): assert sys.modules["google.colab"].output.register_callback.called assert bqstorage_mock.called # BQ storage client was used - assert isinstance(return_value, pandas.DataFrame) - assert len(return_value) == len(result) # verify row count - assert list(return_value) == list(result) # verify column names + assert return_value is None @pytest.mark.skipif(