llamactl 0.3.22__py3-none-any.whl → 0.3.24__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/app.py +7 -4
- llama_deploy/cli/auth/client.py +19 -2
- llama_deploy/cli/client.py +4 -1
- llama_deploy/cli/commands/aliased_group.py +11 -3
- llama_deploy/cli/commands/auth.py +105 -37
- llama_deploy/cli/commands/deployment.py +47 -17
- llama_deploy/cli/commands/dev.py +47 -10
- llama_deploy/cli/commands/env.py +30 -5
- llama_deploy/cli/commands/init.py +51 -10
- llama_deploy/cli/commands/pkg.py +2 -2
- llama_deploy/cli/commands/serve.py +21 -15
- llama_deploy/cli/config/_config.py +4 -4
- llama_deploy/cli/config/_migrations.py +7 -2
- llama_deploy/cli/config/auth_service.py +1 -1
- llama_deploy/cli/config/migrations/0001_init.sql +1 -1
- llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +0 -2
- llama_deploy/cli/pkg/options.py +4 -1
- llama_deploy/cli/pkg/utils.py +8 -5
- llama_deploy/cli/textual/deployment_form.py +5 -3
- llama_deploy/cli/textual/deployment_help.py +8 -7
- llama_deploy/cli/textual/deployment_monitor.py +8 -5
- llama_deploy/cli/textual/git_validation.py +45 -8
- llama_deploy/cli/textual/github_callback_server.py +12 -12
- llama_deploy/cli/textual/llama_loader.py +25 -19
- llama_deploy/cli/textual/secrets_form.py +2 -1
- llama_deploy/cli/textual/styles.tcss +1 -1
- llama_deploy/cli/utils/retry.py +49 -0
- {llamactl-0.3.22.dist-info → llamactl-0.3.24.dist-info}/METADATA +7 -5
- llamactl-0.3.24.dist-info/RECORD +47 -0
- llamactl-0.3.22.dist-info/RECORD +0 -46
- {llamactl-0.3.22.dist-info → llamactl-0.3.24.dist-info}/WHEEL +0 -0
- {llamactl-0.3.22.dist-info → llamactl-0.3.24.dist-info}/entry_points.txt +0 -0
llama_deploy/cli/app.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError
|
|
2
2
|
from importlib.metadata import version as pkg_version
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
from llama_deploy.cli.commands.aliased_group import AliasedGroup
|
|
6
|
-
from llama_deploy.cli.config.env_service import service
|
|
7
7
|
from llama_deploy.cli.options import global_options
|
|
8
8
|
from rich import print as rprint
|
|
9
9
|
from rich.console import Console
|
|
@@ -12,10 +12,13 @@ from rich.text import Text
|
|
|
12
12
|
console = Console(highlight=False)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def print_version(ctx: click.Context, param: click.
|
|
15
|
+
def print_version(ctx: click.Context, param: click.Parameter, value: Any) -> None:
|
|
16
16
|
"""Print the version of llama_deploy"""
|
|
17
|
+
|
|
18
|
+
from llama_deploy.cli.config.env_service import service
|
|
19
|
+
|
|
17
20
|
if not value or ctx.resilient_parsing:
|
|
18
|
-
return
|
|
21
|
+
return None
|
|
19
22
|
try:
|
|
20
23
|
ver = pkg_version("llamactl")
|
|
21
24
|
console.print(Text.assemble("client version: ", (ver, "green")))
|
|
@@ -65,5 +68,5 @@ def print_version(ctx: click.Context, param: click.Option, value: bool) -> None:
|
|
|
65
68
|
help="Print client and server versions of LlamaDeploy",
|
|
66
69
|
)
|
|
67
70
|
@global_options
|
|
68
|
-
def app():
|
|
71
|
+
def app() -> None:
|
|
69
72
|
pass
|
llama_deploy/cli/auth/client.py
CHANGED
|
@@ -2,16 +2,30 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import sys
|
|
5
6
|
from types import TracebackType
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
AsyncContextManager,
|
|
10
|
+
AsyncGenerator,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
)
|
|
7
14
|
|
|
8
15
|
import httpx
|
|
9
16
|
import jwt
|
|
17
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
10
18
|
from jwt.algorithms import RSAAlgorithm # type: ignore[possibly-unbound-import]
|
|
11
19
|
from llama_deploy.cli.config.schema import DeviceOIDC
|
|
12
20
|
from llama_deploy.core.client.ssl_util import get_httpx_verify_param
|
|
13
21
|
from pydantic import BaseModel
|
|
14
22
|
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
from typing import Self
|
|
25
|
+
else:
|
|
26
|
+
from typing_extensions import Self
|
|
27
|
+
|
|
28
|
+
|
|
15
29
|
logger = logging.getLogger(__name__)
|
|
16
30
|
|
|
17
31
|
|
|
@@ -328,7 +342,10 @@ async def decode_jwt_claims(
|
|
|
328
342
|
if key.kty != "RSA":
|
|
329
343
|
raise ValueError("Unsupported JWK kty; only RSA is supported")
|
|
330
344
|
key_json = key.model_dump_json()
|
|
331
|
-
|
|
345
|
+
raw_key = RSAAlgorithm.from_jwk(key_json)
|
|
346
|
+
if not isinstance(raw_key, RSAPublicKey):
|
|
347
|
+
raise ValueError("Unsupported RSA key type; expected RSAPublicKey from JWKS")
|
|
348
|
+
public_key = raw_key
|
|
332
349
|
|
|
333
350
|
return jwt.decode(
|
|
334
351
|
token,
|
llama_deploy/cli/client.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
2
|
from typing import AsyncGenerator
|
|
3
3
|
|
|
4
|
-
from llama_deploy.cli.config.env_service import service
|
|
5
4
|
from llama_deploy.core.client.manage_client import ControlPlaneClient, ProjectClient
|
|
6
5
|
from rich import print as rprint
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def get_control_plane_client() -> ControlPlaneClient:
|
|
9
|
+
from llama_deploy.cli.config.env_service import service
|
|
10
|
+
|
|
10
11
|
auth_svc = service.current_auth_service()
|
|
11
12
|
profile = service.current_auth_service().get_current_profile()
|
|
12
13
|
if profile:
|
|
@@ -23,6 +24,8 @@ def get_control_plane_client() -> ControlPlaneClient:
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def get_project_client() -> ProjectClient:
|
|
27
|
+
from llama_deploy.cli.config.env_service import service
|
|
28
|
+
|
|
26
29
|
auth_svc = service.current_auth_service()
|
|
27
30
|
profile = auth_svc.get_current_profile()
|
|
28
31
|
if not profile:
|
|
@@ -27,7 +27,15 @@ class AliasedGroup(click.Group):
|
|
|
27
27
|
|
|
28
28
|
def resolve_command(
|
|
29
29
|
self, ctx: click.Context, args: list[str]
|
|
30
|
-
) -> tuple[str
|
|
30
|
+
) -> tuple[str, click.Command, list[str]]:
|
|
31
31
|
# always return the full command name
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
cmd_name, cmd, args = super().resolve_command(ctx, args)
|
|
33
|
+
assert cmd is not None
|
|
34
|
+
full_name: str = (
|
|
35
|
+
cmd.name
|
|
36
|
+
if cmd.name
|
|
37
|
+
else cmd_name
|
|
38
|
+
if isinstance(cmd_name, str)
|
|
39
|
+
else "<none>" # shouldn't happen
|
|
40
|
+
)
|
|
41
|
+
return (full_name, cmd, args)
|
|
@@ -1,28 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
3
5
|
import os
|
|
4
6
|
import platform
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
7
|
-
import webbrowser
|
|
8
9
|
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
9
11
|
|
|
10
12
|
import click
|
|
11
|
-
import httpx
|
|
12
|
-
import questionary
|
|
13
|
-
from dotenv import set_key
|
|
14
|
-
from llama_deploy.cli.auth.client import (
|
|
15
|
-
DeviceAuthorizationRequest,
|
|
16
|
-
OIDCClient,
|
|
17
|
-
PlatformAuthClient,
|
|
18
|
-
PlatformAuthDiscoveryClient,
|
|
19
|
-
TokenRequestDeviceCode,
|
|
20
|
-
decode_jwt_claims,
|
|
21
|
-
decode_jwt_claims_from_device_oidc,
|
|
22
|
-
)
|
|
23
|
-
from llama_deploy.cli.config._config import ConfigManager
|
|
24
|
-
from llama_deploy.cli.config.auth_service import AuthService
|
|
25
|
-
from llama_deploy.cli.config.env_service import service
|
|
26
13
|
from llama_deploy.cli.styles import (
|
|
27
14
|
ACTIVE_INDICATOR,
|
|
28
15
|
HEADER_COLOR,
|
|
@@ -30,19 +17,34 @@ from llama_deploy.cli.styles import (
|
|
|
30
17
|
PRIMARY_COL,
|
|
31
18
|
WARNING,
|
|
32
19
|
)
|
|
33
|
-
from llama_deploy.cli.utils.env_inject import env_vars_from_profile
|
|
34
|
-
from llama_deploy.core.client.manage_client import (
|
|
35
|
-
ControlPlaneClient,
|
|
36
|
-
)
|
|
37
|
-
from llama_deploy.core.schema.projects import ProjectSummary
|
|
38
20
|
from rich import print as rprint
|
|
39
21
|
from rich.table import Table
|
|
40
22
|
from rich.text import Text
|
|
41
23
|
|
|
42
24
|
from ..app import app, console
|
|
43
|
-
from ..config.schema import Auth, DeviceOIDC
|
|
44
25
|
from ..options import global_options, interactive_option
|
|
45
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from llama_deploy.cli.config.auth_service import AuthService
|
|
29
|
+
from llama_deploy.cli.config.env_service import EnvService
|
|
30
|
+
from llama_deploy.core.schema.projects import ProjectSummary
|
|
31
|
+
|
|
32
|
+
from ..config.schema import Auth, DeviceOIDC
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_ClickPath = getattr(click, "Path")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_service() -> EnvService:
|
|
39
|
+
"""Return the EnvService instance lazily.
|
|
40
|
+
|
|
41
|
+
Imports ``service`` only when needed so CLI startup stays fast and tests
|
|
42
|
+
can patch ``llama_deploy.cli.config.env_service.service`` directly.
|
|
43
|
+
"""
|
|
44
|
+
from llama_deploy.cli.config.env_service import service # local import on purpose
|
|
45
|
+
|
|
46
|
+
return service
|
|
47
|
+
|
|
46
48
|
|
|
47
49
|
# Specific auth/login flow exceptions
|
|
48
50
|
class NoProjectsFoundError(Exception):
|
|
@@ -78,7 +80,7 @@ def create_api_key_profile(
|
|
|
78
80
|
) -> None:
|
|
79
81
|
"""Authenticate with an API key and create a profile in the current environment."""
|
|
80
82
|
try:
|
|
81
|
-
auth_svc =
|
|
83
|
+
auth_svc = _get_service().current_auth_service()
|
|
82
84
|
|
|
83
85
|
# Non-interactive mode: require both api-key and project-id
|
|
84
86
|
if not interactive:
|
|
@@ -144,7 +146,7 @@ def device_login() -> None:
|
|
|
144
146
|
def list_profiles() -> None:
|
|
145
147
|
"""List all logged in users/tokens"""
|
|
146
148
|
try:
|
|
147
|
-
auth_svc =
|
|
149
|
+
auth_svc = _get_service().current_auth_service()
|
|
148
150
|
profiles = auth_svc.list_profiles()
|
|
149
151
|
current = auth_svc.get_current_profile()
|
|
150
152
|
|
|
@@ -184,6 +186,9 @@ def list_profiles() -> None:
|
|
|
184
186
|
@global_options
|
|
185
187
|
def destroy_database() -> None:
|
|
186
188
|
"""Destroy the database"""
|
|
189
|
+
import questionary
|
|
190
|
+
from llama_deploy.cli.config._config import ConfigManager
|
|
191
|
+
|
|
187
192
|
if not questionary.confirm(
|
|
188
193
|
"Are you sure you want to destroy all of your local logins? This action cannot be undone."
|
|
189
194
|
).ask():
|
|
@@ -196,7 +201,7 @@ def destroy_database() -> None:
|
|
|
196
201
|
@global_options
|
|
197
202
|
def config_database() -> None:
|
|
198
203
|
"""Config the database"""
|
|
199
|
-
path =
|
|
204
|
+
path = _get_service().config_manager().db_path
|
|
200
205
|
rprint(f"[bold]{path}[/bold]")
|
|
201
206
|
|
|
202
207
|
|
|
@@ -206,7 +211,7 @@ def config_database() -> None:
|
|
|
206
211
|
@interactive_option
|
|
207
212
|
def switch_profile(name: str | None, interactive: bool) -> None:
|
|
208
213
|
"""Switch to a different profile"""
|
|
209
|
-
auth_svc =
|
|
214
|
+
auth_svc = _get_service().current_auth_service()
|
|
210
215
|
try:
|
|
211
216
|
selected_auth = _select_profile(auth_svc, name, interactive)
|
|
212
217
|
if not selected_auth:
|
|
@@ -228,7 +233,7 @@ def switch_profile(name: str | None, interactive: bool) -> None:
|
|
|
228
233
|
def delete_profile(name: str | None, interactive: bool) -> None:
|
|
229
234
|
"""Logout from a profile and wipe all associated data"""
|
|
230
235
|
try:
|
|
231
|
-
auth_svc =
|
|
236
|
+
auth_svc = _get_service().current_auth_service()
|
|
232
237
|
auth = _select_profile(auth_svc, name, interactive)
|
|
233
238
|
if not auth:
|
|
234
239
|
rprint(f"[{WARNING}]No profile selected[/]")
|
|
@@ -253,7 +258,9 @@ def me() -> None:
|
|
|
253
258
|
Assumes the stored API key is a JWT (e.g., OIDC id_token).
|
|
254
259
|
"""
|
|
255
260
|
try:
|
|
256
|
-
|
|
261
|
+
from llama_deploy.cli.auth.client import decode_jwt_claims_from_device_oidc
|
|
262
|
+
|
|
263
|
+
auth_svc = _get_service().current_auth_service()
|
|
257
264
|
profile = auth_svc.get_current_profile()
|
|
258
265
|
if not profile or not profile.device_oidc:
|
|
259
266
|
raise click.ClickException(
|
|
@@ -274,10 +281,12 @@ def me() -> None:
|
|
|
274
281
|
@global_options
|
|
275
282
|
def change_project(project_id: str | None, interactive: bool) -> None:
|
|
276
283
|
"""Change the active project for the current profile"""
|
|
284
|
+
import questionary
|
|
285
|
+
|
|
286
|
+
auth_svc = _get_service().current_auth_service()
|
|
277
287
|
profile = validate_authenticated_profile(interactive)
|
|
278
288
|
if project_id and profile.project_id == project_id:
|
|
279
289
|
return
|
|
280
|
-
auth_svc = service.current_auth_service()
|
|
281
290
|
if project_id:
|
|
282
291
|
if auth_svc.env.requires_auth:
|
|
283
292
|
projects = _list_projects(auth_svc)
|
|
@@ -337,7 +346,7 @@ def change_project(project_id: str | None, interactive: bool) -> None:
|
|
|
337
346
|
"--env-file",
|
|
338
347
|
"env_file",
|
|
339
348
|
default=Path(".env"),
|
|
340
|
-
type=
|
|
349
|
+
type=_ClickPath(dir_okay=False, resolve_path=True, path_type=Path),
|
|
341
350
|
help="Path to the .env file to write",
|
|
342
351
|
)
|
|
343
352
|
@interactive_option
|
|
@@ -351,7 +360,10 @@ def inject_env_vars(
|
|
|
351
360
|
based on the current profile. Always overwrites and creates the file if missing.
|
|
352
361
|
"""
|
|
353
362
|
try:
|
|
354
|
-
|
|
363
|
+
from dotenv import set_key
|
|
364
|
+
from llama_deploy.cli.utils.env_inject import env_vars_from_profile
|
|
365
|
+
|
|
366
|
+
auth_svc = _get_service().current_auth_service()
|
|
355
367
|
profile = auth_svc.get_current_profile()
|
|
356
368
|
if not profile:
|
|
357
369
|
if interactive:
|
|
@@ -401,6 +413,10 @@ async def _create_or_update_agent_api_key(auth_svc: AuthService, profile: Auth)
|
|
|
401
413
|
"""
|
|
402
414
|
Mutates and updates the profile with an agent API key if it does not exist or is invalid.
|
|
403
415
|
"""
|
|
416
|
+
import httpx
|
|
417
|
+
from llama_deploy.cli.auth.client import PlatformAuthClient
|
|
418
|
+
from llama_deploy.cli.utils.retry import run_with_network_retries
|
|
419
|
+
|
|
404
420
|
if profile.api_key is not None:
|
|
405
421
|
async with PlatformAuthClient(profile.api_url, profile.api_key) as client:
|
|
406
422
|
try:
|
|
@@ -415,14 +431,30 @@ async def _create_or_update_agent_api_key(auth_svc: AuthService, profile: Auth)
|
|
|
415
431
|
if profile.api_key is None:
|
|
416
432
|
async with auth_svc.profile_client(profile) as client:
|
|
417
433
|
name = f"{profile.name} llamactl on {profile.device_oidc.device_name if profile.device_oidc else 'unknown'}"
|
|
418
|
-
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
api_key = await run_with_network_retries(
|
|
437
|
+
lambda: client.create_agent_api_key(name)
|
|
438
|
+
)
|
|
439
|
+
except httpx.HTTPStatusError:
|
|
440
|
+
# Do not treat HTTP errors as transient; re-raise for normal handling.
|
|
441
|
+
raise
|
|
442
|
+
except httpx.RequestError as e:
|
|
443
|
+
detail = str(e) or e.__class__.__name__
|
|
444
|
+
raise click.ClickException(
|
|
445
|
+
"Network error while provisioning an API token for llamactl. "
|
|
446
|
+
"Your login may have succeeded, but we could not create a CLI API token. "
|
|
447
|
+
"Please check your internet connection and try again. "
|
|
448
|
+
f"Details: {detail}"
|
|
449
|
+
) from e
|
|
450
|
+
|
|
419
451
|
profile.api_key = api_key.token
|
|
420
452
|
profile.api_key_id = api_key.id
|
|
421
453
|
auth_svc.update_profile(profile)
|
|
422
454
|
|
|
423
455
|
|
|
424
456
|
def _create_device_profile() -> Auth:
|
|
425
|
-
auth_svc =
|
|
457
|
+
auth_svc = _get_service().current_auth_service()
|
|
426
458
|
if not auth_svc.env.requires_auth:
|
|
427
459
|
raise click.ClickException("This environment does not support authentication")
|
|
428
460
|
|
|
@@ -440,14 +472,39 @@ def _create_device_profile() -> Auth:
|
|
|
440
472
|
if not selected_project_id:
|
|
441
473
|
# User cancelled selection despite having projects
|
|
442
474
|
raise click.ClickException("No project selected")
|
|
475
|
+
|
|
443
476
|
created = auth_svc.create_or_update_profile_from_oidc(
|
|
444
477
|
selected_project_id, oidc_device
|
|
445
478
|
)
|
|
446
|
-
|
|
479
|
+
|
|
480
|
+
# Ensure login is atomic: if provisioning the CLI API key fails, clean up the
|
|
481
|
+
# partially created profile so we don't leave a "logged-in but unusable" state.
|
|
482
|
+
try:
|
|
483
|
+
asyncio.run(_create_or_update_agent_api_key(auth_svc, created))
|
|
484
|
+
except Exception:
|
|
485
|
+
try:
|
|
486
|
+
asyncio.run(auth_svc.delete_profile(created.name))
|
|
487
|
+
except Exception:
|
|
488
|
+
# Best-effort cleanup; original error is more important for the user.
|
|
489
|
+
pass
|
|
490
|
+
raise
|
|
491
|
+
|
|
447
492
|
return created
|
|
448
493
|
|
|
449
494
|
|
|
450
495
|
async def _run_device_authentication(base_url: str) -> DeviceOIDC:
|
|
496
|
+
import webbrowser
|
|
497
|
+
|
|
498
|
+
from llama_deploy.cli.auth.client import (
|
|
499
|
+
DeviceAuthorizationRequest,
|
|
500
|
+
OIDCClient,
|
|
501
|
+
PlatformAuthDiscoveryClient,
|
|
502
|
+
TokenRequestDeviceCode,
|
|
503
|
+
decode_jwt_claims,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
from ..config.schema import DeviceOIDC
|
|
507
|
+
|
|
451
508
|
device_name = _auto_device_name()
|
|
452
509
|
# 1) Discover upstream and CLI client_id via client
|
|
453
510
|
async with PlatformAuthDiscoveryClient(base_url) as discovery:
|
|
@@ -549,8 +606,9 @@ def validate_authenticated_profile(interactive: bool) -> Auth:
|
|
|
549
606
|
- If environment requires_auth: run token flow inline.
|
|
550
607
|
- Else: create profile without token after selecting a project.
|
|
551
608
|
"""
|
|
609
|
+
import questionary
|
|
552
610
|
|
|
553
|
-
auth_svc =
|
|
611
|
+
auth_svc = _get_service().current_auth_service()
|
|
554
612
|
existing = auth_svc.get_current_profile()
|
|
555
613
|
if existing:
|
|
556
614
|
return existing
|
|
@@ -599,6 +657,8 @@ def validate_authenticated_profile(interactive: bool) -> Auth:
|
|
|
599
657
|
|
|
600
658
|
|
|
601
659
|
def _prompt_for_api_key() -> str:
|
|
660
|
+
import questionary
|
|
661
|
+
|
|
602
662
|
entered = questionary.password("Enter API key token to login").ask()
|
|
603
663
|
if entered:
|
|
604
664
|
return entered.strip()
|
|
@@ -609,7 +669,9 @@ def _list_projects(
|
|
|
609
669
|
auth_svc: AuthService,
|
|
610
670
|
api_key: str | None = None,
|
|
611
671
|
) -> list[ProjectSummary]:
|
|
612
|
-
async def _run():
|
|
672
|
+
async def _run() -> list[ProjectSummary]:
|
|
673
|
+
from llama_deploy.core.client.manage_client import ControlPlaneClient
|
|
674
|
+
|
|
613
675
|
profile = auth_svc.get_current_profile()
|
|
614
676
|
async with ControlPlaneClient.ctx(
|
|
615
677
|
auth_svc.env.api_url,
|
|
@@ -624,6 +686,8 @@ def _list_projects(
|
|
|
624
686
|
def _prompt_validate_api_key_and_list_projects(
|
|
625
687
|
auth_svc: AuthService, api_key: str
|
|
626
688
|
) -> list[ProjectSummary]:
|
|
689
|
+
import httpx
|
|
690
|
+
|
|
627
691
|
try:
|
|
628
692
|
return _list_projects(auth_svc, api_key)
|
|
629
693
|
except httpx.HTTPStatusError as e:
|
|
@@ -645,6 +709,8 @@ def _prompt_validate_api_key_and_list_projects(
|
|
|
645
709
|
def _select_or_enter_project(
|
|
646
710
|
projects: list[ProjectSummary], requires_auth: bool
|
|
647
711
|
) -> str | None:
|
|
712
|
+
import questionary
|
|
713
|
+
|
|
648
714
|
if not projects:
|
|
649
715
|
return None
|
|
650
716
|
# select the only authorized project if there is only one
|
|
@@ -693,13 +759,15 @@ def _select_profile(
|
|
|
693
759
|
return None
|
|
694
760
|
|
|
695
761
|
try:
|
|
762
|
+
import questionary
|
|
763
|
+
|
|
696
764
|
profiles = auth_svc.list_profiles()
|
|
697
765
|
|
|
698
766
|
if not profiles:
|
|
699
767
|
rprint(f"[{WARNING}]No profiles found[/]")
|
|
700
768
|
return None
|
|
701
769
|
|
|
702
|
-
choices = []
|
|
770
|
+
choices: list[Any] = []
|
|
703
771
|
current = auth_svc.get_current_profile()
|
|
704
772
|
|
|
705
773
|
for profile in profiles:
|
|
@@ -9,8 +9,6 @@ git ref, reads the config, and runs your app.
|
|
|
9
9
|
import asyncio
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
|
-
import questionary
|
|
13
|
-
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
14
12
|
from llama_deploy.cli.styles import HEADER_COLOR, MUTED_COL, PRIMARY_COL, WARNING
|
|
15
13
|
from llama_deploy.core.schema.deployments import (
|
|
16
14
|
DeploymentHistoryResponse,
|
|
@@ -22,16 +20,7 @@ from rich.table import Table
|
|
|
22
20
|
from rich.text import Text
|
|
23
21
|
|
|
24
22
|
from ..app import app, console
|
|
25
|
-
from ..client import get_project_client, project_client_context
|
|
26
|
-
from ..interactive_prompts.session_utils import (
|
|
27
|
-
is_interactive_session,
|
|
28
|
-
)
|
|
29
|
-
from ..interactive_prompts.utils import (
|
|
30
|
-
confirm_action,
|
|
31
|
-
)
|
|
32
23
|
from ..options import global_options, interactive_option
|
|
33
|
-
from ..textual.deployment_form import create_deployment_form, edit_deployment_form
|
|
34
|
-
from ..textual.deployment_monitor import monitor_deployment_screen
|
|
35
24
|
|
|
36
25
|
|
|
37
26
|
@app.group(
|
|
@@ -50,6 +39,10 @@ def deployments() -> None:
|
|
|
50
39
|
@interactive_option
|
|
51
40
|
def list_deployments(interactive: bool) -> None:
|
|
52
41
|
"""List deployments for the configured project."""
|
|
42
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
43
|
+
|
|
44
|
+
from ..client import get_project_client
|
|
45
|
+
|
|
53
46
|
validate_authenticated_profile(interactive)
|
|
54
47
|
try:
|
|
55
48
|
client = get_project_client()
|
|
@@ -95,11 +88,16 @@ def list_deployments(interactive: bool) -> None:
|
|
|
95
88
|
@interactive_option
|
|
96
89
|
def get_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
97
90
|
"""Get details of a specific deployment"""
|
|
91
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
92
|
+
|
|
93
|
+
from ..client import get_project_client
|
|
94
|
+
from ..textual.deployment_monitor import monitor_deployment_screen
|
|
95
|
+
|
|
98
96
|
validate_authenticated_profile(interactive)
|
|
99
97
|
try:
|
|
100
98
|
client = get_project_client()
|
|
101
99
|
|
|
102
|
-
deployment_id = select_deployment(deployment_id)
|
|
100
|
+
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
103
101
|
if not deployment_id:
|
|
104
102
|
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
105
103
|
return
|
|
@@ -144,6 +142,9 @@ def create_deployment(
|
|
|
144
142
|
interactive: bool,
|
|
145
143
|
) -> None:
|
|
146
144
|
"""Interactively create a new deployment"""
|
|
145
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
146
|
+
|
|
147
|
+
from ..textual.deployment_form import create_deployment_form
|
|
147
148
|
|
|
148
149
|
if not interactive:
|
|
149
150
|
raise click.ClickException(
|
|
@@ -168,6 +169,13 @@ def create_deployment(
|
|
|
168
169
|
@interactive_option
|
|
169
170
|
def delete_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
170
171
|
"""Delete a deployment"""
|
|
172
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
173
|
+
|
|
174
|
+
from ..client import get_project_client
|
|
175
|
+
from ..interactive_prompts.utils import (
|
|
176
|
+
confirm_action,
|
|
177
|
+
)
|
|
178
|
+
|
|
171
179
|
validate_authenticated_profile(interactive)
|
|
172
180
|
try:
|
|
173
181
|
client = get_project_client()
|
|
@@ -196,6 +204,11 @@ def delete_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
|
196
204
|
@interactive_option
|
|
197
205
|
def edit_deployment(deployment_id: str | None, interactive: bool) -> None:
|
|
198
206
|
"""Interactively edit a deployment"""
|
|
207
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
208
|
+
|
|
209
|
+
from ..client import get_project_client
|
|
210
|
+
from ..textual.deployment_form import edit_deployment_form
|
|
211
|
+
|
|
199
212
|
validate_authenticated_profile(interactive)
|
|
200
213
|
try:
|
|
201
214
|
client = get_project_client()
|
|
@@ -236,9 +249,13 @@ def refresh_deployment(
|
|
|
236
249
|
deployment_id: str | None, git_ref: str | None, interactive: bool
|
|
237
250
|
) -> None:
|
|
238
251
|
"""Update the deployment, pulling the latest code from it's branch"""
|
|
252
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
253
|
+
|
|
254
|
+
from ..client import get_project_client
|
|
255
|
+
|
|
239
256
|
validate_authenticated_profile(interactive)
|
|
240
257
|
try:
|
|
241
|
-
deployment_id = select_deployment(deployment_id)
|
|
258
|
+
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
242
259
|
if not deployment_id:
|
|
243
260
|
rprint(f"[{WARNING}]No deployment selected[/]")
|
|
244
261
|
return
|
|
@@ -283,6 +300,10 @@ def refresh_deployment(
|
|
|
283
300
|
@interactive_option
|
|
284
301
|
def show_history(deployment_id: str | None, interactive: bool) -> None:
|
|
285
302
|
"""Show release history for a deployment."""
|
|
303
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
304
|
+
|
|
305
|
+
from ..client import project_client_context
|
|
306
|
+
|
|
286
307
|
validate_authenticated_profile(interactive)
|
|
287
308
|
try:
|
|
288
309
|
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
@@ -326,6 +347,14 @@ def show_history(deployment_id: str | None, interactive: bool) -> None:
|
|
|
326
347
|
@interactive_option
|
|
327
348
|
def rollback(deployment_id: str | None, git_sha: str | None, interactive: bool) -> None:
|
|
328
349
|
"""Rollback a deployment to a previous git sha."""
|
|
350
|
+
import questionary
|
|
351
|
+
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
352
|
+
|
|
353
|
+
from ..client import project_client_context
|
|
354
|
+
from ..interactive_prompts.utils import (
|
|
355
|
+
confirm_action,
|
|
356
|
+
)
|
|
357
|
+
|
|
329
358
|
validate_authenticated_profile(interactive)
|
|
330
359
|
try:
|
|
331
360
|
deployment_id = select_deployment(deployment_id, interactive=interactive)
|
|
@@ -387,22 +416,23 @@ def rollback(deployment_id: str | None, git_sha: str | None, interactive: bool)
|
|
|
387
416
|
raise click.Abort()
|
|
388
417
|
|
|
389
418
|
|
|
390
|
-
def select_deployment(
|
|
391
|
-
deployment_id: str | None = None, interactive: bool = is_interactive_session()
|
|
392
|
-
) -> str | None:
|
|
419
|
+
def select_deployment(deployment_id: str | None, interactive: bool) -> str | None:
|
|
393
420
|
"""
|
|
394
421
|
Select a deployment interactively if ID not provided.
|
|
395
422
|
Returns the selected deployment ID or None if cancelled.
|
|
396
423
|
|
|
397
424
|
In non-interactive sessions, returns None if deployment_id is not provided.
|
|
398
425
|
"""
|
|
426
|
+
import questionary
|
|
427
|
+
|
|
428
|
+
from ..client import get_project_client
|
|
429
|
+
|
|
399
430
|
if deployment_id:
|
|
400
431
|
return deployment_id
|
|
401
432
|
|
|
402
433
|
# Don't attempt interactive selection in non-interactive sessions
|
|
403
434
|
if not interactive:
|
|
404
435
|
return None
|
|
405
|
-
|
|
406
436
|
client = get_project_client()
|
|
407
437
|
deployments = asyncio.run(client.list_deployments())
|
|
408
438
|
|
llama_deploy/cli/commands/dev.py
CHANGED
|
@@ -6,14 +6,6 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
from click.exceptions import Abort, Exit
|
|
9
|
-
from llama_deploy.appserver.app import prepare_server, start_preflight_in_target_venv
|
|
10
|
-
from llama_deploy.appserver.deployment_config_parser import get_deployment_config
|
|
11
|
-
from llama_deploy.appserver.settings import configure_settings, settings
|
|
12
|
-
from llama_deploy.appserver.workflow_loader import (
|
|
13
|
-
load_environment_variables,
|
|
14
|
-
parse_environment_variables,
|
|
15
|
-
validate_required_env_vars,
|
|
16
|
-
)
|
|
17
9
|
from llama_deploy.cli.commands.aliased_group import AliasedGroup
|
|
18
10
|
from llama_deploy.cli.commands.serve import (
|
|
19
11
|
_maybe_inject_llama_cloud_credentials,
|
|
@@ -29,6 +21,8 @@ from rich import print as rprint
|
|
|
29
21
|
|
|
30
22
|
from ..app import app
|
|
31
23
|
|
|
24
|
+
_ClickPath = getattr(click, "Path")
|
|
25
|
+
|
|
32
26
|
|
|
33
27
|
@app.group(
|
|
34
28
|
name="dev",
|
|
@@ -52,7 +46,7 @@ dev.add_command(serve_command, name="serve")
|
|
|
52
46
|
"deployment_file",
|
|
53
47
|
required=False,
|
|
54
48
|
default=DEFAULT_DEPLOYMENT_FILE_PATH,
|
|
55
|
-
type=
|
|
49
|
+
type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),
|
|
56
50
|
)
|
|
57
51
|
@interactive_option
|
|
58
52
|
@global_options
|
|
@@ -98,7 +92,7 @@ def validate_command(deployment_file: Path, interactive: bool) -> None:
|
|
|
98
92
|
"deployment_file",
|
|
99
93
|
"--deployment-file",
|
|
100
94
|
default=DEFAULT_DEPLOYMENT_FILE_PATH,
|
|
101
|
-
type=
|
|
95
|
+
type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),
|
|
102
96
|
help="The deployment file to use for the command",
|
|
103
97
|
)
|
|
104
98
|
@click.option(
|
|
@@ -159,6 +153,15 @@ def _ensure_project_layout(deployment_file: Path, *, command_name: str) -> Path:
|
|
|
159
153
|
def _prepare_environment(
|
|
160
154
|
deployment_file: Path, interactive: bool, *, require_cloud: bool
|
|
161
155
|
) -> tuple[DeploymentConfig, Path]:
|
|
156
|
+
from llama_deploy.appserver.deployment_config_parser import (
|
|
157
|
+
get_deployment_config,
|
|
158
|
+
)
|
|
159
|
+
from llama_deploy.appserver.settings import configure_settings, settings
|
|
160
|
+
from llama_deploy.appserver.workflow_loader import (
|
|
161
|
+
load_environment_variables,
|
|
162
|
+
validate_required_env_vars,
|
|
163
|
+
)
|
|
164
|
+
|
|
162
165
|
_maybe_inject_llama_cloud_credentials(
|
|
163
166
|
deployment_file, interactive, require_cloud=require_cloud
|
|
164
167
|
)
|
|
@@ -173,4 +176,38 @@ def _prepare_environment(
|
|
|
173
176
|
return config, config_parent
|
|
174
177
|
|
|
175
178
|
|
|
179
|
+
def prepare_server(
|
|
180
|
+
*, deployment_file: Path, install: bool, build: bool, install_ui_deps: bool
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Thin wrapper so tests can monkeypatch `dev.prepare_server` without importing appserver at import time."""
|
|
183
|
+
from llama_deploy.appserver.app import prepare_server as _prepare_server
|
|
184
|
+
|
|
185
|
+
_prepare_server(
|
|
186
|
+
deployment_file=deployment_file,
|
|
187
|
+
install=install,
|
|
188
|
+
build=build,
|
|
189
|
+
install_ui_deps=install_ui_deps,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def start_preflight_in_target_venv(*, cwd: Path, deployment_file: Path) -> None:
|
|
194
|
+
"""Thin wrapper so tests can monkeypatch `dev.start_preflight_in_target_venv`."""
|
|
195
|
+
from llama_deploy.appserver.app import (
|
|
196
|
+
start_preflight_in_target_venv as _start_preflight_in_target_venv,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
_start_preflight_in_target_venv(cwd=cwd, deployment_file=deployment_file)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parse_environment_variables(
|
|
203
|
+
config: DeploymentConfig, config_parent: Path
|
|
204
|
+
) -> dict[str, str]:
|
|
205
|
+
"""Wrapper used by tests; imports workflow loader lazily."""
|
|
206
|
+
from llama_deploy.appserver.workflow_loader import (
|
|
207
|
+
parse_environment_variables as _parse_environment_variables,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return _parse_environment_variables(config, config_parent)
|
|
211
|
+
|
|
212
|
+
|
|
176
213
|
__all__ = ["dev", "validate_command", "run_command"]
|