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.
Files changed (41) hide show
  1. llama_deploy/cli/__init__.py +9 -22
  2. llama_deploy/cli/app.py +69 -0
  3. llama_deploy/cli/auth/client.py +362 -0
  4. llama_deploy/cli/client.py +47 -170
  5. llama_deploy/cli/commands/aliased_group.py +33 -0
  6. llama_deploy/cli/commands/auth.py +696 -0
  7. llama_deploy/cli/commands/deployment.py +300 -0
  8. llama_deploy/cli/commands/env.py +211 -0
  9. llama_deploy/cli/commands/init.py +313 -0
  10. llama_deploy/cli/commands/serve.py +239 -0
  11. llama_deploy/cli/config/_config.py +390 -0
  12. llama_deploy/cli/config/_migrations.py +65 -0
  13. llama_deploy/cli/config/auth_service.py +130 -0
  14. llama_deploy/cli/config/env_service.py +67 -0
  15. llama_deploy/cli/config/migrations/0001_init.sql +35 -0
  16. llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +24 -0
  17. llama_deploy/cli/config/migrations/__init__.py +7 -0
  18. llama_deploy/cli/config/schema.py +61 -0
  19. llama_deploy/cli/env.py +5 -3
  20. llama_deploy/cli/interactive_prompts/session_utils.py +37 -0
  21. llama_deploy/cli/interactive_prompts/utils.py +6 -72
  22. llama_deploy/cli/options.py +27 -5
  23. llama_deploy/cli/py.typed +0 -0
  24. llama_deploy/cli/styles.py +10 -0
  25. llama_deploy/cli/textual/deployment_form.py +263 -36
  26. llama_deploy/cli/textual/deployment_help.py +53 -0
  27. llama_deploy/cli/textual/deployment_monitor.py +466 -0
  28. llama_deploy/cli/textual/git_validation.py +20 -21
  29. llama_deploy/cli/textual/github_callback_server.py +17 -14
  30. llama_deploy/cli/textual/llama_loader.py +13 -1
  31. llama_deploy/cli/textual/secrets_form.py +28 -8
  32. llama_deploy/cli/textual/styles.tcss +49 -8
  33. llama_deploy/cli/utils/env_inject.py +23 -0
  34. {llamactl-0.2.7a1.dist-info → llamactl-0.3.0.dist-info}/METADATA +9 -6
  35. llamactl-0.3.0.dist-info/RECORD +38 -0
  36. {llamactl-0.2.7a1.dist-info → llamactl-0.3.0.dist-info}/WHEEL +1 -1
  37. llama_deploy/cli/commands.py +0 -549
  38. llama_deploy/cli/config.py +0 -173
  39. llama_deploy/cli/textual/profile_form.py +0 -171
  40. llamactl-0.2.7a1.dist-info/RECORD +0 -19
  41. {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,7 @@
1
+ """SQL migrations for llamactl configuration database.
2
+
3
+ Files are applied in lexicographic order and must start with:
4
+
5
+ PRAGMA user_version=N;
6
+
7
+ """
@@ -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 rich import print as rprint
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"[yellow]Warning: Could not parse environment variables from string: {e}[/yellow]"
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 ..client import get_client
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
@@ -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
- def global_options(f):
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[value])
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(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
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"