Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-trams-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trackio": minor
---

feat:Remove `gradio` dependency in `trackio` -- only `gradio_client` is needed locally anymore. Also lazily import `pandas` and remove it as a dependency
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ authors = [
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"pandas<3.0.0",
"huggingface-hub>=1.10.0,<2",
"gradio[oauth]>=6.10.0,<7.0.0",
"gradio-client>=2.0.0,<3.0.0",
"starlette>=1.0.0,<2",
"python-multipart>=0.0.9,<1.0.0",
"uvicorn[standard]>=0.30.0,<1.0.0",
"numpy<3.0.0",
"pillow<13.0.0",
"orjson>=3.0,<4.0.0",
Expand Down Expand Up @@ -54,6 +56,9 @@ apple-gpu = [
spaces = [
"pyarrow>=21.0",
]
mcp = [
"mcp>=1.0.0,<2.0.0",
]

[project.scripts]
trackio = "trackio.cli:main"
Expand Down
213 changes: 213 additions & 0 deletions tests/e2e-local/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import asyncio
import tempfile
from pathlib import Path

import httpx
import pytest

import trackio
import trackio.utils as trackio_utils
from trackio import Api
from trackio.remote_client import RemoteClient as Client
from trackio.sqlite_storage import SQLiteStorage


Expand Down Expand Up @@ -163,3 +172,207 @@ def test_rename_run(temp_dir, image_ndarray):

new_config = SQLiteStorage.get_run_config(project=project, run=new_name)
assert new_config is not None


def test_local_dashboard_supports_remote_client(temp_dir):
project = "test_local_client"
run_name = "client-run"

trackio.init(project=project, name=run_name)
trackio.log(metrics={"loss": 0.1})
trackio.finish()

app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

try:
client = Client(url, verbose=False)
projects = client.predict(api_name="/get_all_projects")
runs = client.predict(project, api_name="/get_runs_for_project")
settings = client.predict(api_name="/get_settings")

assert project in projects
assert runs == [run_name]
assert "logo_urls" in settings
finally:
trackio.delete_project(project, force=True)
app.close()


def test_local_dashboard_returns_400_for_missing_required_parameter(temp_dir):
app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

try:
resp = httpx.post(
f"{url.rstrip('/')}/api/get_runs_for_project",
json={},
timeout=5,
)

assert resp.status_code == 400
assert resp.json() == {"error": "Missing required parameter: project"}
finally:
app.close()


def test_local_dashboard_file_endpoint_only_serves_trackio_paths(
temp_dir, image_ndarray
):
project = "test_local_file_endpoint"
run_name = "file-run"

trackio.init(project=project, name=run_name)
trackio.log(metrics={"image": trackio.Image(image_ndarray, caption="allowed")})
trackio.finish()

logs = SQLiteStorage.get_logs(project=project, run=run_name)
rel_path = logs[0]["image"]["file_path"]
allowed_path = trackio_utils.MEDIA_DIR / rel_path

app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

try:
allowed_resp = httpx.get(
f"{url.rstrip('/')}/file",
params={"path": str(allowed_path)},
timeout=5,
)
blocked_resp = httpx.get(
f"{url.rstrip('/')}/file",
params={"path": "/etc/hosts"},
timeout=5,
)

assert allowed_resp.status_code == 200
assert blocked_resp.status_code == 404
finally:
trackio.delete_project(project, force=True)
app.close()


def test_local_dashboard_upload_api_accepts_only_server_uploaded_paths(temp_dir):
project = "test_local_upload_guard"
source_path = Path(tempfile.gettempdir()) / "trackio-upload-source.txt"
source_text = "uploaded through server"
source_path.write_text(source_text)
blocked_target = trackio_utils.MEDIA_DIR / project / "files" / "blocked.txt"
allowed_target = None

app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

try:
with source_path.open("rb") as handle:
upload_resp = httpx.post(
f"{url.rstrip('/')}/api/upload",
files={"files": (source_path.name, handle)},
timeout=5,
)
upload_resp.raise_for_status()
uploaded_path = upload_resp.json()["paths"][0]
allowed_target = (
trackio_utils.MEDIA_DIR
/ project
/ "files"
/ "allowed.txt"
/ Path(uploaded_path).name
)

allowed_resp = httpx.post(
f"{url.rstrip('/')}/api/bulk_upload_media",
json={
"uploads": [
{
"project": project,
"run": None,
"step": None,
"relative_path": "allowed.txt",
"uploaded_file": {"path": uploaded_path},
}
],
"hf_token": None,
},
timeout=5,
)
blocked_resp = httpx.post(
f"{url.rstrip('/')}/api/bulk_upload_media",
json={
"uploads": [
{
"project": project,
"run": None,
"step": None,
"relative_path": "blocked.txt",
"uploaded_file": {"path": "/etc/hosts"},
}
],
"hf_token": None,
},
timeout=5,
)

assert allowed_resp.status_code == 200
assert allowed_target is not None
assert allowed_target.read_text() == source_text
assert blocked_resp.status_code == 400
assert blocked_resp.json() == {
"error": "Uploaded file was not created by this Trackio server."
}
assert not blocked_target.exists()
finally:
source_path.unlink(missing_ok=True)
trackio.delete_project(project, force=True)
app.close()


def test_local_dashboard_supports_mcp(temp_dir):
pytest.importorskip("mcp")

project = "test_local_mcp"
run_name = "mcp-run"

trackio.init(project=project, name=run_name)
trackio.log(metrics={"loss": 0.1})
trackio.finish()

app, url, _, _ = trackio.show(
block_thread=False,
open_browser=False,
mcp_server=True,
)

async def check_mcp() -> None:
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client(f"{url.rstrip('/')}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools = await session.list_tools()
tool_names = {tool.name for tool in tools.tools}
assert "get_all_projects" in tool_names
assert "get_run_summary" in tool_names

projects = await session.call_tool("get_all_projects")
assert project in projects.structuredContent["result"]

runs = await session.call_tool(
"get_runs_for_project",
{"project": project},
)
assert runs.structuredContent["result"] == [run_name]

run_summary = await session.call_tool(
"get_run_summary",
{"project": project, "run": run_name},
)
assert run_summary.structuredContent["run"] == run_name
assert run_summary.structuredContent["num_logs"] == 1

try:
asyncio.run(check_mcp())
finally:
trackio.delete_project(project, force=True)
app.close()
17 changes: 7 additions & 10 deletions tests/e2e-local/test_table_with_images.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""End-to-end test for Table with TrackioImage functionality."""

import pandas as pd

import trackio
from trackio.media import TrackioImage
from trackio.sqlite_storage import SQLiteStorage
Expand All @@ -16,15 +14,14 @@ def test_table_mixed_images_and_regular_data(image_ndarray, temp_dir):

img = TrackioImage(image_ndarray, caption="Only Image")

df = pd.DataFrame(
{
"experiment": ["exp1", "exp2", "exp3"],
"result_image": [img, None, img],
"score": [0.75, 0.80, 0.85],
}
table = Table(
columns=["experiment", "result_image", "score"],
data=[
["exp1", img, 0.75],
["exp2", None, 0.80],
["exp3", img, 0.85],
],
)

table = Table(dataframe=df)
trackio.log({"mixed_results": table})
trackio.finish()

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e-spaces/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import huggingface_hub
import pytest
from gradio_client import Client
from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError

from trackio import deploy, utils
from trackio.remote_client import RemoteClient as Client


@pytest.fixture(scope="session")
Expand Down
3 changes: 1 addition & 2 deletions tests/e2e-spaces/test_data_robustness.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import secrets
import time

from gradio_client import Client

import trackio
from trackio.remote_client import RemoteClient as Client
from trackio.sqlite_storage import SQLiteStorage


Expand Down
2 changes: 1 addition & 1 deletion tests/e2e-spaces/test_metrics_on_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import huggingface_hub
import pytest
from gradio_client import Client

import trackio
from trackio import utils
from trackio.remote_client import RemoteClient as Client


def _predict_run_summary(
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e-spaces/test_spaces_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import numpy as np
import pytest
from gradio_client import Client

import trackio
from trackio.remote_client import RemoteClient as Client


def _predict_run_summary(
Expand Down
16 changes: 8 additions & 8 deletions tests/e2e-spaces/test_sync_and_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from pathlib import Path

import huggingface_hub
import pandas as pd
from gradio_client import Client
import pyarrow.parquet as pq

import trackio
from trackio import deploy, utils
from trackio.remote_client import RemoteClient as Client


def _wait_for_space_ready(space_id, timeout=300):
Expand All @@ -30,7 +30,7 @@ def _download_parquet_from_bucket(bucket_id, remote_name="metrics.parquet"):
files=[(remote_name, str(local_path))],
token=huggingface_hub.utils.get_token(),
)
return pd.read_parquet(local_path)
return pq.read_table(local_path).to_pylist()


def _cleanup_space(space_id):
Expand Down Expand Up @@ -104,7 +104,7 @@ def test_sync_to_static_space_incremental(test_space_id, temp_dir):

df1 = _download_parquet_from_bucket(bucket_id)
assert len(df1) == 2
assert "loss" in df1.columns
assert "loss" in df1[0]

trackio.init(project=project_name, name=run_name)
trackio.log({"loss": 0.1})
Expand All @@ -115,7 +115,7 @@ def test_sync_to_static_space_incremental(test_space_id, temp_dir):

df2 = _download_parquet_from_bucket(bucket_id)
assert len(df2) == 4
assert sorted(df2["loss"].tolist()) == [0.05, 0.1, 0.3, 0.5]
assert sorted(row["loss"] for row in df2) == [0.05, 0.1, 0.3, 0.5]
finally:
_cleanup_space(space_id)
_cleanup_bucket(bucket_id)
Expand Down Expand Up @@ -154,9 +154,9 @@ def test_sync_gradio_then_freeze_to_static(test_space_id, temp_dir):

df = _download_parquet_from_bucket(frozen_bucket_id)
assert len(df) == 3
assert "loss" in df.columns
assert "acc" in df.columns
assert sorted(df["loss"].tolist()) == [0.1, 0.3, 0.5]
assert "loss" in df[0]
assert "acc" in df[0]
assert sorted(row["loss"] for row in df) == [0.1, 0.3, 0.5]
finally:
_cleanup_space(frozen_space_id)
_cleanup_bucket(frozen_bucket_id)
3 changes: 1 addition & 2 deletions tests/e2e-spaces/test_throughput.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import threading
import time

from gradio_client import Client

import trackio
from trackio.remote_client import RemoteClient as Client


def test_burst_2000_logs_single_process(test_space_id, wait_for_client):
Expand Down
Loading
Loading