diff --git a/arcade/arcade/cli/main.py b/arcade/arcade/cli/main.py index 6b520db9..844e4f7f 100644 --- a/arcade/arcade/cli/main.py +++ b/arcade/arcade/cli/main.py @@ -4,7 +4,6 @@ import threading import uuid import webbrowser from typing import Any, Optional -from urllib.parse import urlencode import typer from arcadepy import Arcade @@ -26,7 +25,8 @@ from arcade.cli.display import ( from arcade.cli.launcher import start_servers from arcade.cli.utils import ( OrderCommands, - compute_base_url, + compute_engine_base_url, + compute_login_url, create_cli_catalog, delete_deprecated_config_file, get_eval_files, @@ -58,6 +58,12 @@ def login( "--host", help="The Arcade Cloud host to log in to.", ), + port: Optional[int] = typer.Option( + None, + "-p", + "--port", + help="The port of the Arcade Cloud host (if running locally).", + ), ) -> None: """ Logs the user into Arcade Cloud. @@ -75,9 +81,7 @@ def login( try: # Open the browser for user login - callback_uri = "http://localhost:9905/callback" - params = urlencode({"callback_uri": callback_uri, "state": state}) - login_url = f"https://{host}/api/v1/auth/cli_login?{params}" + login_url = compute_login_url(host, state, port) console.print("Opening a browser to log you in...") if not webbrowser.open(login_url): @@ -142,7 +146,13 @@ def show( DEFAULT_ENGINE_HOST, "-h", "--host", - help="The Arcade Engine address to send chat requests to.", + help="The Arcade Engine address to show the tools/toolkits of.", + ), + local: bool = typer.Option( + False, + "--local", + "-l", + help="Show the local environment's catalog instead of an Arcade Engine's catalog.", ), port: Optional[int] = typer.Option( None, @@ -165,9 +175,8 @@ def show( """ Show the available toolkits or detailed information about a specific tool. """ - local_hosts = ["localhost", "127.0.0.1", "0.0.0.0"] # noqa: S104 try: - if host in local_hosts: + if local: catalog = create_cli_catalog(toolkit=toolkit) tools = [t.definition for t in list(catalog)] else: @@ -243,7 +252,7 @@ def chat( ) config = validate_and_get_config() - base_url = compute_base_url(force_tls, force_no_tls, host, port) + base_url = compute_engine_base_url(force_tls, force_no_tls, host, port) client = Arcade(api_key=config.api.key, base_url=base_url) user_email = config.user.email if config.user else None @@ -358,7 +367,7 @@ def evals( execute any functions decorated with @tool_eval, and display the results. """ config = validate_and_get_config() - base_url = compute_base_url(force_tls, force_no_tls, host, port) + base_url = compute_engine_base_url(force_tls, force_no_tls, host, port) models_list = models.split(",") # Use 'models_list' to avoid shadowing diff --git a/arcade/arcade/cli/utils.py b/arcade/arcade/cli/utils.py index e01f04fd..0023311f 100644 --- a/arcade/arcade/cli/utils.py +++ b/arcade/arcade/cli/utils.py @@ -5,7 +5,7 @@ import webbrowser from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Union, cast -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse import idna import typer @@ -69,7 +69,7 @@ def create_cli_catalog( return catalog -def compute_base_url( +def compute_engine_base_url( force_tls: bool, force_no_tls: bool, host: str, @@ -78,6 +78,8 @@ def compute_base_url( """ Compute the base URL for the Arcade Engine from the provided overrides. + Treats 127.0.0.1 and 0.0.0.0 as aliases for localhost. + force_no_tls takes precedence over force_tls. For example, if both are set to True, the resulting URL will use http. @@ -102,6 +104,9 @@ def compute_base_url( Returns: str: The fully constructed URL for the Arcade Engine. """ + # "Use 127.0.0.1" and "0.0.0.0" as aliases for "localhost" + host = "localhost" if host in ["127.0.0.1", "0.0.0.0"] else host # noqa: S104 + # Determine TLS setting based on input flags if force_no_tls: is_tls = False @@ -150,6 +155,25 @@ def compute_base_url( return f"{protocol}://{encoded_host}" +def compute_login_url(host: str, state: str, port: int | None) -> str: + """ + Compute the full URL for the CLI login endpoint. + """ + callback_uri = "http://localhost:9905/callback" + params = urlencode({"callback_uri": callback_uri, "state": state}) + + port = port if port else 8000 + + login_base_url = ( + f"http://localhost:{port}" + if host in ["localhost", "127.0.0.1", "0.0.0.0"] # noqa: S104 + else f"https://{host}" + ) + endpoint = "/api/v1/auth/cli_login" + + return f"{login_base_url}{endpoint}?{params}" + + def get_tools_from_engine( host: str, port: int | None = None, @@ -158,7 +182,7 @@ def get_tools_from_engine( toolkit: str | None = None, ) -> list[ToolDefinition]: config = validate_and_get_config() - base_url = compute_base_url(force_tls, force_no_tls, host, port) + base_url = compute_engine_base_url(force_tls, force_no_tls, host, port) client = Arcade(api_key=config.api.key, base_url=base_url) tools = [] diff --git a/arcade/tests/cli/test_utils.py b/arcade/tests/cli/test_utils.py index 6dbac2c1..490c623e 100644 --- a/arcade/tests/cli/test_utils.py +++ b/arcade/tests/cli/test_utils.py @@ -1,8 +1,9 @@ import pytest -from arcade.cli.utils import compute_base_url +from arcade.cli.utils import compute_engine_base_url, compute_login_url -DEFAULT_HOST = "api.arcade-ai.com" +DEFAULT_CLOUD_HOST = "cloud.arcade-ai.com" +DEFAULT_ENGINE_HOST = "api.arcade-ai.com" LOCALHOST = "localhost" DEFAULT_PORT = None DEFAULT_FORCE_TLS = False @@ -14,7 +15,7 @@ DEFAULT_FORCE_NO_TLS = False [ pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": DEFAULT_PORT, "force_tls": DEFAULT_FORCE_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS, @@ -34,7 +35,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": 9099, "force_tls": DEFAULT_FORCE_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS, @@ -54,7 +55,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": DEFAULT_PORT, "force_tls": True, "force_no_tls": DEFAULT_FORCE_NO_TLS, @@ -74,7 +75,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": 9099, "force_tls": True, "force_no_tls": DEFAULT_FORCE_NO_TLS, @@ -94,7 +95,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": DEFAULT_PORT, "force_tls": DEFAULT_FORCE_TLS, "force_no_tls": True, @@ -114,7 +115,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": 9099, "force_tls": DEFAULT_FORCE_TLS, "force_no_tls": True, @@ -134,7 +135,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": DEFAULT_PORT, "force_tls": True, "force_no_tls": True, @@ -154,7 +155,7 @@ DEFAULT_FORCE_NO_TLS = False ), pytest.param( { - "host_input": DEFAULT_HOST, + "host_input": DEFAULT_ENGINE_HOST, "port_input": 9099, "force_tls": True, "force_no_tls": True, @@ -185,7 +186,7 @@ DEFAULT_FORCE_NO_TLS = False ], ) def test_compute_base_url(inputs: dict, expected_output: str): - base_url = compute_base_url( + base_url = compute_engine_base_url( inputs["force_tls"], inputs["force_no_tls"], inputs["host_input"], @@ -193,3 +194,34 @@ def test_compute_base_url(inputs: dict, expected_output: str): ) assert base_url == expected_output + + +@pytest.mark.parametrize( + "inputs, expected_output", + [ + pytest.param( + {"host_input": DEFAULT_CLOUD_HOST, "port_input": DEFAULT_PORT, "state": "123"}, + "https://cloud.arcade-ai.com/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Flocalhost%3A9905%2Fcallback&state=123", + id="default", + ), + pytest.param( + {"host_input": "localhost", "port_input": 9099, "state": "123"}, + "http://localhost:9099/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Flocalhost%3A9905%2Fcallback&state=123", + id="localhost with custom port", + ), + pytest.param( + {"host_input": "localhost", "port_input": DEFAULT_PORT, "state": "123"}, + "http://localhost:8000/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Flocalhost%3A9905%2Fcallback&state=123", + id="localhost", + ), + pytest.param( + {"host_input": DEFAULT_CLOUD_HOST, "port_input": 8000, "state": "123"}, + "https://cloud.arcade-ai.com/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Flocalhost%3A9905%2Fcallback&state=123", + id="cloud host with an ignored custom port", + ), + ], +) +def test_compute_login_url(inputs: dict, expected_output: str): + login_url = compute_login_url(inputs["host_input"], inputs["state"], inputs["port_input"]) + + assert login_url == expected_output