idun-agent-engine 0.3.9__py3-none-any.whl → 0.4.1__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.
- idun_agent_engine/_version.py +1 -1
- idun_agent_engine/agent/adk/adk.py +5 -2
- idun_agent_engine/agent/langgraph/langgraph.py +1 -1
- idun_agent_engine/core/app_factory.py +1 -1
- idun_agent_engine/core/config_builder.py +11 -5
- idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
- idun_agent_engine/mcp/__init__.py +18 -2
- idun_agent_engine/mcp/helpers.py +135 -43
- idun_agent_engine/mcp/registry.py +7 -1
- idun_agent_engine/server/lifespan.py +22 -0
- idun_agent_engine/telemetry/__init__.py +19 -0
- idun_agent_engine/telemetry/config.py +29 -0
- idun_agent_engine/telemetry/telemetry.py +248 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/METADATA +12 -8
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/RECORD +45 -17
- idun_platform_cli/groups/agent/package.py +3 -0
- idun_platform_cli/groups/agent/serve.py +2 -0
- idun_platform_cli/groups/init.py +25 -0
- idun_platform_cli/main.py +3 -0
- idun_platform_cli/telemetry.py +54 -0
- idun_platform_cli/tui/__init__.py +0 -0
- idun_platform_cli/tui/css/__init__.py +0 -0
- idun_platform_cli/tui/css/create_agent.py +912 -0
- idun_platform_cli/tui/css/main.py +89 -0
- idun_platform_cli/tui/main.py +87 -0
- idun_platform_cli/tui/schemas/__init__.py +0 -0
- idun_platform_cli/tui/schemas/create_agent.py +60 -0
- idun_platform_cli/tui/screens/__init__.py +0 -0
- idun_platform_cli/tui/screens/create_agent.py +622 -0
- idun_platform_cli/tui/utils/__init__.py +0 -0
- idun_platform_cli/tui/utils/config.py +182 -0
- idun_platform_cli/tui/validators/__init__.py +0 -0
- idun_platform_cli/tui/validators/guardrails.py +88 -0
- idun_platform_cli/tui/validators/mcps.py +84 -0
- idun_platform_cli/tui/validators/observability.py +65 -0
- idun_platform_cli/tui/widgets/__init__.py +19 -0
- idun_platform_cli/tui/widgets/chat_widget.py +153 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +356 -0
- idun_platform_cli/tui/widgets/identity_widget.py +252 -0
- idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
- idun_platform_cli/tui/widgets/memory_widget.py +195 -0
- idun_platform_cli/tui/widgets/observability_widget.py +382 -0
- idun_platform_cli/tui/widgets/serve_widget.py +82 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""MCPs configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.widgets import Static, Input, Button, RadioSet, RadioButton, TextArea, OptionList
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets.option_list import Option
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
MCP_TEMPLATES = {
|
|
11
|
+
"time": {
|
|
12
|
+
"name": "time-reference",
|
|
13
|
+
"transport": "stdio",
|
|
14
|
+
"command": "docker",
|
|
15
|
+
"args": ["run", "-i", "--rm", "mcp/time"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MCPsWidget(Widget):
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self.mcp_servers = []
|
|
24
|
+
self.next_server_id = 0
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
template_selector = self.query_one("#template_selector", OptionList)
|
|
28
|
+
if template_selector.option_count > 0:
|
|
29
|
+
template_selector.highlighted = 0
|
|
30
|
+
|
|
31
|
+
def compose(self) -> ComposeResult:
|
|
32
|
+
templates_section = Vertical(classes="mcps-templates-section")
|
|
33
|
+
templates_section.border_title = "MCP Templates"
|
|
34
|
+
templates_row = Horizontal(classes="templates-row")
|
|
35
|
+
templates_row.compose_add_child(Static("Select template:", classes="mcp-label"))
|
|
36
|
+
option_list = OptionList(id="template_selector", classes="template-selector")
|
|
37
|
+
for template_name in MCP_TEMPLATES.keys():
|
|
38
|
+
option_list.add_option(Option(template_name.title(), id=template_name))
|
|
39
|
+
templates_row.compose_add_child(option_list)
|
|
40
|
+
templates_row.compose_add_child(Button("Add from Template", id="add_from_template_button", classes="add-template-btn"))
|
|
41
|
+
templates_section.compose_add_child(templates_row)
|
|
42
|
+
yield templates_section
|
|
43
|
+
|
|
44
|
+
yield Button("+ Add Custom MCP Server", id="add_custom_mcp_button", classes="add-custom-btn")
|
|
45
|
+
|
|
46
|
+
yield Vertical(id="mcps_container", classes="mcps-container")
|
|
47
|
+
|
|
48
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
49
|
+
button_id = event.button.id
|
|
50
|
+
|
|
51
|
+
if button_id == "add_custom_mcp_button":
|
|
52
|
+
self._add_mcp_server()
|
|
53
|
+
elif button_id == "add_from_template_button":
|
|
54
|
+
self._add_from_template()
|
|
55
|
+
elif button_id and str(button_id).startswith("remove_mcp_"):
|
|
56
|
+
index = int(str(button_id).replace("remove_mcp_", ""))
|
|
57
|
+
self._remove_mcp_server(index)
|
|
58
|
+
|
|
59
|
+
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
|
60
|
+
radio_id = event.radio_set.id
|
|
61
|
+
if radio_id and str(radio_id).startswith("mcp_transport_"):
|
|
62
|
+
index = int(str(radio_id).replace("mcp_transport_", ""))
|
|
63
|
+
if event.pressed:
|
|
64
|
+
transport = str(event.pressed.id)
|
|
65
|
+
self._update_transport_fields(index, transport)
|
|
66
|
+
|
|
67
|
+
def _add_from_template(self) -> None:
|
|
68
|
+
template_selector = self.query_one("#template_selector", OptionList)
|
|
69
|
+
if template_selector.highlighted is not None:
|
|
70
|
+
option_id = template_selector.get_option_at_index(template_selector.highlighted).id
|
|
71
|
+
if option_id and str(option_id) in MCP_TEMPLATES:
|
|
72
|
+
template_data = MCP_TEMPLATES[str(option_id)].copy()
|
|
73
|
+
self._add_mcp_server(template_data)
|
|
74
|
+
else:
|
|
75
|
+
self.app.notify("Please select a template first", severity="warning")
|
|
76
|
+
|
|
77
|
+
def _add_mcp_server(self, template_data: dict = None) -> None:
|
|
78
|
+
server_id = self.next_server_id
|
|
79
|
+
self.next_server_id += 1
|
|
80
|
+
self.mcp_servers.append(server_id)
|
|
81
|
+
|
|
82
|
+
card = self._create_mcp_card(server_id, template_data)
|
|
83
|
+
container = self.query_one("#mcps_container", Vertical)
|
|
84
|
+
container.mount(card)
|
|
85
|
+
|
|
86
|
+
def _remove_mcp_server(self, index: int) -> None:
|
|
87
|
+
if index in self.mcp_servers:
|
|
88
|
+
self.mcp_servers.remove(index)
|
|
89
|
+
card = self.query_one(f"#mcp_card_{index}")
|
|
90
|
+
card.remove()
|
|
91
|
+
|
|
92
|
+
def _create_mcp_card(self, index: int, template_data: dict = None) -> Vertical:
|
|
93
|
+
card = Vertical(id=f"mcp_card_{index}", classes="mcp-card")
|
|
94
|
+
card.border_title = f"MCP Server {index + 1}"
|
|
95
|
+
|
|
96
|
+
header = Horizontal(classes="mcp-header")
|
|
97
|
+
name_value = template_data.get("name", "") if template_data else ""
|
|
98
|
+
header.compose_add_child(Static(name_value or f"Server {index + 1}", id=f"mcp_name_display_{index}", classes="mcp-name-display"))
|
|
99
|
+
header.compose_add_child(Button("Remove", id=f"remove_mcp_{index}", classes="remove-mcp-btn"))
|
|
100
|
+
card.compose_add_child(header)
|
|
101
|
+
|
|
102
|
+
name_row = Horizontal(classes="mcp-field-row")
|
|
103
|
+
name_row.compose_add_child(Static("Name:", classes="mcp-label"))
|
|
104
|
+
name_row.compose_add_child(Input(value=name_value, placeholder="server-name", id=f"mcp_name_{index}", classes="mcp-input"))
|
|
105
|
+
card.compose_add_child(name_row)
|
|
106
|
+
|
|
107
|
+
transport_row = Horizontal(classes="mcp-field-row")
|
|
108
|
+
transport_row.compose_add_child(Static("Transport:", classes="mcp-label"))
|
|
109
|
+
radio_set = RadioSet(id=f"mcp_transport_{index}")
|
|
110
|
+
|
|
111
|
+
transport_value = template_data.get("transport", "streamable_http") if template_data else "streamable_http"
|
|
112
|
+
radio_set.compose_add_child(RadioButton("stdio", id="stdio", value=(transport_value == "stdio")))
|
|
113
|
+
radio_set.compose_add_child(RadioButton("sse", id="sse", value=(transport_value == "sse")))
|
|
114
|
+
radio_set.compose_add_child(RadioButton("streamable_http", id="streamable_http", value=(transport_value == "streamable_http")))
|
|
115
|
+
radio_set.compose_add_child(RadioButton("websocket", id="websocket", value=(transport_value == "websocket")))
|
|
116
|
+
transport_row.compose_add_child(radio_set)
|
|
117
|
+
card.compose_add_child(transport_row)
|
|
118
|
+
|
|
119
|
+
http_fields = Vertical(id=f"mcp_http_fields_{index}", classes="http-fields-container")
|
|
120
|
+
http_fields.border_title = "HTTP Configuration"
|
|
121
|
+
|
|
122
|
+
url_row = Horizontal(classes="mcp-field-row")
|
|
123
|
+
url_row.compose_add_child(Static("URL:", classes="mcp-label"))
|
|
124
|
+
url_value = template_data.get("url", "") if template_data else ""
|
|
125
|
+
url_row.compose_add_child(Input(value=url_value, placeholder="https://api.example.com/mcp", id=f"mcp_url_{index}", classes="mcp-input"))
|
|
126
|
+
http_fields.compose_add_child(url_row)
|
|
127
|
+
|
|
128
|
+
headers_row = Horizontal(classes="mcp-field-row")
|
|
129
|
+
headers_row.compose_add_child(Static("Headers (JSON):", classes="mcp-label"))
|
|
130
|
+
headers_value = template_data.get("headers", "") if template_data else ""
|
|
131
|
+
headers_row.compose_add_child(TextArea(text=str(headers_value) if headers_value else "", id=f"mcp_headers_{index}", classes="mcp-textarea"))
|
|
132
|
+
http_fields.compose_add_child(headers_row)
|
|
133
|
+
|
|
134
|
+
http_fields.display = transport_value in ["sse", "streamable_http", "websocket"]
|
|
135
|
+
card.compose_add_child(http_fields)
|
|
136
|
+
|
|
137
|
+
stdio_fields = Vertical(id=f"mcp_stdio_fields_{index}", classes="stdio-fields-container")
|
|
138
|
+
stdio_fields.border_title = "Stdio Configuration"
|
|
139
|
+
|
|
140
|
+
command_row = Horizontal(classes="mcp-field-row")
|
|
141
|
+
command_row.compose_add_child(Static("Command:", classes="mcp-label"))
|
|
142
|
+
command_value = template_data.get("command", "") if template_data else ""
|
|
143
|
+
command_row.compose_add_child(Input(value=command_value, placeholder="npx", id=f"mcp_command_{index}", classes="mcp-input"))
|
|
144
|
+
stdio_fields.compose_add_child(command_row)
|
|
145
|
+
|
|
146
|
+
args_row = Horizontal(classes="mcp-field-row")
|
|
147
|
+
args_row.compose_add_child(Static("Args (one per line):", classes="mcp-label"))
|
|
148
|
+
args_value = ""
|
|
149
|
+
if template_data and "args" in template_data:
|
|
150
|
+
args_list = template_data["args"]
|
|
151
|
+
if isinstance(args_list, list):
|
|
152
|
+
args_value = "\n".join(args_list)
|
|
153
|
+
args_textarea = TextArea(text=args_value, id=f"mcp_args_{index}", classes="mcp-textarea")
|
|
154
|
+
args_textarea.placeholder = "run\n-i\n--rm"
|
|
155
|
+
args_row.compose_add_child(args_textarea)
|
|
156
|
+
stdio_fields.compose_add_child(args_row)
|
|
157
|
+
|
|
158
|
+
env_row = Horizontal(classes="mcp-field-row")
|
|
159
|
+
env_row.compose_add_child(Static("Env Vars (JSON):", classes="mcp-label"))
|
|
160
|
+
env_value = ""
|
|
161
|
+
if template_data and "env" in template_data:
|
|
162
|
+
import json
|
|
163
|
+
env_value = json.dumps(template_data["env"], indent=2)
|
|
164
|
+
env_row.compose_add_child(TextArea(text=env_value, id=f"mcp_env_{index}", classes="mcp-textarea"))
|
|
165
|
+
stdio_fields.compose_add_child(env_row)
|
|
166
|
+
|
|
167
|
+
stdio_fields.display = transport_value == "stdio"
|
|
168
|
+
card.compose_add_child(stdio_fields)
|
|
169
|
+
|
|
170
|
+
return card
|
|
171
|
+
|
|
172
|
+
def _update_transport_fields(self, index: int, transport: str) -> None:
|
|
173
|
+
http_fields = self.query_one(f"#mcp_http_fields_{index}")
|
|
174
|
+
stdio_fields = self.query_one(f"#mcp_stdio_fields_{index}")
|
|
175
|
+
|
|
176
|
+
if transport in ["sse", "streamable_http", "websocket"]:
|
|
177
|
+
http_fields.display = True
|
|
178
|
+
stdio_fields.display = False
|
|
179
|
+
elif transport == "stdio":
|
|
180
|
+
http_fields.display = False
|
|
181
|
+
stdio_fields.display = True
|
|
182
|
+
|
|
183
|
+
def get_data(self) -> list[dict] | None:
|
|
184
|
+
servers_data = []
|
|
185
|
+
|
|
186
|
+
for server_id in self.mcp_servers:
|
|
187
|
+
try:
|
|
188
|
+
name_input = self.query_one(f"#mcp_name_{server_id}", Input)
|
|
189
|
+
name = name_input.value
|
|
190
|
+
|
|
191
|
+
if not name:
|
|
192
|
+
self.app.notify(f"Server {server_id + 1}: Name is required", severity="error")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
radio_set = self.query_one(f"#mcp_transport_{server_id}", RadioSet)
|
|
196
|
+
transport = "streamable_http"
|
|
197
|
+
if radio_set.pressed_button:
|
|
198
|
+
transport = str(radio_set.pressed_button.id)
|
|
199
|
+
|
|
200
|
+
server_config = {
|
|
201
|
+
"name": name,
|
|
202
|
+
"transport": transport,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if transport in ["sse", "streamable_http", "websocket"]:
|
|
206
|
+
url_input = self.query_one(f"#mcp_url_{server_id}", Input)
|
|
207
|
+
server_config["url"] = url_input.value
|
|
208
|
+
|
|
209
|
+
headers_textarea = self.query_one(f"#mcp_headers_{server_id}", TextArea)
|
|
210
|
+
if headers_textarea.text.strip():
|
|
211
|
+
server_config["headers"] = headers_textarea.text.strip()
|
|
212
|
+
|
|
213
|
+
elif transport == "stdio":
|
|
214
|
+
command_input = self.query_one(f"#mcp_command_{server_id}", Input)
|
|
215
|
+
server_config["command"] = command_input.value
|
|
216
|
+
|
|
217
|
+
args_textarea = self.query_one(f"#mcp_args_{server_id}", TextArea)
|
|
218
|
+
server_config["args"] = args_textarea.text
|
|
219
|
+
|
|
220
|
+
env_textarea = self.query_one(f"#mcp_env_{server_id}", TextArea)
|
|
221
|
+
if env_textarea.text.strip():
|
|
222
|
+
server_config["env"] = env_textarea.text.strip()
|
|
223
|
+
|
|
224
|
+
servers_data.append(server_config)
|
|
225
|
+
|
|
226
|
+
except Exception:
|
|
227
|
+
self.app.notify(f"Error reading MCP server {server_id + 1}: check your configuration.", severity="error")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
return servers_data
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Memory and checkpoint configuration widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.css.query import NoMatches
|
|
11
|
+
from textual.reactive import reactive
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
from textual.widgets import Input, RadioButton, RadioSet, Static
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from idun_agent_schema.engine.langgraph import CheckpointConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoryWidget(Widget):
|
|
20
|
+
selected_type = reactive("memory")
|
|
21
|
+
|
|
22
|
+
def compose(self) -> ComposeResult:
|
|
23
|
+
main_section = Vertical(classes="memory-main")
|
|
24
|
+
main_section.border_title = "Checkpoint Configuration"
|
|
25
|
+
|
|
26
|
+
with main_section, Horizontal(classes="field-row framework-row"):
|
|
27
|
+
yield Static("Type:", classes="field-label")
|
|
28
|
+
with RadioSet(id="checkpoint_type_select"):
|
|
29
|
+
yield RadioButton("In-Memory", id="memory", value=True)
|
|
30
|
+
yield RadioButton("SQLite", id="sqlite")
|
|
31
|
+
yield RadioButton("PostgreSQL", id="postgres")
|
|
32
|
+
|
|
33
|
+
config_container = Vertical(
|
|
34
|
+
classes="checkpoint-config-container",
|
|
35
|
+
id="checkpoint_config",
|
|
36
|
+
)
|
|
37
|
+
yield config_container
|
|
38
|
+
|
|
39
|
+
def on_mount(self) -> None:
|
|
40
|
+
self._update_checkpoint_config()
|
|
41
|
+
|
|
42
|
+
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
|
43
|
+
if event.radio_set.id == "checkpoint_type_select":
|
|
44
|
+
self.selected_type = str(event.pressed.id)
|
|
45
|
+
self._update_checkpoint_config()
|
|
46
|
+
|
|
47
|
+
def _update_checkpoint_config(self) -> None:
|
|
48
|
+
try:
|
|
49
|
+
config_container = self.query_one("#checkpoint_config", Vertical)
|
|
50
|
+
config_container.remove_children()
|
|
51
|
+
|
|
52
|
+
if self.selected_type == "memory":
|
|
53
|
+
pass
|
|
54
|
+
elif self.selected_type == "sqlite":
|
|
55
|
+
self._render_sqlite_config(config_container)
|
|
56
|
+
elif self.selected_type == "postgres":
|
|
57
|
+
self._render_postgres_config(config_container)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def _render_sqlite_config(self, container: Vertical) -> None:
|
|
62
|
+
config_section = Vertical(
|
|
63
|
+
Horizontal(
|
|
64
|
+
Static("DB URL:", classes="field-label"),
|
|
65
|
+
Input(
|
|
66
|
+
placeholder="sqlite:///./checkpoints.db",
|
|
67
|
+
id="sqlite_db_url",
|
|
68
|
+
classes="field-input",
|
|
69
|
+
),
|
|
70
|
+
classes="field-row",
|
|
71
|
+
),
|
|
72
|
+
classes="checkpoint-fields-section",
|
|
73
|
+
)
|
|
74
|
+
config_section.border_title = "SQLite Configuration"
|
|
75
|
+
container.mount(config_section)
|
|
76
|
+
|
|
77
|
+
def _render_postgres_config(self, container: Vertical) -> None:
|
|
78
|
+
config_section = Vertical(
|
|
79
|
+
Horizontal(
|
|
80
|
+
Static("DB URL:", classes="field-label"),
|
|
81
|
+
Input(
|
|
82
|
+
placeholder="postgresql://user:pass@localhost:5432/db",
|
|
83
|
+
id="postgres_db_url",
|
|
84
|
+
classes="field-input",
|
|
85
|
+
),
|
|
86
|
+
classes="field-row",
|
|
87
|
+
),
|
|
88
|
+
classes="checkpoint-fields-section",
|
|
89
|
+
)
|
|
90
|
+
config_section.border_title = "PostgreSQL Configuration"
|
|
91
|
+
container.mount(config_section)
|
|
92
|
+
|
|
93
|
+
def get_data(self) -> CheckpointConfig | None:
|
|
94
|
+
from idun_agent_schema.engine.langgraph import (
|
|
95
|
+
CheckpointConfig,
|
|
96
|
+
InMemoryCheckpointConfig,
|
|
97
|
+
PostgresCheckpointConfig,
|
|
98
|
+
SqliteCheckpointConfig,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
radio_set = self.query_one("#checkpoint_type_select", RadioSet)
|
|
103
|
+
|
|
104
|
+
checkpoint_type = "memory"
|
|
105
|
+
if radio_set.pressed_button:
|
|
106
|
+
checkpoint_type = str(radio_set.pressed_button.id)
|
|
107
|
+
|
|
108
|
+
if checkpoint_type == "memory":
|
|
109
|
+
return InMemoryCheckpointConfig(type="memory")
|
|
110
|
+
|
|
111
|
+
elif checkpoint_type == "sqlite":
|
|
112
|
+
try:
|
|
113
|
+
db_url_input = self.query_one("#sqlite_db_url", Input)
|
|
114
|
+
db_url = db_url_input.value.strip()
|
|
115
|
+
except NoMatches:
|
|
116
|
+
self.app.notify(
|
|
117
|
+
"Configuration error. Please reselect checkpoint type.",
|
|
118
|
+
severity="error",
|
|
119
|
+
)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if not db_url:
|
|
123
|
+
self.app.notify(
|
|
124
|
+
"SQLite DB URL is required",
|
|
125
|
+
severity="error",
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
if not db_url.startswith("sqlite:///"):
|
|
130
|
+
self.app.notify(
|
|
131
|
+
"SQLite URL must start with 'sqlite:///'",
|
|
132
|
+
severity="error",
|
|
133
|
+
)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
return SqliteCheckpointConfig(type="sqlite", db_url=db_url)
|
|
138
|
+
except ValidationError:
|
|
139
|
+
self.app.notify(
|
|
140
|
+
"Invalid SQLite configuration. Check your URL format.",
|
|
141
|
+
severity="error",
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
elif checkpoint_type == "postgres":
|
|
146
|
+
try:
|
|
147
|
+
db_url_input = self.query_one("#postgres_db_url", Input)
|
|
148
|
+
db_url = db_url_input.value.strip()
|
|
149
|
+
except NoMatches:
|
|
150
|
+
self.app.notify(
|
|
151
|
+
"Configuration error. Please reselect checkpoint type.",
|
|
152
|
+
severity="error",
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
if not db_url:
|
|
157
|
+
self.app.notify(
|
|
158
|
+
"PostgreSQL DB URL is required",
|
|
159
|
+
severity="error",
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if not (
|
|
164
|
+
db_url.startswith("postgresql://")
|
|
165
|
+
or db_url.startswith("postgres://")
|
|
166
|
+
):
|
|
167
|
+
self.app.notify(
|
|
168
|
+
"PostgreSQL URL must start with 'postgresql://' or 'postgres://'",
|
|
169
|
+
severity="error",
|
|
170
|
+
)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
return PostgresCheckpointConfig(type="postgres", db_url=db_url)
|
|
175
|
+
except ValidationError:
|
|
176
|
+
self.app.notify(
|
|
177
|
+
"Invalid PostgreSQL configuration. Check your URL format.",
|
|
178
|
+
severity="error",
|
|
179
|
+
)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
except NoMatches:
|
|
183
|
+
self.app.notify(
|
|
184
|
+
"Error reading checkpoint configuration",
|
|
185
|
+
severity="error",
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
except Exception:
|
|
189
|
+
self.app.notify(
|
|
190
|
+
"Error validating checkpoint configuration",
|
|
191
|
+
severity="error",
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
return None
|