llamactl 0.2.7a1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llama_deploy/cli/__init__.py +9 -22
- llama_deploy/cli/app.py +69 -0
- llama_deploy/cli/auth/client.py +362 -0
- llama_deploy/cli/client.py +47 -170
- llama_deploy/cli/commands/aliased_group.py +33 -0
- llama_deploy/cli/commands/auth.py +696 -0
- llama_deploy/cli/commands/deployment.py +300 -0
- llama_deploy/cli/commands/env.py +211 -0
- llama_deploy/cli/commands/init.py +313 -0
- llama_deploy/cli/commands/serve.py +239 -0
- llama_deploy/cli/config/_config.py +390 -0
- llama_deploy/cli/config/_migrations.py +65 -0
- llama_deploy/cli/config/auth_service.py +130 -0
- llama_deploy/cli/config/env_service.py +67 -0
- llama_deploy/cli/config/migrations/0001_init.sql +35 -0
- llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +24 -0
- llama_deploy/cli/config/migrations/__init__.py +7 -0
- llama_deploy/cli/config/schema.py +61 -0
- llama_deploy/cli/env.py +5 -3
- llama_deploy/cli/interactive_prompts/session_utils.py +37 -0
- llama_deploy/cli/interactive_prompts/utils.py +6 -72
- llama_deploy/cli/options.py +27 -5
- llama_deploy/cli/py.typed +0 -0
- llama_deploy/cli/styles.py +10 -0
- llama_deploy/cli/textual/deployment_form.py +263 -36
- llama_deploy/cli/textual/deployment_help.py +53 -0
- llama_deploy/cli/textual/deployment_monitor.py +466 -0
- llama_deploy/cli/textual/git_validation.py +20 -21
- llama_deploy/cli/textual/github_callback_server.py +17 -14
- llama_deploy/cli/textual/llama_loader.py +13 -1
- llama_deploy/cli/textual/secrets_form.py +28 -8
- llama_deploy/cli/textual/styles.tcss +49 -8
- llama_deploy/cli/utils/env_inject.py +23 -0
- {llamactl-0.2.7a1.dist-info → llamactl-0.3.0.dist-info}/METADATA +9 -6
- llamactl-0.3.0.dist-info/RECORD +38 -0
- {llamactl-0.2.7a1.dist-info → llamactl-0.3.0.dist-info}/WHEEL +1 -1
- llama_deploy/cli/commands.py +0 -549
- llama_deploy/cli/config.py +0 -173
- llama_deploy/cli/textual/profile_form.py +0 -171
- llamactl-0.2.7a1.dist-info/RECORD +0 -19
- {llamactl-0.2.7a1.dist-info → llamactl-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
PRAGMA user_version=2;
|
|
2
|
+
|
|
3
|
+
-- Add new fields to profiles: api_key_id and device_oidc (stored as JSON string)
|
|
4
|
+
ALTER TABLE profiles ADD COLUMN api_key_id TEXT;
|
|
5
|
+
ALTER TABLE profiles ADD COLUMN device_oidc TEXT;
|
|
6
|
+
|
|
7
|
+
-- Add synthetic identifier for profiles
|
|
8
|
+
ALTER TABLE profiles ADD COLUMN id TEXT;
|
|
9
|
+
|
|
10
|
+
-- Populate existing rows with random UUIDv4 values
|
|
11
|
+
UPDATE profiles
|
|
12
|
+
SET id = lower(
|
|
13
|
+
hex(randomblob(4)) || '-' ||
|
|
14
|
+
hex(randomblob(2)) || '-' ||
|
|
15
|
+
'4' || substr(hex(randomblob(2)), 2) || '-' ||
|
|
16
|
+
substr('89ab', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(2)), 2) || '-' ||
|
|
17
|
+
hex(randomblob(6))
|
|
18
|
+
)
|
|
19
|
+
WHERE id IS NULL;
|
|
20
|
+
|
|
21
|
+
-- Ensure id values are unique
|
|
22
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_id ON profiles(id);
|
|
23
|
+
|
|
24
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Auth:
|
|
10
|
+
"""Auth Profile configuration"""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
name: str
|
|
14
|
+
api_url: str
|
|
15
|
+
project_id: str
|
|
16
|
+
api_key: str | None = None
|
|
17
|
+
# reference to the API key if we created it from device oauth, to be cleaned up
|
|
18
|
+
# once de-authenticated
|
|
19
|
+
api_key_id: str | None = None
|
|
20
|
+
device_oidc: DeviceOIDC | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeviceOIDC(BaseModel):
|
|
24
|
+
"""Device OIDC configuration"""
|
|
25
|
+
|
|
26
|
+
# A name for this device, derived from the host. Used in API key name.
|
|
27
|
+
device_name: str
|
|
28
|
+
# A unique user ID to identify the user in the API. Prevents duplicate logins.
|
|
29
|
+
user_id: str
|
|
30
|
+
# email of the user
|
|
31
|
+
email: str
|
|
32
|
+
# OIDC client ID
|
|
33
|
+
client_id: str
|
|
34
|
+
# OIDC discovery URL
|
|
35
|
+
discovery_url: str
|
|
36
|
+
# usually 5m long JWT. For calling APIs.
|
|
37
|
+
device_access_token: str
|
|
38
|
+
# usually opaque, used to get new access tokens
|
|
39
|
+
device_refresh_token: str | None = None
|
|
40
|
+
# usually 1h long JWT. Contains user info (email, name, etc.)
|
|
41
|
+
device_id_token: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Environment:
|
|
46
|
+
"""Environment configuration stored in SQLite.
|
|
47
|
+
|
|
48
|
+
Note: `api_url`, `requires_auth`, and `min_llamactl_version` are persisted
|
|
49
|
+
in the environments table.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
api_url: str
|
|
53
|
+
requires_auth: bool
|
|
54
|
+
min_llamactl_version: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
DEFAULT_ENVIRONMENT = Environment(
|
|
58
|
+
api_url="https://api.cloud.llamaindex.ai",
|
|
59
|
+
requires_auth=True,
|
|
60
|
+
min_llamactl_version=None,
|
|
61
|
+
)
|
llama_deploy/cli/env.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Environment variable handling utilities for llamactl"""
|
|
2
2
|
|
|
3
|
-
from typing import Dict
|
|
4
3
|
from io import StringIO
|
|
5
|
-
from
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
6
|
from dotenv import dotenv_values
|
|
7
|
+
from llama_deploy.cli.styles import WARNING
|
|
8
|
+
from rich import print as rprint
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def load_env_secrets_from_string(env_content: str) -> Dict[str, str]:
|
|
@@ -25,6 +27,6 @@ def load_env_secrets_from_string(env_content: str) -> Dict[str, str]:
|
|
|
25
27
|
return {k: str(v) for k, v in secrets.items() if v is not None}
|
|
26
28
|
except Exception as e:
|
|
27
29
|
rprint(
|
|
28
|
-
f"[
|
|
30
|
+
f"[{WARNING}]Warning: Could not parse environment variables from string: {e}[/]"
|
|
29
31
|
)
|
|
30
32
|
return {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Utilities for detecting and handling interactive CLI sessions."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@functools.cache
|
|
9
|
+
def is_interactive_session() -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Detect if the current CLI session is interactive.
|
|
12
|
+
|
|
13
|
+
Returns True if the session is interactive (user can be prompted),
|
|
14
|
+
False if it's non-interactive (e.g., CI/CD, scripted environment).
|
|
15
|
+
|
|
16
|
+
This function checks multiple indicators:
|
|
17
|
+
- Whether stdin/stdout are connected to a TTY
|
|
18
|
+
- Explicit non-interactive environment variables
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
>>> if is_interactive_session():
|
|
22
|
+
... user_input = questionary.text("Enter value:").ask()
|
|
23
|
+
... else:
|
|
24
|
+
... raise click.ClickException("Value required in non-interactive mode")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Check if stdin and stdout are TTYs
|
|
28
|
+
# This is the most reliable indicator for interactive sessions
|
|
29
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Additional check for TERM environment variable
|
|
33
|
+
# Some environments set TERM=dumb for non-interactive sessions
|
|
34
|
+
if os.environ.get("TERM") == "dumb":
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
return True
|
|
@@ -1,86 +1,20 @@
|
|
|
1
1
|
"""Shared utilities for CLI operations"""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
3
|
import questionary
|
|
6
|
-
from rich import print as rprint
|
|
7
4
|
from rich.console import Console
|
|
8
5
|
|
|
9
|
-
from
|
|
10
|
-
from ..config import config_manager
|
|
6
|
+
from .session_utils import is_interactive_session
|
|
11
7
|
|
|
12
8
|
console = Console()
|
|
13
9
|
|
|
14
10
|
|
|
15
|
-
def select_deployment(deployment_id: Optional[str] = None) -> Optional[str]:
|
|
16
|
-
"""
|
|
17
|
-
Select a deployment interactively if ID not provided.
|
|
18
|
-
Returns the selected deployment ID or None if cancelled.
|
|
19
|
-
"""
|
|
20
|
-
if deployment_id:
|
|
21
|
-
return deployment_id
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
client = get_client()
|
|
25
|
-
deployments = client.list_deployments()
|
|
26
|
-
|
|
27
|
-
if not deployments:
|
|
28
|
-
rprint(
|
|
29
|
-
f"[yellow]No deployments found for project {client.project_id}[/yellow]"
|
|
30
|
-
)
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
choices = []
|
|
34
|
-
for deployment in deployments:
|
|
35
|
-
name = deployment.name
|
|
36
|
-
deployment_id = deployment.id
|
|
37
|
-
status = deployment.status
|
|
38
|
-
choices.append(
|
|
39
|
-
questionary.Choice(
|
|
40
|
-
title=f"{name} ({deployment_id}) - {status}", value=deployment_id
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
return questionary.select("Select deployment:", choices=choices).ask()
|
|
45
|
-
|
|
46
|
-
except Exception as e:
|
|
47
|
-
rprint(f"[red]Error loading deployments: {e}[/red]")
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def select_profile(profile_name: Optional[str] = None) -> Optional[str]:
|
|
52
|
-
"""
|
|
53
|
-
Select a profile interactively if name not provided.
|
|
54
|
-
Returns the selected profile name or None if cancelled.
|
|
55
|
-
"""
|
|
56
|
-
if profile_name:
|
|
57
|
-
return profile_name
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
profiles = config_manager.list_profiles()
|
|
61
|
-
|
|
62
|
-
if not profiles:
|
|
63
|
-
rprint("[yellow]No profiles found[/yellow]")
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
choices = []
|
|
67
|
-
current_name = config_manager.get_current_profile_name()
|
|
68
|
-
|
|
69
|
-
for profile in profiles:
|
|
70
|
-
title = f"{profile.name} ({profile.api_url})"
|
|
71
|
-
if profile.name == current_name:
|
|
72
|
-
title += " [current]"
|
|
73
|
-
choices.append(questionary.Choice(title=title, value=profile.name))
|
|
74
|
-
|
|
75
|
-
return questionary.select("Select profile:", choices=choices).ask()
|
|
76
|
-
|
|
77
|
-
except Exception as e:
|
|
78
|
-
rprint(f"[red]Error loading profiles: {e}[/red]")
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
|
|
82
11
|
def confirm_action(message: str, default: bool = False) -> bool:
|
|
83
12
|
"""
|
|
84
13
|
Ask for confirmation with a consistent interface.
|
|
14
|
+
|
|
15
|
+
In non-interactive sessions, returns the default value without prompting.
|
|
85
16
|
"""
|
|
17
|
+
if not is_interactive_session():
|
|
18
|
+
return default
|
|
19
|
+
|
|
86
20
|
return questionary.confirm(message, default=default).ask() or False
|
llama_deploy/cli/options.py
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Callable, ParamSpec, TypeVar
|
|
3
|
+
|
|
2
4
|
import click
|
|
5
|
+
from llama_deploy.cli.interactive_prompts.session_utils import is_interactive_session
|
|
6
|
+
|
|
7
|
+
from .debug import setup_file_logging
|
|
3
8
|
|
|
9
|
+
P = ParamSpec("P")
|
|
10
|
+
R = TypeVar("R")
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
|
|
13
|
+
def global_options(f: Callable[P, R]) -> Callable[P, R]:
|
|
6
14
|
"""Common decorator to add global options to command groups"""
|
|
7
|
-
from .debug import setup_file_logging
|
|
8
15
|
|
|
9
|
-
def debug_callback(ctx, param, value):
|
|
16
|
+
def debug_callback(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
|
10
17
|
if value:
|
|
11
|
-
setup_file_logging(level=logging._nameToLevel
|
|
18
|
+
setup_file_logging(level=logging._nameToLevel.get(value, logging.INFO))
|
|
12
19
|
return value
|
|
13
20
|
|
|
14
21
|
return click.option(
|
|
15
22
|
"--log-level",
|
|
16
|
-
type=click.Choice(
|
|
23
|
+
type=click.Choice(
|
|
24
|
+
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
|
|
25
|
+
),
|
|
17
26
|
help="Enable debug logging to file",
|
|
18
27
|
callback=debug_callback,
|
|
19
28
|
expose_value=False,
|
|
20
29
|
is_eager=True,
|
|
30
|
+
hidden=True,
|
|
31
|
+
)(f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def interactive_option(f: Callable[P, R]) -> Callable[P, R]:
|
|
35
|
+
"""Add an interactive option to the command"""
|
|
36
|
+
|
|
37
|
+
default = is_interactive_session()
|
|
38
|
+
return click.option(
|
|
39
|
+
"--interactive/--no-interactive",
|
|
40
|
+
help="Run in interactive mode. If not provided, will default to the current session's interactive state.",
|
|
41
|
+
is_flag=True,
|
|
42
|
+
default=default,
|
|
21
43
|
)(f)
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# A place to centralize design tokens to simplify tweaking the appearance of the CLI
|
|
2
|
+
# See https://rich.readthedocs.io/en/stable/appendix/colors.html
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
HEADER_COLOR = "cornflower_blue"
|
|
6
|
+
HEADER_COLOR_HEX = "#5f87ff"
|
|
7
|
+
PRIMARY_COL = "default"
|
|
8
|
+
MUTED_COL = "grey46"
|
|
9
|
+
WARNING = "yellow"
|
|
10
|
+
ACTIVE_INDICATOR = "magenta"
|