llamactl 0.3.0a12__py3-none-any.whl → 0.3.0a13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llama_deploy/cli/__init__.py +2 -2
- llama_deploy/cli/app.py +3 -2
- llama_deploy/cli/client.py +26 -5
- llama_deploy/cli/commands/auth.py +382 -0
- llama_deploy/cli/commands/deployment.py +89 -38
- llama_deploy/cli/config.py +91 -23
- llama_deploy/cli/interactive_prompts/session_utils.py +37 -0
- llama_deploy/cli/interactive_prompts/utils.py +12 -37
- llama_deploy/cli/options.py +24 -1
- llama_deploy/cli/platform_client.py +52 -0
- llama_deploy/cli/textual/api_key_profile_form.py +563 -0
- llama_deploy/cli/textual/deployment_form.py +11 -10
- llama_deploy/cli/textual/deployment_monitor.py +98 -105
- llama_deploy/cli/textual/git_validation.py +11 -9
- llama_deploy/cli/textual/styles.tcss +21 -5
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a13.dist-info}/METADATA +3 -3
- llamactl-0.3.0a13.dist-info/RECORD +29 -0
- llama_deploy/cli/commands/profile.py +0 -217
- llama_deploy/cli/textual/profile_form.py +0 -170
- llamactl-0.3.0a12.dist-info/RECORD +0 -27
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a13.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a12.dist-info → llamactl-0.3.0a13.dist-info}/entry_points.txt +0 -0
|
@@ -7,11 +7,11 @@ import hashlib
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Iterator
|
|
11
10
|
|
|
12
|
-
from llama_deploy.cli.client import
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
from llama_deploy.cli.client import (
|
|
12
|
+
project_client_context,
|
|
13
|
+
)
|
|
14
|
+
from llama_deploy.core.schema import LogEvent
|
|
15
15
|
from llama_deploy.core.schema.deployments import DeploymentResponse
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
from textual import events
|
|
@@ -78,7 +78,6 @@ class DeploymentMonitorWidget(Widget):
|
|
|
78
78
|
error_message = reactive("", recompose=False)
|
|
79
79
|
wrap_enabled = reactive(False, recompose=False)
|
|
80
80
|
autoscroll_enabled = reactive(True, recompose=False)
|
|
81
|
-
stream_closer: Closer | None = None
|
|
82
81
|
|
|
83
82
|
def __init__(self, deployment_id: str) -> None:
|
|
84
83
|
super().__init__()
|
|
@@ -87,12 +86,12 @@ class DeploymentMonitorWidget(Widget):
|
|
|
87
86
|
# Persist content written to the RichLog across recomposes
|
|
88
87
|
self._log_buffer: list[Text] = []
|
|
89
88
|
|
|
90
|
-
def on_mount(self) -> None:
|
|
89
|
+
async def on_mount(self) -> None:
|
|
91
90
|
# Kick off initial fetch and start logs stream in background
|
|
92
|
-
self.run_worker(self._fetch_deployment()
|
|
93
|
-
self.run_worker(self._stream_logs
|
|
91
|
+
self.run_worker(self._fetch_deployment())
|
|
92
|
+
self.run_worker(self._stream_logs())
|
|
94
93
|
# Start periodic polling of deployment status
|
|
95
|
-
self.run_worker(self._poll_deployment_status()
|
|
94
|
+
self.run_worker(self._poll_deployment_status())
|
|
96
95
|
|
|
97
96
|
def compose(self) -> ComposeResult:
|
|
98
97
|
yield Static("Deployment Status", classes="primary-message")
|
|
@@ -141,81 +140,117 @@ class DeploymentMonitorWidget(Widget):
|
|
|
141
140
|
|
|
142
141
|
async def _fetch_deployment(self) -> None:
|
|
143
142
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
async with project_client_context() as client:
|
|
144
|
+
self.deployment = await client.get_deployment(
|
|
145
|
+
self.deployment_id, include_events=True
|
|
146
|
+
)
|
|
148
147
|
# Clear any previous error on success
|
|
149
148
|
self.error_message = ""
|
|
150
149
|
except Exception as e: # pragma: no cover - network errors
|
|
151
150
|
self.error_message = f"Failed to fetch deployment: {e}"
|
|
152
151
|
|
|
153
|
-
def _stream_logs(self) -> None:
|
|
154
|
-
"""Consume the
|
|
152
|
+
async def _stream_logs(self) -> None:
|
|
153
|
+
"""Consume the async log iterator, batch updates, and reconnect with backoff."""
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
client = get_client()
|
|
159
|
-
|
|
160
|
-
def _sleep_with_cancel(total_seconds: float) -> None:
|
|
155
|
+
async def _sleep_with_cancel(total_seconds: float) -> None:
|
|
161
156
|
step = 0.2
|
|
162
157
|
remaining = total_seconds
|
|
163
158
|
while remaining > 0 and not self._stop_stream.is_set():
|
|
164
|
-
|
|
159
|
+
await asyncio.sleep(min(step, remaining))
|
|
165
160
|
remaining -= step
|
|
166
161
|
|
|
162
|
+
# Batching configuration: small latency to reduce UI churn while staying responsive
|
|
163
|
+
batch_max_latency_seconds = 0.1
|
|
164
|
+
batch_max_items = 200
|
|
165
|
+
|
|
167
166
|
base_backoff_seconds = 0.2
|
|
168
167
|
backoff_seconds = base_backoff_seconds
|
|
169
168
|
max_backoff_seconds = 30.0
|
|
170
169
|
|
|
171
170
|
while not self._stop_stream.is_set():
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
connect_started_at = time.monotonic()
|
|
172
|
+
# On any (re)connect, clear existing content
|
|
173
|
+
self._reset_log_view_for_reconnect()
|
|
174
|
+
|
|
175
|
+
queue: asyncio.Queue[LogEvent] = asyncio.Queue(maxsize=10000)
|
|
176
|
+
producer_done = asyncio.Event()
|
|
177
|
+
|
|
178
|
+
async def _producer() -> None:
|
|
179
|
+
try:
|
|
180
|
+
async with project_client_context() as client:
|
|
181
|
+
async for event in client.stream_deployment_logs(
|
|
182
|
+
self.deployment_id,
|
|
183
|
+
include_init_containers=True,
|
|
184
|
+
tail_lines=10000,
|
|
185
|
+
):
|
|
186
|
+
if self._stop_stream.is_set():
|
|
187
|
+
break
|
|
188
|
+
try:
|
|
189
|
+
await queue.put(event)
|
|
190
|
+
except Exception:
|
|
191
|
+
# If queue put fails due to cancellation/shutdown, stop
|
|
192
|
+
break
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Surface error via error message and rely on reconnect loop
|
|
195
|
+
if not self._stop_stream.is_set():
|
|
196
|
+
self._set_error_message(
|
|
197
|
+
f"Log stream failed: {e}. Reconnecting..."
|
|
198
|
+
)
|
|
199
|
+
finally:
|
|
200
|
+
producer_done.set()
|
|
201
|
+
|
|
202
|
+
async def _consumer() -> None:
|
|
203
|
+
batch: list[LogEvent] = []
|
|
204
|
+
next_deadline = time.monotonic() + batch_max_latency_seconds
|
|
205
|
+
while not self._stop_stream.is_set():
|
|
206
|
+
# Stop once producer finished and queue drained
|
|
207
|
+
if producer_done.is_set() and queue.empty():
|
|
208
|
+
if batch:
|
|
209
|
+
self._handle_log_events(batch)
|
|
210
|
+
batch = []
|
|
211
|
+
break
|
|
212
|
+
timeout = max(0.0, next_deadline - time.monotonic())
|
|
184
213
|
try:
|
|
185
|
-
|
|
214
|
+
item = await asyncio.wait_for(queue.get(), timeout=timeout)
|
|
215
|
+
batch.append(item)
|
|
216
|
+
if len(batch) >= batch_max_items:
|
|
217
|
+
self._handle_log_events(batch)
|
|
218
|
+
batch = []
|
|
219
|
+
next_deadline = time.monotonic() + batch_max_latency_seconds
|
|
220
|
+
except asyncio.TimeoutError:
|
|
221
|
+
if batch:
|
|
222
|
+
self._handle_log_events(batch)
|
|
223
|
+
batch = []
|
|
224
|
+
next_deadline = time.monotonic() + batch_max_latency_seconds
|
|
186
225
|
except Exception:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# Stream connected; consume until end
|
|
191
|
-
for events in buffered_stream:
|
|
192
|
-
if self._stop_stream.is_set():
|
|
226
|
+
# On any unexpected error, flush and exit, reconnect will handle
|
|
227
|
+
if batch:
|
|
228
|
+
self._handle_log_events(batch)
|
|
193
229
|
break
|
|
194
|
-
# Marshal UI updates back to the main thread via the App
|
|
195
|
-
self.app.call_from_thread(self._handle_log_events, events)
|
|
196
|
-
if self._stop_stream.is_set():
|
|
197
|
-
break
|
|
198
|
-
# Stream ended without explicit error; attempt reconnect
|
|
199
|
-
self.app.call_from_thread(
|
|
200
|
-
self._set_error_message, "Log stream disconnected. Reconnecting..."
|
|
201
|
-
)
|
|
202
|
-
except Exception as e:
|
|
203
|
-
if self._stop_stream.is_set():
|
|
204
|
-
break
|
|
205
|
-
# Surface the error to the UI and attempt reconnect with backoff
|
|
206
|
-
self.app.call_from_thread(
|
|
207
|
-
self._set_error_message, f"Log stream failed: {e}. Reconnecting..."
|
|
208
|
-
)
|
|
209
230
|
|
|
210
|
-
|
|
231
|
+
producer_task = asyncio.create_task(_producer())
|
|
232
|
+
try:
|
|
233
|
+
await _consumer()
|
|
234
|
+
finally:
|
|
235
|
+
# Ensure producer is not left running
|
|
236
|
+
try:
|
|
237
|
+
producer_task.cancel()
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
if self._stop_stream.is_set():
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# If we reached here, the stream ended or failed; attempt reconnect with backoff
|
|
245
|
+
self._set_error_message("Log stream disconnected. Reconnecting...")
|
|
246
|
+
|
|
247
|
+
# Duration-aware backoff (smaller when the previous connection lived longer)
|
|
211
248
|
connection_lifetime = 0.0
|
|
212
249
|
try:
|
|
213
250
|
connection_lifetime = max(0.0, time.monotonic() - connect_started_at)
|
|
214
251
|
except Exception:
|
|
215
252
|
connection_lifetime = 0.0
|
|
216
253
|
|
|
217
|
-
# If the connection lived longer than the current backoff window,
|
|
218
|
-
# reset to base so the next reconnect is immediate.
|
|
219
254
|
if connection_lifetime >= backoff_seconds:
|
|
220
255
|
backoff_seconds = base_backoff_seconds
|
|
221
256
|
else:
|
|
@@ -223,7 +258,7 @@ class DeploymentMonitorWidget(Widget):
|
|
|
223
258
|
|
|
224
259
|
delay = max(0.0, backoff_seconds - connection_lifetime)
|
|
225
260
|
if delay > 0:
|
|
226
|
-
_sleep_with_cancel(delay)
|
|
261
|
+
await _sleep_with_cancel(delay)
|
|
227
262
|
|
|
228
263
|
def _reset_log_view_for_reconnect(self) -> None:
|
|
229
264
|
"""Clear UI and buffers so new stream replaces previous content."""
|
|
@@ -326,9 +361,6 @@ class DeploymentMonitorWidget(Widget):
|
|
|
326
361
|
def on_unmount(self) -> None:
|
|
327
362
|
# Attempt to stop the streaming loop
|
|
328
363
|
self._stop_stream.set()
|
|
329
|
-
if self.stream_closer is not None:
|
|
330
|
-
self.stream_closer()
|
|
331
|
-
self.stream_closer = None
|
|
332
364
|
|
|
333
365
|
# Reactive watchers to update widgets in place instead of recomposing
|
|
334
366
|
def watch_error_message(self, message: str) -> None:
|
|
@@ -383,12 +415,12 @@ class DeploymentMonitorWidget(Widget):
|
|
|
383
415
|
|
|
384
416
|
async def _poll_deployment_status(self) -> None:
|
|
385
417
|
"""Periodically refresh deployment status to reflect updates in the UI."""
|
|
386
|
-
client = get_client()
|
|
387
418
|
while not self._stop_stream.is_set():
|
|
388
419
|
try:
|
|
389
|
-
|
|
390
|
-
self.
|
|
391
|
-
|
|
420
|
+
async with project_client_context() as client:
|
|
421
|
+
self.deployment = await client.get_deployment(
|
|
422
|
+
self.deployment_id, include_events=True
|
|
423
|
+
)
|
|
392
424
|
# Clear any previous error on success
|
|
393
425
|
if self.error_message:
|
|
394
426
|
self.error_message = ""
|
|
@@ -434,42 +466,3 @@ def monitor_deployment_screen(deployment_id: str) -> None:
|
|
|
434
466
|
"""Launch the standalone deployment monitor screen."""
|
|
435
467
|
app = DeploymentMonitorApp(deployment_id)
|
|
436
468
|
app.run()
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def _buffer_log_lines(iter: Iterator[LogEvent]) -> Iterator[list[LogEvent]]:
|
|
440
|
-
"""Batch log events into small lists using a background reader.
|
|
441
|
-
|
|
442
|
-
This reduces UI churn while still reacting quickly. On shutdown we
|
|
443
|
-
absorb stream read errors that are expected when the connection is
|
|
444
|
-
closed from another thread.
|
|
445
|
-
"""
|
|
446
|
-
buffer: list[LogEvent] = []
|
|
447
|
-
bg_error: Exception | None = None
|
|
448
|
-
done = threading.Event()
|
|
449
|
-
|
|
450
|
-
def pump() -> None:
|
|
451
|
-
nonlocal bg_error
|
|
452
|
-
try:
|
|
453
|
-
for event in iter:
|
|
454
|
-
buffer.append(event)
|
|
455
|
-
except Exception as e:
|
|
456
|
-
bg_error = e
|
|
457
|
-
finally:
|
|
458
|
-
done.set()
|
|
459
|
-
|
|
460
|
-
t = threading.Thread(target=pump, daemon=True)
|
|
461
|
-
t.start()
|
|
462
|
-
try:
|
|
463
|
-
while not done.is_set():
|
|
464
|
-
if buffer:
|
|
465
|
-
# Yield a snapshot and clear in-place to avoid reallocating list
|
|
466
|
-
yield list(buffer)
|
|
467
|
-
buffer.clear()
|
|
468
|
-
time.sleep(0.5)
|
|
469
|
-
if bg_error is not None:
|
|
470
|
-
raise bg_error
|
|
471
|
-
finally:
|
|
472
|
-
try:
|
|
473
|
-
t.join(timeout=0.1)
|
|
474
|
-
except Exception:
|
|
475
|
-
pass
|
|
@@ -282,14 +282,14 @@ class GitValidationWidget(Widget):
|
|
|
282
282
|
self.error_message = ""
|
|
283
283
|
try:
|
|
284
284
|
client = get_client()
|
|
285
|
-
|
|
285
|
+
self.validation_response = await client.validate_repository(
|
|
286
286
|
repo_url=self.repo_url, deployment_id=self.deployment_id, pat=pat
|
|
287
287
|
)
|
|
288
|
-
self.validation_response = response
|
|
289
288
|
|
|
290
|
-
|
|
289
|
+
resp = self.validation_response
|
|
290
|
+
if resp and resp.accessible:
|
|
291
291
|
# Success - post result message with appropriate messaging
|
|
292
|
-
if
|
|
292
|
+
if resp.pat_is_obsolete:
|
|
293
293
|
# Show success message about PAT obsolescence before proceeding
|
|
294
294
|
self.current_state = "success"
|
|
295
295
|
self.error_message = "Repository accessible via GitHub App. Your Personal Access Token is now obsolete and will be removed."
|
|
@@ -308,22 +308,24 @@ class GitValidationWidget(Widget):
|
|
|
308
308
|
self.error_message = ""
|
|
309
309
|
try:
|
|
310
310
|
client = get_client()
|
|
311
|
-
|
|
311
|
+
self.validation_response = await client.validate_repository(
|
|
312
312
|
repo_url=self.repo_url, deployment_id=self.deployment_id
|
|
313
313
|
)
|
|
314
|
-
self.validation_response = response
|
|
315
314
|
|
|
316
|
-
|
|
315
|
+
resp = self.validation_response
|
|
316
|
+
if resp and resp.accessible:
|
|
317
317
|
# Success - post result message with appropriate messaging
|
|
318
318
|
self.current_state = "success"
|
|
319
319
|
self.post_message(
|
|
320
320
|
ValidationResultMessage(
|
|
321
|
-
self.repo_url, "" if
|
|
321
|
+
self.repo_url, "" if resp.pat_is_obsolete else None
|
|
322
322
|
)
|
|
323
323
|
)
|
|
324
324
|
else:
|
|
325
325
|
# Failed - stay in github_auth and show error
|
|
326
|
-
self.error_message =
|
|
326
|
+
self.error_message = (
|
|
327
|
+
f"Still not accessible: {resp.message if resp else ''}"
|
|
328
|
+
)
|
|
327
329
|
|
|
328
330
|
except Exception as e:
|
|
329
331
|
# Failed - stay in github_auth and show error
|
|
@@ -30,6 +30,10 @@ Container {
|
|
|
30
30
|
height: auto;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
.two-column-form-grid .full-width {
|
|
34
|
+
column-span: 2;
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
/* =============================================== */
|
|
34
38
|
/* FORM ELEMENTS */
|
|
35
39
|
/* =============================================== */
|
|
@@ -96,7 +100,7 @@ Input.disabled {
|
|
|
96
100
|
background: $error-muted;
|
|
97
101
|
border-left: heavy $error;
|
|
98
102
|
margin: 0 0 1 0;
|
|
99
|
-
padding: 0 0 0 1
|
|
103
|
+
padding: 0 0 0 1;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
.primary-message {
|
|
@@ -104,7 +108,7 @@ Input.disabled {
|
|
|
104
108
|
background: $primary-muted;
|
|
105
109
|
border-left: heavy $primary;
|
|
106
110
|
margin: 0 0 1 0;
|
|
107
|
-
padding: 0 0 0 1
|
|
111
|
+
padding: 0 0 0 1;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
.secondary-message {
|
|
@@ -112,15 +116,23 @@ Input.disabled {
|
|
|
112
116
|
background: $secondary-muted;
|
|
113
117
|
border-left: heavy $secondary;
|
|
114
118
|
margin: 0 0 1 0;
|
|
115
|
-
padding: 0 0 0 1
|
|
119
|
+
padding: 0 0 0 1;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
.success-message {
|
|
119
123
|
color: $text-success;
|
|
120
124
|
background: $success-muted;
|
|
121
125
|
border-left: heavy $success;
|
|
122
|
-
padding: 1;
|
|
123
|
-
margin:
|
|
126
|
+
padding: 0 0 0 1;
|
|
127
|
+
margin: 0 0 0 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.warning-message {
|
|
131
|
+
color: $text-warning;
|
|
132
|
+
background: $warning-muted;
|
|
133
|
+
border-left: heavy $warning;
|
|
134
|
+
padding: 0 0 0 1;
|
|
135
|
+
margin: 0 0 0 0;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
.hidden {
|
|
@@ -142,6 +154,10 @@ Input.disabled {
|
|
|
142
154
|
width: 1fr;
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
.align-right {
|
|
158
|
+
align: right middle;
|
|
159
|
+
}
|
|
160
|
+
|
|
145
161
|
/* =============================================== */
|
|
146
162
|
/* BUTTONS & ACTIONS */
|
|
147
163
|
/* =============================================== */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llamactl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a13
|
|
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.0a13,<0.4.0
|
|
9
|
+
Requires-Dist: llama-deploy-appserver>=0.3.0a13,<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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
llama_deploy/cli/__init__.py,sha256=06afbb37cf01a3880f0487c3ab83a44ce72153b6ef8c2d6c0ed4324f03a5ae8f,411
|
|
2
|
+
llama_deploy/cli/app.py,sha256=a909fcb06f6c2179986fefa5b561e597840aa14a0ee8c038b238d267c3e2a178,2345
|
|
3
|
+
llama_deploy/cli/client.py,sha256=383601a4b9278972b4d8350a277d517d9b22a22776c1c4d6cd3f07db53f8d871,2098
|
|
4
|
+
llama_deploy/cli/commands/aliased_group.py,sha256=bc41007c97b7b93981217dbd4d4591df2b6c9412a2d9ed045b0ec5655ed285f2,1066
|
|
5
|
+
llama_deploy/cli/commands/auth.py,sha256=145dfb175223ee0262677eed69806eb5e1b160574e9b480da6311ae80241141f,12806
|
|
6
|
+
llama_deploy/cli/commands/deployment.py,sha256=7464e09ad53f082667a1e7e3f6c0040d01fcb312cbc92cf182efc66260a55e78,10454
|
|
7
|
+
llama_deploy/cli/commands/init.py,sha256=51b2de1e35ff34bc15c9dfec72fbad08aaf528c334df168896d36458a4e9401c,6307
|
|
8
|
+
llama_deploy/cli/commands/serve.py,sha256=4d47850397ba172944df56a934a51bedb52403cbd3f9b000b1ced90a31c75049,2721
|
|
9
|
+
llama_deploy/cli/config.py,sha256=0864c6e25646c28062b1adc081039bd59cbd9d87ab7e4a6e465660c6231f0489,8951
|
|
10
|
+
llama_deploy/cli/debug.py,sha256=e85a72d473bbe1645eb31772f7349bde703d45704166f767385895c440afc762,496
|
|
11
|
+
llama_deploy/cli/env.py,sha256=6ebc24579815b3787829c81fd5bb9f31698a06e62c0128a788559f962b33a7af,1016
|
|
12
|
+
llama_deploy/cli/interactive_prompts/session_utils.py,sha256=b996f2eddf70d6c49636c4797d246d212fce0950fe7e9a3f59cf6a1bf7ae26f5,1142
|
|
13
|
+
llama_deploy/cli/interactive_prompts/utils.py,sha256=2b29148f2cf2be8d1642fd800f529602cecb03997259790a3b8e1b879604cee2,1736
|
|
14
|
+
llama_deploy/cli/options.py,sha256=9eb6f1c5d89f83cbf5b288d3a85c38acd929168d6afdf4b04d664df419aa1816,1543
|
|
15
|
+
llama_deploy/cli/platform_client.py,sha256=69de23dc79a8f5922afc9e3bac1b633a531340ebbefeb7838e3a88419faa754c,1451
|
|
16
|
+
llama_deploy/cli/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
|
17
|
+
llama_deploy/cli/textual/api_key_profile_form.py,sha256=cf059426d758136318c3b6f340e2741e79ab1ec613b8741ef3d2a402214ea2c4,22253
|
|
18
|
+
llama_deploy/cli/textual/deployment_form.py,sha256=1cf186b765d10a1bdd7394f22ddd7598d75dba8c9a8a7a8be74e6151da2f31dc,20941
|
|
19
|
+
llama_deploy/cli/textual/deployment_help.py,sha256=d43e9ff29db71a842cf8b491545763d581ede3132b8af518c73af85a40950046,2464
|
|
20
|
+
llama_deploy/cli/textual/deployment_monitor.py,sha256=7bcf3f0213401c2432fdb5a9d9acf468a4afe83b2d86d7f2852319768e6f2534,17231
|
|
21
|
+
llama_deploy/cli/textual/git_validation.py,sha256=94c95b61d0cbc490566a406b4886c9c12e1d1793dc14038a5be37119223c9568,13419
|
|
22
|
+
llama_deploy/cli/textual/github_callback_server.py,sha256=dc74c510f8a98ef6ffaab0f6d11c7ea86ee77ca5adbc7725a2a29112bae24191,7556
|
|
23
|
+
llama_deploy/cli/textual/llama_loader.py,sha256=33cb32a46dd40bcf889c553e44f2672c410e26bd1d4b17aa6cca6d0a5d59c2c4,1468
|
|
24
|
+
llama_deploy/cli/textual/secrets_form.py,sha256=a43fbd81aad034d0d60906bfd917c107f9ace414648b0f63ac0b29eeba4050db,7061
|
|
25
|
+
llama_deploy/cli/textual/styles.tcss,sha256=b1a54dc5fb0e0aa12cbf48807e9e6a94b9926838b8058dae1336a134f02e92b0,3327
|
|
26
|
+
llamactl-0.3.0a13.dist-info/WHEEL,sha256=66530aef82d5020ef5af27ae0123c71abb9261377c5bc519376c671346b12918,79
|
|
27
|
+
llamactl-0.3.0a13.dist-info/entry_points.txt,sha256=b67e1eb64305058751a651a80f2d2268b5f7046732268421e796f64d4697f83c,52
|
|
28
|
+
llamactl-0.3.0a13.dist-info/METADATA,sha256=87e30b0475ce6d5eaf7d400e4c5c00d41d2cabb0ae54abc02ab686659c1b1e15,3177
|
|
29
|
+
llamactl-0.3.0a13.dist-info/RECORD,,
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
from llama_deploy.cli.client import get_control_plane_client
|
|
3
|
-
from rich import print as rprint
|
|
4
|
-
from rich.table import Table
|
|
5
|
-
|
|
6
|
-
from ..app import app, console
|
|
7
|
-
from ..config import config_manager
|
|
8
|
-
from ..interactive_prompts.utils import (
|
|
9
|
-
select_profile,
|
|
10
|
-
)
|
|
11
|
-
from ..options import global_options
|
|
12
|
-
from ..textual.profile_form import create_profile_form, edit_profile_form
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Create sub-applications for organizing commands
|
|
16
|
-
@app.group(
|
|
17
|
-
help="Login to manage deployments and switch between projects",
|
|
18
|
-
no_args_is_help=True,
|
|
19
|
-
)
|
|
20
|
-
@global_options
|
|
21
|
-
def profiles() -> None:
|
|
22
|
-
"""Manage profiles"""
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Profile commands
|
|
27
|
-
@profiles.command("create")
|
|
28
|
-
@global_options
|
|
29
|
-
@click.option("--name", help="Profile name")
|
|
30
|
-
@click.option("--api-url", help="API server URL")
|
|
31
|
-
@click.option("--project-id", help="Default project ID")
|
|
32
|
-
def create_profile(
|
|
33
|
-
name: str | None, api_url: str | None, project_id: str | None
|
|
34
|
-
) -> None:
|
|
35
|
-
"""Create a new profile"""
|
|
36
|
-
try:
|
|
37
|
-
# If all required args are provided via CLI, skip interactive mode
|
|
38
|
-
if name and api_url:
|
|
39
|
-
# Use CLI args directly
|
|
40
|
-
profile = config_manager.create_profile(name, api_url, project_id)
|
|
41
|
-
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
42
|
-
|
|
43
|
-
# Automatically switch to the new profile
|
|
44
|
-
config_manager.set_current_profile(name)
|
|
45
|
-
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
# Use interactive creation
|
|
49
|
-
profile = create_profile_form()
|
|
50
|
-
if profile is None:
|
|
51
|
-
rprint("[yellow]Cancelled[/yellow]")
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
rprint(f"[green]Created profile '{profile.name}'[/green]")
|
|
56
|
-
|
|
57
|
-
# Automatically switch to the new profile
|
|
58
|
-
config_manager.set_current_profile(profile.name)
|
|
59
|
-
rprint(f"[green]Switched to profile '{profile.name}'[/green]")
|
|
60
|
-
except Exception as e:
|
|
61
|
-
rprint(f"[red]Error creating profile: {e}[/red]")
|
|
62
|
-
raise click.Abort()
|
|
63
|
-
|
|
64
|
-
except ValueError as e:
|
|
65
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
66
|
-
raise click.Abort()
|
|
67
|
-
except Exception as e:
|
|
68
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
69
|
-
raise click.Abort()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@profiles.command("list")
|
|
73
|
-
@global_options
|
|
74
|
-
def list_profiles() -> None:
|
|
75
|
-
"""List all profiles"""
|
|
76
|
-
try:
|
|
77
|
-
profiles = config_manager.list_profiles()
|
|
78
|
-
current_name = config_manager.get_current_profile_name()
|
|
79
|
-
|
|
80
|
-
if not profiles:
|
|
81
|
-
rprint("[yellow]No profiles found[/yellow]")
|
|
82
|
-
rprint("Create one with: [cyan]llamactl profile create[/cyan]")
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
table = Table(title="Profiles")
|
|
86
|
-
table.add_column("Name", style="cyan")
|
|
87
|
-
table.add_column("API URL", style="green")
|
|
88
|
-
table.add_column("Active Project", style="yellow")
|
|
89
|
-
table.add_column("Current", style="magenta")
|
|
90
|
-
|
|
91
|
-
for profile in profiles:
|
|
92
|
-
is_current = "✓" if profile.name == current_name else ""
|
|
93
|
-
active_project = profile.active_project_id or "-"
|
|
94
|
-
table.add_row(profile.name, profile.api_url, active_project, is_current)
|
|
95
|
-
|
|
96
|
-
console.print(table)
|
|
97
|
-
|
|
98
|
-
except Exception as e:
|
|
99
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
100
|
-
raise click.Abort()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@profiles.command("switch")
|
|
104
|
-
@global_options
|
|
105
|
-
@click.argument("name", required=False)
|
|
106
|
-
def switch_profile(name: str | None) -> None:
|
|
107
|
-
"""Switch to a different profile"""
|
|
108
|
-
try:
|
|
109
|
-
name = select_profile(name)
|
|
110
|
-
if not name:
|
|
111
|
-
rprint("[yellow]No profile selected[/yellow]")
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
profile = config_manager.get_profile(name)
|
|
115
|
-
if not profile:
|
|
116
|
-
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
117
|
-
raise click.Abort()
|
|
118
|
-
|
|
119
|
-
config_manager.set_current_profile(name)
|
|
120
|
-
rprint(f"[green]Switched to profile '{name}'[/green]")
|
|
121
|
-
|
|
122
|
-
except Exception as e:
|
|
123
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
124
|
-
raise click.Abort()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@profiles.command("delete")
|
|
128
|
-
@global_options
|
|
129
|
-
@click.argument("name", required=False)
|
|
130
|
-
def delete_profile(name: str | None) -> None:
|
|
131
|
-
"""Delete a profile"""
|
|
132
|
-
try:
|
|
133
|
-
name = select_profile(name)
|
|
134
|
-
if not name:
|
|
135
|
-
rprint("[yellow]No profile selected[/yellow]")
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
profile = config_manager.get_profile(name)
|
|
139
|
-
if not profile:
|
|
140
|
-
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
141
|
-
raise click.Abort()
|
|
142
|
-
|
|
143
|
-
if config_manager.delete_profile(name):
|
|
144
|
-
rprint(f"[green]Deleted profile '{name}'[/green]")
|
|
145
|
-
else:
|
|
146
|
-
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
147
|
-
|
|
148
|
-
except Exception as e:
|
|
149
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
150
|
-
raise click.Abort()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@profiles.command("edit")
|
|
154
|
-
@global_options
|
|
155
|
-
@click.argument("name", required=False)
|
|
156
|
-
def edit_profile(name: str | None) -> None:
|
|
157
|
-
"""Edit a profile"""
|
|
158
|
-
try:
|
|
159
|
-
name = select_profile(name)
|
|
160
|
-
if not name:
|
|
161
|
-
rprint("[yellow]No profile selected[/yellow]")
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
# Get current profile
|
|
165
|
-
maybe_profile = config_manager.get_profile(name)
|
|
166
|
-
if not maybe_profile:
|
|
167
|
-
rprint(f"[red]Profile '{name}' not found[/red]")
|
|
168
|
-
raise click.Abort()
|
|
169
|
-
profile = maybe_profile
|
|
170
|
-
|
|
171
|
-
# Use the interactive edit menu
|
|
172
|
-
updated = edit_profile_form(profile)
|
|
173
|
-
if updated is None:
|
|
174
|
-
rprint("[yellow]Cancelled[/yellow]")
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
current_profile = config_manager.get_current_profile()
|
|
179
|
-
if not current_profile or current_profile.name != updated.name:
|
|
180
|
-
config_manager.set_current_profile(updated.name)
|
|
181
|
-
rprint(f"[green]Updated profile '{profile.name}'[/green]")
|
|
182
|
-
except Exception as e:
|
|
183
|
-
rprint(f"[red]Error updating profile: {e}[/red]")
|
|
184
|
-
raise click.Abort()
|
|
185
|
-
|
|
186
|
-
except Exception as e:
|
|
187
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
188
|
-
raise click.Abort()
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Projects commands
|
|
192
|
-
@profiles.command("list-projects")
|
|
193
|
-
@global_options
|
|
194
|
-
def list_projects() -> None:
|
|
195
|
-
"""List all projects with deployment counts"""
|
|
196
|
-
try:
|
|
197
|
-
client = get_control_plane_client()
|
|
198
|
-
projects = client.list_projects()
|
|
199
|
-
|
|
200
|
-
if not projects:
|
|
201
|
-
rprint("[yellow]No projects found[/yellow]")
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
table = Table(title="Projects")
|
|
205
|
-
table.add_column("Project ID", style="cyan")
|
|
206
|
-
table.add_column("Deployments", style="green")
|
|
207
|
-
|
|
208
|
-
for project in projects:
|
|
209
|
-
project_id = project.project_id
|
|
210
|
-
deployment_count = project.deployment_count
|
|
211
|
-
table.add_row(project_id, str(deployment_count))
|
|
212
|
-
|
|
213
|
-
console.print(table)
|
|
214
|
-
|
|
215
|
-
except Exception as e:
|
|
216
|
-
rprint(f"[red]Error: {e}[/red]")
|
|
217
|
-
raise click.Abort()
|