llamactl 0.3.0a12__tar.gz → 0.3.0a13__tar.gz
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.
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/PKG-INFO +3 -3
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/pyproject.toml +6 -3
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/__init__.py +2 -2
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/app.py +3 -2
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/client.py +26 -5
- llamactl-0.3.0a13/src/llama_deploy/cli/commands/auth.py +382 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/commands/deployment.py +89 -38
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/config.py +91 -23
- llamactl-0.3.0a13/src/llama_deploy/cli/interactive_prompts/session_utils.py +37 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/interactive_prompts/utils.py +12 -37
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/options.py +24 -1
- llamactl-0.3.0a13/src/llama_deploy/cli/platform_client.py +52 -0
- llamactl-0.3.0a13/src/llama_deploy/cli/textual/api_key_profile_form.py +563 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/deployment_form.py +11 -10
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/deployment_monitor.py +98 -105
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/git_validation.py +11 -9
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/styles.tcss +21 -5
- llamactl-0.3.0a12/src/llama_deploy/cli/commands/profile.py +0 -217
- llamactl-0.3.0a12/src/llama_deploy/cli/textual/profile_form.py +0 -170
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/README.md +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/commands/init.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/commands/serve.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/debug.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/env.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/py.typed +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/deployment_help.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
- {llamactl-0.3.0a12 → llamactl-0.3.0a13}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llamactl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a13
|
|
4
4
|
Summary: A command-line interface for managing LlamaDeploy projects and deployments
|
|
5
5
|
Author: Adrian Lyjak
|
|
6
6
|
Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
|
-
Requires-Dist: llama-deploy-core[client]>=0.3.
|
|
9
|
-
Requires-Dist: llama-deploy-appserver>=0.3.
|
|
8
|
+
Requires-Dist: llama-deploy-core[client]>=0.3.0a13,<0.4.0
|
|
9
|
+
Requires-Dist: llama-deploy-appserver>=0.3.0a13,<0.4.0
|
|
10
10
|
Requires-Dist: httpx>=0.24.0
|
|
11
11
|
Requires-Dist: rich>=13.0.0
|
|
12
12
|
Requires-Dist: questionary>=2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "llamactl"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.0a13"
|
|
4
4
|
description = "A command-line interface for managing LlamaDeploy projects and deployments"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -9,8 +9,8 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.11, <4"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"llama-deploy-core[client]>=0.3.
|
|
13
|
-
"llama-deploy-appserver>=0.3.
|
|
12
|
+
"llama-deploy-core[client]>=0.3.0a13,<0.4.0",
|
|
13
|
+
"llama-deploy-appserver>=0.3.0a13,<0.4.0",
|
|
14
14
|
"httpx>=0.24.0",
|
|
15
15
|
"rich>=13.0.0",
|
|
16
16
|
"questionary>=2.0.0",
|
|
@@ -34,6 +34,9 @@ dev = [
|
|
|
34
34
|
"pytest>=8.3.4",
|
|
35
35
|
"pytest-asyncio>=0.25.3",
|
|
36
36
|
"respx>=0.22.0",
|
|
37
|
+
"pytest-xdist>=3.8.0",
|
|
38
|
+
"ty>=0.0.1a19",
|
|
39
|
+
"ruff>=0.12.9",
|
|
37
40
|
]
|
|
38
41
|
|
|
39
42
|
[tool.uv.build-backend]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
from llama_deploy.cli.commands.auth import auth
|
|
1
2
|
from llama_deploy.cli.commands.deployment import deployments
|
|
2
3
|
from llama_deploy.cli.commands.init import init
|
|
3
|
-
from llama_deploy.cli.commands.profile import profiles
|
|
4
4
|
from llama_deploy.cli.commands.serve import serve
|
|
5
5
|
|
|
6
6
|
from .app import app
|
|
@@ -11,7 +11,7 @@ def main() -> None:
|
|
|
11
11
|
app()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
__all__ = ["app", "deployments", "
|
|
14
|
+
__all__ = ["app", "deployments", "auth", "serve", "init"]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
if __name__ == "__main__":
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from importlib.metadata import PackageNotFoundError
|
|
2
3
|
from importlib.metadata import version as pkg_version
|
|
3
4
|
|
|
@@ -26,8 +27,8 @@ def print_version(ctx: click.Context, param: click.Option, value: bool) -> None:
|
|
|
26
27
|
if profile and profile.api_url:
|
|
27
28
|
try:
|
|
28
29
|
cp_client = get_control_plane_client()
|
|
29
|
-
data = cp_client.server_version()
|
|
30
|
-
server_ver = data.
|
|
30
|
+
data = asyncio.run(cp_client.server_version())
|
|
31
|
+
server_ver = data.version
|
|
31
32
|
console.print(
|
|
32
33
|
Text.assemble(
|
|
33
34
|
"server version: ",
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from typing import AsyncGenerator
|
|
3
|
+
|
|
1
4
|
from llama_deploy.cli.config import config_manager
|
|
2
5
|
from llama_deploy.core.client.manage_client import ControlPlaneClient, ProjectClient
|
|
3
6
|
from rich import print as rprint
|
|
4
7
|
|
|
5
8
|
|
|
6
|
-
def get_control_plane_client(
|
|
9
|
+
def get_control_plane_client(
|
|
10
|
+
base_url: str | None = None, api_key: str | None = None
|
|
11
|
+
) -> ControlPlaneClient:
|
|
7
12
|
profile = config_manager.get_current_profile()
|
|
8
13
|
if not profile and not base_url:
|
|
9
14
|
rprint("\n[bold red]No profile configured![/bold red]")
|
|
@@ -13,11 +18,14 @@ def get_control_plane_client(base_url: str | None = None) -> ControlPlaneClient:
|
|
|
13
18
|
resolved_base_url = (base_url or (profile.api_url if profile else "")).rstrip("/")
|
|
14
19
|
if not resolved_base_url:
|
|
15
20
|
raise ValueError("API URL is required")
|
|
16
|
-
|
|
21
|
+
resolved_api_key = api_key or (profile.api_key_auth_token if profile else None)
|
|
22
|
+
return ControlPlaneClient(resolved_base_url, resolved_api_key)
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
def get_project_client(
|
|
20
|
-
base_url: str | None = None,
|
|
26
|
+
base_url: str | None = None,
|
|
27
|
+
project_id: str | None = None,
|
|
28
|
+
api_key: str | None = None,
|
|
21
29
|
) -> ProjectClient:
|
|
22
30
|
profile = config_manager.get_current_profile()
|
|
23
31
|
if not profile:
|
|
@@ -28,7 +36,20 @@ def get_project_client(
|
|
|
28
36
|
resolved_base_url = (base_url or profile.api_url or "").rstrip("/")
|
|
29
37
|
if not resolved_base_url:
|
|
30
38
|
raise ValueError("API URL is required")
|
|
31
|
-
resolved_project_id = project_id or profile.
|
|
39
|
+
resolved_project_id = project_id or profile.project_id
|
|
32
40
|
if not resolved_project_id:
|
|
33
41
|
raise ValueError("Project ID is required")
|
|
34
|
-
|
|
42
|
+
resolved_api_key = api_key or profile.api_key_auth_token
|
|
43
|
+
return ProjectClient(resolved_base_url, resolved_project_id, resolved_api_key)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@asynccontextmanager
|
|
47
|
+
async def project_client_context() -> AsyncGenerator[ProjectClient, None]:
|
|
48
|
+
client = get_project_client()
|
|
49
|
+
try:
|
|
50
|
+
yield client
|
|
51
|
+
finally:
|
|
52
|
+
try:
|
|
53
|
+
await client.aclose()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import questionary
|
|
5
|
+
from llama_deploy.cli.client import get_control_plane_client
|
|
6
|
+
from llama_deploy.cli.interactive_prompts.session_utils import is_interactive_session
|
|
7
|
+
from rich import print as rprint
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from ..app import app, console
|
|
11
|
+
from ..config import Profile, config_manager
|
|
12
|
+
from ..interactive_prompts.utils import (
|
|
13
|
+
select_profile,
|
|
14
|
+
)
|
|
15
|
+
from ..options import global_options, interactive_option
|
|
16
|
+
from ..textual.api_key_profile_form import (
|
|
17
|
+
create_api_key_profile_form,
|
|
18
|
+
edit_api_key_profile_form,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Create sub-applications for organizing commands
|
|
23
|
+
@app.group(
|
|
24
|
+
help="Login to llama cloud control plane to manage deployments",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
@global_options
|
|
28
|
+
def auth() -> None:
|
|
29
|
+
"""Login to llama cloud control plane"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Profile commands
|
|
34
|
+
@auth.command("login")
|
|
35
|
+
@global_options
|
|
36
|
+
@click.option(
|
|
37
|
+
"--api-url",
|
|
38
|
+
help="Specify a custom control plane API URL to log into",
|
|
39
|
+
default="https://api.cloud.llamaindex.ai",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--name",
|
|
43
|
+
help="Specify a memorable name for the API key login when creating non-interactively",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--project-id",
|
|
47
|
+
help="Project ID to use for the login when creating non-interactively",
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--api-key",
|
|
51
|
+
help="Advanced: Control plane/Llama Cloud Bearer API key. Only needed if control plane is authenticated",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--login-url", help="Advanced: Custom login URL for initiating OpenID Connect flow"
|
|
55
|
+
)
|
|
56
|
+
@interactive_option
|
|
57
|
+
def create_login_profile(
|
|
58
|
+
name: str | None,
|
|
59
|
+
api_url: str,
|
|
60
|
+
project_id: str | None,
|
|
61
|
+
api_key: str | None,
|
|
62
|
+
login_url: str | None,
|
|
63
|
+
interactive: bool,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Login to llama cloud control plane as a new profile. May specify name, project ID, and API URL when creating non-interactively"""
|
|
66
|
+
try:
|
|
67
|
+
# If all required args are provided via CLI, skip interactive mode
|
|
68
|
+
if name and project_id:
|
|
69
|
+
# Use CLI args directly
|
|
70
|
+
profile = config_manager.create_profile(name, api_url, project_id, api_key)
|
|
71
|
+
rprint(f"[green]Manually created profile '{profile.name}'[/green]")
|
|
72
|
+
|
|
73
|
+
# Automatically switch to the new profile
|
|
74
|
+
config_manager.set_current_profile(name)
|
|
75
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
76
|
+
return
|
|
77
|
+
elif not interactive:
|
|
78
|
+
raise click.ClickException(
|
|
79
|
+
"No --name or --project-id provided. Run `llamactl auth login --help` for more information."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Use interactive creation
|
|
83
|
+
profile = create_api_key_profile_form(
|
|
84
|
+
api_url=api_url,
|
|
85
|
+
project_id=project_id,
|
|
86
|
+
api_key_auth_token=api_key,
|
|
87
|
+
)
|
|
88
|
+
if profile is None:
|
|
89
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
rprint(f"[green]Logged in as '{profile.name}'[/green]")
|
|
94
|
+
|
|
95
|
+
# Automatically switch to the new profile
|
|
96
|
+
config_manager.set_current_profile(profile.name)
|
|
97
|
+
rprint(f"[green]Switched to profile '{profile.name}'[/green]")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
rprint(f"[red]Error creating profile: {e}[/red]")
|
|
100
|
+
raise click.Abort()
|
|
101
|
+
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
104
|
+
raise click.Abort()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
107
|
+
raise click.Abort()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@auth.command("token")
|
|
111
|
+
@global_options
|
|
112
|
+
@click.option(
|
|
113
|
+
"--api-url",
|
|
114
|
+
help="Specify a custom control plane API URL to log into",
|
|
115
|
+
default="https://api.cloud.llamaindex.ai",
|
|
116
|
+
)
|
|
117
|
+
@click.option(
|
|
118
|
+
"--name",
|
|
119
|
+
help="Specify a memorable name for the API key login when creating non-interactively",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--project-id",
|
|
123
|
+
help="Project ID to use for the login when creating non-interactively",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--api-key",
|
|
127
|
+
help="API key to use for the login when creating non-interactively",
|
|
128
|
+
)
|
|
129
|
+
@interactive_option
|
|
130
|
+
def create_api_key_profile(
|
|
131
|
+
api_url: str,
|
|
132
|
+
name: str | None,
|
|
133
|
+
project_id: str | None,
|
|
134
|
+
api_key: str | None,
|
|
135
|
+
interactive: bool,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Authenticate with an API key rather than logging in"""
|
|
138
|
+
if not interactive:
|
|
139
|
+
if not name or not project_id:
|
|
140
|
+
raise click.ClickException(
|
|
141
|
+
"No --name or --project-id provided. Run `llamactl auth create-token --help` for more information."
|
|
142
|
+
)
|
|
143
|
+
profile = config_manager.create_profile(name, api_url, project_id, api_key)
|
|
144
|
+
rprint(f"[green]Created API key profile '{profile.name}'[/green]")
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
profile = create_api_key_profile_form(
|
|
148
|
+
name=name,
|
|
149
|
+
api_url=api_url,
|
|
150
|
+
project_id=project_id,
|
|
151
|
+
api_key_auth_token=api_key,
|
|
152
|
+
)
|
|
153
|
+
if profile is None:
|
|
154
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
155
|
+
return
|
|
156
|
+
rprint(f"[green]Created API key profile '{profile.name}'[/green]")
|
|
157
|
+
config_manager.set_current_profile(profile.name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@auth.command("list")
|
|
161
|
+
@global_options
|
|
162
|
+
def list_profiles() -> None:
|
|
163
|
+
"""List all logged in profiles"""
|
|
164
|
+
try:
|
|
165
|
+
profiles = config_manager.list_profiles()
|
|
166
|
+
current_name = config_manager.get_current_profile_name()
|
|
167
|
+
|
|
168
|
+
if not profiles:
|
|
169
|
+
rprint("[yellow]No profiles found[/yellow]")
|
|
170
|
+
rprint("Create one with: [cyan]llamactl profile create[/cyan]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
table = Table(title="Profiles")
|
|
174
|
+
table.add_column("Name", style="cyan")
|
|
175
|
+
table.add_column("API URL", style="green")
|
|
176
|
+
table.add_column("Active Project", style="yellow")
|
|
177
|
+
table.add_column("Current", style="magenta")
|
|
178
|
+
|
|
179
|
+
for profile in profiles:
|
|
180
|
+
is_current = "✓" if profile.name == current_name else ""
|
|
181
|
+
active_project = profile.project_id or "-"
|
|
182
|
+
table.add_row(profile.name, profile.api_url, active_project, is_current)
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
188
|
+
raise click.Abort()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@auth.command("switch")
|
|
192
|
+
@global_options
|
|
193
|
+
@click.argument("name", required=False)
|
|
194
|
+
@interactive_option
|
|
195
|
+
def switch_profile(name: str | None, interactive: bool) -> None:
|
|
196
|
+
"""Switch to a different profile"""
|
|
197
|
+
try:
|
|
198
|
+
name = select_profile(name) if interactive else name
|
|
199
|
+
if not name:
|
|
200
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
profile = config_manager.get_profile(name)
|
|
204
|
+
if not profile:
|
|
205
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
206
|
+
raise click.Abort()
|
|
207
|
+
|
|
208
|
+
config_manager.set_current_profile(name)
|
|
209
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
213
|
+
raise click.Abort()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@auth.command("logout")
|
|
217
|
+
@global_options
|
|
218
|
+
@click.argument("name", required=False)
|
|
219
|
+
@interactive_option
|
|
220
|
+
def delete_profile(name: str | None, interactive: bool) -> None:
|
|
221
|
+
"""Logout from a profile and wipe all associated data"""
|
|
222
|
+
try:
|
|
223
|
+
name = select_profile(name) if interactive else name
|
|
224
|
+
if not name:
|
|
225
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
profile = config_manager.get_profile(name)
|
|
229
|
+
if not profile:
|
|
230
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
231
|
+
raise click.Abort()
|
|
232
|
+
|
|
233
|
+
if config_manager.delete_profile(name):
|
|
234
|
+
rprint(f"[green]Logged out from '{name}'[/green]")
|
|
235
|
+
else:
|
|
236
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
240
|
+
raise click.Abort()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@auth.command("edit-token")
|
|
244
|
+
@global_options
|
|
245
|
+
@click.argument("name", required=False)
|
|
246
|
+
def edit_api_key_profile(name: str | None) -> None:
|
|
247
|
+
"""Edit an API key profile"""
|
|
248
|
+
if is_interactive_session():
|
|
249
|
+
raise click.ClickException(
|
|
250
|
+
"Interactive editing of API key profiles is not supported. You can instead delete and `llamactl auth create-token` to create a new profile."
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
name = select_profile(name)
|
|
254
|
+
if not name:
|
|
255
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Get current profile
|
|
259
|
+
maybe_profile = config_manager.get_profile(name)
|
|
260
|
+
if not maybe_profile:
|
|
261
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
262
|
+
raise click.Abort()
|
|
263
|
+
profile = maybe_profile
|
|
264
|
+
|
|
265
|
+
# Use the interactive edit menu
|
|
266
|
+
updated = edit_api_key_profile_form(profile)
|
|
267
|
+
if updated is None:
|
|
268
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
current_profile = config_manager.get_current_profile()
|
|
273
|
+
if not current_profile or current_profile.name != updated.name:
|
|
274
|
+
config_manager.set_current_profile(updated.name)
|
|
275
|
+
rprint(f"[green]Updated profile '{profile.name}'[/green]")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
rprint(f"[red]Error updating profile: {e}[/red]")
|
|
278
|
+
raise click.Abort()
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
282
|
+
raise click.Abort()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Projects commands
|
|
286
|
+
@auth.command("project")
|
|
287
|
+
@click.argument("project_id", required=False)
|
|
288
|
+
@interactive_option
|
|
289
|
+
@global_options
|
|
290
|
+
def change_project(project_id: str | None, interactive: bool) -> None:
|
|
291
|
+
"""Change the active project for the current profile"""
|
|
292
|
+
profile = validate_authenticated_profile(interactive)
|
|
293
|
+
if project_id:
|
|
294
|
+
config_manager.set_project(profile.name, project_id)
|
|
295
|
+
rprint(f"[green]Set active project to '{project_id}'[/green]")
|
|
296
|
+
return
|
|
297
|
+
if not interactive:
|
|
298
|
+
raise click.ClickException(
|
|
299
|
+
"No --project-id provided. Run `llamactl auth project --help` for more information."
|
|
300
|
+
)
|
|
301
|
+
try:
|
|
302
|
+
client = get_control_plane_client()
|
|
303
|
+
projects = asyncio.run(client.list_projects())
|
|
304
|
+
|
|
305
|
+
if not projects:
|
|
306
|
+
rprint("[yellow]No projects found[/yellow]")
|
|
307
|
+
return
|
|
308
|
+
result = questionary.select(
|
|
309
|
+
"Select a project",
|
|
310
|
+
choices=[
|
|
311
|
+
questionary.Choice(
|
|
312
|
+
title=f"{project.project_name} ({project.deployment_count} deployments)",
|
|
313
|
+
value=project.project_id,
|
|
314
|
+
)
|
|
315
|
+
for project in projects
|
|
316
|
+
],
|
|
317
|
+
).ask()
|
|
318
|
+
if result:
|
|
319
|
+
config_manager.set_project(profile.name, result)
|
|
320
|
+
rprint(f"[green]Set active project to '{result}'[/green]")
|
|
321
|
+
else:
|
|
322
|
+
rprint("[yellow]No project selected[/yellow]")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
325
|
+
raise click.Abort()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def validate_authenticated_profile(interactive: bool) -> Profile:
|
|
329
|
+
"""Validate that the user is authenticated"""
|
|
330
|
+
|
|
331
|
+
profile = config_manager.get_current_profile()
|
|
332
|
+
if profile:
|
|
333
|
+
return profile
|
|
334
|
+
elif not interactive:
|
|
335
|
+
raise click.ClickException(
|
|
336
|
+
"No profile configured. Run `llamactl profile create` to create a profile."
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
profiles = config_manager.list_profiles()
|
|
340
|
+
if len(profiles) > 1:
|
|
341
|
+
selected_profile = select_profile()
|
|
342
|
+
if not selected_profile:
|
|
343
|
+
raise click.ClickException("No profile selected")
|
|
344
|
+
config_manager.set_current_profile(selected_profile)
|
|
345
|
+
found_profile = config_manager.get_profile(selected_profile)
|
|
346
|
+
if found_profile is None:
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
f"Unexpected error: Profile '{selected_profile}' not found"
|
|
349
|
+
)
|
|
350
|
+
return found_profile
|
|
351
|
+
else:
|
|
352
|
+
selected_profile_obj = create_profile_interactive()
|
|
353
|
+
if selected_profile_obj is None:
|
|
354
|
+
raise click.ClickException("No profile selected")
|
|
355
|
+
return selected_profile_obj
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create_profile_interactive(
|
|
359
|
+
api_url: str = "https://api.cloud.llamaindex.ai",
|
|
360
|
+
project_id: str | None = None,
|
|
361
|
+
api_key_auth_token: str | None = None,
|
|
362
|
+
) -> Profile | None:
|
|
363
|
+
should_continue = questionary.select(
|
|
364
|
+
"This action requires you to authenticate with LlamaCloud. Continue?",
|
|
365
|
+
choices=[
|
|
366
|
+
questionary.Choice(title="Add API Key", value="add_api_key"),
|
|
367
|
+
questionary.Choice(title="Cancel", value="cancel"),
|
|
368
|
+
],
|
|
369
|
+
).ask()
|
|
370
|
+
if should_continue == "add_api_key":
|
|
371
|
+
profile_form = create_api_key_profile_form(
|
|
372
|
+
api_url=api_url,
|
|
373
|
+
project_id=project_id,
|
|
374
|
+
api_key_auth_token=api_key_auth_token,
|
|
375
|
+
)
|
|
376
|
+
if profile_form is None:
|
|
377
|
+
raise click.ClickException("No profile selected")
|
|
378
|
+
profile = profile_form.to_profile()
|
|
379
|
+
config_manager.set_current_profile(profile.name)
|
|
380
|
+
return profile
|
|
381
|
+
|
|
382
|
+
return None
|