llamactl 0.3.0a13__py3-none-any.whl → 0.3.0a15__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 -1
- llama_deploy/cli/app.py +4 -7
- llama_deploy/cli/client.py +18 -32
- llama_deploy/cli/commands/auth.py +230 -235
- llama_deploy/cli/commands/deployment.py +24 -36
- llama_deploy/cli/commands/env.py +206 -0
- llama_deploy/cli/config/_config.py +385 -0
- llama_deploy/cli/config/auth_service.py +68 -0
- llama_deploy/cli/config/env_service.py +64 -0
- llama_deploy/cli/config/schema.py +31 -0
- llama_deploy/cli/interactive_prompts/utils.py +0 -39
- llama_deploy/cli/options.py +0 -9
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a15.dist-info}/METADATA +3 -3
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a15.dist-info}/RECORD +16 -13
- llama_deploy/cli/config.py +0 -241
- llama_deploy/cli/textual/api_key_profile_form.py +0 -563
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a15.dist-info}/WHEEL +0 -0
- {llamactl-0.3.0a13.dist-info → llamactl-0.3.0a15.dist-info}/entry_points.txt +0 -0
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
"""Textual-based forms for CLI interactions"""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from enum import Enum
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import List
|
|
9
|
-
|
|
10
|
-
from llama_deploy.core.client.manage_client import ClientError, ControlPlaneClient
|
|
11
|
-
from llama_deploy.core.schema.projects import ProjectSummary
|
|
12
|
-
from textual import events, work
|
|
13
|
-
from textual.app import App, ComposeResult
|
|
14
|
-
from textual.containers import Container, HorizontalGroup, Widget
|
|
15
|
-
from textual.content import Content
|
|
16
|
-
from textual.reactive import reactive
|
|
17
|
-
from textual.validation import Length
|
|
18
|
-
from textual.widgets import Button, Input, Label, Select, Static
|
|
19
|
-
|
|
20
|
-
from ..config import Profile, config_manager
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class ValidationState(Enum):
|
|
26
|
-
"""States for API validation"""
|
|
27
|
-
|
|
28
|
-
IDLE = "idle"
|
|
29
|
-
VALIDATING = "validating"
|
|
30
|
-
VALID = "valid"
|
|
31
|
-
NETWORK_ERROR = "network_error"
|
|
32
|
-
AUTH_REQUIRED = "auth_required"
|
|
33
|
-
AUTH_INVALID = "auth_invalid"
|
|
34
|
-
ERROR = "error"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclass
|
|
38
|
-
class APIKeyProfileForm:
|
|
39
|
-
"""Form data for profile editing/creation"""
|
|
40
|
-
|
|
41
|
-
name: str = ""
|
|
42
|
-
api_url: str = ""
|
|
43
|
-
project_id: str = ""
|
|
44
|
-
api_key_auth_token: str = ""
|
|
45
|
-
existing_name: str | None = None
|
|
46
|
-
|
|
47
|
-
@classmethod
|
|
48
|
-
def from_profile(cls, profile: Profile) -> "APIKeyProfileForm":
|
|
49
|
-
"""Create form from existing profile"""
|
|
50
|
-
return cls(
|
|
51
|
-
name=profile.name,
|
|
52
|
-
api_url=profile.api_url,
|
|
53
|
-
project_id=profile.project_id,
|
|
54
|
-
api_key_auth_token=profile.api_key_auth_token or "",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
def to_profile(self) -> Profile:
|
|
58
|
-
"""Create profile from form data"""
|
|
59
|
-
return Profile(
|
|
60
|
-
name=self.name,
|
|
61
|
-
api_url=self.api_url,
|
|
62
|
-
project_id=self.project_id,
|
|
63
|
-
api_key_auth_token=self.api_key_auth_token,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def validate_api_connection(
|
|
68
|
-
api_url: str, api_key: str | None = None
|
|
69
|
-
) -> tuple[ValidationState, List[ProjectSummary], str]:
|
|
70
|
-
"""Validate API connection and return projects if successful"""
|
|
71
|
-
try:
|
|
72
|
-
# Create ControlPlaneClient with optional auth
|
|
73
|
-
client = ControlPlaneClient(api_url, api_key)
|
|
74
|
-
|
|
75
|
-
# Try to list projects (async client)
|
|
76
|
-
projects = asyncio.run(client.list_projects())
|
|
77
|
-
|
|
78
|
-
return ValidationState.VALID, projects, "Connected successfully"
|
|
79
|
-
|
|
80
|
-
except ClientError as e:
|
|
81
|
-
if e.status_code == 401:
|
|
82
|
-
return ValidationState.AUTH_INVALID, [], "API key is not valid"
|
|
83
|
-
elif e.status_code == 403:
|
|
84
|
-
return ValidationState.AUTH_REQUIRED, [], "API requires an API key"
|
|
85
|
-
elif e.status_code and 400 <= e.status_code < 500:
|
|
86
|
-
return ValidationState.NETWORK_ERROR, [], "Invalid API URL"
|
|
87
|
-
else:
|
|
88
|
-
return ValidationState.ERROR, [], f"Server error: {str(e)}"
|
|
89
|
-
except Exception as e:
|
|
90
|
-
error_msg = str(e).lower()
|
|
91
|
-
if (
|
|
92
|
-
"connection" in error_msg
|
|
93
|
-
or "timeout" in error_msg
|
|
94
|
-
or "resolve" in error_msg
|
|
95
|
-
):
|
|
96
|
-
return ValidationState.NETWORK_ERROR, [], "Cannot connect to API URL"
|
|
97
|
-
else:
|
|
98
|
-
return ValidationState.ERROR, [], f"Connection error: {str(e)}"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class APIKeyProfileEditApp(App[APIKeyProfileForm | None]):
|
|
102
|
-
"""Textual app for editing profiles"""
|
|
103
|
-
|
|
104
|
-
CSS_PATH = Path(__file__).parent / "styles.tcss"
|
|
105
|
-
|
|
106
|
-
name: reactive[str] = reactive("")
|
|
107
|
-
api_url: reactive[str] = reactive("")
|
|
108
|
-
project_id: reactive[str] = reactive("")
|
|
109
|
-
api_key_auth_token: reactive[str] = reactive("")
|
|
110
|
-
|
|
111
|
-
validation_state: reactive[ValidationState] = reactive(ValidationState.IDLE)
|
|
112
|
-
validation_message: reactive[str] = reactive("")
|
|
113
|
-
|
|
114
|
-
# Structural toggles → recompose when these change
|
|
115
|
-
available_projects: reactive[List[ProjectSummary]] = reactive([], recompose=True)
|
|
116
|
-
manual_project_mode: reactive[bool] = reactive(False, recompose=True)
|
|
117
|
-
api_key_required: reactive[bool] = reactive(False, recompose=True)
|
|
118
|
-
|
|
119
|
-
# Error banner content
|
|
120
|
-
form_error_message: reactive[str] = reactive("", recompose=True)
|
|
121
|
-
|
|
122
|
-
def __init__(
|
|
123
|
-
self, initial_data: APIKeyProfileForm, prompt_message: str | None = None
|
|
124
|
-
):
|
|
125
|
-
super().__init__()
|
|
126
|
-
self.form_data = initial_data
|
|
127
|
-
self.prompt_message = prompt_message
|
|
128
|
-
# Track last validated values to prevent redundant validation
|
|
129
|
-
self._last_validated_api_url = ""
|
|
130
|
-
self._last_validated_api_key = ""
|
|
131
|
-
# Initialize fields from provided data
|
|
132
|
-
self.name = self.form_data.name
|
|
133
|
-
self.api_url = self.form_data.api_url
|
|
134
|
-
self.project_id = self.form_data.project_id
|
|
135
|
-
self.api_key_auth_token = self.form_data.api_key_auth_token or ""
|
|
136
|
-
|
|
137
|
-
def on_mount(self) -> None:
|
|
138
|
-
self.theme = "tokyo-night"
|
|
139
|
-
# Initialize tracked values from form data to prevent unnecessary initial validation
|
|
140
|
-
# Force initial validation by resetting last validated sentinels
|
|
141
|
-
self._last_validated_api_url = ""
|
|
142
|
-
self._last_validated_api_key = ""
|
|
143
|
-
|
|
144
|
-
# Trigger initial validation if we have API URL
|
|
145
|
-
if self.api_url:
|
|
146
|
-
self._trigger_validation()
|
|
147
|
-
|
|
148
|
-
# ----- Reactive watchers (targeted updates to avoid focus loss) -----
|
|
149
|
-
def watch_validation_message(self, old: str, new: str) -> None:
|
|
150
|
-
try:
|
|
151
|
-
message_widget = self.query_one("#validation-message", Static)
|
|
152
|
-
message_widget.update(new)
|
|
153
|
-
# Toggle visibility
|
|
154
|
-
message_widget.display = bool(new)
|
|
155
|
-
# Update style based on state
|
|
156
|
-
css_class = (
|
|
157
|
-
"success-message"
|
|
158
|
-
if self.validation_state == ValidationState.VALID
|
|
159
|
-
else "warning-message"
|
|
160
|
-
)
|
|
161
|
-
message_widget.set_classes(f"{css_class} full-width")
|
|
162
|
-
except Exception:
|
|
163
|
-
pass
|
|
164
|
-
|
|
165
|
-
def watch_validation_state(
|
|
166
|
-
self, old: ValidationState, new: ValidationState
|
|
167
|
-
) -> None:
|
|
168
|
-
# Re-use same updater for message to refresh styling
|
|
169
|
-
self.watch_validation_message(self.validation_message, self.validation_message)
|
|
170
|
-
|
|
171
|
-
def on_key(self, event: events.Key) -> None:
|
|
172
|
-
"""Handle key events, including Ctrl+C"""
|
|
173
|
-
if event.key == "ctrl+c":
|
|
174
|
-
self.exit(None)
|
|
175
|
-
|
|
176
|
-
@work(exclusive=True, thread=True)
|
|
177
|
-
async def _validate_api_worker(self) -> None:
|
|
178
|
-
"""Worker to validate API connection"""
|
|
179
|
-
# Use the latest state rather than querying widgets (avoids race conditions)
|
|
180
|
-
api_url = self.api_url.strip()
|
|
181
|
-
api_key_value = self.api_key_auth_token.strip()
|
|
182
|
-
api_key = api_key_value or None
|
|
183
|
-
|
|
184
|
-
# Check if values have actually changed since last validation
|
|
185
|
-
if (
|
|
186
|
-
api_url == self._last_validated_api_url
|
|
187
|
-
and api_key == self._last_validated_api_key
|
|
188
|
-
):
|
|
189
|
-
logger.debug(
|
|
190
|
-
f"Skipping validation - no changes detected (URL: {api_url}, Key: {'***' if api_key else 'None'})"
|
|
191
|
-
)
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
if not api_url:
|
|
195
|
-
self.validation_state = ValidationState.IDLE
|
|
196
|
-
self.validation_message = ""
|
|
197
|
-
self.available_projects = []
|
|
198
|
-
self.api_key_required = False
|
|
199
|
-
# Reset tracked values when empty
|
|
200
|
-
self._last_validated_api_url = ""
|
|
201
|
-
self._last_validated_api_key = ""
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
# Update tracked values before validation
|
|
205
|
-
self._last_validated_api_url = api_url
|
|
206
|
-
self._last_validated_api_key = api_key
|
|
207
|
-
|
|
208
|
-
self.validation_state = ValidationState.VALIDATING
|
|
209
|
-
self.validation_message = "Validating connection..."
|
|
210
|
-
logger.debug(
|
|
211
|
-
f"Validating connection to {api_url} with API key {'***' if api_key else 'None'}"
|
|
212
|
-
)
|
|
213
|
-
state, projects, message = validate_api_connection(api_url, api_key)
|
|
214
|
-
logger.debug(
|
|
215
|
-
f"Validation result: {state}, {len(projects)} projects, '{message}'"
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
# Commit validation results
|
|
219
|
-
self.validation_state = state
|
|
220
|
-
self.validation_message = message
|
|
221
|
-
self.available_projects = projects
|
|
222
|
-
self.api_key_required = state == ValidationState.AUTH_REQUIRED
|
|
223
|
-
|
|
224
|
-
# If we got projects but user is in manual mode and project is empty, suggest switching
|
|
225
|
-
if state == ValidationState.VALID and projects and self.manual_project_mode:
|
|
226
|
-
try:
|
|
227
|
-
project_input = self.query_one("#project_id", Input)
|
|
228
|
-
if not project_input.value.strip():
|
|
229
|
-
self.validation_message = f"Connected successfully. Found {len(projects)} projects - consider using project selector."
|
|
230
|
-
except Exception:
|
|
231
|
-
# Project input might not exist in selector mode
|
|
232
|
-
pass
|
|
233
|
-
|
|
234
|
-
def _trigger_validation(self) -> None:
|
|
235
|
-
"""Trigger validation worker"""
|
|
236
|
-
self._validate_api_worker()
|
|
237
|
-
|
|
238
|
-
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
239
|
-
"""Handle input enter/tab to trigger validation"""
|
|
240
|
-
if event.input.id == "api_url":
|
|
241
|
-
self.api_url = event.input.value.strip()
|
|
242
|
-
self._trigger_validation()
|
|
243
|
-
elif event.input.id == "api_key_auth_token":
|
|
244
|
-
self.api_key_auth_token = event.input.value.strip()
|
|
245
|
-
self._trigger_validation()
|
|
246
|
-
|
|
247
|
-
def on_input_changed(self, event: Input.Changed) -> None:
|
|
248
|
-
"""Handle input changes with debouncing to avoid excessive validation"""
|
|
249
|
-
if event.input.id in ("api_url", "api_key_auth_token", "name", "project_id"):
|
|
250
|
-
# Cancel any existing validation timer
|
|
251
|
-
if hasattr(self, "_validation_timer") and self._validation_timer:
|
|
252
|
-
self._validation_timer.stop()
|
|
253
|
-
|
|
254
|
-
# Update state from input immediately
|
|
255
|
-
if event.input.id == "api_url":
|
|
256
|
-
self.api_url = event.value.strip()
|
|
257
|
-
elif event.input.id == "api_key_auth_token":
|
|
258
|
-
self.api_key_auth_token = event.value.strip()
|
|
259
|
-
elif event.input.id == "name":
|
|
260
|
-
self.name = event.value.strip()
|
|
261
|
-
elif event.input.id == "project_id":
|
|
262
|
-
self.project_id = event.value.strip()
|
|
263
|
-
|
|
264
|
-
# Set a new timer to validate after 1 second of no typing (only for api fields)
|
|
265
|
-
if event.input.id in ("api_url", "api_key_auth_token"):
|
|
266
|
-
self._validation_timer = self.set_timer(1.0, self._trigger_validation)
|
|
267
|
-
|
|
268
|
-
def compose(self) -> ComposeResult:
|
|
269
|
-
with Container(classes="form-container"):
|
|
270
|
-
title = (
|
|
271
|
-
"Edit Profile"
|
|
272
|
-
if self.form_data.existing_name
|
|
273
|
-
else "Create API Key Profile"
|
|
274
|
-
)
|
|
275
|
-
yield Static(title, classes="primary-message")
|
|
276
|
-
yield Static(
|
|
277
|
-
Content.from_markup(
|
|
278
|
-
"Configure a new API key profile to authenticate with the LlamaCloud control plane. This is stored locally in your OS's config directory."
|
|
279
|
-
),
|
|
280
|
-
classes="info-message mb-1",
|
|
281
|
-
)
|
|
282
|
-
yield Static(
|
|
283
|
-
self.form_error_message,
|
|
284
|
-
id="error-message",
|
|
285
|
-
classes=f"error-message {'hidden visible' if self.form_error_message else 'hidden'}",
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
# Validation status message (always present but hidden when empty)
|
|
289
|
-
css_class = (
|
|
290
|
-
"success-message"
|
|
291
|
-
if self.validation_state == ValidationState.VALID
|
|
292
|
-
else "warning-message"
|
|
293
|
-
)
|
|
294
|
-
yield Static(
|
|
295
|
-
self.validation_message,
|
|
296
|
-
id="validation-message",
|
|
297
|
-
classes=f"{css_class} full-width {'hidden' if not self.validation_message else ''}",
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
with Static(classes="two-column-form-grid mb-1"):
|
|
301
|
-
yield Label(
|
|
302
|
-
Content.from_markup("Profile Name[red]*[/]"),
|
|
303
|
-
classes="required form-label",
|
|
304
|
-
shrink=True,
|
|
305
|
-
)
|
|
306
|
-
yield Input(
|
|
307
|
-
value=self.name,
|
|
308
|
-
placeholder="A memorable name for this API key",
|
|
309
|
-
validators=[Length(minimum=1)],
|
|
310
|
-
id="name",
|
|
311
|
-
compact=True,
|
|
312
|
-
)
|
|
313
|
-
yield Label(
|
|
314
|
-
Content.from_markup("API URL[red]*[/]"),
|
|
315
|
-
classes="required form-label",
|
|
316
|
-
shrink=True,
|
|
317
|
-
)
|
|
318
|
-
yield Input(
|
|
319
|
-
value=self.api_url,
|
|
320
|
-
placeholder="https://api.cloud.llamaindex.ai",
|
|
321
|
-
validators=[Length(minimum=1)],
|
|
322
|
-
id="api_url",
|
|
323
|
-
compact=True,
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
# API Key field - make required if auth is required
|
|
327
|
-
api_key_label = (
|
|
328
|
-
"API Key[red]*[/]" if self.api_key_required else "API Key"
|
|
329
|
-
)
|
|
330
|
-
yield Label(
|
|
331
|
-
Content.from_markup(api_key_label),
|
|
332
|
-
id="api-key-label",
|
|
333
|
-
classes="form-label",
|
|
334
|
-
shrink=True,
|
|
335
|
-
)
|
|
336
|
-
yield Input(
|
|
337
|
-
value=self.api_key_auth_token,
|
|
338
|
-
placeholder="API key auth token. Only required if control plane is authenticated",
|
|
339
|
-
id="api_key_auth_token",
|
|
340
|
-
validators=[Length(minimum=1)] if self.api_key_required else [],
|
|
341
|
-
compact=True,
|
|
342
|
-
)
|
|
343
|
-
# Project selection area
|
|
344
|
-
yield Label(
|
|
345
|
-
Content.from_markup("Project ID[red]*[/]"),
|
|
346
|
-
classes="required form-label",
|
|
347
|
-
shrink=True,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
# Project input area with toggle
|
|
351
|
-
with Widget(id="project-input-area"):
|
|
352
|
-
if not self.manual_project_mode and self.available_projects:
|
|
353
|
-
# Show project selector
|
|
354
|
-
options = [
|
|
355
|
-
(
|
|
356
|
-
f"{p.project_name} ({p.deployment_count} deployments)",
|
|
357
|
-
p.project_id,
|
|
358
|
-
)
|
|
359
|
-
for p in self.available_projects
|
|
360
|
-
]
|
|
361
|
-
# Find a valid initial value
|
|
362
|
-
project_ids = [p.project_id for p in self.available_projects]
|
|
363
|
-
initial_value = (
|
|
364
|
-
self.project_id
|
|
365
|
-
if self.project_id in project_ids
|
|
366
|
-
else project_ids[0]
|
|
367
|
-
if project_ids
|
|
368
|
-
else None
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
if initial_value is not None:
|
|
372
|
-
yield Select(
|
|
373
|
-
options=options,
|
|
374
|
-
value=initial_value,
|
|
375
|
-
id="project_select",
|
|
376
|
-
allow_blank=False,
|
|
377
|
-
compact=True,
|
|
378
|
-
)
|
|
379
|
-
else:
|
|
380
|
-
# Fallback to manual input if no projects available
|
|
381
|
-
yield Input(
|
|
382
|
-
value=self.project_id,
|
|
383
|
-
placeholder="Enter project ID manually",
|
|
384
|
-
validators=[Length(minimum=1)],
|
|
385
|
-
id="project_id",
|
|
386
|
-
compact=True,
|
|
387
|
-
)
|
|
388
|
-
else:
|
|
389
|
-
# Show manual input
|
|
390
|
-
yield Input(
|
|
391
|
-
value=self.project_id,
|
|
392
|
-
placeholder="Enter project ID manually",
|
|
393
|
-
validators=[Length(minimum=1)],
|
|
394
|
-
id="project_id",
|
|
395
|
-
compact=True,
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
yield Static()
|
|
399
|
-
# Mode toggle button
|
|
400
|
-
if self.available_projects:
|
|
401
|
-
toggle_text = (
|
|
402
|
-
"Enter Project ID Manually"
|
|
403
|
-
if not self.manual_project_mode
|
|
404
|
-
else "Select Existing Project"
|
|
405
|
-
)
|
|
406
|
-
yield Button(
|
|
407
|
-
toggle_text,
|
|
408
|
-
id="toggle_project_mode",
|
|
409
|
-
classes="align-right",
|
|
410
|
-
variant="default",
|
|
411
|
-
compact=True,
|
|
412
|
-
)
|
|
413
|
-
else:
|
|
414
|
-
yield Static()
|
|
415
|
-
|
|
416
|
-
with HorizontalGroup(classes="button-row"):
|
|
417
|
-
yield Button("Save", variant="primary", id="save", compact=True)
|
|
418
|
-
yield Button("Cancel", variant="default", id="cancel", compact=True)
|
|
419
|
-
|
|
420
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
421
|
-
if event.button.id == "save":
|
|
422
|
-
if self._validate_form():
|
|
423
|
-
result = self._get_form_data()
|
|
424
|
-
try:
|
|
425
|
-
if result.existing_name:
|
|
426
|
-
config_manager.delete_profile(result.existing_name)
|
|
427
|
-
profile = config_manager.create_profile(
|
|
428
|
-
result.name,
|
|
429
|
-
result.api_url,
|
|
430
|
-
result.project_id,
|
|
431
|
-
result.api_key_auth_token,
|
|
432
|
-
)
|
|
433
|
-
self.exit(profile)
|
|
434
|
-
except Exception as e:
|
|
435
|
-
self._handle_error(e)
|
|
436
|
-
|
|
437
|
-
elif event.button.id == "cancel":
|
|
438
|
-
self.exit(None)
|
|
439
|
-
|
|
440
|
-
elif event.button.id == "toggle_project_mode":
|
|
441
|
-
# Keep current project ID when switching modes
|
|
442
|
-
current_project_id = self._get_current_project_id()
|
|
443
|
-
self.manual_project_mode = not self.manual_project_mode
|
|
444
|
-
self.project_id = current_project_id
|
|
445
|
-
|
|
446
|
-
def on_select_changed(self, event: Select.Changed) -> None:
|
|
447
|
-
"""Handle project selection changes"""
|
|
448
|
-
if event.select.id == "project_select" and event.value:
|
|
449
|
-
# Update state when project is selected
|
|
450
|
-
self.project_id = event.value
|
|
451
|
-
logger.debug(f"Project selected: {event.value}")
|
|
452
|
-
|
|
453
|
-
def _handle_error(self, error: Exception) -> None:
|
|
454
|
-
self.form_error_message = f"Error creating profile: {error}"
|
|
455
|
-
|
|
456
|
-
def _validate_form(self) -> bool:
|
|
457
|
-
"""Validate required fields"""
|
|
458
|
-
name_input = self.query_one("#name", Input)
|
|
459
|
-
api_url_input = self.query_one("#api_url", Input)
|
|
460
|
-
api_key_input = self.query_one("#api_key_auth_token", Input)
|
|
461
|
-
errors = []
|
|
462
|
-
|
|
463
|
-
# Clear previous error state
|
|
464
|
-
name_input.remove_class("error")
|
|
465
|
-
api_url_input.remove_class("error")
|
|
466
|
-
api_key_input.remove_class("error")
|
|
467
|
-
|
|
468
|
-
if not name_input.value.strip():
|
|
469
|
-
name_input.add_class("error")
|
|
470
|
-
errors.append("Profile name is required")
|
|
471
|
-
|
|
472
|
-
if not api_url_input.value.strip():
|
|
473
|
-
api_url_input.add_class("error")
|
|
474
|
-
errors.append("API URL is required")
|
|
475
|
-
|
|
476
|
-
# Validate API key if required
|
|
477
|
-
if self.api_key_required and not api_key_input.value.strip():
|
|
478
|
-
api_key_input.add_class("error")
|
|
479
|
-
errors.append("API key is required")
|
|
480
|
-
|
|
481
|
-
# Validate project ID
|
|
482
|
-
project_id = self._get_current_project_id()
|
|
483
|
-
if not project_id:
|
|
484
|
-
# Add error class to appropriate element
|
|
485
|
-
if self.manual_project_mode or not self.available_projects:
|
|
486
|
-
try:
|
|
487
|
-
project_input = self.query_one("#project_id", Input)
|
|
488
|
-
project_input.add_class("error")
|
|
489
|
-
except Exception:
|
|
490
|
-
pass
|
|
491
|
-
else:
|
|
492
|
-
try:
|
|
493
|
-
project_select = self.query_one("#project_select", Select)
|
|
494
|
-
project_select.add_class("error")
|
|
495
|
-
except Exception:
|
|
496
|
-
pass
|
|
497
|
-
errors.append("Project ID is required")
|
|
498
|
-
|
|
499
|
-
if errors:
|
|
500
|
-
self.form_error_message = "; ".join(errors)
|
|
501
|
-
return False
|
|
502
|
-
else:
|
|
503
|
-
self.form_error_message = ""
|
|
504
|
-
return True
|
|
505
|
-
|
|
506
|
-
def _get_current_project_id(self) -> str:
|
|
507
|
-
"""Get the current project ID from either selector or input"""
|
|
508
|
-
if self.manual_project_mode or not self.available_projects:
|
|
509
|
-
try:
|
|
510
|
-
project_input = self.query_one("#project_id", Input)
|
|
511
|
-
return project_input.value.strip()
|
|
512
|
-
except Exception:
|
|
513
|
-
return ""
|
|
514
|
-
else:
|
|
515
|
-
try:
|
|
516
|
-
project_select = self.query_one("#project_select", Select)
|
|
517
|
-
return project_select.value or ""
|
|
518
|
-
except Exception:
|
|
519
|
-
return ""
|
|
520
|
-
|
|
521
|
-
def _get_form_data(self) -> APIKeyProfileForm:
|
|
522
|
-
"""Extract form data from inputs"""
|
|
523
|
-
name_input = self.query_one("#name", Input)
|
|
524
|
-
api_url_input = self.query_one("#api_url", Input)
|
|
525
|
-
api_key_input = self.query_one("#api_key_auth_token", Input)
|
|
526
|
-
|
|
527
|
-
return APIKeyProfileForm(
|
|
528
|
-
name=name_input.value.strip(),
|
|
529
|
-
api_url=api_url_input.value.strip(),
|
|
530
|
-
project_id=self._get_current_project_id(),
|
|
531
|
-
api_key_auth_token=api_key_input.value.strip(),
|
|
532
|
-
existing_name=self.form_data.existing_name,
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
def edit_api_key_profile_form(
|
|
537
|
-
profile: Profile,
|
|
538
|
-
prompt_message: str | None = None,
|
|
539
|
-
) -> APIKeyProfileForm | None:
|
|
540
|
-
"""Launch profile edit form and return result"""
|
|
541
|
-
initial_data = APIKeyProfileForm.from_profile(profile)
|
|
542
|
-
initial_data.existing_name = profile.name or None
|
|
543
|
-
app = APIKeyProfileEditApp(initial_data, prompt_message)
|
|
544
|
-
return app.run()
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
def create_api_key_profile_form(
|
|
548
|
-
api_url: str = "https://api.cloud.llamaindex.ai",
|
|
549
|
-
name: str | None = None,
|
|
550
|
-
project_id: str | None = None,
|
|
551
|
-
api_key_auth_token: str | None = None,
|
|
552
|
-
prompt_message: str | None = None,
|
|
553
|
-
) -> APIKeyProfileForm | None:
|
|
554
|
-
"""Launch profile creation form and return result"""
|
|
555
|
-
return edit_api_key_profile_form(
|
|
556
|
-
Profile(
|
|
557
|
-
name=name or "",
|
|
558
|
-
api_url=api_url,
|
|
559
|
-
project_id=project_id or "",
|
|
560
|
-
api_key_auth_token=api_key_auth_token or "",
|
|
561
|
-
),
|
|
562
|
-
prompt_message,
|
|
563
|
-
)
|
|
File without changes
|
|
File without changes
|