llamactl 0.3.0a19__py3-none-any.whl → 0.3.0a21__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/auth/client.py +362 -0
- llama_deploy/cli/client.py +14 -5
- llama_deploy/cli/commands/auth.py +300 -33
- llama_deploy/cli/commands/deployment.py +32 -38
- llama_deploy/cli/commands/env.py +19 -14
- llama_deploy/cli/commands/init.py +137 -34
- llama_deploy/cli/commands/serve.py +29 -12
- llama_deploy/cli/config/_config.py +178 -202
- llama_deploy/cli/config/_migrations.py +65 -0
- llama_deploy/cli/config/auth_service.py +64 -2
- llama_deploy/cli/config/env_service.py +15 -14
- 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 +30 -0
- llama_deploy/cli/env.py +2 -1
- llama_deploy/cli/styles.py +10 -0
- llama_deploy/cli/textual/deployment_form.py +63 -7
- llama_deploy/cli/textual/deployment_monitor.py +71 -108
- llama_deploy/cli/textual/github_callback_server.py +4 -4
- llama_deploy/cli/textual/secrets_form.py +4 -0
- llama_deploy/cli/textual/styles.tcss +7 -5
- {llamactl-0.3.0a19.dist-info → llamactl-0.3.0a21.dist-info}/METADATA +5 -3
- llamactl-0.3.0a21.dist-info/RECORD +37 -0
- llama_deploy/cli/platform_client.py +0 -52
- llamactl-0.3.0a19.dist-info/RECORD +0 -32
- {llamactl-0.3.0a19.dist-info → llamactl-0.3.0a21.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a19.dist-info → llamactl-0.3.0a21.dist-info}/entry_points.txt +0 -0
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import webbrowser
|
|
2
8
|
|
|
3
9
|
import click
|
|
10
|
+
import httpx
|
|
4
11
|
import questionary
|
|
5
|
-
from llama_deploy.cli.client import
|
|
12
|
+
from llama_deploy.cli.auth.client import (
|
|
13
|
+
DeviceAuthorizationRequest,
|
|
14
|
+
OIDCClient,
|
|
15
|
+
PlatformAuthClient,
|
|
16
|
+
PlatformAuthDiscoveryClient,
|
|
17
|
+
TokenRequestDeviceCode,
|
|
18
|
+
decode_jwt_claims,
|
|
19
|
+
decode_jwt_claims_from_device_oidc,
|
|
20
|
+
)
|
|
21
|
+
from llama_deploy.cli.config._config import ConfigManager
|
|
6
22
|
from llama_deploy.cli.config.auth_service import AuthService
|
|
7
23
|
from llama_deploy.cli.config.env_service import service
|
|
8
|
-
from llama_deploy.
|
|
24
|
+
from llama_deploy.cli.styles import (
|
|
25
|
+
ACTIVE_INDICATOR,
|
|
26
|
+
HEADER_COLOR,
|
|
27
|
+
MUTED_COL,
|
|
28
|
+
PRIMARY_COL,
|
|
29
|
+
WARNING,
|
|
30
|
+
)
|
|
31
|
+
from llama_deploy.core.client.manage_client import (
|
|
32
|
+
ControlPlaneClient,
|
|
33
|
+
)
|
|
9
34
|
from llama_deploy.core.schema.projects import ProjectSummary
|
|
10
35
|
from rich import print as rprint
|
|
11
36
|
from rich.table import Table
|
|
12
37
|
from rich.text import Text
|
|
13
38
|
|
|
14
39
|
from ..app import app, console
|
|
15
|
-
from ..config.schema import Auth
|
|
40
|
+
from ..config.schema import Auth, DeviceOIDC
|
|
16
41
|
from ..options import global_options, interactive_option
|
|
17
42
|
|
|
18
43
|
|
|
@@ -61,14 +86,14 @@ def create_api_key_profile(
|
|
|
61
86
|
|
|
62
87
|
# Interactive mode: prompt for token (masked) and validate
|
|
63
88
|
token_value = api_key or _prompt_for_api_key()
|
|
64
|
-
projects =
|
|
89
|
+
projects = _prompt_validate_api_key_and_list_projects(auth_svc, token_value)
|
|
65
90
|
|
|
66
91
|
# Select or enter project ID
|
|
67
92
|
selected_project_id = project_id or _select_or_enter_project(
|
|
68
93
|
projects, auth_svc.env.requires_auth
|
|
69
94
|
)
|
|
70
95
|
if not selected_project_id:
|
|
71
|
-
rprint("[
|
|
96
|
+
rprint(f"[{WARNING}]No project selected[/]")
|
|
72
97
|
return
|
|
73
98
|
|
|
74
99
|
# Create and set profile
|
|
@@ -81,28 +106,47 @@ def create_api_key_profile(
|
|
|
81
106
|
raise click.Abort()
|
|
82
107
|
|
|
83
108
|
|
|
109
|
+
@auth.command("login")
|
|
110
|
+
@global_options
|
|
111
|
+
def device_login() -> None:
|
|
112
|
+
"""Login via web browser"""
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
created = _create_device_profile()
|
|
116
|
+
rprint(
|
|
117
|
+
f"[green]Created login profile '{created.name}' and set as current[/green]"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
122
|
+
raise click.Abort()
|
|
123
|
+
|
|
124
|
+
|
|
84
125
|
@auth.command("list")
|
|
85
126
|
@global_options
|
|
86
127
|
def list_profiles() -> None:
|
|
87
|
-
"""List all logged in
|
|
128
|
+
"""List all logged in users/tokens"""
|
|
88
129
|
try:
|
|
89
130
|
auth_svc = service.current_auth_service()
|
|
90
131
|
profiles = auth_svc.list_profiles()
|
|
91
132
|
current = auth_svc.get_current_profile()
|
|
92
133
|
|
|
93
134
|
if not profiles:
|
|
94
|
-
rprint("[
|
|
95
|
-
|
|
135
|
+
rprint(f"[{WARNING}]No profiles found[/]")
|
|
136
|
+
if auth_svc.env.requires_auth:
|
|
137
|
+
rprint("Create one with: [cyan]llamactl auth login[/cyan]")
|
|
138
|
+
else:
|
|
139
|
+
rprint("Create one with: [cyan]llamactl auth token[/cyan]")
|
|
96
140
|
return
|
|
97
141
|
|
|
98
|
-
table = Table(show_edge=False, box=None, header_style="bold
|
|
99
|
-
table.add_column(" Name")
|
|
100
|
-
table.add_column("Active Project", style=
|
|
142
|
+
table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
|
|
143
|
+
table.add_column(" Name", style=PRIMARY_COL)
|
|
144
|
+
table.add_column("Active Project", style=MUTED_COL)
|
|
101
145
|
|
|
102
146
|
for profile in profiles:
|
|
103
147
|
text = Text()
|
|
104
148
|
if profile == current:
|
|
105
|
-
text.append("* ", style=
|
|
149
|
+
text.append("* ", style=ACTIVE_INDICATOR)
|
|
106
150
|
else:
|
|
107
151
|
text.append(" ")
|
|
108
152
|
text.append(profile.name)
|
|
@@ -119,6 +163,26 @@ def list_profiles() -> None:
|
|
|
119
163
|
raise click.Abort()
|
|
120
164
|
|
|
121
165
|
|
|
166
|
+
@auth.command("destroy", hidden=True)
|
|
167
|
+
@global_options
|
|
168
|
+
def destroy_database() -> None:
|
|
169
|
+
"""Destroy the database"""
|
|
170
|
+
if not questionary.confirm(
|
|
171
|
+
"Are you sure you want to destroy all of your local logins? This action cannot be undone."
|
|
172
|
+
).ask():
|
|
173
|
+
return
|
|
174
|
+
ConfigManager(init_database=False).destroy_database()
|
|
175
|
+
rprint("[green]Database destroyed[/green]")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@auth.command("show-db", hidden=True)
|
|
179
|
+
@global_options
|
|
180
|
+
def config_database() -> None:
|
|
181
|
+
"""Config the database"""
|
|
182
|
+
path = service.config_manager().db_path
|
|
183
|
+
rprint(f"[bold]{path}[/bold]")
|
|
184
|
+
|
|
185
|
+
|
|
122
186
|
@auth.command("switch")
|
|
123
187
|
@global_options
|
|
124
188
|
@click.argument("name", required=False)
|
|
@@ -129,7 +193,7 @@ def switch_profile(name: str | None, interactive: bool) -> None:
|
|
|
129
193
|
try:
|
|
130
194
|
selected_auth = _select_profile(auth_svc, name, interactive)
|
|
131
195
|
if not selected_auth:
|
|
132
|
-
rprint("[
|
|
196
|
+
rprint(f"[{WARNING}]No profile selected[/]")
|
|
133
197
|
return
|
|
134
198
|
|
|
135
199
|
auth_svc.set_current_profile(selected_auth.name)
|
|
@@ -150,10 +214,10 @@ def delete_profile(name: str | None, interactive: bool) -> None:
|
|
|
150
214
|
auth_svc = service.current_auth_service()
|
|
151
215
|
auth = _select_profile(auth_svc, name, interactive)
|
|
152
216
|
if not auth:
|
|
153
|
-
rprint("[
|
|
217
|
+
rprint(f"[{WARNING}]No profile selected[/]")
|
|
154
218
|
return
|
|
155
219
|
|
|
156
|
-
if auth_svc.delete_profile(auth.name):
|
|
220
|
+
if asyncio.run(auth_svc.delete_profile(auth.name)):
|
|
157
221
|
rprint(f"[green]Logged out from '{auth.name}'[/green]")
|
|
158
222
|
else:
|
|
159
223
|
rprint(f"[red]Profile '{auth.name}' not found[/red]")
|
|
@@ -163,6 +227,29 @@ def delete_profile(name: str | None, interactive: bool) -> None:
|
|
|
163
227
|
raise click.Abort()
|
|
164
228
|
|
|
165
229
|
|
|
230
|
+
# Very simple introspection: decode current token via provider JWKS
|
|
231
|
+
@auth.command("me", hidden=True)
|
|
232
|
+
@global_options
|
|
233
|
+
def me() -> None:
|
|
234
|
+
"""Print JWT claims for the current profile's token using provider JWKS.
|
|
235
|
+
|
|
236
|
+
Assumes the stored API key is a JWT (e.g., OIDC id_token).
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
auth_svc = service.current_auth_service()
|
|
240
|
+
profile = auth_svc.get_current_profile()
|
|
241
|
+
if not profile or not profile.device_oidc:
|
|
242
|
+
raise click.ClickException(
|
|
243
|
+
"No OIDC profile selected. Run `llamactl auth login` or switch to an existing OIDC profile."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
claims = asyncio.run(decode_jwt_claims_from_device_oidc(profile.device_oidc))
|
|
247
|
+
click.echo(json.dumps(claims, indent=2, sort_keys=True))
|
|
248
|
+
except Exception as e:
|
|
249
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
250
|
+
raise click.Abort()
|
|
251
|
+
|
|
252
|
+
|
|
166
253
|
# Projects commands
|
|
167
254
|
@auth.command("project")
|
|
168
255
|
@click.argument("project_id", required=False)
|
|
@@ -175,19 +262,25 @@ def change_project(project_id: str | None, interactive: bool) -> None:
|
|
|
175
262
|
return
|
|
176
263
|
auth_svc = service.current_auth_service()
|
|
177
264
|
if project_id:
|
|
265
|
+
if auth_svc.env.requires_auth:
|
|
266
|
+
projects = _list_projects(auth_svc)
|
|
267
|
+
if not next(
|
|
268
|
+
(project for project in projects if project.project_id == project_id),
|
|
269
|
+
None,
|
|
270
|
+
):
|
|
271
|
+
raise click.ClickException(f"Project {project_id} not found")
|
|
178
272
|
auth_svc.set_project(profile.name, project_id)
|
|
179
|
-
rprint(f"
|
|
273
|
+
rprint(f"Set active project to [bold green]{project_id}[/]")
|
|
180
274
|
return
|
|
181
275
|
if not interactive:
|
|
182
276
|
raise click.ClickException(
|
|
183
277
|
"No --project-id provided. Run `llamactl auth project --help` for more information."
|
|
184
278
|
)
|
|
185
279
|
try:
|
|
186
|
-
|
|
187
|
-
projects = asyncio.run(client.list_projects())
|
|
280
|
+
projects = _list_projects(auth_svc)
|
|
188
281
|
|
|
189
282
|
if not projects:
|
|
190
|
-
rprint("[
|
|
283
|
+
rprint(f"[{WARNING}]No projects found[/]")
|
|
191
284
|
return
|
|
192
285
|
result = questionary.select(
|
|
193
286
|
"Select a project",
|
|
@@ -208,15 +301,173 @@ def change_project(project_id: str | None, interactive: bool) -> None:
|
|
|
208
301
|
project_id = questionary.text("Enter project ID").ask()
|
|
209
302
|
result = project_id
|
|
210
303
|
if result:
|
|
304
|
+
selected_project = next(
|
|
305
|
+
(project for project in projects if project.project_id == result), None
|
|
306
|
+
)
|
|
307
|
+
name = selected_project.project_name if selected_project else result
|
|
211
308
|
auth_svc.set_project(profile.name, result)
|
|
212
|
-
rprint(f"
|
|
309
|
+
rprint(f"Set active project to [bold {PRIMARY_COL}]{name}[/]")
|
|
213
310
|
else:
|
|
214
|
-
rprint("[
|
|
311
|
+
rprint(f"[{WARNING}]No project selected[/]")
|
|
215
312
|
except Exception as e:
|
|
216
313
|
rprint(f"[red]Error: {e}[/red]")
|
|
217
314
|
raise click.Abort()
|
|
218
315
|
|
|
219
316
|
|
|
317
|
+
def _auto_device_name() -> str:
|
|
318
|
+
try:
|
|
319
|
+
if sys.platform == "darwin": # macOS
|
|
320
|
+
return (
|
|
321
|
+
subprocess.check_output(["scutil", "--get", "ComputerName"])
|
|
322
|
+
.decode()
|
|
323
|
+
.strip()
|
|
324
|
+
)
|
|
325
|
+
elif sys.platform.startswith("win"):
|
|
326
|
+
return os.environ["COMPUTERNAME"]
|
|
327
|
+
else: # Linux / Unix
|
|
328
|
+
return platform.node()
|
|
329
|
+
except Exception:
|
|
330
|
+
return platform.node()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def _create_or_update_agent_api_key(auth_svc: AuthService, profile: Auth) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Mutates and updates the profile with an agent API key if it does not exist or is invalid.
|
|
336
|
+
"""
|
|
337
|
+
if profile.api_key is not None:
|
|
338
|
+
async with PlatformAuthClient(profile.api_url, profile.api_key) as client:
|
|
339
|
+
try:
|
|
340
|
+
await client.me()
|
|
341
|
+
except httpx.HTTPStatusError as e:
|
|
342
|
+
if e.response.status_code == 401:
|
|
343
|
+
# must have been deleted
|
|
344
|
+
profile.api_key = None
|
|
345
|
+
profile.api_key_id = None
|
|
346
|
+
else:
|
|
347
|
+
raise
|
|
348
|
+
if profile.api_key is None:
|
|
349
|
+
async with auth_svc.profile_client(profile) as client:
|
|
350
|
+
name = f"{profile.name} llamactl on {profile.device_oidc.device_name if profile.device_oidc else 'unknown'}"
|
|
351
|
+
api_key = await client.create_agent_api_key(name)
|
|
352
|
+
profile.api_key = api_key.token
|
|
353
|
+
profile.api_key_id = api_key.id
|
|
354
|
+
auth_svc.update_profile(profile)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _create_device_profile() -> Auth:
|
|
358
|
+
auth_svc = service.current_auth_service()
|
|
359
|
+
if not auth_svc.env.requires_auth:
|
|
360
|
+
raise click.ClickException("This environment does not support authentication")
|
|
361
|
+
|
|
362
|
+
base_url = auth_svc.env.api_url.rstrip("/")
|
|
363
|
+
|
|
364
|
+
oidc_device = asyncio.run(_run_device_authentication(base_url))
|
|
365
|
+
|
|
366
|
+
# Obtain or prompt for project ID and create profile
|
|
367
|
+
projects = _list_projects(auth_svc, oidc_device.device_access_token)
|
|
368
|
+
selected_project_id = _select_or_enter_project(projects, True)
|
|
369
|
+
if not selected_project_id:
|
|
370
|
+
raise click.ClickException(
|
|
371
|
+
"Project is required. Does this user have access to any projects?"
|
|
372
|
+
)
|
|
373
|
+
created = auth_svc.create_or_update_profile_from_oidc(
|
|
374
|
+
selected_project_id, oidc_device
|
|
375
|
+
)
|
|
376
|
+
asyncio.run(_create_or_update_agent_api_key(auth_svc, created))
|
|
377
|
+
return created
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def _run_device_authentication(base_url: str) -> DeviceOIDC:
|
|
381
|
+
device_name = _auto_device_name()
|
|
382
|
+
# 1) Discover upstream and CLI client_id via client
|
|
383
|
+
async with PlatformAuthDiscoveryClient(base_url) as discovery:
|
|
384
|
+
disc = await discovery.oidc_discovery()
|
|
385
|
+
upstream = disc.discovery_url
|
|
386
|
+
client_ids = disc.client_ids or {}
|
|
387
|
+
client_ids_list = list(client_ids.values())
|
|
388
|
+
client_id = client_ids.get("cli") or (
|
|
389
|
+
client_ids_list[0] if len(client_ids_list) == 1 else None
|
|
390
|
+
)
|
|
391
|
+
if not client_id:
|
|
392
|
+
raise click.ClickException(
|
|
393
|
+
"Expected 'cli' Client ID not found from auth discovery"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# 2) Device flow via typed OIDC client
|
|
397
|
+
async with OIDCClient() as oidc:
|
|
398
|
+
provider = await oidc.fetch_provider_configuration(upstream)
|
|
399
|
+
device_endpoint = provider.device_authorization_endpoint
|
|
400
|
+
token_endpoint = provider.token_endpoint
|
|
401
|
+
if not device_endpoint or not token_endpoint:
|
|
402
|
+
raise click.ClickException("Device Authorization not supported by provider")
|
|
403
|
+
|
|
404
|
+
scope_value = " ".join(sorted({"openid", "profile", "email", "offline_access"}))
|
|
405
|
+
|
|
406
|
+
# 3) Start device authorization
|
|
407
|
+
da = await oidc.device_authorization(
|
|
408
|
+
device_endpoint,
|
|
409
|
+
DeviceAuthorizationRequest(client_id=client_id, scope=scope_value),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
rprint(
|
|
413
|
+
"[bold]complete authentication by visiting the verification URI and confirming the device:[/bold]"
|
|
414
|
+
)
|
|
415
|
+
if da.verification_uri:
|
|
416
|
+
rprint(
|
|
417
|
+
f"Verification URI: {da.verification_uri} (will open in your browser if supported)"
|
|
418
|
+
)
|
|
419
|
+
if da.user_code:
|
|
420
|
+
rprint(f"User Code: {da.user_code} to confirm the device")
|
|
421
|
+
if da.verification_uri_complete:
|
|
422
|
+
try:
|
|
423
|
+
webbrowser.open(da.verification_uri_complete)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
# 4) Poll token endpoint
|
|
428
|
+
interval = int(da.interval or 5)
|
|
429
|
+
while True:
|
|
430
|
+
await asyncio.sleep(interval)
|
|
431
|
+
token = await oidc.token_with_device_code(
|
|
432
|
+
token_endpoint,
|
|
433
|
+
TokenRequestDeviceCode(
|
|
434
|
+
device_code=da.device_code,
|
|
435
|
+
client_id=client_id,
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
if token.error in {"authorization_pending", "slow_down"}:
|
|
439
|
+
if token.error == "slow_down":
|
|
440
|
+
interval += 5
|
|
441
|
+
continue
|
|
442
|
+
if token.error:
|
|
443
|
+
raise click.ClickException(
|
|
444
|
+
f"Token polling failed: {token.error} {token.error_description or ''}"
|
|
445
|
+
)
|
|
446
|
+
if token.id_token:
|
|
447
|
+
if not token.access_token:
|
|
448
|
+
raise click.ClickException(
|
|
449
|
+
"Device flow failed: token response missing access_token"
|
|
450
|
+
)
|
|
451
|
+
claims = await decode_jwt_claims(token.id_token, provider.jwks_uri)
|
|
452
|
+
email = claims.get("email")
|
|
453
|
+
if not email:
|
|
454
|
+
raise click.ClickException(
|
|
455
|
+
"Device flow failed: email not found in token"
|
|
456
|
+
)
|
|
457
|
+
user_id = claims.get("sub") or email
|
|
458
|
+
return DeviceOIDC(
|
|
459
|
+
device_name=device_name,
|
|
460
|
+
email=email,
|
|
461
|
+
user_id=user_id,
|
|
462
|
+
client_id=client_id,
|
|
463
|
+
discovery_url=upstream,
|
|
464
|
+
device_access_token=token.access_token,
|
|
465
|
+
device_refresh_token=token.refresh_token,
|
|
466
|
+
device_id_token=token.id_token,
|
|
467
|
+
)
|
|
468
|
+
raise click.ClickException("Device flow failed: unexpected token response")
|
|
469
|
+
|
|
470
|
+
|
|
220
471
|
def validate_authenticated_profile(interactive: bool) -> Auth:
|
|
221
472
|
"""Validate that the user is authenticated within the current environment.
|
|
222
473
|
|
|
@@ -259,7 +510,7 @@ def validate_authenticated_profile(interactive: bool) -> Auth:
|
|
|
259
510
|
# No profiles exist for this env
|
|
260
511
|
if current_env.requires_auth:
|
|
261
512
|
# Inline token flow
|
|
262
|
-
created =
|
|
513
|
+
created = _create_device_profile()
|
|
263
514
|
return created
|
|
264
515
|
else:
|
|
265
516
|
# No auth required: select project and create a default profile without token
|
|
@@ -282,22 +533,38 @@ def _prompt_for_api_key() -> str:
|
|
|
282
533
|
raise click.ClickException("No API key entered")
|
|
283
534
|
|
|
284
535
|
|
|
285
|
-
def
|
|
286
|
-
auth_svc: AuthService,
|
|
536
|
+
def _list_projects(
|
|
537
|
+
auth_svc: AuthService,
|
|
538
|
+
api_key: str | None = None,
|
|
287
539
|
) -> list[ProjectSummary]:
|
|
288
540
|
async def _run():
|
|
289
|
-
|
|
541
|
+
profile = auth_svc.get_current_profile()
|
|
542
|
+
async with ControlPlaneClient.ctx(
|
|
543
|
+
auth_svc.env.api_url,
|
|
544
|
+
api_key or (profile.api_key if profile else None),
|
|
545
|
+
None if api_key is not None else auth_svc.auth_middleware(profile),
|
|
546
|
+
) as client:
|
|
290
547
|
return await client.list_projects()
|
|
291
548
|
|
|
549
|
+
return asyncio.run(_run())
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _prompt_validate_api_key_and_list_projects(
|
|
553
|
+
auth_svc: AuthService, api_key: str
|
|
554
|
+
) -> list[ProjectSummary]:
|
|
292
555
|
try:
|
|
293
|
-
return
|
|
294
|
-
except
|
|
295
|
-
if
|
|
556
|
+
return _list_projects(auth_svc, api_key)
|
|
557
|
+
except httpx.HTTPStatusError as e:
|
|
558
|
+
if e.response.status_code == 401:
|
|
296
559
|
rprint("[red]Invalid API key. Please try again.[/red]")
|
|
297
|
-
return
|
|
298
|
-
|
|
560
|
+
return _prompt_validate_api_key_and_list_projects(
|
|
561
|
+
auth_svc, _prompt_for_api_key()
|
|
562
|
+
)
|
|
563
|
+
if e.response.status_code == 403:
|
|
299
564
|
rprint("[red]This environment requires a valid API key.[/red]")
|
|
300
|
-
return
|
|
565
|
+
return _prompt_validate_api_key_and_list_projects(
|
|
566
|
+
auth_svc, _prompt_for_api_key()
|
|
567
|
+
)
|
|
301
568
|
raise
|
|
302
569
|
except Exception as e:
|
|
303
570
|
raise click.ClickException(f"Failed to validate API key: {e}")
|
|
@@ -327,7 +594,7 @@ def _select_or_enter_project(
|
|
|
327
594
|
|
|
328
595
|
def _token_flow_for_env(auth_service: AuthService) -> Auth:
|
|
329
596
|
token_value = _prompt_for_api_key()
|
|
330
|
-
projects =
|
|
597
|
+
projects = _prompt_validate_api_key_and_list_projects(auth_service, token_value)
|
|
331
598
|
project_id = _select_or_enter_project(projects, auth_service.env.requires_auth)
|
|
332
599
|
if not project_id:
|
|
333
600
|
raise click.ClickException("No project selected")
|
|
@@ -357,7 +624,7 @@ def _select_profile(
|
|
|
357
624
|
profiles = auth_svc.list_profiles()
|
|
358
625
|
|
|
359
626
|
if not profiles:
|
|
360
|
-
rprint("[
|
|
627
|
+
rprint(f"[{WARNING}]No profiles found[/]")
|
|
361
628
|
return None
|
|
362
629
|
|
|
363
630
|
choices = []
|
|
@@ -11,6 +11,7 @@ import asyncio
|
|
|
11
11
|
import click
|
|
12
12
|
import questionary
|
|
13
13
|
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
14
|
+
from llama_deploy.cli.styles import HEADER_COLOR, MUTED_COL, PRIMARY_COL, WARNING
|
|
14
15
|
from llama_deploy.core.schema.deployments import DeploymentUpdate
|
|
15
16
|
from rich import print as rprint
|
|
16
17
|
from rich.table import Table
|
|
@@ -52,15 +53,15 @@ def list_deployments(interactive: bool) -> None:
|
|
|
52
53
|
|
|
53
54
|
if not deployments:
|
|
54
55
|
rprint(
|
|
55
|
-
f"[
|
|
56
|
+
f"[{WARNING}]No deployments found for project {client.project_id}[/]"
|
|
56
57
|
)
|
|
57
58
|
return
|
|
58
59
|
|
|
59
|
-
table = Table(show_edge=False, box=None, header_style="bold
|
|
60
|
-
table.add_column("Name")
|
|
61
|
-
table.add_column("Status", style=
|
|
62
|
-
table.add_column("URL", style=
|
|
63
|
-
table.add_column("Repository", style=
|
|
60
|
+
table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
|
|
61
|
+
table.add_column("Name", style=PRIMARY_COL)
|
|
62
|
+
table.add_column("Status", style=MUTED_COL)
|
|
63
|
+
table.add_column("URL", style=MUTED_COL)
|
|
64
|
+
table.add_column("Repository", style=MUTED_COL)
|
|
64
65
|
|
|
65
66
|
for deployment in deployments:
|
|
66
67
|
name = deployment.id
|
|
@@ -96,7 +97,7 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
96
97
|
|
|
97
98
|
deployment_id = select_deployment(deployment_id)
|
|
98
99
|
if not deployment_id:
|
|
99
|
-
rprint("[
|
|
100
|
+
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
100
101
|
return
|
|
101
102
|
if interactive:
|
|
102
103
|
monitor_deployment_screen(deployment_id)
|
|
@@ -104,9 +105,9 @@ def get_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
104
105
|
|
|
105
106
|
deployment = asyncio.run(client.get_deployment(deployment_id))
|
|
106
107
|
|
|
107
|
-
table = Table(show_edge=False, box=None, header_style="bold
|
|
108
|
-
table.add_column("Property", style=
|
|
109
|
-
table.add_column("Value")
|
|
108
|
+
table = Table(show_edge=False, box=None, header_style=f"bold {HEADER_COLOR}")
|
|
109
|
+
table.add_column("Property", style=MUTED_COL, justify="right")
|
|
110
|
+
table.add_column("Value", style=PRIMARY_COL)
|
|
110
111
|
|
|
111
112
|
table.add_row("ID", Text(deployment.id))
|
|
112
113
|
table.add_row("Project ID", Text(deployment.project_id))
|
|
@@ -148,7 +149,7 @@ def create_deployment(
|
|
|
148
149
|
# Use interactive creation
|
|
149
150
|
deployment_form = create_deployment_form()
|
|
150
151
|
if deployment_form is None:
|
|
151
|
-
rprint("[
|
|
152
|
+
rprint(f"[{WARNING}]Cancelled[/]")
|
|
152
153
|
return
|
|
153
154
|
|
|
154
155
|
rprint(
|
|
@@ -168,12 +169,12 @@ def delete_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
168
169
|
|
|
169
170
|
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
170
171
|
if not deployment_id:
|
|
171
|
-
rprint("[
|
|
172
|
+
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
172
173
|
return
|
|
173
174
|
|
|
174
175
|
if interactive:
|
|
175
176
|
if not confirm_action(f"Delete deployment '{deployment_id}'?"):
|
|
176
|
-
rprint("[
|
|
177
|
+
rprint(f"[{WARNING}]Cancelled[/]")
|
|
177
178
|
return
|
|
178
179
|
|
|
179
180
|
asyncio.run(client.delete_deployment(deployment_id))
|
|
@@ -196,7 +197,7 @@ def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
196
197
|
|
|
197
198
|
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
198
199
|
if not deployment_id:
|
|
199
|
-
rprint("[
|
|
200
|
+
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
200
201
|
return
|
|
201
202
|
|
|
202
203
|
# Get current deployment details
|
|
@@ -205,7 +206,7 @@ def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
205
206
|
# Use the interactive edit form
|
|
206
207
|
updated_deployment = edit_deployment_form(current_deployment)
|
|
207
208
|
if updated_deployment is None:
|
|
208
|
-
rprint("[
|
|
209
|
+
rprint(f"[{WARNING}]Cancelled[/]")
|
|
209
210
|
return
|
|
210
211
|
|
|
211
212
|
rprint(
|
|
@@ -227,7 +228,7 @@ def refresh_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
227
228
|
try:
|
|
228
229
|
deployment_id = select_deployment(deployment_id)
|
|
229
230
|
if not deployment_id:
|
|
230
|
-
rprint("[
|
|
231
|
+
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
231
232
|
return
|
|
232
233
|
|
|
233
234
|
# Get current deployment details to show what we're refreshing
|
|
@@ -278,29 +279,22 @@ def select_deployment(
|
|
|
278
279
|
if not interactive:
|
|
279
280
|
return None
|
|
280
281
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
deployments = asyncio.run(client.list_deployments())
|
|
282
|
+
client = get_project_client()
|
|
283
|
+
deployments = asyncio.run(client.list_deployments())
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
return None
|
|
285
|
+
if not deployments:
|
|
286
|
+
rprint(f"[{WARNING}]No deployments found for project {client.project_id}[/]")
|
|
287
|
+
return None
|
|
290
288
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
289
|
+
choices = []
|
|
290
|
+
for deployment in deployments:
|
|
291
|
+
name = deployment.name
|
|
292
|
+
deployment_id = deployment.id
|
|
293
|
+
status = deployment.status
|
|
294
|
+
choices.append(
|
|
295
|
+
questionary.Choice(
|
|
296
|
+
title=f"{name} ({deployment_id}) - {status}", value=deployment_id
|
|
300
297
|
)
|
|
298
|
+
)
|
|
301
299
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
except Exception as e:
|
|
305
|
-
rprint(f"[red]Error loading deployments: {e}[/red]")
|
|
306
|
-
return None
|
|
300
|
+
return questionary.select("Select deployment:", choices=choices).ask()
|