Skip to content

Commit 5dd1ebd

Browse files
committed
feat: set vscode and jupyter environments in the BQ jobs (#1527)
* feat: set vscode and jupyter environments in the BQ jobs In this change we are including the vscode and jupyter environments in the application name set in the BigQuery jobs. This would help understand the BigFrames usage coming from those environments. * remove print, overlook all exceptions during extension detection * slight formatting fix
1 parent c4f6717 commit 5dd1ebd

File tree

3 files changed

+189
-2
lines changed

3 files changed

+189
-2
lines changed

bigframes/session/clients.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import bigframes.exceptions as bfe
3636
import bigframes.version
3737

38+
from . import environment
39+
3840
_ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT"
3941
_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__} ibis/9.2.0"
4042
_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
@@ -57,6 +59,21 @@ def _get_default_credentials_with_project():
5759
return pydata_google_auth.default(scopes=_SCOPES, use_local_webserver=False)
5860

5961

62+
def _get_application_names():
63+
apps = [_APPLICATION_NAME]
64+
65+
if environment.is_vscode():
66+
apps.append("vscode")
67+
if environment.is_vscode_google_cloud_code_extension_installed():
68+
apps.append(environment.GOOGLE_CLOUD_CODE_EXTENSION_NAME)
69+
elif environment.is_jupyter():
70+
apps.append("jupyter")
71+
if environment.is_jupyter_bigquery_plugin_installed():
72+
apps.append(environment.BIGQUERY_JUPYTER_PLUGIN_NAME)
73+
74+
return " ".join(apps)
75+
76+
6077
class ClientsProvider:
6178
"""Provides client instances necessary to perform cloud operations."""
6279

@@ -91,9 +108,9 @@ def __init__(
91108
)
92109

93110
self._application_name = (
94-
f"{_APPLICATION_NAME} {application_name}"
111+
f"{_get_application_names()} {application_name}"
95112
if application_name
96-
else _APPLICATION_NAME
113+
else _get_application_names()
97114
)
98115
self._project = project
99116

bigframes/session/environment.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import importlib
16+
import json
17+
import os
18+
19+
# The identifier for GCP VS Code extension
20+
# https://cloud.google.com/code/docs/vscode/install
21+
GOOGLE_CLOUD_CODE_EXTENSION_NAME = "googlecloudtools.cloudcode"
22+
23+
24+
# The identifier for BigQuery Jupyter notebook plugin
25+
# https://cloud.google.com/bigquery/docs/jupyterlab-plugin
26+
BIGQUERY_JUPYTER_PLUGIN_NAME = "bigquery_jupyter_plugin"
27+
28+
29+
def _is_vscode_extension_installed(extension_id: str) -> bool:
30+
"""
31+
Checks if a given Visual Studio Code extension is installed.
32+
33+
Args:
34+
extension_id: The ID of the extension (e.g., "ms-python.python").
35+
36+
Returns:
37+
True if the extension is installed, False otherwise.
38+
"""
39+
try:
40+
# Determine the user's VS Code extensions directory.
41+
user_home = os.path.expanduser("~")
42+
if os.name == "nt": # Windows
43+
vscode_extensions_dir = os.path.join(user_home, ".vscode", "extensions")
44+
elif os.name == "posix": # macOS and Linux
45+
vscode_extensions_dir = os.path.join(user_home, ".vscode", "extensions")
46+
else:
47+
raise OSError("Unsupported operating system.")
48+
49+
# Check if the extensions directory exists.
50+
if os.path.exists(vscode_extensions_dir):
51+
# Iterate through the subdirectories in the extensions directory.
52+
for item in os.listdir(vscode_extensions_dir):
53+
item_path = os.path.join(vscode_extensions_dir, item)
54+
if os.path.isdir(item_path) and item.startswith(extension_id + "-"):
55+
# Check if the folder starts with the extension ID.
56+
# Further check for manifest file, as a more robust check.
57+
manifest_path = os.path.join(item_path, "package.json")
58+
if os.path.exists(manifest_path):
59+
try:
60+
with open(manifest_path, "r", encoding="utf-8") as f:
61+
json.load(f)
62+
return True
63+
except (FileNotFoundError, json.JSONDecodeError):
64+
# Corrupted or incomplete extension, or manifest missing.
65+
pass
66+
except Exception:
67+
pass
68+
69+
return False
70+
71+
72+
def _is_package_installed(package_name: str) -> bool:
73+
"""
74+
Checks if a Python package is installed.
75+
76+
Args:
77+
package_name: The name of the package to check (e.g., "requests", "numpy").
78+
79+
Returns:
80+
True if the package is installed, False otherwise.
81+
"""
82+
try:
83+
importlib.import_module(package_name)
84+
return True
85+
except Exception:
86+
return False
87+
88+
89+
def is_vscode() -> bool:
90+
return os.getenv("VSCODE_PID") is not None
91+
92+
93+
def is_jupyter() -> bool:
94+
return os.getenv("JPY_PARENT_PID") is not None
95+
96+
97+
def is_vscode_google_cloud_code_extension_installed() -> bool:
98+
return _is_vscode_extension_installed(GOOGLE_CLOUD_CODE_EXTENSION_NAME)
99+
100+
101+
def is_jupyter_bigquery_plugin_installed() -> bool:
102+
return _is_package_installed(BIGQUERY_JUPYTER_PLUGIN_NAME)

tests/unit/session/test_clients.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
1516
from typing import Optional
1617
import unittest.mock as mock
1718

@@ -99,6 +100,33 @@ def assert_clients_w_user_agent(
99100
assert_constructed_w_user_agent(provider.resourcemanagerclient, expected_user_agent)
100101

101102

103+
def assert_constructed_wo_user_agent(
104+
mock_client: mock.Mock, not_expected_user_agent: str
105+
):
106+
assert (
107+
not_expected_user_agent
108+
not in mock_client.call_args.kwargs["client_info"].to_user_agent()
109+
)
110+
111+
112+
def assert_clients_wo_user_agent(
113+
provider: clients.ClientsProvider, not_expected_user_agent: str
114+
):
115+
assert_constructed_wo_user_agent(provider.bqclient, not_expected_user_agent)
116+
assert_constructed_wo_user_agent(
117+
provider.bqconnectionclient, not_expected_user_agent
118+
)
119+
assert_constructed_wo_user_agent(
120+
provider.bqstoragereadclient, not_expected_user_agent
121+
)
122+
assert_constructed_wo_user_agent(
123+
provider.cloudfunctionsclient, not_expected_user_agent
124+
)
125+
assert_constructed_wo_user_agent(
126+
provider.resourcemanagerclient, not_expected_user_agent
127+
)
128+
129+
102130
def test_user_agent_default(monkeypatch):
103131
monkeypatch_client_constructors(monkeypatch)
104132
provider = create_clients_provider(application_name=None)
@@ -113,3 +141,43 @@ def test_user_agent_custom(monkeypatch):
113141
# We still need to include attribution to bigframes, even if there's also a
114142
# partner using the package.
115143
assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}")
144+
145+
146+
@mock.patch.dict(os.environ, {}, clear=True)
147+
def test_user_agent_not_in_vscode(monkeypatch):
148+
monkeypatch_client_constructors(monkeypatch)
149+
provider = create_clients_provider()
150+
assert_clients_wo_user_agent(provider, "vscode")
151+
152+
# We still need to include attribution to bigframes
153+
assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}")
154+
155+
156+
@mock.patch.dict(os.environ, {"VSCODE_PID": "12345"}, clear=True)
157+
def test_user_agent_in_vscode(monkeypatch):
158+
monkeypatch_client_constructors(monkeypatch)
159+
provider = create_clients_provider()
160+
assert_clients_w_user_agent(provider, "vscode")
161+
162+
# We still need to include attribution to bigframes
163+
assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}")
164+
165+
166+
@mock.patch.dict(os.environ, {}, clear=True)
167+
def test_user_agent_not_in_jupyter(monkeypatch):
168+
monkeypatch_client_constructors(monkeypatch)
169+
provider = create_clients_provider()
170+
assert_clients_wo_user_agent(provider, "jupyter")
171+
172+
# We still need to include attribution to bigframes
173+
assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}")
174+
175+
176+
@mock.patch.dict(os.environ, {"JPY_PARENT_PID": "12345"}, clear=True)
177+
def test_user_agent_in_jupyter(monkeypatch):
178+
monkeypatch_client_constructors(monkeypatch)
179+
provider = create_clients_provider()
180+
assert_clients_w_user_agent(provider, "jupyter")
181+
182+
# We still need to include attribution to bigframes
183+
assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}")

0 commit comments

Comments
 (0)