diff --git a/README.md b/README.md index 14faaac..55e9bee 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,10 @@ When MLflow is enabled, `train start` creates an MLflow run for the SageMaker jo To open the managed SageMaker MLflow UI, request a fresh presigned URL: ```bash -qc-cli infra mlflow-url --config config.yaml +qc-cli mlflow open --config config.yaml ``` -This works for `mode: create` and for `mode: existing` when the existing server is managed by Amazon SageMaker. In `create` mode, the command uses the CLI-managed tracking server name. In `existing` mode, it uses `mlflow.tracking_server_name`. If the existing MLflow server is external to SageMaker, open it with that server's own URL instead. +This opens a browser to a fresh presigned URL. It works for `mode: create` and for `mode: existing` when the existing server is managed by Amazon SageMaker. In `create` mode, the command uses the CLI-managed tracking server name. In `existing` mode, it uses `mlflow.tracking_server_name`. If the existing MLflow server is external to SageMaker, open it with that server's own URL instead. ## Commands @@ -125,6 +125,12 @@ qc-cli init --output Write config to a custom path qc-cli init --force Overwrite an existing config file ``` +### `mlflow` + +``` +qc-cli mlflow open Open a presigned MLflow UI URL in a browser +``` + ### `infra` ``` @@ -132,7 +138,6 @@ qc-cli infra setup Deploy the CDK stack qc-cli infra setup --no-bootstrap Deploy without running CDK bootstrap qc-cli infra setup --cloudformation-execution-policy Set CDK bootstrap execution policy ARN qc-cli infra status Show CDK stack/resource status -qc-cli infra mlflow-url Print a presigned MLflow UI URL qc-cli infra destroy Destroy stack, retaining S3 data qc-cli infra destroy --yes Destroy stack without confirmation qc-cli infra destroy --delete-bucket-data Destroy stack and delete S3 data diff --git a/src/commands/infra.py b/src/commands/infra.py index a27ef9b..0381d4c 100644 --- a/src/commands/infra.py +++ b/src/commands/infra.py @@ -150,35 +150,6 @@ def status(config: str = CONFIG_OPT) -> None: CONSOLE.print(table) -@app.command(name="mlflow-url") -def mlflow_url(config: str = CONFIG_OPT) -> None: - """Print a presigned URL for the configured MLflow tracking server.""" - cfg = load_cfg(config) - tracking_server_name = cfg.effective_mlflow_tracking_server_name - if not tracking_server_name: - CONSOLE.print("[red]MLflow is disabled in config.yaml.[/red]") - raise typer.Exit(1) - - try: - url = mlflow.create_presigned_tracking_server_url( - cfg.aws.region, - cfg.aws.profile, - tracking_server_name, - ) - except Exception as e: - CONSOLE.print("[yellow]Could not create a SageMaker MLflow UI URL.[/yellow]") - CONSOLE.print(f"Tracking server: [cyan]{tracking_server_name}[/cyan]") - CONSOLE.print(f"Reason: {e}") - CONSOLE.print( - "This command can create presigned URLs only for MLflow tracking servers managed by " - "Amazon SageMaker. If this is an external MLflow server, open it with that server's own URL." - ) - raise typer.Exit(1) - - CONSOLE.print(f"MLflow tracking server: [cyan]{tracking_server_name}[/cyan]") - CONSOLE.print(f"MLflow UI: {url}") - - @app.command() def destroy( config: str = CONFIG_OPT, diff --git a/src/commands/mlflow.py b/src/commands/mlflow.py new file mode 100644 index 0000000..8fd3ef2 --- /dev/null +++ b/src/commands/mlflow.py @@ -0,0 +1,41 @@ +import webbrowser + +import typer + +from src.aws import mlflow as aws_mlflow +from src.commands.utils import CONFIG_OPT, CONSOLE, load_cfg + +app = typer.Typer(help="Manage MLflow tracking server access") + + +@app.command(name="open") +def open_mlflow(config: str = CONFIG_OPT) -> None: + """Open a presigned URL for the configured MLflow tracking server.""" + cfg = load_cfg(config) + tracking_server_name = cfg.effective_mlflow_tracking_server_name + if not tracking_server_name: + CONSOLE.print("[red]MLflow is disabled in config.yaml.[/red]") + raise typer.Exit(1) + + try: + url = aws_mlflow.create_presigned_tracking_server_url( + cfg.aws.region, + cfg.aws.profile, + tracking_server_name, + ) + except Exception as e: + CONSOLE.print("[yellow]Could not create a SageMaker MLflow UI URL.[/yellow]") + CONSOLE.print(f"Tracking server: [cyan]{tracking_server_name}[/cyan]") + CONSOLE.print(f"Reason: {e}") + CONSOLE.print( + "This command can create presigned URLs only for MLflow tracking servers managed by " + "Amazon SageMaker. If this is an external MLflow server, open it with that server's own URL." + ) + raise typer.Exit(1) + + CONSOLE.print(f"MLflow tracking server: [cyan]{tracking_server_name}[/cyan]") + CONSOLE.print(f"MLflow UI: {url}") + if webbrowser.open(url): + CONSOLE.print("[green]✓[/green] Opened MLflow UI in your browser.") + else: + CONSOLE.print("[yellow]Could not open a browser automatically. Open the URL above manually.[/yellow]") diff --git a/src/commands/train.py b/src/commands/train.py index 5404d1a..eb96c92 100644 --- a/src/commands/train.py +++ b/src/commands/train.py @@ -101,7 +101,7 @@ def start(config: str = CONFIG_OPT) -> None: CONSOLE.print(f"[green]✓[/green] Job submitted: [bold]{job_name}[/bold]") if run_id: CONSOLE.print(f"MLflow run: [cyan]{run_id}[/cyan]") - CONSOLE.print("Open MLflow: [cyan]qc-cli infra mlflow-url[/cyan]") + CONSOLE.print("Open MLflow: [cyan]qc-cli mlflow open[/cyan]") CONSOLE.print("Track progress: [cyan]qc-cli train status[/cyan]") @@ -151,7 +151,7 @@ def status( st.set_latest_experiment_model_version(version) CONSOLE.print(f"MLflow model version: [cyan]{version}[/cyan] ([cyan]experiment-latest[/cyan])") if run_id and cfg.mlflow.mode is not MlflowMode.disabled: - CONSOLE.print("Open MLflow: [cyan]qc-cli infra mlflow-url[/cyan]") + CONSOLE.print("Open MLflow: [cyan]qc-cli mlflow open[/cyan]") @app.command(name="list") diff --git a/src/main.py b/src/main.py index 61df2ee..70224f2 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,6 @@ import typer -from src.commands import ai_hub, infra, init, train, upload +from src.commands import ai_hub, infra, init, mlflow, train, upload app = typer.Typer( help="qc-cli: End-to-end model managment for Qualcomm AI Hub.", @@ -8,6 +8,7 @@ app = typer.Typer( ) app.add_typer(init.app) app.add_typer(upload.app) +app.add_typer(mlflow.app, name="mlflow") app.add_typer(infra.app, name="infra") app.add_typer(train.app, name="train") app.add_typer(ai_hub.app, name="ai-hub")