dataeval-flow 0.1.0__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.
- dataeval_flow/__init__.py +93 -0
- dataeval_flow/__main__.py +149 -0
- dataeval_flow/_app/__init__.py +5 -0
- dataeval_flow/_app/_model/__init__.py +5 -0
- dataeval_flow/_app/_model/_coerce.py +126 -0
- dataeval_flow/_app/_model/_discover.py +171 -0
- dataeval_flow/_app/_model/_execution.py +108 -0
- dataeval_flow/_app/_model/_introspect.py +280 -0
- dataeval_flow/_app/_model/_item.py +213 -0
- dataeval_flow/_app/_model/_registry.py +255 -0
- dataeval_flow/_app/_model/_state.py +322 -0
- dataeval_flow/_app/_model/_undo.py +61 -0
- dataeval_flow/_app/_panes/__init__.py +35 -0
- dataeval_flow/_app/_panes/_config_pane.py +173 -0
- dataeval_flow/_app/_panes/_result_pane.py +125 -0
- dataeval_flow/_app/_panes/_task_pane.py +91 -0
- dataeval_flow/_app/_panes/_widgets.py +111 -0
- dataeval_flow/_app/_screens/__init__.py +25 -0
- dataeval_flow/_app/_screens/_base.py +242 -0
- dataeval_flow/_app/_screens/_detail.py +333 -0
- dataeval_flow/_app/_screens/_model.py +102 -0
- dataeval_flow/_app/_screens/_params.py +80 -0
- dataeval_flow/_app/_screens/_pathpicker.py +68 -0
- dataeval_flow/_app/_screens/_section.py +621 -0
- dataeval_flow/_app/_screens/_settings.py +183 -0
- dataeval_flow/_app/_viewmodel/__init__.py +15 -0
- dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
- dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
- dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
- dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
- dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
- dataeval_flow/_app/app.py +742 -0
- dataeval_flow/_app/cli.py +592 -0
- dataeval_flow/_logging.py +102 -0
- dataeval_flow/cache.py +1355 -0
- dataeval_flow/config/__init__.py +80 -0
- dataeval_flow/config/_loader.py +79 -0
- dataeval_flow/config/_merge.py +92 -0
- dataeval_flow/config/_models.py +115 -0
- dataeval_flow/config/_paths.py +85 -0
- dataeval_flow/config/schemas/__init__.py +112 -0
- dataeval_flow/config/schemas/_dataset.py +111 -0
- dataeval_flow/config/schemas/_extractor.py +119 -0
- dataeval_flow/config/schemas/_metadata.py +28 -0
- dataeval_flow/config/schemas/_preprocessor.py +18 -0
- dataeval_flow/config/schemas/_selection.py +100 -0
- dataeval_flow/config/schemas/_task.py +89 -0
- dataeval_flow/config/schemas/_workflow.py +135 -0
- dataeval_flow/dataset.py +635 -0
- dataeval_flow/embeddings.py +135 -0
- dataeval_flow/metadata.py +48 -0
- dataeval_flow/preprocessing.py +141 -0
- dataeval_flow/py.typed +0 -0
- dataeval_flow/runner.py +118 -0
- dataeval_flow/selection.py +50 -0
- dataeval_flow/workflow/__init__.py +328 -0
- dataeval_flow/workflow/_text_report.py +511 -0
- dataeval_flow/workflow/base.py +69 -0
- dataeval_flow/workflow/orchestrator.py +454 -0
- dataeval_flow/workflows/__init__.py +1 -0
- dataeval_flow/workflows/analysis/__init__.py +38 -0
- dataeval_flow/workflows/analysis/outputs.py +202 -0
- dataeval_flow/workflows/analysis/params.py +114 -0
- dataeval_flow/workflows/analysis/workflow.py +1313 -0
- dataeval_flow/workflows/cleaning/__init__.py +23 -0
- dataeval_flow/workflows/cleaning/outputs.py +200 -0
- dataeval_flow/workflows/cleaning/params.py +160 -0
- dataeval_flow/workflows/cleaning/report.py +304 -0
- dataeval_flow/workflows/cleaning/workflow.py +794 -0
- dataeval_flow/workflows/drift/__init__.py +1 -0
- dataeval_flow/workflows/drift/outputs.py +144 -0
- dataeval_flow/workflows/drift/params.py +332 -0
- dataeval_flow/workflows/drift/report.py +201 -0
- dataeval_flow/workflows/drift/workflow.py +647 -0
- dataeval_flow/workflows/ood/__init__.py +1 -0
- dataeval_flow/workflows/ood/outputs.py +134 -0
- dataeval_flow/workflows/ood/params.py +161 -0
- dataeval_flow/workflows/ood/report.py +311 -0
- dataeval_flow/workflows/ood/workflow.py +728 -0
- dataeval_flow/workflows/prioritization/__init__.py +1 -0
- dataeval_flow/workflows/prioritization/outputs.py +122 -0
- dataeval_flow/workflows/prioritization/params.py +124 -0
- dataeval_flow/workflows/prioritization/report.py +117 -0
- dataeval_flow/workflows/prioritization/workflow.py +587 -0
- dataeval_flow/workflows/splitting/__init__.py +25 -0
- dataeval_flow/workflows/splitting/outputs.py +101 -0
- dataeval_flow/workflows/splitting/params.py +61 -0
- dataeval_flow/workflows/splitting/report.py +485 -0
- dataeval_flow/workflows/splitting/workflow.py +371 -0
- dataeval_flow-0.1.0.dist-info/METADATA +305 -0
- dataeval_flow-0.1.0.dist-info/RECORD +94 -0
- dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
- dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
- dataeval_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Base modal class and shared utilities for modal dialogs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from textual.app import ComposeResult, ScreenStackError
|
|
11
|
+
from textual.containers import Horizontal
|
|
12
|
+
from textual.css.query import NoMatches
|
|
13
|
+
from textual.screen import ModalScreen
|
|
14
|
+
from textual.widgets import Button, Checkbox, Input, Select
|
|
15
|
+
|
|
16
|
+
from dataeval_flow._app._model._item import DELETE_SENTINEL
|
|
17
|
+
from dataeval_flow._app._screens._pathpicker import PathPickerScreen
|
|
18
|
+
|
|
19
|
+
_log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Value helpers
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _select_value(sel: Select) -> str:
|
|
28
|
+
v = sel.value
|
|
29
|
+
if v is Select.BLANK or v is Select.NULL or v is None:
|
|
30
|
+
return ""
|
|
31
|
+
return str(v)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Shared modal CSS
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
_MODAL_CSS = """
|
|
39
|
+
.modal-dialog {
|
|
40
|
+
width: 70;
|
|
41
|
+
height: auto;
|
|
42
|
+
max-height: 85%;
|
|
43
|
+
border: round $accent 40%;
|
|
44
|
+
background: $surface;
|
|
45
|
+
padding: 1 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.modal-dialog Vertical {
|
|
49
|
+
height: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
SectionModal,
|
|
53
|
+
ComponentModal {
|
|
54
|
+
align: center middle;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.modal-dialog Label {
|
|
58
|
+
margin: 1 0 0 0;
|
|
59
|
+
text-style: bold;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.modal-dialog Input {
|
|
63
|
+
margin: 0 0 1 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.modal-dialog Select {
|
|
67
|
+
margin: 0 0 1 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.modal-button-bar {
|
|
71
|
+
height: auto;
|
|
72
|
+
padding: 1 0 0 0;
|
|
73
|
+
align: right middle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.modal-button-bar Button {
|
|
77
|
+
margin: 0 0 0 1;
|
|
78
|
+
min-width: 10;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.browse-row {
|
|
82
|
+
height: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.browse-row Input {
|
|
86
|
+
width: 1fr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.browse-row Button {
|
|
90
|
+
width: auto;
|
|
91
|
+
min-width: 10;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.conditional-field {
|
|
95
|
+
height: auto;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.hidden {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.step-row {
|
|
103
|
+
height: auto;
|
|
104
|
+
padding: 0 1;
|
|
105
|
+
margin: 0 0 0 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.step-row:hover {
|
|
109
|
+
background: $accent 8%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.step-row Static {
|
|
113
|
+
width: 1fr;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.step-row Button {
|
|
117
|
+
width: auto;
|
|
118
|
+
min-width: 4;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#modal-step-list {
|
|
122
|
+
height: auto;
|
|
123
|
+
max-height: 15;
|
|
124
|
+
margin: 0 0 1 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#modal-params-form {
|
|
128
|
+
height: auto;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.step-section-title {
|
|
132
|
+
text-style: bold;
|
|
133
|
+
padding: 1 0 0 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.nested-group {
|
|
137
|
+
margin: 0 0 1 1;
|
|
138
|
+
padding: 0 0 0 1;
|
|
139
|
+
border-left: tall $accent 40%;
|
|
140
|
+
}
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# ComponentModal — base class
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ComponentModal(ModalScreen[dict | str | None]):
|
|
150
|
+
"""Base modal for creating/editing pipeline components."""
|
|
151
|
+
|
|
152
|
+
CSS = _MODAL_CSS
|
|
153
|
+
BINDINGS = [("escape", "cancel", "Cancel")]
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
existing: dict[str, Any] | None = None,
|
|
158
|
+
data_dir: Path | None = None,
|
|
159
|
+
**kwargs: Any,
|
|
160
|
+
) -> None:
|
|
161
|
+
super().__init__(**kwargs)
|
|
162
|
+
self._existing = existing
|
|
163
|
+
self._data_dir = data_dir
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def is_edit_mode(self) -> bool:
|
|
167
|
+
return self._existing is not None
|
|
168
|
+
|
|
169
|
+
def _collect_raw(self) -> dict[str, Any] | None:
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
|
|
172
|
+
def _check_dirty(self) -> bool:
|
|
173
|
+
raise NotImplementedError
|
|
174
|
+
|
|
175
|
+
def _collect(self) -> dict[str, Any] | None:
|
|
176
|
+
raise NotImplementedError
|
|
177
|
+
|
|
178
|
+
def _update_ok_state(self) -> None:
|
|
179
|
+
try:
|
|
180
|
+
btn = self.query_one("#btn-modal-ok", Button)
|
|
181
|
+
btn.disabled = not self._check_dirty()
|
|
182
|
+
except NoMatches:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def compose_buttons(self) -> ComposeResult:
|
|
186
|
+
with Horizontal(classes="modal-button-bar"):
|
|
187
|
+
yield Button("OK", id="btn-modal-ok", variant="primary", disabled=True)
|
|
188
|
+
yield Button("Cancel", id="btn-modal-cancel")
|
|
189
|
+
if self.is_edit_mode:
|
|
190
|
+
yield Button("Delete", id="btn-modal-delete", variant="error")
|
|
191
|
+
|
|
192
|
+
def _safe_dismiss(self, result: dict | str | None) -> None:
|
|
193
|
+
with contextlib.suppress(ScreenStackError):
|
|
194
|
+
self.dismiss(result)
|
|
195
|
+
|
|
196
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
197
|
+
btn = event.button.id or ""
|
|
198
|
+
if btn == "btn-modal-ok":
|
|
199
|
+
result = self._collect()
|
|
200
|
+
if result is not None:
|
|
201
|
+
self._safe_dismiss(result)
|
|
202
|
+
elif btn == "btn-modal-cancel":
|
|
203
|
+
self._safe_dismiss(None)
|
|
204
|
+
elif btn == "btn-modal-delete":
|
|
205
|
+
self._safe_dismiss(DELETE_SENTINEL)
|
|
206
|
+
|
|
207
|
+
def on_input_changed(self, _event: Input.Changed) -> None:
|
|
208
|
+
self._update_ok_state()
|
|
209
|
+
|
|
210
|
+
def on_select_changed(self, _event: Select.Changed) -> None:
|
|
211
|
+
self._update_ok_state()
|
|
212
|
+
|
|
213
|
+
def on_checkbox_changed(self, _event: Checkbox.Changed) -> None:
|
|
214
|
+
self._update_ok_state()
|
|
215
|
+
|
|
216
|
+
def action_cancel(self) -> None:
|
|
217
|
+
with contextlib.suppress(ScreenStackError):
|
|
218
|
+
self.dismiss(None)
|
|
219
|
+
|
|
220
|
+
def _browse_for_input(self, input_id: str) -> None:
|
|
221
|
+
try:
|
|
222
|
+
current = self.query_one(f"#{input_id}", Input).value.strip() or "."
|
|
223
|
+
except NoMatches:
|
|
224
|
+
current = "."
|
|
225
|
+
|
|
226
|
+
data_dir = self._data_dir
|
|
227
|
+
|
|
228
|
+
def _on_result(result: str | None) -> None:
|
|
229
|
+
if result is None:
|
|
230
|
+
return
|
|
231
|
+
# Auto-relativize absolute paths against data_dir for portability
|
|
232
|
+
if Path(result).is_absolute() and data_dir is not None:
|
|
233
|
+
from dataeval_flow.config._paths import relativize_to_data_dir
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
result = relativize_to_data_dir(result, data_dir)
|
|
237
|
+
except ValueError:
|
|
238
|
+
_log.warning("Browsed path '%s' is not under data root '%s'", result, data_dir)
|
|
239
|
+
with contextlib.suppress(NoMatches):
|
|
240
|
+
self.query_one(f"#{input_id}", Input).value = result
|
|
241
|
+
|
|
242
|
+
self.app.push_screen(PathPickerScreen(start_path=current, mode="folder"), callback=_on_result)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Result detail modal for the dashboard.
|
|
2
|
+
|
|
3
|
+
Full-screen modal showing metadata, finding summaries, and expandable
|
|
4
|
+
detail sections for a single task's ``WorkflowResult``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from textual.app import ComposeResult
|
|
12
|
+
from textual.containers import Vertical, VerticalScroll
|
|
13
|
+
from textual.css.query import NoMatches
|
|
14
|
+
from textual.screen import ModalScreen
|
|
15
|
+
from textual.widgets import Button, DataTable, Static
|
|
16
|
+
|
|
17
|
+
from dataeval_flow._app._viewmodel._result_vm import ResultViewModel
|
|
18
|
+
|
|
19
|
+
__all__ = ["ErrorDetailModal", "ResultDetailModal"]
|
|
20
|
+
|
|
21
|
+
_SEVERITY_MARKUP: dict[str, str] = {
|
|
22
|
+
"ok": "[green][ok][/green]",
|
|
23
|
+
"info": "[blue][..][/blue]",
|
|
24
|
+
"warning": "[bold red][!!][/bold red]",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Plain-text markers emitted by _summary_line() → Rich-markup replacements
|
|
28
|
+
_MARKER_COLORS: list[tuple[str, str]] = [
|
|
29
|
+
(" [!!]", " [bold red]\\[!!][/bold red]"),
|
|
30
|
+
(" [ok]", " [green]\\[ok][/green]"),
|
|
31
|
+
(" [..]", " [blue]\\[..][/blue]"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _colorize_marker(line: str) -> str:
|
|
36
|
+
"""Replace the trailing plain-text severity marker with a colored Rich-markup version."""
|
|
37
|
+
for plain, colored in _MARKER_COLORS:
|
|
38
|
+
if line.endswith(plain):
|
|
39
|
+
return line[: -len(plain)] + colored
|
|
40
|
+
return line
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_CSS = """
|
|
44
|
+
ResultDetailModal {
|
|
45
|
+
align: center middle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#rd-dialog {
|
|
49
|
+
width: 90%;
|
|
50
|
+
height: 90%;
|
|
51
|
+
border: round $accent 40%;
|
|
52
|
+
background: $surface;
|
|
53
|
+
padding: 1 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#rd-scroll {
|
|
57
|
+
height: 1fr;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#rd-title {
|
|
61
|
+
text-style: bold;
|
|
62
|
+
margin: 0 0 1 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.rd-metadata {
|
|
66
|
+
margin: 0 0 1 0;
|
|
67
|
+
color: $text-muted;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.rd-summary-line {
|
|
71
|
+
height: auto;
|
|
72
|
+
padding: 0 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.rd-health {
|
|
76
|
+
margin: 1 0;
|
|
77
|
+
text-style: bold;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.rd-finding-header {
|
|
81
|
+
height: 3;
|
|
82
|
+
padding: 0 1;
|
|
83
|
+
margin: 1 0 0 0;
|
|
84
|
+
background: $boost;
|
|
85
|
+
content-align: left middle;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.rd-finding-header:focus {
|
|
89
|
+
background: $accent 20%;
|
|
90
|
+
color: $text;
|
|
91
|
+
border-left: tall $accent;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.rd-finding-detail {
|
|
95
|
+
padding: 0 1 0 2;
|
|
96
|
+
height: auto;
|
|
97
|
+
background: $surface;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.rd-separator {
|
|
101
|
+
height: 1;
|
|
102
|
+
margin: 1 0;
|
|
103
|
+
background: $accent 15%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#rd-buttons {
|
|
107
|
+
height: auto;
|
|
108
|
+
align: right middle;
|
|
109
|
+
margin: 1 0 0 0;
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _FindingHeader(Static):
|
|
115
|
+
"""Clickable finding header — toggles detail expansion."""
|
|
116
|
+
|
|
117
|
+
can_focus = True
|
|
118
|
+
|
|
119
|
+
def __init__(self, content: str, finding_idx: int, **kw: Any) -> None:
|
|
120
|
+
super().__init__(content, **kw)
|
|
121
|
+
self.finding_idx = finding_idx
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ResultDetailModal(ModalScreen[None]):
|
|
125
|
+
"""Full-screen modal showing detailed results for a single task."""
|
|
126
|
+
|
|
127
|
+
CSS = _CSS
|
|
128
|
+
BINDINGS = [("escape", "close", "Close")]
|
|
129
|
+
|
|
130
|
+
def __init__(self, task_name: str, result: Any, **kw: Any) -> None:
|
|
131
|
+
super().__init__(**kw)
|
|
132
|
+
self._task_name = task_name
|
|
133
|
+
self._result = result
|
|
134
|
+
self._rvm = ResultViewModel(result)
|
|
135
|
+
self._expanded_findings: set[int] = set()
|
|
136
|
+
self._gen: int = 0
|
|
137
|
+
|
|
138
|
+
def compose(self) -> ComposeResult:
|
|
139
|
+
with Vertical(id="rd-dialog"):
|
|
140
|
+
yield Static(f"[bold]Result: {self._task_name}[/bold]", id="rd-title", markup=True)
|
|
141
|
+
with VerticalScroll(id="rd-scroll"):
|
|
142
|
+
yield from self._compose_content()
|
|
143
|
+
with Vertical(id="rd-buttons"):
|
|
144
|
+
yield Button("Close", id="btn-rd-close", variant="primary")
|
|
145
|
+
|
|
146
|
+
def _compose_content(self) -> ComposeResult:
|
|
147
|
+
# Metadata
|
|
148
|
+
for line in self._rvm.metadata_lines():
|
|
149
|
+
yield Static(f"[dim]{line}[/dim]", classes="rd-metadata", markup=True)
|
|
150
|
+
|
|
151
|
+
yield Static("", classes="rd-separator")
|
|
152
|
+
|
|
153
|
+
# Summary
|
|
154
|
+
yield Static("[bold]SUMMARY[/bold]", markup=True)
|
|
155
|
+
summaries = self._rvm.finding_summaries()
|
|
156
|
+
for idx, _fs in enumerate(summaries):
|
|
157
|
+
line = self._rvm.finding_summary_markup(idx)
|
|
158
|
+
line = _colorize_marker(line)
|
|
159
|
+
yield Static(f" {line}", classes="rd-summary-line", markup=True)
|
|
160
|
+
|
|
161
|
+
# Health
|
|
162
|
+
health = self._rvm.health_line()
|
|
163
|
+
if "warning" in health.lower():
|
|
164
|
+
yield Static(f"[bold red] {health}[/bold red]", classes="rd-health", markup=True)
|
|
165
|
+
else:
|
|
166
|
+
yield Static(f"[green] {health}[/green]", classes="rd-health", markup=True)
|
|
167
|
+
|
|
168
|
+
yield Static("", classes="rd-separator")
|
|
169
|
+
|
|
170
|
+
# Finding detail sections
|
|
171
|
+
gen = self._gen
|
|
172
|
+
for idx, fs in enumerate(summaries):
|
|
173
|
+
expanded = idx in self._expanded_findings
|
|
174
|
+
arrow = "\u25bc" if expanded else "\u25b6"
|
|
175
|
+
marker = _SEVERITY_MARKUP.get(fs.severity, _SEVERITY_MARKUP["info"])
|
|
176
|
+
header = _FindingHeader(
|
|
177
|
+
f"{arrow} DETAIL: {fs.title} {marker}",
|
|
178
|
+
finding_idx=idx,
|
|
179
|
+
classes="rd-finding-header",
|
|
180
|
+
id=f"rd-fh-{gen}-{idx}",
|
|
181
|
+
markup=True,
|
|
182
|
+
)
|
|
183
|
+
yield header
|
|
184
|
+
|
|
185
|
+
if expanded:
|
|
186
|
+
# Try DataTable for tabular findings
|
|
187
|
+
table_data = self._rvm.finding_table_data(idx)
|
|
188
|
+
if table_data is not None:
|
|
189
|
+
headers, rows = table_data
|
|
190
|
+
dt = DataTable(id=f"rd-dt-{gen}-{idx}")
|
|
191
|
+
yield dt
|
|
192
|
+
# DataTable columns/rows added in _populate_tables
|
|
193
|
+
else:
|
|
194
|
+
# Rich markup detail
|
|
195
|
+
detail_text = self._rvm.finding_detail_markup(idx)
|
|
196
|
+
if detail_text.strip():
|
|
197
|
+
yield Static(detail_text, classes="rd-finding-detail", markup=False)
|
|
198
|
+
|
|
199
|
+
def on_mount(self) -> None:
|
|
200
|
+
"""Populate DataTable widgets after initial compose."""
|
|
201
|
+
self._populate_tables()
|
|
202
|
+
|
|
203
|
+
def _populate_tables(self) -> None:
|
|
204
|
+
"""Fill DataTable widgets with data for expanded findings."""
|
|
205
|
+
gen = self._gen
|
|
206
|
+
for idx in self._expanded_findings:
|
|
207
|
+
table_data = self._rvm.finding_table_data(idx)
|
|
208
|
+
if table_data is not None:
|
|
209
|
+
headers, rows = table_data
|
|
210
|
+
try:
|
|
211
|
+
dt = self.query_one(f"#rd-dt-{gen}-{idx}", DataTable)
|
|
212
|
+
for h in headers:
|
|
213
|
+
dt.add_column(h)
|
|
214
|
+
for row in rows:
|
|
215
|
+
dt.add_row(*row)
|
|
216
|
+
except NoMatches:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
220
|
+
if event.button.id == "btn-rd-close":
|
|
221
|
+
self.dismiss(None)
|
|
222
|
+
|
|
223
|
+
def on_click(self, event: Any) -> None:
|
|
224
|
+
widget = event.widget
|
|
225
|
+
while widget is not None:
|
|
226
|
+
if isinstance(widget, _FindingHeader):
|
|
227
|
+
self._toggle_finding(widget.finding_idx)
|
|
228
|
+
event.stop()
|
|
229
|
+
return
|
|
230
|
+
if isinstance(widget, VerticalScroll):
|
|
231
|
+
break
|
|
232
|
+
widget = widget.parent
|
|
233
|
+
|
|
234
|
+
async def _on_key(self, event: Any) -> None:
|
|
235
|
+
if event.key == "enter":
|
|
236
|
+
focused = self.app.focused
|
|
237
|
+
if isinstance(focused, _FindingHeader):
|
|
238
|
+
self._toggle_finding(focused.finding_idx)
|
|
239
|
+
event.stop()
|
|
240
|
+
event.prevent_default()
|
|
241
|
+
return
|
|
242
|
+
await super()._on_key(event)
|
|
243
|
+
|
|
244
|
+
def _toggle_finding(self, idx: int) -> None:
|
|
245
|
+
if idx in self._expanded_findings:
|
|
246
|
+
self._expanded_findings.discard(idx)
|
|
247
|
+
else:
|
|
248
|
+
self._expanded_findings.add(idx)
|
|
249
|
+
self._rebuild_content()
|
|
250
|
+
|
|
251
|
+
def _rebuild_content(self) -> None:
|
|
252
|
+
self._gen += 1
|
|
253
|
+
try:
|
|
254
|
+
scroll = self.query_one("#rd-scroll", VerticalScroll)
|
|
255
|
+
except NoMatches:
|
|
256
|
+
return
|
|
257
|
+
scroll.remove_children()
|
|
258
|
+
for widget in self._compose_content():
|
|
259
|
+
scroll.mount(widget)
|
|
260
|
+
self._populate_tables()
|
|
261
|
+
|
|
262
|
+
def action_close(self) -> None:
|
|
263
|
+
self.dismiss(None)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Error detail modal (for failed tasks)
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
_ERROR_CSS = """
|
|
271
|
+
ErrorDetailModal {
|
|
272
|
+
align: center middle;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#ed-dialog {
|
|
276
|
+
width: 80%;
|
|
277
|
+
height: auto;
|
|
278
|
+
max-height: 70%;
|
|
279
|
+
border: round $error 50%;
|
|
280
|
+
background: $surface;
|
|
281
|
+
padding: 1 2;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#ed-title {
|
|
285
|
+
text-style: bold;
|
|
286
|
+
color: $error;
|
|
287
|
+
margin: 0 0 1 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#ed-error {
|
|
291
|
+
height: auto;
|
|
292
|
+
max-height: 40;
|
|
293
|
+
padding: 1;
|
|
294
|
+
background: $boost;
|
|
295
|
+
margin: 1 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#ed-buttons {
|
|
299
|
+
height: auto;
|
|
300
|
+
align: right middle;
|
|
301
|
+
margin: 1 0 0 0;
|
|
302
|
+
}
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class ErrorDetailModal(ModalScreen[None]):
|
|
307
|
+
"""Modal showing error details for a failed task."""
|
|
308
|
+
|
|
309
|
+
CSS = _ERROR_CSS
|
|
310
|
+
BINDINGS = [("escape", "close", "Close")]
|
|
311
|
+
|
|
312
|
+
def __init__(self, task_name: str, error: str, **kw: Any) -> None:
|
|
313
|
+
super().__init__(**kw)
|
|
314
|
+
self._task_name = task_name
|
|
315
|
+
self._error = error
|
|
316
|
+
|
|
317
|
+
def compose(self) -> ComposeResult:
|
|
318
|
+
with Vertical(id="ed-dialog"):
|
|
319
|
+
yield Static(
|
|
320
|
+
f"[bold]FAILED: {self._task_name}[/bold]",
|
|
321
|
+
id="ed-title",
|
|
322
|
+
markup=True,
|
|
323
|
+
)
|
|
324
|
+
yield Static(self._error, id="ed-error")
|
|
325
|
+
with Vertical(id="ed-buttons"):
|
|
326
|
+
yield Button("Close", id="btn-ed-close", variant="primary")
|
|
327
|
+
|
|
328
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
329
|
+
if event.button.id == "btn-ed-close":
|
|
330
|
+
self.dismiss(None)
|
|
331
|
+
|
|
332
|
+
def action_close(self) -> None:
|
|
333
|
+
self.dismiss(None)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""ModelModal — flattened model creation/editing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
9
|
+
from textual.css.query import NoMatches
|
|
10
|
+
from textual.widgets import Button, Input, Label, Select, Static
|
|
11
|
+
|
|
12
|
+
from dataeval_flow._app._screens._base import ComponentModal, _select_value
|
|
13
|
+
from dataeval_flow._app._viewmodel._model_vm import MODEL_TYPES, ModelViewModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelModal(ComponentModal):
|
|
17
|
+
"""Create or edit a model entry (flattened — no nested extractor).
|
|
18
|
+
|
|
19
|
+
Delegates business logic to :class:`ModelViewModel`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, existing: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
23
|
+
super().__init__(existing, **kwargs)
|
|
24
|
+
self._vm = ModelViewModel(existing)
|
|
25
|
+
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
title = "Edit Model" if self.is_edit_mode else "New Model"
|
|
28
|
+
with VerticalScroll(classes="modal-dialog"):
|
|
29
|
+
yield Static(f"[bold]{title}[/bold]")
|
|
30
|
+
yield Label("Name:")
|
|
31
|
+
yield Input(id="md-model-name", placeholder="e.g. resnet50")
|
|
32
|
+
yield Label("Type:")
|
|
33
|
+
yield Select(
|
|
34
|
+
[(t, t) for t in MODEL_TYPES],
|
|
35
|
+
id="md-model-type",
|
|
36
|
+
prompt="Select type",
|
|
37
|
+
)
|
|
38
|
+
with Vertical(id="md-model-path-group", classes="conditional-field hidden"):
|
|
39
|
+
yield Label("Model Path:")
|
|
40
|
+
with Horizontal(classes="browse-row"):
|
|
41
|
+
yield Input(id="md-model-path", placeholder="e.g. ./models/resnet50.onnx")
|
|
42
|
+
yield Button("Browse", id="btn-browse-md-model-path")
|
|
43
|
+
with Vertical(id="md-model-vocab-group", classes="conditional-field hidden"):
|
|
44
|
+
yield Label("Vocab Size:")
|
|
45
|
+
yield Input(id="md-model-vocab", placeholder="2048")
|
|
46
|
+
yield from self.compose_buttons()
|
|
47
|
+
|
|
48
|
+
def on_mount(self) -> None:
|
|
49
|
+
if self._existing:
|
|
50
|
+
try:
|
|
51
|
+
self.query_one("#md-model-name", Input).value = self._existing.get("name", "")
|
|
52
|
+
model_type = self._existing.get("type", "")
|
|
53
|
+
if model_type:
|
|
54
|
+
self.query_one("#md-model-type", Select).value = model_type
|
|
55
|
+
if self._existing.get("model_path"):
|
|
56
|
+
self.query_one("#md-model-path", Input).value = self._existing["model_path"]
|
|
57
|
+
if self._existing.get("vocab_size"):
|
|
58
|
+
self.query_one("#md-model-vocab", Input).value = str(self._existing["vocab_size"])
|
|
59
|
+
except NoMatches:
|
|
60
|
+
pass
|
|
61
|
+
self._toggle_fields()
|
|
62
|
+
|
|
63
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
64
|
+
if event.button.id == "btn-browse-md-model-path":
|
|
65
|
+
self._browse_for_input("md-model-path")
|
|
66
|
+
return
|
|
67
|
+
super().on_button_pressed(event)
|
|
68
|
+
|
|
69
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
70
|
+
if event.select.id == "md-model-type":
|
|
71
|
+
self._toggle_fields()
|
|
72
|
+
super().on_select_changed(event)
|
|
73
|
+
|
|
74
|
+
def _toggle_fields(self) -> None:
|
|
75
|
+
model_type = _select_value(self.query_one("#md-model-type", Select))
|
|
76
|
+
path_group = self.query_one("#md-model-path-group")
|
|
77
|
+
vocab_group = self.query_one("#md-model-vocab-group")
|
|
78
|
+
if ModelViewModel.needs_path(model_type):
|
|
79
|
+
path_group.remove_class("hidden")
|
|
80
|
+
else:
|
|
81
|
+
path_group.add_class("hidden")
|
|
82
|
+
if ModelViewModel.needs_vocab(model_type):
|
|
83
|
+
vocab_group.remove_class("hidden")
|
|
84
|
+
else:
|
|
85
|
+
vocab_group.add_class("hidden")
|
|
86
|
+
|
|
87
|
+
def _collect_raw(self) -> dict[str, Any] | None:
|
|
88
|
+
return self._vm.build_result(
|
|
89
|
+
name=self.query_one("#md-model-name", Input).value.strip(),
|
|
90
|
+
model_type=_select_value(self.query_one("#md-model-type", Select)),
|
|
91
|
+
model_path=self.query_one("#md-model-path", Input).value.strip(),
|
|
92
|
+
vocab_size_str=self.query_one("#md-model-vocab", Input).value.strip(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _check_dirty(self) -> bool:
|
|
96
|
+
return self._vm.check_dirty(self._collect_raw())
|
|
97
|
+
|
|
98
|
+
def _collect(self) -> dict[str, Any] | None:
|
|
99
|
+
result = self._collect_raw()
|
|
100
|
+
if result is None:
|
|
101
|
+
self.app.notify(ModelViewModel.validation_message(), severity="error")
|
|
102
|
+
return result
|