llamactl 0.2.7a1__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 +32 -0
- llama_deploy/cli/client.py +173 -0
- llama_deploy/cli/commands.py +549 -0
- llama_deploy/cli/config.py +173 -0
- llama_deploy/cli/debug.py +16 -0
- llama_deploy/cli/env.py +30 -0
- llama_deploy/cli/interactive_prompts/utils.py +86 -0
- llama_deploy/cli/options.py +21 -0
- llama_deploy/cli/textual/deployment_form.py +409 -0
- llama_deploy/cli/textual/git_validation.py +357 -0
- llama_deploy/cli/textual/github_callback_server.py +207 -0
- llama_deploy/cli/textual/llama_loader.py +52 -0
- llama_deploy/cli/textual/profile_form.py +171 -0
- llama_deploy/cli/textual/secrets_form.py +186 -0
- llama_deploy/cli/textual/styles.tcss +162 -0
- llamactl-0.2.7a1.dist-info/METADATA +122 -0
- llamactl-0.2.7a1.dist-info/RECORD +19 -0
- llamactl-0.2.7a1.dist-info/WHEEL +4 -0
- llamactl-0.2.7a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from llama_deploy.appserver.client import Client as ApiserverClient
|
|
8
|
+
from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
|
|
9
|
+
from llama_deploy.core.schema.deployments import DeploymentUpdate
|
|
10
|
+
from rich import print as rprint
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed
|
|
14
|
+
|
|
15
|
+
from .client import get_client
|
|
16
|
+
from .config import config_manager
|
|
17
|
+
from .interactive_prompts.utils import (
|
|
18
|
+
confirm_action,
|
|
19
|
+
select_deployment,
|
|
20
|
+
select_profile,
|
|
21
|
+
)
|
|
22
|
+
from .options import global_options
|
|
23
|
+
from .textual.deployment_form import create_deployment_form, edit_deployment_form
|
|
24
|
+
from .textual.profile_form import create_profile_form, edit_profile_form
|
|
25
|
+
|
|
26
|
+
RETRY_WAIT_SECONDS = 1
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Create sub-applications for organizing commands
|
|
31
|
+
@click.group(help="Manage profiles", no_args_is_help=True)
|
|
32
|
+
@global_options
|
|
33
|
+
def profile() -> None:
|
|
34
|
+
"""Manage profiles"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group(help="Manage projects", no_args_is_help=True)
|
|
39
|
+
@global_options
|
|
40
|
+
def projects() -> None:
|
|
41
|
+
"""Manage projects"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.group(help="Manage deployments", no_args_is_help=True)
|
|
46
|
+
@global_options
|
|
47
|
+
def deployments() -> None:
|
|
48
|
+
"""Manage deployments"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Profile commands
|
|
53
|
+
@profile.command("create")
|
|
54
|
+
@global_options
|
|
55
|
+
@click.option("--name", help="Profile name")
|
|
56
|
+
@click.option("--api-url", help="API server URL")
|
|
57
|
+
@click.option("--project-id", help="Default project ID")
|
|
58
|
+
def create_profile(
|
|
59
|
+
name: Optional[str], api_url: Optional[str], project_id: Optional[str]
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Create a new profile"""
|
|
62
|
+
try:
|
|
63
|
+
# If all required args are provided via CLI, skip interactive mode
|
|
64
|
+
if name and api_url:
|
|
65
|
+
# Use CLI args directly
|
|
66
|
+
profile = config_manager.create_profile(name, api_url, project_id)
|
|
67
|
+
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
68
|
+
|
|
69
|
+
# Automatically switch to the new profile
|
|
70
|
+
config_manager.set_current_profile(name)
|
|
71
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Use interactive creation
|
|
75
|
+
profile = create_profile_form()
|
|
76
|
+
if profile is None:
|
|
77
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
82
|
+
|
|
83
|
+
# Automatically switch to the new profile
|
|
84
|
+
config_manager.set_current_profile(profile.name)
|
|
85
|
+
rprint(f"[green]Switched to profile '{profile.name}'[/green]")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
rprint(f"[red]Error creating profile: {e}[/red]")
|
|
88
|
+
raise click.Abort()
|
|
89
|
+
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
92
|
+
raise click.Abort()
|
|
93
|
+
except Exception as e:
|
|
94
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
95
|
+
raise click.Abort()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@profile.command("list")
|
|
99
|
+
@global_options
|
|
100
|
+
def list_profiles() -> None:
|
|
101
|
+
"""List all profiles"""
|
|
102
|
+
try:
|
|
103
|
+
profiles = config_manager.list_profiles()
|
|
104
|
+
current_name = config_manager.get_current_profile_name()
|
|
105
|
+
|
|
106
|
+
if not profiles:
|
|
107
|
+
rprint("[yellow]No profiles found[/yellow]")
|
|
108
|
+
rprint("Create one with: [cyan]llamactl profile create[/cyan]")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
table = Table(title="Profiles")
|
|
112
|
+
table.add_column("Name", style="cyan")
|
|
113
|
+
table.add_column("API URL", style="green")
|
|
114
|
+
table.add_column("Active Project", style="yellow")
|
|
115
|
+
table.add_column("Current", style="magenta")
|
|
116
|
+
|
|
117
|
+
for profile in profiles:
|
|
118
|
+
is_current = "✓" if profile.name == current_name else ""
|
|
119
|
+
active_project = profile.active_project_id or "-"
|
|
120
|
+
table.add_row(profile.name, profile.api_url, active_project, is_current)
|
|
121
|
+
|
|
122
|
+
console.print(table)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
126
|
+
raise click.Abort()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@profile.command("switch")
|
|
130
|
+
@global_options
|
|
131
|
+
@click.argument("name", required=False)
|
|
132
|
+
def switch_profile(name: Optional[str]) -> None:
|
|
133
|
+
"""Switch to a different profile"""
|
|
134
|
+
try:
|
|
135
|
+
name = select_profile(name)
|
|
136
|
+
if not name:
|
|
137
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
profile = config_manager.get_profile(name)
|
|
141
|
+
if not profile:
|
|
142
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
143
|
+
raise click.Abort()
|
|
144
|
+
|
|
145
|
+
config_manager.set_current_profile(name)
|
|
146
|
+
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
150
|
+
raise click.Abort()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@profile.command("delete")
|
|
154
|
+
@global_options
|
|
155
|
+
@click.argument("name", required=False)
|
|
156
|
+
def delete_profile(name: Optional[str]) -> None:
|
|
157
|
+
"""Delete a profile"""
|
|
158
|
+
try:
|
|
159
|
+
name = select_profile(name)
|
|
160
|
+
if not name:
|
|
161
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
profile = config_manager.get_profile(name)
|
|
165
|
+
if not profile:
|
|
166
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
167
|
+
raise click.Abort()
|
|
168
|
+
|
|
169
|
+
if config_manager.delete_profile(name):
|
|
170
|
+
rprint(f"[green]Deleted profile '{name}'[/green]")
|
|
171
|
+
else:
|
|
172
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
176
|
+
raise click.Abort()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@profile.command("edit")
|
|
180
|
+
@global_options
|
|
181
|
+
@click.argument("name", required=False)
|
|
182
|
+
def edit_profile(name: Optional[str]) -> None:
|
|
183
|
+
"""Edit a profile"""
|
|
184
|
+
try:
|
|
185
|
+
name = select_profile(name)
|
|
186
|
+
if not name:
|
|
187
|
+
rprint("[yellow]No profile selected[/yellow]")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Get current profile
|
|
191
|
+
maybe_profile = config_manager.get_profile(name)
|
|
192
|
+
if not maybe_profile:
|
|
193
|
+
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
194
|
+
raise click.Abort()
|
|
195
|
+
profile = maybe_profile
|
|
196
|
+
|
|
197
|
+
# Use the interactive edit menu
|
|
198
|
+
updated = edit_profile_form(profile)
|
|
199
|
+
if updated is None:
|
|
200
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
current_profile = config_manager.get_current_profile()
|
|
205
|
+
if not current_profile or current_profile.name != updated.name:
|
|
206
|
+
config_manager.set_current_profile(updated.name)
|
|
207
|
+
rprint(f"[green]Updated profile '{profile.name}'[/green]")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
rprint(f"[red]Error updating profile: {e}[/red]")
|
|
210
|
+
raise click.Abort()
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
214
|
+
raise click.Abort()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Projects commands
|
|
218
|
+
@projects.command("list")
|
|
219
|
+
@global_options
|
|
220
|
+
def list_projects() -> None:
|
|
221
|
+
"""List all projects with deployment counts"""
|
|
222
|
+
try:
|
|
223
|
+
client = get_client()
|
|
224
|
+
projects = client.list_projects()
|
|
225
|
+
|
|
226
|
+
if not projects:
|
|
227
|
+
rprint("[yellow]No projects found[/yellow]")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
table = Table(title="Projects")
|
|
231
|
+
table.add_column("Project ID", style="cyan")
|
|
232
|
+
table.add_column("Deployments", style="green")
|
|
233
|
+
|
|
234
|
+
for project in projects:
|
|
235
|
+
project_id = project.project_id
|
|
236
|
+
deployment_count = project.deployment_count
|
|
237
|
+
table.add_row(project_id, str(deployment_count))
|
|
238
|
+
|
|
239
|
+
console.print(table)
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
243
|
+
raise click.Abort()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Health check command (at root level)
|
|
247
|
+
@click.command()
|
|
248
|
+
@global_options
|
|
249
|
+
def health_check() -> None:
|
|
250
|
+
"""Check if the API server is healthy"""
|
|
251
|
+
try:
|
|
252
|
+
client = get_client()
|
|
253
|
+
health = client.health_check()
|
|
254
|
+
|
|
255
|
+
status = health.get("status", "unknown")
|
|
256
|
+
if status == "ok":
|
|
257
|
+
rprint("[green]API server is healthy[/green]")
|
|
258
|
+
else:
|
|
259
|
+
rprint(f"[yellow]API server status: {status}[/yellow]")
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
263
|
+
raise click.Abort()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Deployments commands
|
|
267
|
+
@deployments.command("list")
|
|
268
|
+
@global_options
|
|
269
|
+
def list_deployments() -> None:
|
|
270
|
+
"""List deployments for the configured project"""
|
|
271
|
+
try:
|
|
272
|
+
client = get_client()
|
|
273
|
+
deployments = client.list_deployments()
|
|
274
|
+
|
|
275
|
+
if not deployments:
|
|
276
|
+
rprint(
|
|
277
|
+
f"[yellow]No deployments found for project {client.project_id}[/yellow]"
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
table = Table(title=f"Deployments for project {client.project_id}")
|
|
282
|
+
table.add_column("Name", style="cyan")
|
|
283
|
+
table.add_column("ID", style="yellow")
|
|
284
|
+
table.add_column("Status", style="green")
|
|
285
|
+
table.add_column("Repository", style="blue")
|
|
286
|
+
table.add_column("Deployment File", style="magenta")
|
|
287
|
+
table.add_column("Git Ref", style="white")
|
|
288
|
+
table.add_column("PAT", style="red")
|
|
289
|
+
table.add_column("Secrets", style="bright_green")
|
|
290
|
+
|
|
291
|
+
for deployment in deployments:
|
|
292
|
+
name = deployment.name
|
|
293
|
+
deployment_id = deployment.id
|
|
294
|
+
status = deployment.status
|
|
295
|
+
repo_url = deployment.repo_url
|
|
296
|
+
deployment_file_path = deployment.deployment_file_path
|
|
297
|
+
git_ref = deployment.git_ref
|
|
298
|
+
has_pat = "✓" if deployment.has_personal_access_token else "-"
|
|
299
|
+
secret_names = deployment.secret_names
|
|
300
|
+
secrets_display = str(len(secret_names)) if secret_names else "-"
|
|
301
|
+
|
|
302
|
+
table.add_row(
|
|
303
|
+
name,
|
|
304
|
+
deployment_id,
|
|
305
|
+
status,
|
|
306
|
+
repo_url,
|
|
307
|
+
deployment_file_path,
|
|
308
|
+
git_ref,
|
|
309
|
+
has_pat,
|
|
310
|
+
secrets_display,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
console.print(table)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
317
|
+
raise click.Abort()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@deployments.command("get")
|
|
321
|
+
@global_options
|
|
322
|
+
@click.argument("deployment_id", required=False)
|
|
323
|
+
def get_deployment(deployment_id: Optional[str]) -> None:
|
|
324
|
+
"""Get details of a specific deployment"""
|
|
325
|
+
try:
|
|
326
|
+
client = get_client()
|
|
327
|
+
|
|
328
|
+
deployment_id = select_deployment(deployment_id)
|
|
329
|
+
if not deployment_id:
|
|
330
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
deployment = client.get_deployment(deployment_id)
|
|
334
|
+
|
|
335
|
+
table = Table(title=f"Deployment: {deployment.name}")
|
|
336
|
+
table.add_column("Property", style="cyan")
|
|
337
|
+
table.add_column("Value", style="green")
|
|
338
|
+
|
|
339
|
+
table.add_row("ID", deployment.id)
|
|
340
|
+
table.add_row("Project ID", deployment.project_id)
|
|
341
|
+
table.add_row("Status", deployment.status)
|
|
342
|
+
table.add_row("Repository", deployment.repo_url)
|
|
343
|
+
table.add_row("Deployment File", deployment.deployment_file_path)
|
|
344
|
+
table.add_row("Git Ref", deployment.git_ref)
|
|
345
|
+
table.add_row("Has PAT", str(deployment.has_personal_access_token))
|
|
346
|
+
|
|
347
|
+
apiserver_url = deployment.apiserver_url
|
|
348
|
+
if apiserver_url:
|
|
349
|
+
table.add_row("API Server URL", str(apiserver_url))
|
|
350
|
+
|
|
351
|
+
secret_names = deployment.secret_names
|
|
352
|
+
if secret_names:
|
|
353
|
+
table.add_row("Secrets", ", ".join(secret_names))
|
|
354
|
+
|
|
355
|
+
console.print(table)
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
359
|
+
raise click.Abort()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@deployments.command("create")
|
|
363
|
+
@global_options
|
|
364
|
+
@click.option("--repo-url", help="HTTP(S) Git Repository URL")
|
|
365
|
+
@click.option("--name", help="Deployment name")
|
|
366
|
+
@click.option("--deployment-file-path", help="Path to deployment file")
|
|
367
|
+
@click.option("--git-ref", help="Git reference (branch, tag, or commit)")
|
|
368
|
+
@click.option(
|
|
369
|
+
"--personal-access-token", help="Git Personal Access Token (HTTP Basic Auth)"
|
|
370
|
+
)
|
|
371
|
+
def create_deployment(
|
|
372
|
+
repo_url: Optional[str],
|
|
373
|
+
name: Optional[str],
|
|
374
|
+
deployment_file_path: Optional[str],
|
|
375
|
+
git_ref: Optional[str],
|
|
376
|
+
personal_access_token: Optional[str],
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Create a new deployment"""
|
|
379
|
+
|
|
380
|
+
# Use interactive creation
|
|
381
|
+
deployment_form = create_deployment_form()
|
|
382
|
+
if deployment_form is None:
|
|
383
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
rprint(
|
|
387
|
+
f"[green]Created deployment: {deployment_form.name} (id: {deployment_form.id})[/green]"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@deployments.command("delete")
|
|
392
|
+
@global_options
|
|
393
|
+
@click.argument("deployment_id", required=False)
|
|
394
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
395
|
+
def delete_deployment(deployment_id: Optional[str], confirm: bool) -> None:
|
|
396
|
+
"""Delete a deployment"""
|
|
397
|
+
try:
|
|
398
|
+
client = get_client()
|
|
399
|
+
|
|
400
|
+
deployment_id = select_deployment(deployment_id)
|
|
401
|
+
if not deployment_id:
|
|
402
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if not confirm:
|
|
406
|
+
if not confirm_action(f"Delete deployment '{deployment_id}'?"):
|
|
407
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
client.delete_deployment(deployment_id)
|
|
411
|
+
rprint(f"[green]Deleted deployment: {deployment_id}[/green]")
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
414
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
415
|
+
raise click.Abort()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@deployments.command("edit")
|
|
419
|
+
@global_options
|
|
420
|
+
@click.argument("deployment_id", required=False)
|
|
421
|
+
def edit_deployment(deployment_id: Optional[str]) -> None:
|
|
422
|
+
"""Interactively edit a deployment"""
|
|
423
|
+
try:
|
|
424
|
+
client = get_client()
|
|
425
|
+
|
|
426
|
+
deployment_id = select_deployment(deployment_id)
|
|
427
|
+
if not deployment_id:
|
|
428
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
# Get current deployment details
|
|
432
|
+
current_deployment = client.get_deployment(deployment_id)
|
|
433
|
+
|
|
434
|
+
# Use the interactive edit form
|
|
435
|
+
updated_deployment = edit_deployment_form(current_deployment)
|
|
436
|
+
if updated_deployment is None:
|
|
437
|
+
rprint("[yellow]Cancelled[/yellow]")
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
rprint(
|
|
441
|
+
f"[green]Successfully updated deployment: {updated_deployment.name}[/green]"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
446
|
+
raise click.Abort()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@deployments.command("refresh")
|
|
450
|
+
@global_options
|
|
451
|
+
@click.argument("deployment_id", required=False)
|
|
452
|
+
def refresh_deployment(deployment_id: Optional[str]) -> None:
|
|
453
|
+
"""Refresh a deployment with the latest code from its git reference"""
|
|
454
|
+
try:
|
|
455
|
+
client = get_client()
|
|
456
|
+
|
|
457
|
+
deployment_id = select_deployment(deployment_id)
|
|
458
|
+
if not deployment_id:
|
|
459
|
+
rprint("[yellow]No deployment selected[/yellow]")
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
# Get current deployment details to show what we're refreshing
|
|
463
|
+
current_deployment = client.get_deployment(deployment_id)
|
|
464
|
+
deployment_name = current_deployment.name
|
|
465
|
+
old_git_sha = current_deployment.git_sha or ""
|
|
466
|
+
|
|
467
|
+
# Create an empty update to force git SHA refresh with spinner
|
|
468
|
+
with console.status(f"Refreshing {deployment_name}..."):
|
|
469
|
+
deployment_update = DeploymentUpdate()
|
|
470
|
+
updated_deployment = client.update_deployment(
|
|
471
|
+
deployment_id, deployment_update, force_git_sha_update=True
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Show the git SHA change with short SHAs
|
|
475
|
+
new_git_sha = updated_deployment.git_sha or ""
|
|
476
|
+
old_short = old_git_sha[:7] if old_git_sha else "none"
|
|
477
|
+
new_short = new_git_sha[:7] if new_git_sha else "none"
|
|
478
|
+
|
|
479
|
+
if old_git_sha == new_git_sha:
|
|
480
|
+
rprint(f"No changes: already at {new_short}")
|
|
481
|
+
else:
|
|
482
|
+
rprint(f"Updated: {old_short} → {new_short}")
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
486
|
+
raise click.Abort()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@click.command("serve")
|
|
490
|
+
@click.argument(
|
|
491
|
+
"deployment_file",
|
|
492
|
+
required=False,
|
|
493
|
+
default=DEFAULT_DEPLOYMENT_FILE_PATH,
|
|
494
|
+
type=click.Path(dir_okay=False, resolve_path=True, path_type=Path), # type: ignore
|
|
495
|
+
)
|
|
496
|
+
@global_options
|
|
497
|
+
def serve(deployment_file: Path) -> None:
|
|
498
|
+
"""Run llama_deploy API Server in the foreground. If no deployment_file is provided, will look for a llama_deploy.yaml in the current directory."""
|
|
499
|
+
if not deployment_file.exists():
|
|
500
|
+
rprint(f"[red]Deployment file '{deployment_file}' not found[/red]")
|
|
501
|
+
raise click.Abort()
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
env = os.environ.copy()
|
|
505
|
+
env["LLAMA_DEPLOY_APISERVER_DEPLOYMENTS_PATH"] = str(deployment_file.parent)
|
|
506
|
+
|
|
507
|
+
client = ApiserverClient()
|
|
508
|
+
|
|
509
|
+
uvicorn_p = subprocess.Popen(
|
|
510
|
+
[
|
|
511
|
+
"uvicorn",
|
|
512
|
+
"llama_deploy.appserver.app:app",
|
|
513
|
+
"--host",
|
|
514
|
+
"localhost",
|
|
515
|
+
"--port",
|
|
516
|
+
"4501",
|
|
517
|
+
],
|
|
518
|
+
env=env,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
retrying = Retrying(
|
|
522
|
+
stop=stop_after_attempt(5), wait=wait_fixed(RETRY_WAIT_SECONDS)
|
|
523
|
+
)
|
|
524
|
+
try:
|
|
525
|
+
for attempt in retrying:
|
|
526
|
+
with attempt:
|
|
527
|
+
client.sync.apiserver.deployments.create(
|
|
528
|
+
deployment_file.open("rb"),
|
|
529
|
+
base_path=deployment_file.parent,
|
|
530
|
+
local=True,
|
|
531
|
+
)
|
|
532
|
+
except RetryError as e:
|
|
533
|
+
uvicorn_p.terminate()
|
|
534
|
+
last: Optional[BaseException] = e.last_attempt.exception(0)
|
|
535
|
+
last_msg = ""
|
|
536
|
+
if last is not None:
|
|
537
|
+
last_msg = ": " + (
|
|
538
|
+
str(last.message) if hasattr(last, "message") else str(last)
|
|
539
|
+
)
|
|
540
|
+
raise click.ClickException(f"Failed to create deployment{last_msg}")
|
|
541
|
+
|
|
542
|
+
uvicorn_p.wait()
|
|
543
|
+
|
|
544
|
+
except KeyboardInterrupt:
|
|
545
|
+
print("Shutting down...")
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
rprint(f"[red]Error: {e}[/red]")
|
|
549
|
+
raise click.Abort()
|