Skip to content

Commit dd3ae02

Browse files
committed
Disallow private static Trackio snapshots
1 parent 675ef66 commit dd3ae02

6 files changed

Lines changed: 122 additions & 24 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ trackio.sync(
218218

219219
This uploads your local project database to a new or existing Space. The Space will display all your logged experiments and metrics, and if a custom frontend is configured or passed explicitly it will be deployed there too.
220220

221+
Static Trackio Spaces (`sdk="static"`) are read-only browser-only snapshots, so their snapshot data must be public. Use the default Gradio Space for private dashboards; `sdk="static"` does not support `private=True`.
222+
221223
**Example workflow:**
222224

223225
```py

docs/source/cli_commands.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Deploy as a static Space (reads from an HF Bucket, no server needed):
3939
trackio sync --project "my-project" --space-id "username/space_id" --sdk static
4040
```
4141

42+
Static Spaces serve data directly from the browser and therefore require public snapshot data. `--sdk static --private` is not supported; use the default Gradio SDK for private dashboards.
43+
4244
Sync all projects that have unsynced data to their configured Spaces:
4345

4446
```sh
@@ -51,7 +53,7 @@ trackio sync --all
5153
| `--space-id` | The HF Space ID to sync to (e.g. `username/space_id`). If not provided, uses the previously-configured Space |
5254
| `--all` | Sync all projects with unsynced data |
5355
| `--sdk` | `gradio` (default) for a live server, or `static` for a read-only bucket-backed Space |
54-
| `--private` | Make the Space private if creating a new one |
56+
| `--private` | Make the Space private if creating a new Gradio Space. Not supported with `--sdk static` |
5557
| `--force` | Overwrite the existing database without prompting |
5658

5759
## Freeze Command
@@ -73,10 +75,11 @@ trackio freeze --space-id "username/my-space" --project "my-project" --new-space
7375
| `--space-id` | The source Gradio Space ID (required) |
7476
| `--project` | The project to freeze (required) |
7577
| `--new-space-id` | The destination static Space ID. Defaults to `{space_id}_static` |
76-
| `--private` | Make the new static Space private |
78+
| `--private` | Not supported for static frozen snapshots |
7779

7880
> **Note:** The source must be a Gradio Space with a bucket mounted at `/data`. If the destination Space already exists and is not a Trackio static Space, `freeze` will refuse to overwrite it.
7981
> The frozen Space is a snapshot. Later metrics synced to the original Gradio Space do not appear in the frozen static Space unless you run `freeze` again.
82+
> Static frozen snapshots require public destination data, so `trackio freeze --private` is not supported.
8083
8184
## List Commands
8285

docs/source/deploy_embed.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ trackio.sync(project="my-project", space_id="username/space_id", sdk="static")
4242
trackio sync --project "my-project" --space-id "username/space_id" --sdk static
4343
```
4444

45-
Static Spaces are lightweight and free — they serve a read-only dashboard backed by Parquet files in an HF Bucket.
45+
Static Spaces are lightweight and free — they serve a read-only dashboard backed by Parquet files in an HF Bucket. Because the dashboard runs entirely in the browser, static Trackio Spaces require public snapshot data and do not support `private=True`. Use the default Gradio Space (`sdk="gradio"`) for private dashboards.
4646

4747
## Freezing a Space Snapshot
4848

@@ -61,6 +61,7 @@ trackio freeze --space-id "username/my-space" --project "my-project"
6161
This creates a new static Space (by default named `{space_id}_static`) containing a snapshot of the project's data from the source Space's bucket. The original Space is not modified.
6262

6363
Note that`freeze()` is a one-time snapshot. If new metrics are later uploaded to the original Gradio Space, the frozen static Space will not update automatically.
64+
The source Gradio Space can use a private bucket, but the frozen static snapshot is public data. `freeze(private=True)` is not supported; use a Gradio Space if the frozen dashboard must stay private.
6465

6566
You can customize the destination:
6667

@@ -69,7 +70,6 @@ trackio.freeze(
6970
space_id="username/my-space",
7071
project="my-project",
7172
new_space_id="username/my-snapshot",
72-
private=True,
7373
)
7474
```
7575

tests/unit/test_deploy.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import io
2+
import json
23
from types import SimpleNamespace
34
from unittest.mock import patch
45

6+
import pytest
7+
58
from trackio import deploy
69
from trackio.bucket_storage import _list_bucket_file_paths
710
from trackio.frontend_config import ResolvedFrontend
@@ -133,3 +136,86 @@ def test_deploy_as_static_space_uploads_resolved_frontend(tmp_path, monkeypatch)
133136
assert any(
134137
call["folder_path"] == str(frontend_dir) for call in fake_api.uploaded_folders
135138
)
139+
140+
141+
def test_deploy_as_static_space_does_not_embed_hf_token(tmp_path, monkeypatch):
142+
frontend_dir = tmp_path / "custom-static"
143+
frontend_dir.mkdir()
144+
(frontend_dir / "index.html").write_text("<!doctype html>")
145+
146+
fake_api = _FakeHfApi()
147+
monkeypatch.setattr(deploy.huggingface_hub, "HfApi", lambda: fake_api)
148+
monkeypatch.setattr(deploy.huggingface_hub, "create_repo", lambda *a, **k: None)
149+
monkeypatch.setattr(
150+
deploy,
151+
"resolve_frontend_dir",
152+
lambda frontend_dir=None, announce=False: ResolvedFrontend(
153+
path=frontend_dir.resolve(),
154+
source="argument",
155+
is_custom=True,
156+
),
157+
)
158+
159+
deploy.deploy_as_static_space(
160+
"abidlabs/static-space",
161+
None,
162+
"demo-project",
163+
bucket_id="abidlabs/static-bucket",
164+
private=False,
165+
hf_token="hf_should_not_be_serialized",
166+
frontend_dir=frontend_dir,
167+
)
168+
169+
config_upload = next(
170+
item for item in fake_api.uploaded_files if item["path_in_repo"] == "config.json"
171+
)
172+
config = json.loads(config_upload["payload"])
173+
assert "hf_token" not in config
174+
175+
176+
def test_deploy_as_static_space_rejects_private(monkeypatch):
177+
monkeypatch.setattr(
178+
deploy.huggingface_hub,
179+
"HfApi",
180+
lambda: pytest.fail("HfApi should not be constructed"),
181+
)
182+
183+
with pytest.raises(ValueError, match="private static Trackio Space"):
184+
deploy.deploy_as_static_space(
185+
"abidlabs/static-space",
186+
None,
187+
"demo-project",
188+
private=True,
189+
hf_token="hf_should_not_be_used",
190+
)
191+
192+
193+
def test_sync_static_rejects_private_before_network(monkeypatch):
194+
monkeypatch.setattr(
195+
deploy.huggingface_hub,
196+
"HfApi",
197+
lambda: pytest.fail("HfApi should not be constructed"),
198+
)
199+
200+
with pytest.raises(ValueError, match="private static Trackio Space"):
201+
deploy.sync(
202+
project="demo-project",
203+
space_id="abidlabs/static-space",
204+
sdk="static",
205+
private=True,
206+
)
207+
208+
209+
def test_freeze_rejects_private_before_network(monkeypatch):
210+
monkeypatch.setattr(
211+
deploy.huggingface_hub,
212+
"HfApi",
213+
lambda: pytest.fail("HfApi should not be constructed"),
214+
)
215+
216+
with pytest.raises(ValueError, match="private static Trackio Space"):
217+
deploy.freeze(
218+
space_id="abidlabs/source-space",
219+
project="demo-project",
220+
private=True,
221+
)

trackio/deploy.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@
5050
_RESET = "\033[0m"
5151

5252

53+
def _raise_if_private_static_requested(action: str, private: bool | None) -> None:
54+
if private is True:
55+
raise ValueError(
56+
f"Cannot {action} a private static Trackio Space. Static Spaces run "
57+
"entirely in the browser, so reading private datasets or buckets would "
58+
"require exposing a Hugging Face token to viewers. Use sdk='gradio' for "
59+
"a private dashboard, or omit private=True to create a public static "
60+
"snapshot."
61+
)
62+
63+
5364
def raise_if_space_is_frozen_for_logging(space_id: str) -> None:
5465
try:
5566
info = huggingface_hub.HfApi().space_info(space_id)
@@ -902,12 +913,13 @@ def deploy_as_static_space(
902913
if on_spaces():
903914
return
904915

916+
_raise_if_private_static_requested("deploy", private)
905917
hf_api = huggingface_hub.HfApi()
906918

907919
try:
908920
huggingface_hub.create_repo(
909921
space_id,
910-
private=private,
922+
private=False,
911923
space_sdk="static",
912924
repo_type="space",
913925
exist_ok=True,
@@ -918,7 +930,7 @@ def deploy_as_static_space(
918930
huggingface_hub.login(add_to_git_credential=False)
919931
huggingface_hub.create_repo(
920932
space_id,
921-
private=private,
933+
private=False,
922934
space_sdk="static",
923935
repo_type="space",
924936
exist_ok=True,
@@ -960,8 +972,6 @@ def deploy_as_static_space(
960972
config["bucket_id"] = bucket_id
961973
if dataset_id is not None:
962974
config["dataset_id"] = dataset_id
963-
if hf_token and private:
964-
config["hf_token"] = hf_token
965975

966976
_retry_hf_write(
967977
"Static Space config upload",
@@ -1017,7 +1027,8 @@ def sync(
10171027
private (`bool`, *optional*):
10181028
Whether to make the Space private. If None (default), the repo will be
10191029
public unless the organization's default is private. This value is ignored
1020-
if the repo already exists.
1030+
if the repo already exists. Not supported with ``sdk="static"`` because
1031+
static Trackio dashboards read snapshot data directly from the browser.
10211032
force (`bool`, *optional*, defaults to `False`):
10221033
If `True`, overwrite the existing database without prompting for confirmation.
10231034
If `False`, prompt the user before overwriting an existing database.
@@ -1038,6 +1049,8 @@ def sync(
10381049
"""
10391050
if sdk not in ("gradio", "static"):
10401051
raise ValueError(f"sdk must be 'gradio' or 'static', got '{sdk}'")
1052+
if sdk == "static":
1053+
_raise_if_private_static_requested("sync", private)
10411054
bucket_id_was_explicit = bucket_id is not None
10421055

10431056
if space_id is None:
@@ -1064,18 +1077,16 @@ def _do_sync():
10641077

10651078
if sdk == "static":
10661079
if dataset_id is not None:
1067-
upload_dataset_for_static(project, dataset_id, private=private)
1068-
hf_token = huggingface_hub.utils.get_token() if private else None
1080+
upload_dataset_for_static(project, dataset_id, private=False)
10691081
deploy_as_static_space(
10701082
space_id,
10711083
dataset_id,
10721084
project,
1073-
private=private,
1074-
hf_token=hf_token,
1085+
private=False,
10751086
frontend_dir=frontend_dir,
10761087
)
10771088
elif bucket_id is not None:
1078-
create_bucket_if_not_exists(bucket_id, private=private)
1089+
create_bucket_if_not_exists(bucket_id, private=False)
10791090
upload_project_to_bucket_for_static(project, bucket_id)
10801091
print(
10811092
f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}"
@@ -1085,8 +1096,7 @@ def _do_sync():
10851096
None,
10861097
project,
10871098
bucket_id=bucket_id,
1088-
private=private,
1089-
hf_token=huggingface_hub.utils.get_token() if private else None,
1099+
private=False,
10901100
frontend_dir=frontend_dir,
10911101
)
10921102
else:
@@ -1165,15 +1175,16 @@ def freeze(
11651175
The ID for the new static Space. If not provided, defaults to
11661176
`"{space_id}_static"`.
11671177
private (`bool`, *optional*):
1168-
Whether to make the new Space private. If None (default), the repo
1169-
will be public unless the organization's default is private.
1178+
Not supported. Frozen static dashboards read snapshot data directly
1179+
from the browser, so the destination snapshot must be public.
11701180
bucket_id (`str`, *optional*):
11711181
The ID of the HF Bucket for the new static Space's data storage.
11721182
If not provided, one is auto-generated from the new Space ID.
11731183
11741184
Returns:
11751185
`str`: The Space ID of the newly created static Space.
11761186
"""
1187+
_raise_if_private_static_requested("freeze", private)
11771188
space_id, _, _ = preprocess_space_and_dataset_ids(space_id, None, None)
11781189

11791190
try:
@@ -1214,7 +1225,7 @@ def freeze(
12141225
except RepositoryNotFoundError:
12151226
pass
12161227

1217-
create_bucket_if_not_exists(bucket_id, private=private)
1228+
create_bucket_if_not_exists(bucket_id, private=False)
12181229
export_from_bucket_for_static(source_bucket_id, bucket_id, project)
12191230
print(
12201231
f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}"
@@ -1224,8 +1235,7 @@ def freeze(
12241235
None,
12251236
project,
12261237
bucket_id=bucket_id,
1227-
private=private,
1228-
hf_token=huggingface_hub.utils.get_token() if private else None,
1238+
private=False,
12291239
frontend_dir=frontend_dir,
12301240
)
12311241
return new_space_id

trackio/frontend/src/lib/staticApi.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ function resolveUrl(filename) {
1616
}
1717

1818
function authHeaders() {
19-
if (config.hf_token) {
20-
return { Authorization: `Bearer ${config.hf_token}` };
21-
}
2219
return {};
2320
}
2421

0 commit comments

Comments
 (0)