Skip to content

Commit 0b33bdd

Browse files
matthew29tangcopybara-github
authored andcommitted
feat: Add display experiment button for Ipython environments
PiperOrigin-RevId: 612526515
1 parent 63ad1bf commit 0b33bdd

File tree

4 files changed

+212
-3
lines changed

4 files changed

+212
-3
lines changed

google/cloud/aiplatform/metadata/metadata.py

+25
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from google.cloud.aiplatform.tensorboard import tensorboard_resource
4040
from google.cloud.aiplatform.utils import autologging_utils
41+
from google.cloud.aiplatform.utils import _ipython_utils
4142

4243
from google.cloud.aiplatform_v1.types import execution as execution_v1
4344

@@ -329,8 +330,32 @@ def set_experiment(
329330
if not current_backing_tb and backing_tb:
330331
experiment.assign_backing_tensorboard(tensorboard=backing_tb)
331332

333+
if _ipython_utils.is_ipython_available():
334+
self._display_experiment_button(experiment)
335+
332336
self._experiment = experiment
333337

338+
def _display_experiment_button(
339+
self, experiment: experiment_resources.Experiment
340+
) -> None:
341+
"""Function to generate a link bound to the Vertex experiment"""
342+
try:
343+
project = experiment._metadata_context.project
344+
location = experiment._metadata_context.location
345+
experiment_name = experiment._metadata_context.name
346+
if experiment_name is None or project is None or location is None:
347+
return
348+
except AttributeError:
349+
_LOGGER.warning("Unable to fetch experiment metadata")
350+
return
351+
352+
uri = (
353+
"https://console.cloud.google.com/vertex-ai/experiments/locations/"
354+
+ f"{location}/experiments/{experiment_name}/"
355+
+ f"runs?project={project}"
356+
)
357+
_ipython_utils.display_link("View Experiment", uri, "science")
358+
334359
def set_tensorboard(
335360
self,
336361
tensorboard: Union[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2024 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import sys
19+
from uuid import uuid4
20+
from typing import Optional
21+
22+
23+
def _get_ipython_shell_name() -> str:
24+
if "IPython" in sys.modules:
25+
from IPython import get_ipython
26+
27+
return get_ipython().__class__.__name__
28+
return ""
29+
30+
31+
def is_ipython_available() -> bool:
32+
return _get_ipython_shell_name() != ""
33+
34+
35+
def _get_styles() -> None:
36+
"""Returns the HTML style markup to support custom buttons."""
37+
return """
38+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
39+
<style>
40+
.view-vertex-resource,
41+
.view-vertex-resource:hover,
42+
.view-vertex-resource:visited {
43+
position: relative;
44+
display: inline-flex;
45+
flex-direction: row;
46+
height: 32px;
47+
padding: 0 12px;
48+
margin: 4px 18px;
49+
gap: 4px;
50+
border-radius: 4px;
51+
52+
align-items: center;
53+
justify-content: center;
54+
background-color: rgb(255, 255, 255);
55+
color: rgb(51, 103, 214);
56+
57+
font-family: Roboto,"Helvetica Neue",sans-serif;
58+
font-size: 13px;
59+
font-weight: 500;
60+
text-transform: uppercase;
61+
text-decoration: none !important;
62+
63+
transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1) 0s;
64+
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12);
65+
}
66+
.view-vertex-resource:active {
67+
box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12);
68+
}
69+
.view-vertex-resource:active .view-vertex-ripple::before {
70+
position: absolute;
71+
top: 0;
72+
bottom: 0;
73+
left: 0;
74+
right: 0;
75+
border-radius: 4px;
76+
pointer-events: none;
77+
78+
content: '';
79+
background-color: rgb(51, 103, 214);
80+
opacity: 0.12;
81+
}
82+
.view-vertex-icon {
83+
font-size: 18px;
84+
}
85+
</style>
86+
"""
87+
88+
89+
def display_link(text: str, url: str, icon: Optional[str] = "open_in_new") -> None:
90+
"""Creates and displays the link to open the Vertex resource
91+
92+
Args:
93+
text: The text displayed on the clickable button.
94+
url: The url that the button will lead to.
95+
Only cloud console URIs are allowed.
96+
icon: The icon name on the button (from material-icons library)
97+
98+
Returns:
99+
Dict of custom properties with keys mapped to column names
100+
"""
101+
CLOUD_UI_URL = "https://console.cloud.google.com"
102+
if not url.startswith(CLOUD_UI_URL):
103+
raise ValueError(f"Only urls starting with {CLOUD_UI_URL} are allowed.")
104+
105+
button_id = f"view-vertex-resource-{str(uuid4())}"
106+
107+
# Add the markup for the CSS and link component
108+
html = f"""
109+
{_get_styles()}
110+
<a class="view-vertex-resource" id="{button_id}" href="#view-{button_id}">
111+
<span class="material-icons view-vertex-icon">{icon}</span>
112+
<span>{text}</span>
113+
</a>
114+
"""
115+
116+
# Add the click handler for the link
117+
html += f"""
118+
<script>
119+
(function () {{
120+
const link = document.getElementById('{button_id}');
121+
link.addEventListener('click', (e) => {{
122+
if (window.google?.colab?.openUrl) {{
123+
window.google.colab.openUrl('{url}');
124+
}} else {{
125+
window.open('{url}', '_blank');
126+
}}
127+
e.stopPropagation();
128+
e.preventDefault();
129+
}});
130+
}})();
131+
</script>
132+
"""
133+
134+
from IPython.core.display import display
135+
from IPython.display import HTML
136+
137+
display(HTML(html))

testing/constraints-3.10.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ grpcio-testing==1.34.0
1111
mlflow==1.30.1 # Pinned to speed up installation
1212
pytest-xdist==3.3.1 # Pinned to unbreak unit tests
1313
ray==2.4.0 # Pinned until 2.9.3 is verified for Ray tests
14+
IPython # Added to test supernova rich html buttons

tests/unit/aiplatform/test_metadata.py

+49-3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from google.cloud.aiplatform.tensorboard import tensorboard_resource
7575

7676
from google.cloud.aiplatform import utils
77+
from google.cloud.aiplatform.utils import _ipython_utils
7778

7879
import constants as test_constants
7980

@@ -187,6 +188,22 @@ def get_metadata_store_mock_raise_not_found_exception():
187188
yield get_metadata_store_mock
188189

189190

191+
@pytest.fixture
192+
def ipython_is_available_mock():
193+
with patch.object(_ipython_utils, "is_ipython_available") as ipython_available_mock:
194+
ipython_available_mock.return_value = True
195+
yield ipython_available_mock
196+
197+
198+
@pytest.fixture
199+
def ipython_is_not_available_mock():
200+
with patch.object(
201+
_ipython_utils, "is_ipython_available"
202+
) as ipython_not_available_mock:
203+
ipython_not_available_mock.return_value = False
204+
yield ipython_not_available_mock
205+
206+
190207
@pytest.fixture
191208
def create_metadata_store_mock():
192209
with patch.object(
@@ -1110,6 +1127,38 @@ def setup_method(self):
11101127
def teardown_method(self):
11111128
initializer.global_pool.shutdown(wait=True)
11121129

1130+
@pytest.mark.usefixtures(
1131+
"get_metadata_store_mock",
1132+
"get_experiment_run_run_mock",
1133+
)
1134+
def test_init_experiment_with_ipython_environment(
1135+
self,
1136+
list_default_tensorboard_mock,
1137+
assign_backing_tensorboard_mock,
1138+
ipython_is_available_mock,
1139+
):
1140+
aiplatform.init(
1141+
project=_TEST_PROJECT,
1142+
location=_TEST_LOCATION,
1143+
experiment=_TEST_EXPERIMENT,
1144+
)
1145+
1146+
@pytest.mark.usefixtures(
1147+
"get_metadata_store_mock",
1148+
"get_experiment_run_run_mock",
1149+
)
1150+
def test_init_experiment_with_no_ipython_environment(
1151+
self,
1152+
list_default_tensorboard_mock,
1153+
assign_backing_tensorboard_mock,
1154+
ipython_is_not_available_mock,
1155+
):
1156+
aiplatform.init(
1157+
project=_TEST_PROJECT,
1158+
location=_TEST_LOCATION,
1159+
experiment=_TEST_EXPERIMENT,
1160+
)
1161+
11131162
@pytest.mark.usefixtures("get_or_create_default_tb_none_mock")
11141163
def test_init_experiment_with_existing_metadataStore_and_context(
11151164
self, get_metadata_store_mock, get_experiment_run_run_mock
@@ -1496,7 +1545,6 @@ def test_start_run(
14961545
create_experiment_run_context_mock,
14971546
add_context_children_mock,
14981547
):
1499-
15001548
aiplatform.init(
15011549
project=_TEST_PROJECT,
15021550
location=_TEST_LOCATION,
@@ -1527,7 +1575,6 @@ def test_start_run(
15271575
"get_or_create_default_tb_none_mock",
15281576
)
15291577
def test_start_run_fails_when_run_name_too_long(self):
1530-
15311578
aiplatform.init(
15321579
project=_TEST_PROJECT,
15331580
location=_TEST_LOCATION,
@@ -1846,7 +1893,6 @@ def test_start_execution_and_assign_artifact(
18461893
display_name=_TEST_DISPLAY_NAME,
18471894
metadata=_TEST_METADATA,
18481895
) as exc:
1849-
18501896
exc.assign_input_artifacts([in_artifact])
18511897
exc.assign_output_artifacts([out_artifact])
18521898

0 commit comments

Comments
 (0)