Skip to content

Commit 83cf08f

Browse files
authored
Allow deleting runs on Spaces (via Oauth) and on Colab (#268)
1 parent ad93190 commit 83cf08f

7 files changed

Lines changed: 187 additions & 83 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ requires-python = ">=3.10"
1414
dependencies = [
1515
"pandas<3.0.0",
1616
"huggingface-hub<1.0.0",
17-
"gradio>=5.48.0,<6.0.0",
17+
"gradio[oauth]>=5.48.0,<6.0.0",
1818
"numpy<3.0.0",
1919
"pillow<12.0.0",
2020
"orjson>=3.0,<4.0.0"

trackio/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ sdk_version: {GRADIO_VERSION}
44
app_file: ui/main.py
55
tags:
66
- trackio
7+
hf_oauth: true
8+
hf_oauth_scopes:
9+
- write-repos
710
---

trackio/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323

2424
logging.getLogger("httpx").setLevel(logging.WARNING)
2525

26+
warnings.filterwarnings(
27+
"ignore",
28+
message="Empty session being created. Install gradio\\[oauth\\]",
29+
category=UserWarning,
30+
module="gradio.helpers",
31+
)
32+
2633
__version__ = Path(__file__).parent.joinpath("version.txt").read_text().strip()
2734

2835
__all__ = [

trackio/ui/fns.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44

55
import gradio as gr
6+
import huggingface_hub as hf
67

78
try:
89
import trackio.utils as utils
@@ -21,9 +22,12 @@
2122
CONFIG_COLUMN_MAPPINGS_REVERSE = {v: k for k, v in CONFIG_COLUMN_MAPPINGS.items()}
2223

2324

25+
HfApi = hf.HfApi()
26+
27+
2428
def get_project_info() -> str | None:
2529
dataset_id = os.environ.get("TRACKIO_DATASET_ID")
26-
space_id = os.environ.get("SPACE_ID")
30+
space_id = utils.get_space()
2731
if utils.persistent_storage_enabled():
2832
return "&#10024; Persistent Storage is enabled, logs are stored directly in this Space."
2933
if dataset_id:
@@ -58,15 +62,107 @@ def get_projects(request: gr.Request):
5862
)
5963

6064

61-
def update_navbar_value(project_dd):
65+
def update_navbar_value(project_dd, request: gr.Request):
66+
write_token = None
67+
if hasattr(request, "query_params") and request.query_params:
68+
write_token = request.query_params.get("write_token")
69+
70+
metrics_url = f"?selected_project={project_dd}"
71+
runs_url = f"runs?selected_project={project_dd}"
72+
73+
if write_token:
74+
metrics_url += f"&write_token={write_token}"
75+
runs_url += f"&write_token={write_token}"
76+
6277
return gr.Navbar(
6378
value=[
64-
("Metrics", f"?selected_project={project_dd}"),
65-
("Runs", f"runs?selected_project={project_dd}"),
79+
("Metrics", metrics_url),
80+
("Runs", runs_url),
6681
]
6782
)
6883

6984

85+
def check_hf_token_has_write_access(hf_token: str | None) -> None:
86+
"""
87+
Checks to see if the provided hf_token is valid and has write access to the Space
88+
that Trackio is running in. If the hf_token is valid or if Trackio is not running
89+
on a Space, this function does nothing. Otherwise, it raises a PermissionError.
90+
"""
91+
if os.getenv("SYSTEM") == "spaces": # if we are running in Spaces
92+
# check auth token passed in
93+
if hf_token is None:
94+
raise PermissionError(
95+
"Expected a HF_TOKEN to be provided when logging to a Space"
96+
)
97+
who = HfApi.whoami(hf_token)
98+
owner_name = os.getenv("SPACE_AUTHOR_NAME")
99+
repo_name = os.getenv("SPACE_REPO_NAME")
100+
# make sure the token user is either the author of the space,
101+
# or is a member of an org that is the author.
102+
orgs = [o["name"] for o in who["orgs"]]
103+
if owner_name != who["name"] and owner_name not in orgs:
104+
raise PermissionError(
105+
"Expected the provided hf_token to be the user owner of the space, or be a member of the org owner of the space"
106+
)
107+
# reject fine-grained tokens without specific repo access
108+
access_token = who["auth"]["accessToken"]
109+
if access_token["role"] == "fineGrained":
110+
matched = False
111+
for item in access_token["fineGrained"]["scoped"]:
112+
if (
113+
item["entity"]["type"] == "space"
114+
and item["entity"]["name"] == f"{owner_name}/{repo_name}"
115+
and "repo.write" in item["permissions"]
116+
):
117+
matched = True
118+
break
119+
if (
120+
(
121+
item["entity"]["type"] == "user"
122+
or item["entity"]["type"] == "org"
123+
)
124+
and item["entity"]["name"] == owner_name
125+
and "repo.write" in item["permissions"]
126+
):
127+
matched = True
128+
break
129+
if not matched:
130+
raise PermissionError(
131+
"Expected the provided hf_token with fine grained permissions to provide write access to the space"
132+
)
133+
# reject read-only tokens
134+
elif access_token["role"] != "write":
135+
raise PermissionError(
136+
"Expected the provided hf_token to provide write permissions"
137+
)
138+
139+
140+
def check_oauth_token_has_write_access(oauth_token: str | None) -> None:
141+
"""
142+
Checks to see if the oauth token provided via Gradio's OAuth is valid and has write access
143+
to the Space that Trackio is running in. If the oauth token is valid or if Trackio is not running
144+
on a Space, this function does nothing. Otherwise, it raises a PermissionError.
145+
"""
146+
if not os.getenv("SYSTEM") == "spaces":
147+
return
148+
if oauth_token is None:
149+
raise PermissionError(
150+
"Expected an oauth to be provided when logging to a Space"
151+
)
152+
who = HfApi.whoami(oauth_token)
153+
user_name = who["name"]
154+
owner_name = os.getenv("SPACE_AUTHOR_NAME")
155+
if user_name == owner_name:
156+
return
157+
# check if user is a member of an org that owns the space with write permissions
158+
for org in who["orgs"]:
159+
if org["name"] == owner_name and org["roleInOrg"] == "write":
160+
return
161+
raise PermissionError(
162+
"Expected the oauth token to be the user owner of the space, or be a member of the org owner of the space"
163+
)
164+
165+
70166
def get_group_by_fields(project: str):
71167
configs = SQLiteStorage.get_all_run_configs(project) if project else {}
72168
keys = set()

trackio/ui/main.py

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@
88
from typing import Any
99

1010
import gradio as gr
11-
import huggingface_hub as hf
1211
import numpy as np
1312
import pandas as pd
1413

15-
HfApi = hf.HfApi()
16-
1714
try:
1815
import trackio.utils as utils
1916
from trackio.file_storage import FileStorage
@@ -267,63 +264,13 @@ def toggle_timer(cb_value):
267264
return gr.Timer(active=False)
268265

269266

270-
def check_auth(hf_token: str | None) -> None:
271-
if os.getenv("SYSTEM") == "spaces": # if we are running in Spaces
272-
# check auth token passed in
273-
if hf_token is None:
274-
raise PermissionError(
275-
"Expected a HF_TOKEN to be provided when logging to a Space"
276-
)
277-
who = HfApi.whoami(hf_token)
278-
access_token = who["auth"]["accessToken"]
279-
owner_name = os.getenv("SPACE_AUTHOR_NAME")
280-
repo_name = os.getenv("SPACE_REPO_NAME")
281-
# make sure the token user is either the author of the space,
282-
# or is a member of an org that is the author.
283-
orgs = [o["name"] for o in who["orgs"]]
284-
if owner_name != who["name"] and owner_name not in orgs:
285-
raise PermissionError(
286-
"Expected the provided hf_token to be the user owner of the space, or be a member of the org owner of the space"
287-
)
288-
# reject fine-grained tokens without specific repo access
289-
if access_token["role"] == "fineGrained":
290-
matched = False
291-
for item in access_token["fineGrained"]["scoped"]:
292-
if (
293-
item["entity"]["type"] == "space"
294-
and item["entity"]["name"] == f"{owner_name}/{repo_name}"
295-
and "repo.write" in item["permissions"]
296-
):
297-
matched = True
298-
break
299-
if (
300-
(
301-
item["entity"]["type"] == "user"
302-
or item["entity"]["type"] == "org"
303-
)
304-
and item["entity"]["name"] == owner_name
305-
and "repo.write" in item["permissions"]
306-
):
307-
matched = True
308-
break
309-
if not matched:
310-
raise PermissionError(
311-
"Expected the provided hf_token with fine grained permissions to provide write access to the space"
312-
)
313-
# reject read-only tokens
314-
elif access_token["role"] != "write":
315-
raise PermissionError(
316-
"Expected the provided hf_token to provide write permissions"
317-
)
318-
319-
320267
def upload_db_to_space(
321268
project: str, uploaded_db: gr.FileData, hf_token: str | None
322269
) -> None:
323270
"""
324271
Uploads the database of a local Trackio project to a Hugging Face Space.
325272
"""
326-
check_auth(hf_token)
273+
fns.check_hf_token_has_write_access(hf_token)
327274
db_project_path = SQLiteStorage.get_project_db_path(project)
328275
if os.path.exists(db_project_path):
329276
raise gr.Error(
@@ -337,7 +284,7 @@ def bulk_upload_media(uploads: list[UploadEntry], hf_token: str | None) -> None:
337284
"""
338285
Uploads media files to a Trackio dashboard. Each entry in the list is a tuple of the project, run, and media file to be uploaded.
339286
"""
340-
check_auth(hf_token)
287+
fns.check_hf_token_has_write_access(hf_token)
341288
for upload in uploads:
342289
media_path = FileStorage.init_project_media_path(
343290
upload["project"], upload["run"], upload["step"]
@@ -357,7 +304,7 @@ def log(
357304
is kept for backwards compatibility for users who are connecting to a newer version of
358305
a Trackio Spaces dashboard with an older version of Trackio installed locally.
359306
"""
360-
check_auth(hf_token)
307+
fns.check_hf_token_has_write_access(hf_token)
361308
SQLiteStorage.log(project=project, run=run, metrics=metrics, step=step)
362309

363310

@@ -368,7 +315,7 @@ def bulk_log(
368315
"""
369316
Logs a list of metrics to a Trackio dashboard. Each entry in the list is a dictionary of the project, run, a dictionary of metrics, and optionally, a step and config.
370317
"""
371-
check_auth(hf_token)
318+
fns.check_hf_token_has_write_access(hf_token)
372319

373320
logs_by_run = {}
374321
for log_entry in logs:
@@ -627,12 +574,17 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]):
627574
628575
if (writeToken) {
629576
setCookie('trackio_write_token', writeToken, 7);
630-
631-
urlParams.delete('write_token');
632-
const newUrl = window.location.pathname +
633-
(urlParams.toString() ? '?' + urlParams.toString() : '') +
634-
window.location.hash;
635-
window.history.replaceState({}, document.title, newUrl);
577+
578+
// Only remove write_token from URL if not in iframe
579+
// In iframes, keep it in URL as cookies may be blocked
580+
const inIframe = window.self !== window.top;
581+
if (!inIframe) {
582+
urlParams.delete('write_token');
583+
const newUrl = window.location.pathname +
584+
(urlParams.toString() ? '?' + urlParams.toString() : '') +
585+
window.location.hash;
586+
window.history.replaceState({}, document.title, newUrl);
587+
}
636588
}
637589
})();
638590
</script>
@@ -968,7 +920,7 @@ def update_dashboard(
968920
master_df = pd.DataFrame()
969921

970922
if master_df.empty:
971-
if space_id := os.environ.get("SPACE_ID"):
923+
if space_id := utils.get_space():
972924
gr.Markdown(INSTRUCTIONS_SPACES.format(space_id))
973925
else:
974926
gr.Markdown(INSTRUCTIONS_LOCAL)

0 commit comments

Comments
 (0)