+
Create a model
diff --git a/lelab/datasets.py b/lelab/datasets.py
index b34c85c3..a93fd20e 100644
--- a/lelab/datasets.py
+++ b/lelab/datasets.py
@@ -133,6 +133,16 @@ def list_user_datasets() -> list[dict[str, Any]]:
return out
+def list_local_datasets_with_source() -> list[dict[str, Any]]:
+ """Local-cache datasets tagged with source="local" — a pure filesystem scan.
+
+ Same entry shape as `list_all_datasets`, but never contacts the Hub. For
+ lightweight callers that only need to know what exists on disk (e.g. a
+ poller) and must not add Hub API load.
+ """
+ return [{**d, "source": "local"} for d in list_local_datasets()]
+
+
def list_all_datasets() -> list[dict[str, Any]]:
"""Merged listing: Hub datasets + local cache, with `source` field.
diff --git a/lelab/server.py b/lelab/server.py
index 519e0c72..5b5fb4d3 100644
--- a/lelab/server.py
+++ b/lelab/server.py
@@ -379,11 +379,15 @@ def hf_auth_login(body: HfLoginBody):
@app.get("/datasets")
-def datasets_list():
+def datasets_list(scope: str = "all"):
"""List datasets available to the user — Hub-owned + local cache.
- Each entry carries a `source` field: "local", "hub", or "both".
+ Each entry carries a `source` field: "local", "hub", or "both". Pass
+ `scope=local` for a local-only listing (a pure filesystem scan, no Hub
+ call) — for lightweight pollers that only need what exists on disk.
"""
+ if scope == "local":
+ return dataset_browser.list_local_datasets_with_source()
return dataset_browser.list_all_datasets()
diff --git a/tests/test_datasets.py b/tests/test_datasets.py
index 2b7cf7f0..c4e20b18 100644
--- a/tests/test_datasets.py
+++ b/tests/test_datasets.py
@@ -86,6 +86,22 @@ def test_list_user_datasets_returns_empty_when_not_logged_in(
assert list_user_datasets() == []
+def test_list_local_datasets_with_source_tags_local_without_hub(
+ tmp_lerobot_home: Path,
+) -> None:
+ from lelab.datasets import list_local_datasets_with_source
+
+ _make_dataset(tmp_lerobot_home, "pusht")
+
+ # The local-only listing must never consult the Hub.
+ with patch("lelab.datasets.list_user_datasets") as hub:
+ result = list_local_datasets_with_source()
+ hub.assert_not_called()
+
+ by_id = {d["repo_id"]: d for d in result}
+ assert by_id["pusht"]["source"] == "local"
+
+
def test_list_all_datasets_merges_hub_and_local(
tmp_lerobot_home: Path,
) -> None:
diff --git a/tests/test_server.py b/tests/test_server.py
index 8b10033f..cd2a8d45 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -248,3 +248,32 @@ def boom(source, name=None):
resp = client.post("/jobs/import", json={"source": "/tmp/x"})
assert resp.status_code == 400
assert "No usable model" in resp.json()["detail"]
+
+
+def test_datasets_local_scope_skips_hub_merge(client, monkeypatch) -> None:
+ """`?scope=local` returns the local-only listing and never runs the
+ Hub-merging path (which would issue a Hugging Face API call)."""
+ from lelab import datasets as datasets_mod
+
+ local = [{"repo_id": "pusht", "last_modified": None, "private": False, "source": "local"}]
+
+ def boom() -> list:
+ raise AssertionError("list_all_datasets must not run for scope=local")
+
+ monkeypatch.setattr(datasets_mod, "list_local_datasets_with_source", lambda: local)
+ monkeypatch.setattr(datasets_mod, "list_all_datasets", boom)
+
+ resp = client.get("/datasets?scope=local")
+ assert resp.status_code == 200
+ assert resp.json() == local
+
+
+def test_datasets_default_scope_uses_merged_listing(client, monkeypatch) -> None:
+ from lelab import datasets as datasets_mod
+
+ merged = [{"repo_id": "x", "last_modified": None, "private": False, "source": "both"}]
+ monkeypatch.setattr(datasets_mod, "list_all_datasets", lambda: merged)
+
+ resp = client.get("/datasets")
+ assert resp.status_code == 200
+ assert resp.json() == merged