Skip to content

Commit f7dd1ce

Browse files
Saba9gradio-pr-botabidlabs
authored
feat: add ability to rename runs (#428)
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co> Co-authored-by: Abubakar Abid <islamrealm@gmail.com>
1 parent 2b6c865 commit f7dd1ce

10 files changed

Lines changed: 703 additions & 131 deletions

File tree

.changeset/huge-meals-rescue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trackio": minor
3+
---
4+
5+
feat:feat: add ability to rename runs

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description = "A lightweight, local-first, and free experiment tracking library
88
authors = [
99
{ name = "Abubakar Abid", email = "abubakar@huggingface.co" },
1010
{ name = "Zach Nation", email = "zach@huggingface.co" },
11+
{ name = "Saba Noorassa", email = "saba@huggingface.co" },
1112
]
1213
readme = "README.md"
1314
requires-python = ">=3.10"

tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ def temp_dir(monkeypatch):
2424
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
2525
for name in ["trackio.sqlite_storage"]:
2626
monkeypatch.setattr(f"{name}.TRACKIO_DIR", Path(tmpdir))
27-
for name in ["trackio.media.media", "trackio.media.utils", "trackio.utils"]:
27+
for name in [
28+
"trackio.media.media",
29+
"trackio.media.utils",
30+
"trackio.utils",
31+
"trackio.sqlite_storage",
32+
]:
2833
monkeypatch.setattr(f"{name}.MEDIA_DIR", Path(tmpdir) / "media")
2934
context_vars.current_run.set(None)
3035
context_vars.current_project.set(None)

tests/e2e-local/test_api.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,65 @@ def test_move_run(temp_dir, image_ndarray):
101101

102102
target_config = SQLiteStorage.get_run_config(project=target_project, run=run_name)
103103
assert target_config is not None
104+
105+
106+
def test_rename_run(temp_dir, image_ndarray):
107+
project = "test_rename_project"
108+
old_name = "old_run_name"
109+
new_name = "new_run_name"
110+
111+
trackio.init(project=project, name=old_name)
112+
113+
image1 = trackio.Image(image_ndarray, caption="test_image_1")
114+
image2 = trackio.Image(image_ndarray, caption="test_image_2")
115+
116+
trackio.log(metrics={"loss": 0.1, "acc": 0.9, "img1": image1})
117+
trackio.log(metrics={"loss": 0.2, "acc": 0.95, "img2": image2})
118+
trackio.finish()
119+
120+
old_logs = SQLiteStorage.get_logs(project=project, run=old_name)
121+
assert len(old_logs) == 2
122+
assert old_logs[0]["loss"] == 0.1
123+
assert old_logs[1]["loss"] == 0.2
124+
125+
image1_path = old_logs[0]["img1"].get("file_path")
126+
assert image1_path is not None
127+
normalized_path = str(image1_path).replace("\\", "/")
128+
assert normalized_path.startswith(f"{project}/{old_name}/")
129+
130+
api = Api()
131+
runs = api.runs(project)
132+
run = runs[0]
133+
assert run.name == old_name
134+
135+
result = run.rename(new_name)
136+
assert result is run
137+
assert run.name == new_name
138+
139+
new_logs = SQLiteStorage.get_logs(project=project, run=new_name)
140+
assert len(new_logs) == 2
141+
assert new_logs[0]["loss"] == 0.1
142+
assert new_logs[1]["loss"] == 0.2
143+
144+
new_image1_path = new_logs[0]["img1"].get("file_path")
145+
assert new_image1_path is not None
146+
normalized_new_path1 = str(new_image1_path).replace("\\", "/")
147+
assert normalized_new_path1.startswith(f"{project}/{new_name}/")
148+
149+
new_image2_path = new_logs[1]["img2"].get("file_path")
150+
assert new_image2_path is not None
151+
normalized_new_path2 = str(new_image2_path).replace("\\", "/")
152+
assert normalized_new_path2.startswith(f"{project}/{new_name}/")
153+
154+
old_logs_after = SQLiteStorage.get_logs(project=project, run=old_name)
155+
assert len(old_logs_after) == 0
156+
157+
runs_after = SQLiteStorage.get_runs(project=project)
158+
assert old_name not in runs_after
159+
assert new_name in runs_after
160+
161+
old_config_after = SQLiteStorage.get_run_config(project=project, run=old_name)
162+
assert old_config_after is None
163+
164+
new_config = SQLiteStorage.get_run_config(project=project, run=new_name)
165+
assert new_config is not None

tests/unit/test_sqlite_storage.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,126 @@ def test_get_runs_returns_chronological_order(temp_dir):
274274

275275
runs = SQLiteStorage.get_runs("proj")
276276
assert runs == ["run-z", "run-a", "run-m"]
277+
278+
279+
def test_rename_run(temp_dir):
280+
project = "test_project"
281+
old_name = "old_run"
282+
new_name = "new_run"
283+
284+
config = {"param1": "value1", "_Created": "2023-01-01T00:00:00"}
285+
metrics = [{"accuracy": 0.95, "loss": 0.1}]
286+
SQLiteStorage.bulk_log(project, old_name, metrics, config=config)
287+
288+
assert SQLiteStorage.get_run_config(project, old_name) is not None
289+
assert len(SQLiteStorage.get_logs(project, old_name)) > 0
290+
291+
SQLiteStorage.rename_run(project, old_name, new_name)
292+
293+
assert SQLiteStorage.get_run_config(project, old_name) is None
294+
assert len(SQLiteStorage.get_logs(project, old_name)) == 0
295+
296+
assert SQLiteStorage.get_run_config(project, new_name) is not None
297+
assert len(SQLiteStorage.get_logs(project, new_name)) > 0
298+
299+
new_logs = SQLiteStorage.get_logs(project, new_name)
300+
assert new_logs[0]["accuracy"] == 0.95
301+
assert new_logs[0]["loss"] == 0.1
302+
303+
304+
def test_rename_run_duplicate_name(temp_dir):
305+
project = "test_project"
306+
run1 = "run1"
307+
run2 = "run2"
308+
309+
SQLiteStorage.bulk_log(project, run1, [{"a": 1}])
310+
SQLiteStorage.bulk_log(project, run2, [{"b": 2}])
311+
312+
with pytest.raises(ValueError, match="already exists"):
313+
SQLiteStorage.rename_run(project, run1, run2)
314+
315+
assert len(SQLiteStorage.get_logs(project, run1)) > 0
316+
assert len(SQLiteStorage.get_logs(project, run2)) > 0
317+
318+
319+
def test_rename_run_with_media(temp_dir):
320+
from trackio.utils import MEDIA_DIR
321+
322+
project = "test_project"
323+
old_name = "old_run"
324+
new_name = "new_run"
325+
326+
media_dir = MEDIA_DIR / project / old_name
327+
media_dir.mkdir(parents=True, exist_ok=True)
328+
test_file = media_dir / "test.txt"
329+
test_file.write_text("test content")
330+
331+
metrics = [
332+
{
333+
"image": {
334+
"_type": "trackio.image",
335+
"file_path": f"{project}/{old_name}/test.txt",
336+
"caption": "test",
337+
}
338+
}
339+
]
340+
SQLiteStorage.bulk_log(project, old_name, metrics)
341+
342+
SQLiteStorage.rename_run(project, old_name, new_name)
343+
344+
new_media_dir = MEDIA_DIR / project / new_name
345+
assert new_media_dir.exists()
346+
assert (new_media_dir / "test.txt").exists()
347+
348+
old_media_dir = MEDIA_DIR / project / old_name
349+
assert not old_media_dir.exists()
350+
351+
new_logs = SQLiteStorage.get_logs(project, new_name)
352+
assert len(new_logs) > 0
353+
assert "image" in new_logs[0]
354+
assert new_logs[0]["image"]["file_path"].startswith(f"{project}/{new_name}/")
355+
356+
357+
def test_rename_run_nonexistent(temp_dir):
358+
project = "test_project"
359+
old_name = "nonexistent_run"
360+
new_name = "new_run"
361+
362+
with pytest.raises(ValueError, match="does not exist"):
363+
SQLiteStorage.rename_run(project, old_name, new_name)
364+
365+
366+
def test_rename_run_empty_name(temp_dir):
367+
project = "test_project"
368+
old_name = "old_run"
369+
370+
SQLiteStorage.bulk_log(project, old_name, [{"a": 1}])
371+
372+
with pytest.raises(ValueError, match="cannot be empty"):
373+
SQLiteStorage.rename_run(project, old_name, "")
374+
375+
with pytest.raises(ValueError, match="cannot be empty"):
376+
SQLiteStorage.rename_run(project, old_name, " ")
377+
378+
assert len(SQLiteStorage.get_logs(project, old_name)) > 0
379+
380+
381+
def test_rename_run_with_system_metrics(temp_dir):
382+
project = "test_project"
383+
old_name = "old_run"
384+
new_name = "new_run"
385+
386+
metrics = [{"accuracy": 0.95}]
387+
SQLiteStorage.bulk_log(project, old_name, metrics)
388+
389+
system_metrics = [{"gpu_usage": 80.5}]
390+
SQLiteStorage.bulk_log_system(project, old_name, system_metrics)
391+
392+
SQLiteStorage.rename_run(project, old_name, new_name)
393+
394+
assert len(SQLiteStorage.get_logs(project, new_name)) > 0
395+
assert len(SQLiteStorage.get_system_logs(project, new_name)) > 0
396+
assert len(SQLiteStorage.get_system_logs(project, old_name)) == 0
397+
398+
new_system_logs = SQLiteStorage.get_system_logs(project, new_name)
399+
assert new_system_logs[0]["gpu_usage"] == 80.5

trackio/api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ def move(self, new_project: str) -> bool:
2828
self.project = new_project
2929
return success
3030

31+
def rename(self, new_name: str) -> "Run":
32+
SQLiteStorage.rename_run(self.project, self.name, new_name)
33+
self.name = new_name
34+
return self
35+
3136
def __repr__(self) -> str:
3237
return f"<Run {self.name} in project {self.project}>"
3338

0 commit comments

Comments
 (0)