llamactl 0.3.0a13__tar.gz → 0.3.0a15__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.0a13 → llamactl-0.3.0a15}/PKG-INFO +3 -3
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/pyproject.toml +3 -3
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/__init__.py +2 -1
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/app.py +4 -7
- llamactl-0.3.0a15/src/llama_deploy/cli/client.py +41 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/commands/auth.py +377 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/commands/deployment.py +24 -36
- llamactl-0.3.0a15/src/llama_deploy/cli/commands/env.py +206 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/config/_config.py +385 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/config/auth_service.py +68 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/config/env_service.py +64 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/config/schema.py +31 -0
- llamactl-0.3.0a15/src/llama_deploy/cli/interactive_prompts/utils.py +20 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/options.py +0 -9
- llamactl-0.3.0a13/src/llama_deploy/cli/client.py +0 -55
- llamactl-0.3.0a13/src/llama_deploy/cli/commands/auth.py +0 -382
- llamactl-0.3.0a13/src/llama_deploy/cli/config.py +0 -241
- llamactl-0.3.0a13/src/llama_deploy/cli/interactive_prompts/utils.py +0 -59
- llamactl-0.3.0a13/src/llama_deploy/cli/textual/api_key_profile_form.py +0 -563
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/README.md +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/commands/init.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/commands/serve.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/debug.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/env.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/interactive_prompts/session_utils.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/platform_client.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/py.typed +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/deployment_form.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/deployment_help.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/deployment_monitor.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/git_validation.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
- {llamactl-0.3.0a13 → llamactl-0.3.0a15}/src/llama_deploy/cli/textual/styles.tcss +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llamactl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a15
|
|
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.0a15,<0.4.0
|
|
9
|
+
Requires-Dist: llama-deploy-appserver>=0.3.0a15,<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.0a15"
|
|
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.0a15,<0.4.0",
|
|
13
|
+
"llama-deploy-appserver>=0.3.0a15,<0.4.0",
|
|
14
14
|
"httpx>=0.24.0",
|
|
15
15
|
"rich>=13.0.0",
|
|
16
16
|
"questionary>=2.0.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from llama_deploy.cli.commands.auth import auth
|
|
2
2
|
from llama_deploy.cli.commands.deployment import deployments
|
|
3
|
+
from llama_deploy.cli.commands.env import env_group
|
|
3
4
|
from llama_deploy.cli.commands.init import init
|
|
4
5
|
from llama_deploy.cli.commands.serve import serve
|
|
5
6
|
|
|
@@ -11,7 +12,7 @@ def main() -> None:
|
|
|
11
12
|
app()
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
__all__ = ["app", "deployments", "auth", "serve", "init"]
|
|
15
|
+
__all__ = ["app", "deployments", "auth", "serve", "init", "env_group"]
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
if __name__ == "__main__":
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
from importlib.metadata import PackageNotFoundError
|
|
3
2
|
from importlib.metadata import version as pkg_version
|
|
4
3
|
|
|
5
4
|
import click
|
|
6
|
-
from llama_deploy.cli.client import get_control_plane_client
|
|
7
5
|
from llama_deploy.cli.commands.aliased_group import AliasedGroup
|
|
8
|
-
from llama_deploy.cli.config import
|
|
6
|
+
from llama_deploy.cli.config.env_service import service
|
|
9
7
|
from llama_deploy.cli.options import global_options
|
|
10
8
|
from rich import print as rprint
|
|
11
9
|
from rich.console import Console
|
|
@@ -23,11 +21,10 @@ def print_version(ctx: click.Context, param: click.Option, value: bool) -> None:
|
|
|
23
21
|
console.print(Text.assemble("client version: ", (ver, "green")))
|
|
24
22
|
|
|
25
23
|
# If there is an active profile, attempt to query server version
|
|
26
|
-
|
|
27
|
-
if
|
|
24
|
+
auth_service = service.current_auth_service()
|
|
25
|
+
if auth_service:
|
|
28
26
|
try:
|
|
29
|
-
|
|
30
|
-
data = asyncio.run(cp_client.server_version())
|
|
27
|
+
data = auth_service.fetch_server_version()
|
|
31
28
|
server_ver = data.version
|
|
32
29
|
console.print(
|
|
33
30
|
Text.assemble(
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from typing import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
from llama_deploy.cli.config.env_service import service
|
|
5
|
+
from llama_deploy.core.client.manage_client import ControlPlaneClient, ProjectClient
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_control_plane_client() -> ControlPlaneClient:
|
|
10
|
+
profile = service.current_auth_service().get_current_profile()
|
|
11
|
+
if profile:
|
|
12
|
+
resolved_base_url = profile.api_url.rstrip("/")
|
|
13
|
+
resolved_api_key = profile.api_key
|
|
14
|
+
return ControlPlaneClient(resolved_base_url, resolved_api_key)
|
|
15
|
+
|
|
16
|
+
# Fallback: allow env-scoped client construction for env operations
|
|
17
|
+
env = service.get_current_environment()
|
|
18
|
+
resolved_base_url = env.api_url.rstrip("/")
|
|
19
|
+
return ControlPlaneClient(resolved_base_url, None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_project_client() -> ProjectClient:
|
|
23
|
+
profile = service.current_auth_service().get_current_profile()
|
|
24
|
+
if not profile:
|
|
25
|
+
rprint("\n[bold red]No profile configured![/bold red]")
|
|
26
|
+
rprint("\nTo get started, create a profile with:")
|
|
27
|
+
rprint("[cyan]llamactl auth token[/cyan]")
|
|
28
|
+
raise SystemExit(1)
|
|
29
|
+
return ProjectClient(profile.api_url, profile.project_id, profile.api_key)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def project_client_context() -> AsyncGenerator[ProjectClient, None]:
|
|
34
|
+
client = get_project_client()
|
|
35
|
+
try:
|
|
36
|
+
yield client
|
|
37
|
+
finally:
|
|
38
|
+
try:
|
|
39
|
+
await client.aclose()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
@@ -0,0 +1,377 @@
|
|
|
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.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
|
|
10
|
+
from rich import print as rprint
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from ..app import app, console
|
|
15
|
+
from ..config.schema import Auth
|
|
16
|
+
from ..options import global_options, interactive_option
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Create sub-applications for organizing commands
|
|
20
|
+
@app.group(
|
|
21
|
+
help="Login to llama cloud control plane to manage deployments",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
@global_options
|
|
25
|
+
def auth() -> None:
|
|
26
|
+
"""Login to llama cloud control plane"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@auth.command("token")
|
|
31
|
+
@global_options
|
|
32
|
+
@click.option(
|
|
33
|
+
"--project-id",
|
|
34
|
+
help="Project ID to use for the login when creating non-interactively",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--api-key",
|
|
38
|
+
help="API key to use for the login when creating non-interactively",
|
|
39
|
+
)
|
|
40
|
+
@interactive_option
|
|
41
|
+
def create_api_key_profile(
|
|
42
|
+
project_id: str | None,
|
|
43
|
+
api_key: str | None,
|
|
44
|
+
interactive: bool,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Authenticate with an API key and create a profile in the current environment."""
|
|
47
|
+
try:
|
|
48
|
+
auth_svc = service.current_auth_service()
|
|
49
|
+
|
|
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
|
+
)
|
|
60
|
+
return
|
|
61
|
+
|
|
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)
|
|
65
|
+
|
|
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
|
|
73
|
+
|
|
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
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
81
|
+
raise click.Abort()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@auth.command("list")
|
|
85
|
+
@global_options
|
|
86
|
+
def list_profiles() -> None:
|
|
87
|
+
"""List all logged in profiles"""
|
|
88
|
+
try:
|
|
89
|
+
auth_svc = service.current_auth_service()
|
|
90
|
+
profiles = auth_svc.list_profiles()
|
|
91
|
+
current = auth_svc.get_current_profile()
|
|
92
|
+
|
|
93
|
+
if not profiles:
|
|
94
|
+
rprint("[yellow]No profiles found[/yellow]")
|
|
95
|
+
rprint("Create one with: [cyan]llamactl auth token[/cyan]")
|
|
96
|
+
return
|
|
97
|
+
|
|
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")
|
|
101
|
+
|
|
102
|
+
for profile in profiles:
|
|
103
|
+
text = Text()
|
|
104
|
+
if profile == current:
|
|
105
|
+
text.append("* ", style="magenta")
|
|
106
|
+
else:
|
|
107
|
+
text.append(" ")
|
|
108
|
+
text.append(profile.name)
|
|
109
|
+
active_project = profile.project_id or "-"
|
|
110
|
+
table.add_row(
|
|
111
|
+
text,
|
|
112
|
+
active_project,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
119
|
+
raise click.Abort()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@auth.command("switch")
|
|
123
|
+
@global_options
|
|
124
|
+
@click.argument("name", required=False)
|
|
125
|
+
@interactive_option
|
|
126
|
+
def switch_profile(name: str | None, interactive: bool) -> None:
|
|
127
|
+
"""Switch to a different profile"""
|
|
128
|
+
auth_svc = service.current_auth_service()
|
|
129
|
+
try:
|
|
130
|
+
selected_auth = _select_profile(auth_svc, name, interactive)
|
|
131
|
+
if not selected_auth:
|
|
132
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
auth_svc.set_current_profile(selected_auth.name)
|
|
136
|
+
rprint(f"[green]Switched to profile '{selected_auth.name}'[/green]")
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
140
|
+
raise click.Abort()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@auth.command("logout")
|
|
144
|
+
@global_options
|
|
145
|
+
@click.argument("name", required=False)
|
|
146
|
+
@interactive_option
|
|
147
|
+
def delete_profile(name: str | None, interactive: bool) -> None:
|
|
148
|
+
"""Logout from a profile and wipe all associated data"""
|
|
149
|
+
try:
|
|
150
|
+
auth_svc = service.current_auth_service()
|
|
151
|
+
auth = _select_profile(auth_svc, name, interactive)
|
|
152
|
+
if not auth:
|
|
153
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if auth_svc.delete_profile(auth.name):
|
|
157
|
+
rprint(f"[green]Logged out from '{auth.name}'[/green]")
|
|
158
|
+
else:
|
|
159
|
+
rprint(f"[red]Profile '{auth.name}' not found[/red]")
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
163
|
+
raise click.Abort()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Projects commands
|
|
167
|
+
@auth.command("project")
|
|
168
|
+
@click.argument("project_id", required=False)
|
|
169
|
+
@interactive_option
|
|
170
|
+
@global_options
|
|
171
|
+
def change_project(project_id: str | None, interactive: bool) -> None:
|
|
172
|
+
"""Change the active project for the current profile"""
|
|
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()
|
|
177
|
+
if project_id:
|
|
178
|
+
auth_svc.set_project(profile.name, project_id)
|
|
179
|
+
rprint(f"[green]Set active project to '{project_id}'[/green]")
|
|
180
|
+
return
|
|
181
|
+
if not interactive:
|
|
182
|
+
raise click.ClickException(
|
|
183
|
+
"No --project-id provided. Run `llamactl auth project --help` for more information."
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
client = get_control_plane_client()
|
|
187
|
+
projects = asyncio.run(client.list_projects())
|
|
188
|
+
|
|
189
|
+
if not projects:
|
|
190
|
+
rprint("[yellow]No projects found[/yellow]")
|
|
191
|
+
return
|
|
192
|
+
result = questionary.select(
|
|
193
|
+
"Select a project",
|
|
194
|
+
choices=[
|
|
195
|
+
questionary.Choice(
|
|
196
|
+
title=f"{project.project_name} ({project.deployment_count} deployments)",
|
|
197
|
+
value=project.project_id,
|
|
198
|
+
)
|
|
199
|
+
for project in projects
|
|
200
|
+
]
|
|
201
|
+
+ (
|
|
202
|
+
[questionary.Choice(title="Create new project", value="__CREATE__")]
|
|
203
|
+
if not auth_svc.env.requires_auth
|
|
204
|
+
else []
|
|
205
|
+
),
|
|
206
|
+
).ask()
|
|
207
|
+
if result == "__CREATE__":
|
|
208
|
+
project_id = questionary.text("Enter project ID").ask()
|
|
209
|
+
result = project_id
|
|
210
|
+
if result:
|
|
211
|
+
auth_svc.set_project(profile.name, result)
|
|
212
|
+
rprint(f"[green]Set active project to '{result}'[/green]")
|
|
213
|
+
else:
|
|
214
|
+
rprint("[yellow]No project selected[/yellow]")
|
|
215
|
+
except Exception as e:
|
|
216
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
217
|
+
raise click.Abort()
|
|
218
|
+
|
|
219
|
+
|
|
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
|
+
"""
|
|
229
|
+
|
|
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:
|
|
236
|
+
raise click.ClickException(
|
|
237
|
+
"No profile configured. Run `llamactl auth token` to create a profile."
|
|
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
|
|
264
|
+
else:
|
|
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,
|
|
322
|
+
)
|
|
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
|
|
337
|
+
|
|
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
|
|