From 5066e3db573d5375466c933e8c528aa0f9dde511 Mon Sep 17 00:00:00 2001 From: andrewtruong Date: Fri, 30 Jan 2026 13:08:26 -0500 Subject: [PATCH 1/5] test --- pyproject.toml | 3 + weave/cli/__init__.py | 1 + weave/cli/login.py | 77 ++++++++++++++++++++++++++ weave/cli/main.py | 22 ++++++++ weave/compat/wandb/wandb_thin/login.py | 13 ++++- 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 weave/cli/__init__.py create mode 100644 weave/cli/login.py create mode 100644 weave/cli/main.py diff --git a/pyproject.toml b/pyproject.toml index 13fc5934f28d..cf5406acf199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,9 @@ dependencies = [ "polyfile-weave>=0.5.9", ] +[project.scripts] +weave = "weave.cli.main:main" + [project.optional-dependencies] wandb = ["wandb>=0.17.1"] rich = ["rich"] # Optional dependency for enhanced console output diff --git a/weave/cli/__init__.py b/weave/cli/__init__.py new file mode 100644 index 000000000000..f33a3bdf77bf --- /dev/null +++ b/weave/cli/__init__.py @@ -0,0 +1 @@ +"""Weave CLI package.""" diff --git a/weave/cli/login.py b/weave/cli/login.py new file mode 100644 index 000000000000..9f8bce1c4ac6 --- /dev/null +++ b/weave/cli/login.py @@ -0,0 +1,77 @@ +"""Login command for the Weave CLI.""" + +from __future__ import annotations + +from typing import Literal + +import click + +from weave.compat import wandb +from weave.compat.wandb.wandb_thin.login import _get_default_host as _compat_get_default_host + + +def _get_default_host() -> str: + return _compat_get_default_host() + + +@click.command("login") +@click.argument("key", required=False) +@click.option( + "--anonymous", + type=click.Choice(["allow", "must", "never"], case_sensitive=False), + default=None, + help="Control anonymous login behavior.", +) +@click.option( + "--relogin", + is_flag=True, + default=False, + help="Force a relogin even if an API key is already configured.", +) +@click.option( + "--host", + default=None, + help="W&B host URL, e.g. https://api.wandb.ai", +) +@click.option( + "--verify/--no-verify", + default=False, + help="Verify the API key with the server.", +) +@click.option( + "--timeout", + type=int, + default=None, + help="Seconds to wait for user input.", +) +def login( + key: str | None, + anonymous: Literal["allow", "must", "never"] | None, + relogin: bool, + host: str | None, + verify: bool, + timeout: int | None, +) -> None: + """Log in to Weights & Biases for Weave.""" + normalized_host = host + if normalized_host is not None and not normalized_host.startswith( + ("http://", "https://") + ): + normalized_host = f"https://{normalized_host}" + + try: + success = wandb.login( + anonymous=anonymous, + key=key, + relogin=relogin, + host=normalized_host, + force=relogin, + timeout=timeout, + verify=verify, + referrer="cli", + ) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + + if not success: + raise click.ClickException("Login failed.") diff --git a/weave/cli/main.py b/weave/cli/main.py new file mode 100644 index 000000000000..90b469905e37 --- /dev/null +++ b/weave/cli/main.py @@ -0,0 +1,22 @@ +"""Entry point for the Weave CLI.""" + +from __future__ import annotations + +import logging + +import click + +from weave.cli.login import login as login_command + + +def _configure_logging() -> None: + logging.basicConfig(level=logging.INFO, format="%(message)s") + + +@click.group() +def main() -> None: + """Weave command line interface.""" + _configure_logging() + + +main.add_command(login_command) diff --git a/weave/compat/wandb/wandb_thin/login.py b/weave/compat/wandb/wandb_thin/login.py index e7e4ca8e623e..ae4676405bb2 100644 --- a/weave/compat/wandb/wandb_thin/login.py +++ b/weave/compat/wandb/wandb_thin/login.py @@ -80,6 +80,16 @@ def _clear_setting(key: str) -> None: logger.warning(f"Failed to clear setting {key}: {e}") +def _normalize_host(host: str | None) -> str | None: + """Normalize a host or base URL to a hostname without scheme or trailing slash.""" + if host is None: + return None + host = host.rstrip("/") + if host.startswith(("http://", "https://")): + host = host.split("://", 1)[1] + return host + + def login( anonymous: Literal["must", "allow", "never"] | None = None, key: str | None = None, @@ -165,7 +175,8 @@ def __init__( self._force = force self._timeout = timeout self._key = key - self._host = host if host else _get_default_host() + resolved_host = host if host else _get_default_host() + self._host = _normalize_host(resolved_host) or resolved_host self.is_anonymous = anonymous == "must" def is_apikey_configured(self) -> bool: From bd10726976686d623e29ed6f0dbef2e3631ddc4a Mon Sep 17 00:00:00 2001 From: andrewtruong Date: Mon, 2 Feb 2026 10:43:21 -0500 Subject: [PATCH 2/5] test --- weave/cli/login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/weave/cli/login.py b/weave/cli/login.py index 9f8bce1c4ac6..ba2b64ff0693 100644 --- a/weave/cli/login.py +++ b/weave/cli/login.py @@ -7,7 +7,9 @@ import click from weave.compat import wandb -from weave.compat.wandb.wandb_thin.login import _get_default_host as _compat_get_default_host +from weave.compat.wandb.wandb_thin.login import ( + _get_default_host as _compat_get_default_host, +) def _get_default_host() -> str: From 908e7d07c7bc6d4a1fd3b1bf61baf4a952e49ad6 Mon Sep 17 00:00:00 2001 From: andrewtruong Date: Mon, 2 Feb 2026 10:44:12 -0500 Subject: [PATCH 3/5] test --- weave/cli/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/weave/cli/main.py b/weave/cli/main.py index 90b469905e37..decaf56ef4ff 100644 --- a/weave/cli/main.py +++ b/weave/cli/main.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import cast import click @@ -19,4 +20,5 @@ def main() -> None: _configure_logging() +main = cast(click.Group, main) main.add_command(login_command) From 4d9b52b70f893fd366dd960daea0641d2fe734ec Mon Sep 17 00:00:00 2001 From: andrewtruong Date: Mon, 2 Feb 2026 23:29:29 -0500 Subject: [PATCH 4/5] test --- weave/cli/login.py | 1 - weave/cli/main.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/weave/cli/login.py b/weave/cli/login.py index ba2b64ff0693..3855bf5ee71b 100644 --- a/weave/cli/login.py +++ b/weave/cli/login.py @@ -70,7 +70,6 @@ def login( force=relogin, timeout=timeout, verify=verify, - referrer="cli", ) except Exception as exc: raise click.ClickException(str(exc)) from exc diff --git a/weave/cli/main.py b/weave/cli/main.py index decaf56ef4ff..b90a9b1e6c82 100644 --- a/weave/cli/main.py +++ b/weave/cli/main.py @@ -20,5 +20,5 @@ def main() -> None: _configure_logging() -main = cast(click.Group, main) -main.add_command(login_command) +cli = cast(click.Group, main) +cli.add_command(login_command) From 675f67b33c2535abc18f1e85f170c51624fd5b62 Mon Sep 17 00:00:00 2001 From: andrewtruong Date: Tue, 3 Feb 2026 11:53:42 -0500 Subject: [PATCH 5/5] test --- tests/cli/test_login.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/cli/test_login.py diff --git a/tests/cli/test_login.py b/tests/cli/test_login.py new file mode 100644 index 000000000000..294a8eca5949 --- /dev/null +++ b/tests/cli/test_login.py @@ -0,0 +1,39 @@ +"""Tests for the Weave CLI login command.""" + +from unittest.mock import patch + +from click.testing import CliRunner + +from weave.cli.login import login as login_command + + +def test_cli_login_passes_normalized_host() -> None: + runner = CliRunner() + api_key = "a" * 40 + + with patch("weave.cli.login.wandb.login", return_value=True) as mock_login: + result = runner.invoke( + login_command, + [api_key, "--host", "api.wandb.ai", "--relogin"], + ) + + assert result.exit_code == 0 + mock_login.assert_called_once_with( + anonymous=None, + key=api_key, + relogin=True, + host="https://api.wandb.ai", + force=True, + timeout=None, + verify=False, + ) + + +def test_cli_login_failure_returns_error() -> None: + runner = CliRunner() + + with patch("weave.cli.login.wandb.login", return_value=False): + result = runner.invoke(login_command, []) + + assert result.exit_code != 0 + assert "Login failed." in result.output