Skip to content

Commit 6455028

Browse files
authored
Merge pull request #1858 from transformerlab/add/cli-model-dataset
Add/cli model dataset
2 parents 26faef9 + f1d10af commit 6455028

6 files changed

Lines changed: 565 additions & 48 deletions

File tree

cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "transformerlab-cli"
7-
version = "0.0.26"
7+
version = "0.0.27"
88
description = "Transformer Lab CLI"
99
requires-python = ">=3.10"
1010
authors = [{ name = "Transformer Lab", email = "hello@transformerlab.ai" }]
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import json
2+
import os
3+
4+
import typer
5+
6+
import transformerlab_cli.util.api as api
7+
from transformerlab_cli.state import cli_state
8+
from transformerlab_cli.util.config import check_configs
9+
from transformerlab_cli.util.ui import console, render_table, render_object
10+
11+
app = typer.Typer()
12+
13+
14+
def _extract_error(response) -> str:
15+
try:
16+
return response.json().get("detail", response.text)
17+
except Exception:
18+
return response.text
19+
20+
21+
# ──────────────────────────────────────────────
22+
# list
23+
# ──────────────────────────────────────────────
24+
25+
26+
@app.command("list")
27+
def command_dataset_list():
28+
"""List all dataset groups on the server."""
29+
check_configs(output_format=cli_state.output_format)
30+
31+
endpoint = "/asset_versions/groups?asset_type=dataset"
32+
if cli_state.output_format != "json":
33+
with console.status("[bold success]Fetching datasets...[/bold success]", spinner="dots"):
34+
response = api.get(endpoint)
35+
else:
36+
response = api.get(endpoint)
37+
38+
if response.status_code == 200:
39+
datasets = response.json()
40+
table_columns = ["group_id", "group_name", "latest_version_label", "version_count", "tags"]
41+
render_table(data=datasets, format_type=cli_state.output_format, table_columns=table_columns, title="Datasets")
42+
else:
43+
if cli_state.output_format == "json":
44+
print(json.dumps({"error": f"Failed to fetch datasets. Status code: {response.status_code}"}))
45+
raise typer.Exit(1)
46+
console.print(f"[error]Error:[/error] Failed to fetch datasets. Status code: {response.status_code}")
47+
raise typer.Exit(1)
48+
49+
50+
# ──────────────────────────────────────────────
51+
# info
52+
# ──────────────────────────────────────────────
53+
54+
55+
@app.command("info")
56+
def command_dataset_info(
57+
group_id: str = typer.Argument(..., help="The dataset group_id or group_name to inspect"),
58+
):
59+
"""Show detailed information about a specific dataset group."""
60+
check_configs(output_format=cli_state.output_format)
61+
62+
endpoint = "/asset_versions/groups?asset_type=dataset"
63+
if cli_state.output_format != "json":
64+
with console.status("[bold success]Fetching dataset info...[/bold success]", spinner="dots"):
65+
response = api.get(endpoint)
66+
else:
67+
response = api.get(endpoint)
68+
69+
if response.status_code != 200:
70+
if cli_state.output_format == "json":
71+
print(json.dumps({"error": f"Failed to fetch datasets. Status code: {response.status_code}"}))
72+
raise typer.Exit(1)
73+
console.print(f"[error]Error:[/error] Failed to fetch datasets. {_extract_error(response)}")
74+
raise typer.Exit(1)
75+
76+
datasets = response.json()
77+
dataset = next(
78+
(d for d in datasets if d.get("group_id") == group_id or d.get("group_name") == group_id),
79+
None,
80+
)
81+
82+
if dataset is None:
83+
if cli_state.output_format == "json":
84+
print(json.dumps({"error": f"Dataset '{group_id}' not found."}))
85+
else:
86+
console.print(f"[error]Error:[/error] Dataset [bold]{group_id}[/bold] not found.")
87+
raise typer.Exit(1)
88+
89+
if cli_state.output_format == "json":
90+
print(json.dumps(dataset, indent=2, default=str))
91+
else:
92+
render_object(dataset)
93+
94+
95+
# ──────────────────────────────────────────────
96+
# delete
97+
# ──────────────────────────────────────────────
98+
99+
100+
@app.command("delete")
101+
def command_dataset_delete(
102+
group_id: str = typer.Argument(..., help="The dataset group_id to delete"),
103+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
104+
):
105+
"""Delete a dataset group and all its versions."""
106+
check_configs(output_format=cli_state.output_format)
107+
108+
if not yes and cli_state.output_format != "json":
109+
confirmed = typer.confirm(
110+
f"Are you sure you want to delete dataset group '{group_id}' and ALL its versions?", default=False
111+
)
112+
if not confirmed:
113+
console.print("[warning]Aborted.[/warning]")
114+
raise typer.Exit(0)
115+
116+
if cli_state.output_format != "json":
117+
with console.status(f"[bold success]Deleting dataset group '{group_id}'...[/bold success]", spinner="dots"):
118+
response = api.delete(f"/asset_versions/groups/dataset/{group_id}")
119+
else:
120+
response = api.delete(f"/asset_versions/groups/dataset/{group_id}")
121+
122+
if response.status_code == 200:
123+
body = response.json()
124+
if cli_state.output_format == "json":
125+
print(json.dumps(body))
126+
else:
127+
count = body.get("deleted_count", "?")
128+
console.print(
129+
f"[success]✓[/success] Dataset group [bold]{group_id}[/bold] deleted ({count} version(s) removed)."
130+
)
131+
else:
132+
if cli_state.output_format == "json":
133+
print(json.dumps({"error": f"Failed to delete dataset. Status code: {response.status_code}"}))
134+
raise typer.Exit(1)
135+
console.print(f"[error]Error:[/error] Failed to delete dataset. {_extract_error(response)}")
136+
raise typer.Exit(1)
137+
138+
139+
# ──────────────────────────────────────────────
140+
# edit
141+
# ──────────────────────────────────────────────
142+
143+
144+
@app.command("edit")
145+
def command_dataset_edit(
146+
group_id: str = typer.Argument(..., help="The dataset group_id to update"),
147+
name: str = typer.Option(None, "--name", help="New display name for the dataset group"),
148+
description: str = typer.Option(None, "--description", help="New description for the dataset group"),
149+
):
150+
"""Edit the name or description of a dataset group."""
151+
check_configs(output_format=cli_state.output_format)
152+
153+
payload: dict = {}
154+
if name:
155+
payload["name"] = name
156+
if description:
157+
payload["description"] = description
158+
159+
if not payload:
160+
console.print("[warning]Nothing to update. Provide --name and/or --description.[/warning]")
161+
raise typer.Exit(0)
162+
163+
if cli_state.output_format != "json":
164+
with console.status(f"[bold success]Updating dataset group '{group_id}'...[/bold success]", spinner="dots"):
165+
response = api.patch(f"/asset_versions/groups/dataset/{group_id}", json_data=payload)
166+
else:
167+
response = api.patch(f"/asset_versions/groups/dataset/{group_id}", json_data=payload)
168+
169+
if response.status_code == 200:
170+
body = response.json()
171+
if cli_state.output_format == "json":
172+
print(json.dumps(body, indent=2, default=str))
173+
else:
174+
console.print(f"[success]✓[/success] Dataset group [bold]{group_id}[/bold] updated.")
175+
else:
176+
if cli_state.output_format == "json":
177+
print(json.dumps({"error": f"Failed to update dataset. Status code: {response.status_code}"}))
178+
raise typer.Exit(1)
179+
console.print(f"[error]Error:[/error] Failed to update dataset. {_extract_error(response)}")
180+
raise typer.Exit(1)
181+
182+
183+
# ──────────────────────────────────────────────
184+
# upload
185+
# ──────────────────────────────────────────────
186+
187+
188+
@app.command("upload")
189+
def command_dataset_upload(
190+
dataset_id: str = typer.Argument(..., help="The dataset ID (will be created if it does not exist)"),
191+
files: list[str] = typer.Argument(..., help="One or more local files to upload (.jsonl, .json, or .csv)"),
192+
):
193+
"""Upload local files to a dataset (creates the dataset if it does not exist).
194+
195+
Example:
196+
lab dataset upload my-dataset train.jsonl eval.jsonl
197+
"""
198+
check_configs(output_format=cli_state.output_format)
199+
200+
# Validate all files exist before doing anything
201+
for filepath in files:
202+
if not os.path.isfile(filepath):
203+
console.print(f"[error]Error:[/error] File not found: {filepath}")
204+
raise typer.Exit(1)
205+
206+
# ── Step 1: ensure the dataset exists on the server ──
207+
if cli_state.output_format != "json":
208+
with console.status(f"[bold success]Ensuring dataset '{dataset_id}' exists...[/bold success]", spinner="dots"):
209+
check_response = api.get(f"/data/info?dataset_id={dataset_id}")
210+
else:
211+
check_response = api.get(f"/data/info?dataset_id={dataset_id}")
212+
213+
if check_response.status_code == 200 and check_response.json():
214+
# Dataset already exists
215+
if cli_state.output_format != "json":
216+
console.print(f"[info]Dataset [bold]{dataset_id}[/bold] already exists — uploading files into it.[/info]")
217+
else:
218+
# Create a new dataset
219+
if cli_state.output_format != "json":
220+
with console.status(f"[bold success]Creating dataset '{dataset_id}'...[/bold success]", spinner="dots"):
221+
create_response = api.get(f"/data/new?dataset_id={dataset_id}")
222+
else:
223+
create_response = api.get(f"/data/new?dataset_id={dataset_id}")
224+
225+
if create_response.status_code != 200 or create_response.json().get("status") != "success":
226+
detail = _extract_error(create_response)
227+
if cli_state.output_format == "json":
228+
print(json.dumps({"error": f"Failed to create dataset: {detail}"}))
229+
else:
230+
console.print(f"[error]Error:[/error] Could not create dataset. {detail}")
231+
raise typer.Exit(1)
232+
233+
if cli_state.output_format != "json":
234+
console.print(f"[success]✓[/success] Dataset [bold]{dataset_id}[/bold] created.")
235+
236+
# ── Step 2: upload each file ──
237+
upload_files = []
238+
file_handles = []
239+
try:
240+
for filepath in files:
241+
filename = os.path.basename(filepath)
242+
fh = open(filepath, "rb")
243+
file_handles.append(fh)
244+
upload_files.append(("files", (filename, fh, "application/octet-stream")))
245+
246+
if cli_state.output_format != "json":
247+
with console.status(f"[bold success]Uploading {len(files)} file(s)...[/bold success]", spinner="dots"):
248+
response = api.post(
249+
f"/data/fileupload?dataset_id={dataset_id}",
250+
files=upload_files,
251+
timeout=300.0,
252+
)
253+
else:
254+
response = api.post(
255+
f"/data/fileupload?dataset_id={dataset_id}",
256+
files=upload_files,
257+
timeout=300.0,
258+
)
259+
finally:
260+
for fh in file_handles:
261+
fh.close()
262+
263+
if response.status_code == 200:
264+
body = response.json()
265+
if cli_state.output_format == "json":
266+
print(json.dumps(body))
267+
else:
268+
uploaded = body if isinstance(body, list) else body.get("uploaded_files", files)
269+
console.print(
270+
f"[success]✓[/success] Uploaded [bold]{len(uploaded)}[/bold] file(s) to dataset [bold]{dataset_id}[/bold]."
271+
)
272+
else:
273+
if cli_state.output_format == "json":
274+
print(json.dumps({"error": f"Upload failed. Status code: {response.status_code}"}))
275+
raise typer.Exit(1)
276+
console.print(f"[error]Error:[/error] Upload failed. {_extract_error(response)}")
277+
raise typer.Exit(1)
278+
279+
280+
# ──────────────────────────────────────────────
281+
# download (from HuggingFace Hub)
282+
# ──────────────────────────────────────────────
283+
284+
285+
@app.command("download")
286+
def command_dataset_download(
287+
dataset_id: str = typer.Argument(..., help="HuggingFace dataset ID, e.g. 'Trelis/touch-rugby-rules'"),
288+
config_name: str = typer.Option(None, "--config", help="Dataset config/subset name (optional)"),
289+
):
290+
"""Download a dataset from the HuggingFace Hub to the server."""
291+
check_configs(output_format=cli_state.output_format)
292+
293+
params = f"?dataset_id={dataset_id}"
294+
if config_name:
295+
params += f"&config_name={config_name}"
296+
297+
if cli_state.output_format != "json":
298+
with console.status(
299+
f"[bold success]Downloading '{dataset_id}' from HuggingFace...[/bold success]", spinner="dots"
300+
):
301+
response = api.get(f"/data/download{params}", timeout=300.0)
302+
else:
303+
response = api.get(f"/data/download{params}", timeout=300.0)
304+
305+
if response.status_code == 200:
306+
body = response.json()
307+
if body.get("status") == "success":
308+
if cli_state.output_format == "json":
309+
print(json.dumps(body))
310+
else:
311+
console.print(f"[success]✓[/success] Dataset [bold]{dataset_id}[/bold] downloaded successfully.")
312+
else:
313+
msg = body.get("message", "Unknown error")
314+
if cli_state.output_format == "json":
315+
print(json.dumps({"error": msg}))
316+
else:
317+
console.print(f"[error]Error:[/error] {msg}")
318+
raise typer.Exit(1)
319+
else:
320+
if cli_state.output_format == "json":
321+
print(json.dumps({"error": f"Failed to download dataset. Status code: {response.status_code}"}))
322+
raise typer.Exit(1)
323+
console.print(f"[error]Error:[/error] Failed to download dataset. {_extract_error(response)}")
324+
raise typer.Exit(1)

0 commit comments

Comments
 (0)