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 uuid
import webbrowser import webbrowser
from typing import Any, Optional from typing import Any, Optional
from urllib.parse import urlencode
import typer import typer
from arcadepy import Arcade from arcadepy import Arcade
@ -26,7 +25,8 @@ from arcade.cli.display import (
from arcade.cli.launcher import start_servers from arcade.cli.launcher import start_servers
from arcade.cli.utils import ( from arcade.cli.utils import (
OrderCommands, OrderCommands,
compute_base_url, compute_engine_base_url,
compute_login_url,
create_cli_catalog, create_cli_catalog,
delete_deprecated_config_file, delete_deprecated_config_file,
get_eval_files, get_eval_files,
@ -58,6 +58,12 @@ def login(
"--host", "--host",
help="The Arcade Cloud host to log in to.", 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: ) -> None:
""" """
Logs the user into Arcade Cloud. Logs the user into Arcade Cloud.
@ -75,9 +81,7 @@ def login(
try: try:
# Open the browser for user login # Open the browser for user login
callback_uri = "http://localhost:9905/callback" login_url = compute_login_url(host, state, port)
params = urlencode({"callback_uri": callback_uri, "state": state})
login_url = f"https://{host}/api/v1/auth/cli_login?{params}"
console.print("Opening a browser to log you in...") console.print("Opening a browser to log you in...")
if not webbrowser.open(login_url): if not webbrowser.open(login_url):
@ -142,7 +146,13 @@ def show(
DEFAULT_ENGINE_HOST, DEFAULT_ENGINE_HOST,
"-h", "-h",
"--host", "--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( port: Optional[int] = typer.Option(
None, None,
@ -165,9 +175,8 @@ def show(
""" """
Show the available toolkits or detailed information about a specific tool. 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: try:
if host in local_hosts: if local:
catalog = create_cli_catalog(toolkit=toolkit) catalog = create_cli_catalog(toolkit=toolkit)
tools = [t.definition for t in list(catalog)] tools = [t.definition for t in list(catalog)]
else: else:
@ -243,7 +252,7 @@ def chat(
) )
config = validate_and_get_config() 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) client = Arcade(api_key=config.api.key, base_url=base_url)
user_email = config.user.email if config.user else None 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. execute any functions decorated with @tool_eval, and display the results.
""" """
config = validate_and_get_config() 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 models_list = models.split(",") # Use 'models_list' to avoid shadowing

View file

@ -5,7 +5,7 @@ import webbrowser
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Union, cast from typing import Any, Callable, Union, cast
from urllib.parse import urlparse from urllib.parse import urlencode, urlparse
import idna import idna
import typer import typer
@ -69,7 +69,7 @@ def create_cli_catalog(
return catalog return catalog
def compute_base_url( def compute_engine_base_url(
force_tls: bool, force_tls: bool,
force_no_tls: bool, force_no_tls: bool,
host: str, host: str,
@ -78,6 +78,8 @@ def compute_base_url(
""" """
Compute the base URL for the Arcade Engine from the provided overrides. 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, force_no_tls takes precedence over force_tls. For example, if both are set to True,
the resulting URL will use http. the resulting URL will use http.
@ -102,6 +104,9 @@ def compute_base_url(
Returns: Returns:
str: The fully constructed URL for the Arcade Engine. 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 # Determine TLS setting based on input flags
if force_no_tls: if force_no_tls:
is_tls = False is_tls = False
@ -150,6 +155,25 @@ def compute_base_url(
return f"{protocol}://{encoded_host}" 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( def get_tools_from_engine(
host: str, host: str,
port: int | None = None, port: int | None = None,
@ -158,7 +182,7 @@ def get_tools_from_engine(
toolkit: str | None = None, toolkit: str | None = None,
) -> list[ToolDefinition]: ) -> list[ToolDefinition]:
config = validate_and_get_config() 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) client = Arcade(api_key=config.api.key, base_url=base_url)
tools = [] tools = []

View file

@ -1,8 +1,9 @@
import pytest 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" LOCALHOST = "localhost"
DEFAULT_PORT = None DEFAULT_PORT = None
DEFAULT_FORCE_TLS = False DEFAULT_FORCE_TLS = False
@ -14,7 +15,7 @@ DEFAULT_FORCE_NO_TLS = False
[ [
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": DEFAULT_PORT, "port_input": DEFAULT_PORT,
"force_tls": DEFAULT_FORCE_TLS, "force_tls": DEFAULT_FORCE_TLS,
"force_no_tls": DEFAULT_FORCE_NO_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS,
@ -34,7 +35,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": 9099, "port_input": 9099,
"force_tls": DEFAULT_FORCE_TLS, "force_tls": DEFAULT_FORCE_TLS,
"force_no_tls": DEFAULT_FORCE_NO_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS,
@ -54,7 +55,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": DEFAULT_PORT, "port_input": DEFAULT_PORT,
"force_tls": True, "force_tls": True,
"force_no_tls": DEFAULT_FORCE_NO_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS,
@ -74,7 +75,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": 9099, "port_input": 9099,
"force_tls": True, "force_tls": True,
"force_no_tls": DEFAULT_FORCE_NO_TLS, "force_no_tls": DEFAULT_FORCE_NO_TLS,
@ -94,7 +95,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": DEFAULT_PORT, "port_input": DEFAULT_PORT,
"force_tls": DEFAULT_FORCE_TLS, "force_tls": DEFAULT_FORCE_TLS,
"force_no_tls": True, "force_no_tls": True,
@ -114,7 +115,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": 9099, "port_input": 9099,
"force_tls": DEFAULT_FORCE_TLS, "force_tls": DEFAULT_FORCE_TLS,
"force_no_tls": True, "force_no_tls": True,
@ -134,7 +135,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": DEFAULT_PORT, "port_input": DEFAULT_PORT,
"force_tls": True, "force_tls": True,
"force_no_tls": True, "force_no_tls": True,
@ -154,7 +155,7 @@ DEFAULT_FORCE_NO_TLS = False
), ),
pytest.param( pytest.param(
{ {
"host_input": DEFAULT_HOST, "host_input": DEFAULT_ENGINE_HOST,
"port_input": 9099, "port_input": 9099,
"force_tls": True, "force_tls": True,
"force_no_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): 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_tls"],
inputs["force_no_tls"], inputs["force_no_tls"],
inputs["host_input"], inputs["host_input"],
@ -193,3 +194,34 @@ def test_compute_base_url(inputs: dict, expected_output: str):
) )
assert base_url == expected_output 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