llamactl 0.3.0a13__py3-none-any.whl → 0.3.0a14__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 +2 -1
- llama_deploy/cli/app.py +4 -7
- llama_deploy/cli/client.py +18 -32
- llama_deploy/cli/commands/auth.py +230 -235
- llama_deploy/cli/commands/deployment.py +24 -36
- llama_deploy/cli/commands/env.py +206 -0
- llama_deploy/cli/config/_config.py +385 -0
- llama_deploy/cli/config/auth_service.py +68 -0
- llama_deploy/cli/config/env_service.py +64 -0
- llama_deploy/cli/config/schema.py +31 -0
- llama_deploy/cli/interactive_prompts/utils.py +0 -39
- llama_deploy/cli/options.py +0 -9
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a14.dist-info}/METADATA +3 -3
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a14.dist-info}/RECORD +16 -13
- llama_deploy/cli/config.py +0 -241
- llama_deploy/cli/textual/api_key_profile_form.py +0 -563
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a14.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a14.dist-info}/entry_points.txt +0 -0
|
@@ -3,20 +3,17 @@ import asyncio
|
|
|
3
3
|
import click
|
|
4
4
|
import questionary
|
|
5
5
|
from llama_deploy.cli.client import get_control_plane_client
|
|
6
|
-
from llama_deploy.cli.
|
|
6
|
+
from llama_deploy.cli.config.auth_service import AuthService
|
|
7
|
+
from llama_deploy.cli.config.env_service import service
|
|
8
|
+
from llama_deploy.core.client.manage_client import ClientError, ControlPlaneClient
|
|
9
|
+
from llama_deploy.core.schema.projects import ProjectSummary
|
|
7
10
|
from rich import print as rprint
|
|
8
11
|
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
9
13
|
|
|
10
14
|
from ..app import app, console
|
|
11
|
-
from ..config import
|
|
12
|
-
from ..interactive_prompts.utils import (
|
|
13
|
-
select_profile,
|
|
14
|
-
)
|
|
15
|
+
from ..config.schema import Auth
|
|
15
16
|
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
17
|
|
|
21
18
|
|
|
22
19
|
# Create sub-applications for organizing commands
|
|
@@ -30,156 +27,90 @@ def auth() -> None:
|
|
|
30
27
|
pass
|
|
31
28
|
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
@auth.command("login")
|
|
30
|
+
@auth.command("token")
|
|
35
31
|
@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
32
|
@click.option(
|
|
46
33
|
"--project-id",
|
|
47
34
|
help="Project ID to use for the login when creating non-interactively",
|
|
48
35
|
)
|
|
49
36
|
@click.option(
|
|
50
37
|
"--api-key",
|
|
51
|
-
help="
|
|
52
|
-
)
|
|
53
|
-
@click.option(
|
|
54
|
-
"--login-url", help="Advanced: Custom login URL for initiating OpenID Connect flow"
|
|
38
|
+
help="API key to use for the login when creating non-interactively",
|
|
55
39
|
)
|
|
56
40
|
@interactive_option
|
|
57
|
-
def
|
|
58
|
-
name: str | None,
|
|
59
|
-
api_url: str,
|
|
41
|
+
def create_api_key_profile(
|
|
60
42
|
project_id: str | None,
|
|
61
43
|
api_key: str | None,
|
|
62
|
-
login_url: str | None,
|
|
63
44
|
interactive: bool,
|
|
64
45
|
) -> None:
|
|
65
|
-
"""
|
|
46
|
+
"""Authenticate with an API key and create a profile in the current environment."""
|
|
66
47
|
try:
|
|
67
|
-
|
|
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
|
-
)
|
|
48
|
+
auth_svc = service.current_auth_service()
|
|
81
49
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
rprint(
|
|
50
|
+
# Non-interactive mode: require both api-key and project-id
|
|
51
|
+
if not interactive:
|
|
52
|
+
if not api_key or not project_id:
|
|
53
|
+
raise click.ClickException(
|
|
54
|
+
"--api-key and --project-id are required in non-interactive mode"
|
|
55
|
+
)
|
|
56
|
+
created = auth_svc.create_profile_from_token(project_id, api_key)
|
|
57
|
+
rprint(
|
|
58
|
+
f"[green]Created API key profile '{created.name}' and set as current[/green]"
|
|
59
|
+
)
|
|
90
60
|
return
|
|
91
61
|
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
# Interactive mode: prompt for token (masked) and validate
|
|
63
|
+
token_value = api_key or _prompt_for_api_key()
|
|
64
|
+
projects = _validate_token_and_list_projects(auth_svc, token_value)
|
|
94
65
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
66
|
+
# Select or enter project ID
|
|
67
|
+
selected_project_id = project_id or _select_or_enter_project(
|
|
68
|
+
projects, auth_svc.env.requires_auth
|
|
69
|
+
)
|
|
70
|
+
if not selected_project_id:
|
|
71
|
+
rprint("[yellow]No project selected[/yellow]")
|
|
72
|
+
return
|
|
101
73
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
74
|
+
# Create and set profile
|
|
75
|
+
created = auth_svc.create_profile_from_token(selected_project_id, token_value)
|
|
76
|
+
rprint(
|
|
77
|
+
f"[green]Created API key profile '{created.name}' and set as current[/green]"
|
|
78
|
+
)
|
|
105
79
|
except Exception as e:
|
|
106
80
|
rprint(f"[red]Error: {e}[/red]")
|
|
107
81
|
raise click.Abort()
|
|
108
82
|
|
|
109
83
|
|
|
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
84
|
@auth.command("list")
|
|
161
85
|
@global_options
|
|
162
86
|
def list_profiles() -> None:
|
|
163
87
|
"""List all logged in profiles"""
|
|
164
88
|
try:
|
|
165
|
-
|
|
166
|
-
|
|
89
|
+
auth_svc = service.current_auth_service()
|
|
90
|
+
profiles = auth_svc.list_profiles()
|
|
91
|
+
current = auth_svc.get_current_profile()
|
|
167
92
|
|
|
168
93
|
if not profiles:
|
|
169
94
|
rprint("[yellow]No profiles found[/yellow]")
|
|
170
|
-
rprint("Create one with: [cyan]llamactl
|
|
95
|
+
rprint("Create one with: [cyan]llamactl auth token[/cyan]")
|
|
171
96
|
return
|
|
172
97
|
|
|
173
|
-
table = Table(
|
|
174
|
-
table.add_column("Name"
|
|
175
|
-
table.add_column("
|
|
176
|
-
table.add_column("Active Project", style="yellow")
|
|
177
|
-
table.add_column("Current", style="magenta")
|
|
98
|
+
table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
|
|
99
|
+
table.add_column(" Name")
|
|
100
|
+
table.add_column("Active Project", style="grey46")
|
|
178
101
|
|
|
179
102
|
for profile in profiles:
|
|
180
|
-
|
|
103
|
+
text = Text()
|
|
104
|
+
if profile == current:
|
|
105
|
+
text.append("* ", style="magenta")
|
|
106
|
+
else:
|
|
107
|
+
text.append(" ")
|
|
108
|
+
text.append(profile.name)
|
|
181
109
|
active_project = profile.project_id or "-"
|
|
182
|
-
table.add_row(
|
|
110
|
+
table.add_row(
|
|
111
|
+
text,
|
|
112
|
+
active_project,
|
|
113
|
+
)
|
|
183
114
|
|
|
184
115
|
console.print(table)
|
|
185
116
|
|
|
@@ -194,19 +125,15 @@ def list_profiles() -> None:
|
|
|
194
125
|
@interactive_option
|
|
195
126
|
def switch_profile(name: str | None, interactive: bool) -> None:
|
|
196
127
|
"""Switch to a different profile"""
|
|
128
|
+
auth_svc = service.current_auth_service()
|
|
197
129
|
try:
|
|
198
|
-
|
|
199
|
-
if not
|
|
130
|
+
selected_auth = _select_profile(auth_svc, name, interactive)
|
|
131
|
+
if not selected_auth:
|
|
200
132
|
rprint("[yellow]No profile selected[/yellow]")
|
|
201
133
|
return
|
|
202
134
|
|
|
203
|
-
|
|
204
|
-
|
|
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]")
|
|
135
|
+
auth_svc.set_current_profile(selected_auth.name)
|
|
136
|
+
rprint(f"[green]Switched to profile '{selected_auth.name}'[/green]")
|
|
210
137
|
|
|
211
138
|
except Exception as e:
|
|
212
139
|
rprint(f"[red]Error: {e}[/red]")
|
|
@@ -220,62 +147,16 @@ def switch_profile(name: str | None, interactive: bool) -> None:
|
|
|
220
147
|
def delete_profile(name: str | None, interactive: bool) -> None:
|
|
221
148
|
"""Logout from a profile and wipe all associated data"""
|
|
222
149
|
try:
|
|
223
|
-
|
|
224
|
-
|
|
150
|
+
auth_svc = service.current_auth_service()
|
|
151
|
+
auth = _select_profile(auth_svc, name, interactive)
|
|
152
|
+
if not auth:
|
|
225
153
|
rprint("[yellow]No profile selected[/yellow]")
|
|
226
154
|
return
|
|
227
155
|
|
|
228
|
-
|
|
229
|
-
|
|
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]")
|
|
156
|
+
if auth_svc.delete_profile(auth.name):
|
|
157
|
+
rprint(f"[green]Logged out from '{auth.name}'[/green]")
|
|
235
158
|
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()
|
|
159
|
+
rprint(f"[red]Profile '{auth.name}' not found[/red]")
|
|
279
160
|
|
|
280
161
|
except Exception as e:
|
|
281
162
|
rprint(f"[red]Error: {e}[/red]")
|
|
@@ -290,8 +171,11 @@ def edit_api_key_profile(name: str | None) -> None:
|
|
|
290
171
|
def change_project(project_id: str | None, interactive: bool) -> None:
|
|
291
172
|
"""Change the active project for the current profile"""
|
|
292
173
|
profile = validate_authenticated_profile(interactive)
|
|
174
|
+
if project_id and profile.project_id == project_id:
|
|
175
|
+
return
|
|
176
|
+
auth_svc = service.current_auth_service()
|
|
293
177
|
if project_id:
|
|
294
|
-
|
|
178
|
+
auth_svc.set_project(profile.name, project_id)
|
|
295
179
|
rprint(f"[green]Set active project to '{project_id}'[/green]")
|
|
296
180
|
return
|
|
297
181
|
if not interactive:
|
|
@@ -313,10 +197,18 @@ def change_project(project_id: str | None, interactive: bool) -> None:
|
|
|
313
197
|
value=project.project_id,
|
|
314
198
|
)
|
|
315
199
|
for project in projects
|
|
316
|
-
]
|
|
200
|
+
]
|
|
201
|
+
+ (
|
|
202
|
+
[questionary.Choice(title="Create new project", value="__CREATE__")]
|
|
203
|
+
if not auth_svc.env.requires_auth
|
|
204
|
+
else []
|
|
205
|
+
),
|
|
317
206
|
).ask()
|
|
207
|
+
if result == "__CREATE__":
|
|
208
|
+
project_id = questionary.text("Enter project ID").ask()
|
|
209
|
+
result = project_id
|
|
318
210
|
if result:
|
|
319
|
-
|
|
211
|
+
auth_svc.set_project(profile.name, result)
|
|
320
212
|
rprint(f"[green]Set active project to '{result}'[/green]")
|
|
321
213
|
else:
|
|
322
214
|
rprint("[yellow]No project selected[/yellow]")
|
|
@@ -325,58 +217,161 @@ def change_project(project_id: str | None, interactive: bool) -> None:
|
|
|
325
217
|
raise click.Abort()
|
|
326
218
|
|
|
327
219
|
|
|
328
|
-
def validate_authenticated_profile(interactive: bool) ->
|
|
329
|
-
"""Validate that the user is authenticated
|
|
220
|
+
def validate_authenticated_profile(interactive: bool) -> Auth:
|
|
221
|
+
"""Validate that the user is authenticated within the current environment.
|
|
222
|
+
|
|
223
|
+
- If there is a current profile, return it.
|
|
224
|
+
- If multiple profiles exist in the current environment, prompt to select in interactive mode.
|
|
225
|
+
- If none exist:
|
|
226
|
+
- If environment requires_auth: run token flow inline.
|
|
227
|
+
- Else: create profile without token after selecting a project.
|
|
228
|
+
"""
|
|
330
229
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
230
|
+
auth_svc = service.current_auth_service()
|
|
231
|
+
existing = auth_svc.get_current_profile()
|
|
232
|
+
if existing:
|
|
233
|
+
return existing
|
|
234
|
+
|
|
235
|
+
if not interactive:
|
|
335
236
|
raise click.ClickException(
|
|
336
|
-
"No profile configured. Run `llamactl
|
|
237
|
+
"No profile configured. Run `llamactl auth token` to create a profile."
|
|
337
238
|
)
|
|
239
|
+
|
|
240
|
+
# Filter profiles by current environment
|
|
241
|
+
env_profiles = auth_svc.list_profiles()
|
|
242
|
+
current_env = auth_svc.env
|
|
243
|
+
|
|
244
|
+
if len(env_profiles) > 1:
|
|
245
|
+
# Prompt to select
|
|
246
|
+
choice: Auth | None = questionary.select(
|
|
247
|
+
"Select profile",
|
|
248
|
+
choices=[questionary.Choice(title=p.name, value=p) for p in env_profiles],
|
|
249
|
+
).ask()
|
|
250
|
+
if not choice:
|
|
251
|
+
raise click.ClickException("No profile selected")
|
|
252
|
+
auth_svc.set_current_profile(choice.name)
|
|
253
|
+
return choice
|
|
254
|
+
if len(env_profiles) == 1:
|
|
255
|
+
only = env_profiles[0]
|
|
256
|
+
auth_svc.set_current_profile(only.name)
|
|
257
|
+
return only
|
|
258
|
+
|
|
259
|
+
# No profiles exist for this env
|
|
260
|
+
if current_env.requires_auth:
|
|
261
|
+
# Inline token flow
|
|
262
|
+
created = _token_flow_for_env(auth_svc)
|
|
263
|
+
return created
|
|
338
264
|
else:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
265
|
+
# No auth required: select project and create a default profile without token
|
|
266
|
+
project_id: str | None = questionary.text("Enter project ID").ask()
|
|
267
|
+
if not project_id:
|
|
268
|
+
raise click.ClickException("No project ID provided")
|
|
269
|
+
created = auth_svc.create_profile_from_token(project_id, None)
|
|
270
|
+
return created
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# -----------------------------
|
|
274
|
+
# Helpers for token/profile flow
|
|
275
|
+
# -----------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _prompt_for_api_key() -> str:
|
|
279
|
+
while True:
|
|
280
|
+
entered = questionary.password("Enter API key token").ask()
|
|
281
|
+
if entered:
|
|
282
|
+
return entered.strip()
|
|
283
|
+
rprint("[yellow]API key is required[/yellow]")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _validate_token_and_list_projects(
|
|
287
|
+
auth_svc: AuthService, api_key: str
|
|
288
|
+
) -> list[ProjectSummary]:
|
|
289
|
+
async def _run():
|
|
290
|
+
async with ControlPlaneClient.ctx(auth_svc.env.api_url, api_key) as client:
|
|
291
|
+
return await client.list_projects()
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
return asyncio.run(_run())
|
|
295
|
+
except ClientError as e:
|
|
296
|
+
if getattr(e, "status_code", None) == 401:
|
|
297
|
+
rprint("[red]Invalid API key. Please try again.[/red]")
|
|
298
|
+
return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
|
|
299
|
+
if getattr(e, "status_code", None) == 403:
|
|
300
|
+
rprint("[red]This environment requires a valid API key.[/red]")
|
|
301
|
+
return _validate_token_and_list_projects(auth_svc, _prompt_for_api_key())
|
|
302
|
+
raise
|
|
303
|
+
except Exception as e:
|
|
304
|
+
raise click.ClickException(f"Failed to validate API key: {e}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _select_or_enter_project(
|
|
308
|
+
projects: list[ProjectSummary], requires_auth: bool
|
|
309
|
+
) -> str | None:
|
|
310
|
+
if not projects:
|
|
311
|
+
return None
|
|
312
|
+
# select the only authorized project if there is only one
|
|
313
|
+
elif len(projects) == 1 and requires_auth:
|
|
314
|
+
return projects[0].project_id
|
|
315
|
+
else:
|
|
316
|
+
choice = questionary.select(
|
|
317
|
+
"Select a project",
|
|
318
|
+
choices=[
|
|
319
|
+
questionary.Choice(
|
|
320
|
+
title=f"{p.project_name} ({p.deployment_count} deployments)",
|
|
321
|
+
value=p.project_id,
|
|
349
322
|
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
project_id:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
323
|
+
for p in projects
|
|
324
|
+
],
|
|
325
|
+
).ask()
|
|
326
|
+
return choice
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _token_flow_for_env(auth_service: AuthService) -> Auth:
|
|
330
|
+
token_value = _prompt_for_api_key()
|
|
331
|
+
projects = _validate_token_and_list_projects(auth_service, token_value)
|
|
332
|
+
project_id = _select_or_enter_project(projects, auth_service.env.requires_auth)
|
|
333
|
+
if not project_id:
|
|
334
|
+
raise click.ClickException("No project selected")
|
|
335
|
+
created = auth_service.create_profile_from_token(project_id, token_value)
|
|
336
|
+
return created
|
|
381
337
|
|
|
382
|
-
|
|
338
|
+
|
|
339
|
+
def _select_profile(
|
|
340
|
+
auth_svc: AuthService, profile_name: str | None, is_interactive: bool
|
|
341
|
+
) -> Auth | None:
|
|
342
|
+
"""
|
|
343
|
+
Select a profile interactively if name not provided.
|
|
344
|
+
Returns the selected profile name or None if cancelled.
|
|
345
|
+
|
|
346
|
+
In non-interactive sessions, returns None if profile_name is not provided.
|
|
347
|
+
"""
|
|
348
|
+
if profile_name:
|
|
349
|
+
profile = auth_svc.get_profile(profile_name)
|
|
350
|
+
if profile:
|
|
351
|
+
return profile
|
|
352
|
+
|
|
353
|
+
# Don't attempt interactive selection in non-interactive sessions
|
|
354
|
+
if not is_interactive:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
profiles = auth_svc.list_profiles()
|
|
359
|
+
|
|
360
|
+
if not profiles:
|
|
361
|
+
rprint("[yellow]No profiles found[/yellow]")
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
choices = []
|
|
365
|
+
current = auth_svc.get_current_profile()
|
|
366
|
+
|
|
367
|
+
for profile in profiles:
|
|
368
|
+
title = f"{profile.name} ({profile.api_url})"
|
|
369
|
+
if profile == current:
|
|
370
|
+
title += " [current]"
|
|
371
|
+
choices.append(questionary.Choice(title=title, value=profile))
|
|
372
|
+
|
|
373
|
+
return questionary.select("Select profile:", choices=choices).ask()
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
rprint(f"[red]Error loading profiles: {e}[/red]")
|
|
377
|
+
return None
|
|
@@ -14,6 +14,7 @@ from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
|
14
14
|
from llama_deploy.core.schema.deployments import DeploymentUpdate
|
|
15
15
|
from rich import print as rprint
|
|
16
16
|
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
17
18
|
|
|
18
19
|
from ..app import app, console
|
|
19
20
|
from ..client import get_project_client
|
|
@@ -55,36 +56,23 @@ def list_deployments(interactive: bool) -> None:
|
|
|
55
56
|
)
|
|
56
57
|
return
|
|
57
58
|
|
|
58
|
-
table = Table(
|
|
59
|
-
table.add_column("Name"
|
|
60
|
-
table.add_column("
|
|
61
|
-
table.add_column("
|
|
62
|
-
table.add_column("Repository", style="blue")
|
|
63
|
-
table.add_column("Deployment File", style="magenta")
|
|
64
|
-
table.add_column("Git Ref", style="white")
|
|
65
|
-
table.add_column("PAT", style="red")
|
|
66
|
-
table.add_column("Secrets", style="bright_green")
|
|
59
|
+
table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
|
|
60
|
+
table.add_column("Name")
|
|
61
|
+
table.add_column("Status", style="grey46")
|
|
62
|
+
table.add_column("Repository", style="grey46")
|
|
67
63
|
|
|
68
64
|
for deployment in deployments:
|
|
69
|
-
name = deployment.
|
|
70
|
-
deployment_id = deployment.id
|
|
65
|
+
name = deployment.id
|
|
71
66
|
status = deployment.status
|
|
72
67
|
repo_url = deployment.repo_url
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
secret_names = deployment.secret_names
|
|
77
|
-
secrets_display = str(len(secret_names)) if secret_names else "-"
|
|
68
|
+
gh = "https://github.com/"
|
|
69
|
+
if repo_url.startswith(gh):
|
|
70
|
+
repo_url = "gh:" + repo_url.removeprefix(gh)
|
|
78
71
|
|
|
79
72
|
table.add_row(
|
|
80
73
|
name,
|
|
81
|
-
deployment_id,
|
|
82
74
|
status,
|
|
83
75
|
repo_url,
|
|
84
|
-
deployment_file_path,
|
|
85
|
-
git_ref,
|
|
86
|
-
has_pat,
|
|
87
|
-
secrets_display,
|
|
88
76
|
)
|
|
89
77
|
|
|
90
78
|
console.print(table)
|
|
@@ -114,25 +102,25 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
114
102
|
|
|
115
103
|
deployment = asyncio.run(client.get_deployment(deployment_id))
|
|
116
104
|
|
|
117
|
-
table = Table(
|
|
118
|
-
table.add_column("Property", style="
|
|
119
|
-
table.add_column("Value"
|
|
105
|
+
table = Table(show_edge=False, box=None, header_style="bold cornflower_blue")
|
|
106
|
+
table.add_column("Property", style="grey46", justify="right")
|
|
107
|
+
table.add_column("Value")
|
|
120
108
|
|
|
121
|
-
table.add_row("ID", deployment.id)
|
|
122
|
-
table.add_row("Project ID", deployment.project_id)
|
|
123
|
-
table.add_row("Status", deployment.status)
|
|
124
|
-
table.add_row("Repository", deployment.repo_url)
|
|
125
|
-
table.add_row("Deployment File", deployment.deployment_file_path)
|
|
126
|
-
table.add_row("Git Ref", deployment.git_ref)
|
|
127
|
-
table.add_row("Has PAT", str(deployment.has_personal_access_token))
|
|
109
|
+
table.add_row("ID", Text(deployment.id))
|
|
110
|
+
table.add_row("Project ID", Text(deployment.project_id))
|
|
111
|
+
table.add_row("Status", Text(deployment.status))
|
|
112
|
+
table.add_row("Repository", Text(deployment.repo_url))
|
|
113
|
+
table.add_row("Deployment File", Text(deployment.deployment_file_path))
|
|
114
|
+
table.add_row("Git Ref", Text(deployment.git_ref or "-"))
|
|
128
115
|
|
|
129
116
|
apiserver_url = deployment.apiserver_url
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
table.add_row(
|
|
118
|
+
"API Server URL",
|
|
119
|
+
Text(str(apiserver_url) if apiserver_url else "-"),
|
|
120
|
+
)
|
|
132
121
|
|
|
133
|
-
secret_names = deployment.secret_names
|
|
134
|
-
|
|
135
|
-
table.add_row("Secrets", ", ".join(secret_names))
|
|
122
|
+
secret_names = deployment.secret_names or []
|
|
123
|
+
table.add_row("Secrets", Text("\n".join(secret_names), style="italic"))
|
|
136
124
|
|
|
137
125
|
console.print(table)
|
|
138
126
|
|