CLI Improvements (#151)

1. Fixes bug where arcade login doesn't work for localhost
- `arcade login -h localhost` will open login page at
`http://localhost:8000/...`
    - Optionally specify the port: `arcade login -h localhost -p 8000`


3. Adds `local` flag to `arcade show`
- `-h localhost`, `-h 127.0.0.1`, and `-h 0.0.0.0` shows the tools that
are in the local engine's catalog
    - `--local` show the tools that are in the local environment.
This commit is contained in:
Eric Gustin 2024-11-06 16:25:45 -08:00 committed by GitHub
parent 081865733a
commit c02aee3f14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 24 deletions

View file

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

View file

@ -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 = []

View file

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