|
| 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