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,173 @@
|
|
|
1
|
+
"""Config sidebar pane — rebuild, section update, and focus management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
from textual.css.query import NoMatches
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from dataeval_flow._app._panes._widgets import (
|
|
12
|
+
_CONFIG_SECTIONS,
|
|
13
|
+
SECTION_TITLES,
|
|
14
|
+
CfgItem,
|
|
15
|
+
CfgSectionHeader,
|
|
16
|
+
PaneWidget,
|
|
17
|
+
uid,
|
|
18
|
+
)
|
|
19
|
+
from dataeval_flow._app._viewmodel._rendering import snippet_config_item
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from textual.app import App
|
|
23
|
+
from textual.widget import Widget
|
|
24
|
+
|
|
25
|
+
from dataeval_flow._app._viewmodel._builder_vm import BuilderViewModel
|
|
26
|
+
else:
|
|
27
|
+
App = object
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigPaneMixin(App):
|
|
31
|
+
"""Mixin providing config sidebar rebuild logic for FlowApp.
|
|
32
|
+
|
|
33
|
+
Expects the host class to also inherit from ``App`` and to provide
|
|
34
|
+
``_vm`` and ``_get_pane_widgets`` (both supplied by ``FlowApp``).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
_vm: BuilderViewModel
|
|
39
|
+
|
|
40
|
+
def _get_pane_widgets(self, pane_id: str) -> list[PaneWidget]: ...
|
|
41
|
+
|
|
42
|
+
def _rebuild_config_pane(self) -> None:
|
|
43
|
+
"""Full rebuild of the config sidebar."""
|
|
44
|
+
try:
|
|
45
|
+
container = cast(VerticalScroll, self.query_one("#config-pane", VerticalScroll))
|
|
46
|
+
except NoMatches:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
focused = self.focused
|
|
50
|
+
restore = self._save_config_focus(focused)
|
|
51
|
+
|
|
52
|
+
if restore[0] is not None:
|
|
53
|
+
self.set_focus(None)
|
|
54
|
+
|
|
55
|
+
container.remove_children()
|
|
56
|
+
for category, _ in _CONFIG_SECTIONS:
|
|
57
|
+
self._mount_config_section(container, category)
|
|
58
|
+
|
|
59
|
+
if restore[0] is not None:
|
|
60
|
+
self.call_after_refresh(self._restore_config_focus, *restore)
|
|
61
|
+
|
|
62
|
+
def _rebuild_config_section(self, category: str) -> None:
|
|
63
|
+
"""Rebuild a single config section in place."""
|
|
64
|
+
try:
|
|
65
|
+
container = cast(VerticalScroll, self.query_one("#config-pane", VerticalScroll))
|
|
66
|
+
except NoMatches:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
focused = self.focused
|
|
70
|
+
restore = self._save_config_focus(focused)
|
|
71
|
+
|
|
72
|
+
if restore[0] == category:
|
|
73
|
+
self.set_focus(None)
|
|
74
|
+
|
|
75
|
+
insert_before = self._remove_section_widgets(container, category)
|
|
76
|
+
self._mount_config_section(container, category, before=insert_before)
|
|
77
|
+
|
|
78
|
+
if restore[0] == category:
|
|
79
|
+
self.call_after_refresh(self._restore_config_focus, *restore)
|
|
80
|
+
|
|
81
|
+
def _remove_section_widgets(self, container: VerticalScroll, category: str) -> PaneWidget | None:
|
|
82
|
+
"""Remove all widgets for *category* and return the next section's header (or None)."""
|
|
83
|
+
in_section = False
|
|
84
|
+
insert_before: PaneWidget | None = None
|
|
85
|
+
to_remove: list[Widget] = []
|
|
86
|
+
for child in list(container.children):
|
|
87
|
+
if isinstance(child, CfgSectionHeader) and child.fc_category == category:
|
|
88
|
+
in_section = True
|
|
89
|
+
to_remove.append(child)
|
|
90
|
+
elif in_section:
|
|
91
|
+
if isinstance(child, CfgSectionHeader):
|
|
92
|
+
insert_before = child
|
|
93
|
+
break
|
|
94
|
+
to_remove.append(child)
|
|
95
|
+
for w in to_remove:
|
|
96
|
+
w.remove()
|
|
97
|
+
return insert_before
|
|
98
|
+
|
|
99
|
+
def _mount_config_section(self, container: VerticalScroll, category: str, before: PaneWidget | None = None) -> None:
|
|
100
|
+
"""Mount header + items for one config section."""
|
|
101
|
+
title = SECTION_TITLES.get(category, category)
|
|
102
|
+
items = self._vm.items(category)
|
|
103
|
+
|
|
104
|
+
widgets: list[Static] = []
|
|
105
|
+
widgets.append(
|
|
106
|
+
CfgSectionHeader(
|
|
107
|
+
f"[bold]{title}[/bold] [dim]({len(items)})[/dim]",
|
|
108
|
+
category=category,
|
|
109
|
+
classes="cfg-section-header",
|
|
110
|
+
id=uid("ch"),
|
|
111
|
+
markup=True,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
if not items:
|
|
115
|
+
widgets.append(Static("[dim] (empty)[/dim]"))
|
|
116
|
+
else:
|
|
117
|
+
for idx, item in enumerate(items):
|
|
118
|
+
snippet = snippet_config_item(category, item)
|
|
119
|
+
widgets.append(
|
|
120
|
+
CfgItem(
|
|
121
|
+
snippet,
|
|
122
|
+
category=category,
|
|
123
|
+
index=idx,
|
|
124
|
+
classes="cfg-item",
|
|
125
|
+
id=uid("ci"),
|
|
126
|
+
markup=True,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if before is not None:
|
|
131
|
+
for w in widgets:
|
|
132
|
+
container.mount(w, before=before)
|
|
133
|
+
else:
|
|
134
|
+
for w in widgets:
|
|
135
|
+
container.mount(w)
|
|
136
|
+
|
|
137
|
+
# -- Config focus save/restore -----------------------------------------
|
|
138
|
+
|
|
139
|
+
def _save_config_focus(self, focused: Any) -> tuple[str | None, int | None, bool]:
|
|
140
|
+
"""Extract focus context from the currently focused widget."""
|
|
141
|
+
if isinstance(focused, CfgItem) and focused.pane == "config-pane":
|
|
142
|
+
return focused.fc_category, focused.fc_index, False
|
|
143
|
+
if isinstance(focused, CfgSectionHeader) and focused.pane == "config-pane":
|
|
144
|
+
return focused.fc_category, None, True
|
|
145
|
+
return None, None, False
|
|
146
|
+
|
|
147
|
+
def _restore_config_focus(self, category: str | None, index: int | None, is_header: bool) -> None:
|
|
148
|
+
"""Restore focus by finding the widget with matching attributes."""
|
|
149
|
+
if category is None:
|
|
150
|
+
return
|
|
151
|
+
widgets = self._get_pane_widgets("config-pane")
|
|
152
|
+
if is_header or index is None:
|
|
153
|
+
target = next((w for w in widgets if isinstance(w, CfgSectionHeader) and w.fc_category == category), None)
|
|
154
|
+
else:
|
|
155
|
+
target = next(
|
|
156
|
+
(w for w in widgets if isinstance(w, CfgItem) and w.fc_category == category and w.fc_index == index),
|
|
157
|
+
None,
|
|
158
|
+
)
|
|
159
|
+
if target is None and index > 0:
|
|
160
|
+
target = next(
|
|
161
|
+
(
|
|
162
|
+
w
|
|
163
|
+
for w in reversed(widgets)
|
|
164
|
+
if isinstance(w, CfgItem) and w.fc_category == category and w.fc_index < index
|
|
165
|
+
),
|
|
166
|
+
None,
|
|
167
|
+
)
|
|
168
|
+
if target is None:
|
|
169
|
+
target = next(
|
|
170
|
+
(w for w in widgets if isinstance(w, CfgSectionHeader) and w.fc_category == category), None
|
|
171
|
+
)
|
|
172
|
+
if target is not None:
|
|
173
|
+
self.set_focus(target, scroll_visible=False)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Result pane — rebuild, incremental append/update."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
from textual.css.query import NoMatches
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from dataeval_flow._app._panes._widgets import PaneWidget, ResultCard, uid
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from textual.app import App
|
|
15
|
+
|
|
16
|
+
from dataeval_flow._app._viewmodel._builder_vm import BuilderViewModel
|
|
17
|
+
else:
|
|
18
|
+
App = object
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ResultPaneMixin(App):
|
|
22
|
+
"""Mixin providing result pane rebuild logic for FlowApp.
|
|
23
|
+
|
|
24
|
+
Expects the host class to also inherit from ``App`` and to provide ``_vm``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
_vm: BuilderViewModel
|
|
29
|
+
|
|
30
|
+
def _rebuild_result_pane(self) -> None:
|
|
31
|
+
"""Full rebuild of the result pane (used on undo/redo/load/new)."""
|
|
32
|
+
try:
|
|
33
|
+
container = self.query_one("#result-pane", VerticalScroll)
|
|
34
|
+
except NoMatches:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
container.remove_children()
|
|
38
|
+
executions = [e for e in self._vm.all_executions() if e.status in ("completed", "failed")]
|
|
39
|
+
count = len(executions)
|
|
40
|
+
header_text = f"[bold]Results[/bold] [dim]({count})[/dim]" if count else "[bold]Results[/bold]"
|
|
41
|
+
container.mount(Static(header_text, classes="result-pane-header", id=uid("rh"), markup=True))
|
|
42
|
+
|
|
43
|
+
if not executions:
|
|
44
|
+
container.mount(Static("[dim] (no results yet)[/dim]"))
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
for entry in executions:
|
|
48
|
+
snippet = self._render_result_snippet(entry)
|
|
49
|
+
card = ResultCard(
|
|
50
|
+
snippet,
|
|
51
|
+
task_name=entry.task_name,
|
|
52
|
+
classes="result-card",
|
|
53
|
+
id=uid("rc"),
|
|
54
|
+
markup=True,
|
|
55
|
+
)
|
|
56
|
+
container.mount(card)
|
|
57
|
+
|
|
58
|
+
def _render_result_snippet(self, entry: Any) -> str:
|
|
59
|
+
"""Render a result card snippet for a single execution entry."""
|
|
60
|
+
from dataeval_flow._app._screens._detail import _colorize_marker
|
|
61
|
+
from dataeval_flow._app._viewmodel._result_vm import ResultViewModel
|
|
62
|
+
|
|
63
|
+
if entry.status == "completed" and entry.result is not None:
|
|
64
|
+
rvm = ResultViewModel(entry.result)
|
|
65
|
+
summary = rvm.summary_line()
|
|
66
|
+
warnings = rvm.warning_count()
|
|
67
|
+
severity_tag = " [bold red][!!][/bold red]" if warnings else " [green][ok][/green]"
|
|
68
|
+
lines = [f"[bold]{entry.task_name}[/bold] — {summary}{severity_tag}"]
|
|
69
|
+
for fi in range(rvm.finding_count()):
|
|
70
|
+
fline = _colorize_marker(rvm.finding_summary_markup(fi))
|
|
71
|
+
lines.append(f" {fline}")
|
|
72
|
+
return "\n".join(lines)
|
|
73
|
+
error_brief = (entry.error or "unknown error")[:60]
|
|
74
|
+
return f"[bold]{entry.task_name}[/bold] — [bold red]FAILED[/bold red]: {error_brief}"
|
|
75
|
+
|
|
76
|
+
def _append_or_update_result(self, task_name: str) -> None:
|
|
77
|
+
"""Add or update a single result card without full rebuild."""
|
|
78
|
+
entry = self._vm.task_execution(task_name)
|
|
79
|
+
if entry is None or entry.status not in ("completed", "failed"):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
snippet = self._render_result_snippet(entry)
|
|
83
|
+
|
|
84
|
+
# Try to update existing card (re-run of same task)
|
|
85
|
+
try:
|
|
86
|
+
container = self.query_one("#result-pane", VerticalScroll)
|
|
87
|
+
except NoMatches:
|
|
88
|
+
return
|
|
89
|
+
for child in container.children:
|
|
90
|
+
if isinstance(child, ResultCard) and child.task_name == task_name:
|
|
91
|
+
child.update(snippet)
|
|
92
|
+
self._update_result_header()
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Remove "no results yet" placeholder
|
|
96
|
+
for child in list(container.children):
|
|
97
|
+
is_placeholder = isinstance(child, Static) and not isinstance(child, PaneWidget)
|
|
98
|
+
if is_placeholder and "no results" in str(getattr(child, "content", "")):
|
|
99
|
+
child.remove()
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
# Append new card
|
|
103
|
+
card = ResultCard(
|
|
104
|
+
snippet,
|
|
105
|
+
task_name=task_name,
|
|
106
|
+
classes="result-card",
|
|
107
|
+
id=uid("rc"),
|
|
108
|
+
markup=True,
|
|
109
|
+
)
|
|
110
|
+
container.mount(card)
|
|
111
|
+
self._update_result_header()
|
|
112
|
+
|
|
113
|
+
def _update_result_header(self) -> None:
|
|
114
|
+
"""Update the result pane header count without rebuilding."""
|
|
115
|
+
executions = [e for e in self._vm.all_executions() if e.status in ("completed", "failed")]
|
|
116
|
+
count = len(executions)
|
|
117
|
+
header_text = f"[bold]Results[/bold] [dim]({count})[/dim]" if count else "[bold]Results[/bold]"
|
|
118
|
+
try:
|
|
119
|
+
container = self.query_one("#result-pane", VerticalScroll)
|
|
120
|
+
except NoMatches:
|
|
121
|
+
return
|
|
122
|
+
for child in container.children:
|
|
123
|
+
if isinstance(child, Static) and "result-pane-header" in (child.classes or set()):
|
|
124
|
+
child.update(header_text)
|
|
125
|
+
break
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Task pane — rebuild and targeted card updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
from textual.css.query import NoMatches
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from dataeval_flow._app._panes._widgets import TaskCard, TaskPaneHeader, uid
|
|
12
|
+
from dataeval_flow._app._viewmodel._rendering import snippet_task_with_execution
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from textual.app import App
|
|
16
|
+
|
|
17
|
+
from dataeval_flow._app._viewmodel._builder_vm import BuilderViewModel
|
|
18
|
+
else:
|
|
19
|
+
App = object
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskPaneMixin(App):
|
|
23
|
+
"""Mixin providing task pane rebuild logic for FlowApp.
|
|
24
|
+
|
|
25
|
+
Expects the host class to also inherit from ``App`` and to provide ``_vm``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
_vm: BuilderViewModel
|
|
30
|
+
|
|
31
|
+
def _rebuild_task_pane(self) -> None:
|
|
32
|
+
"""Full rebuild of the task pane (structural changes only)."""
|
|
33
|
+
try:
|
|
34
|
+
container = self.query_one("#task-pane", VerticalScroll)
|
|
35
|
+
except NoMatches:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
focused = self.focused
|
|
39
|
+
restore_index: int | None = None
|
|
40
|
+
if isinstance(focused, TaskCard):
|
|
41
|
+
restore_index = focused.fc_index
|
|
42
|
+
|
|
43
|
+
container.remove_children()
|
|
44
|
+
tasks = self._vm.items("tasks")
|
|
45
|
+
header = TaskPaneHeader(
|
|
46
|
+
f"[bold]Tasks[/bold] [dim]({len(tasks)})[/dim]",
|
|
47
|
+
classes="task-pane-header",
|
|
48
|
+
id=uid("th"),
|
|
49
|
+
markup=True,
|
|
50
|
+
)
|
|
51
|
+
container.mount(header)
|
|
52
|
+
|
|
53
|
+
if not tasks:
|
|
54
|
+
container.mount(Static("[dim] (no tasks defined)[/dim]"))
|
|
55
|
+
else:
|
|
56
|
+
for idx, task in enumerate(tasks):
|
|
57
|
+
task_name = task.get("name", "")
|
|
58
|
+
execution = self._vm.task_execution(task_name)
|
|
59
|
+
snippet = snippet_task_with_execution(task, execution)
|
|
60
|
+
card = TaskCard(
|
|
61
|
+
snippet,
|
|
62
|
+
index=idx,
|
|
63
|
+
task_name=task_name,
|
|
64
|
+
classes="task-card",
|
|
65
|
+
id=uid("tc"),
|
|
66
|
+
markup=True,
|
|
67
|
+
)
|
|
68
|
+
container.mount(card)
|
|
69
|
+
|
|
70
|
+
if restore_index is not None:
|
|
71
|
+
target = next(
|
|
72
|
+
(w for w in container.children if isinstance(w, TaskCard) and w.fc_index == restore_index), None
|
|
73
|
+
)
|
|
74
|
+
if target is not None:
|
|
75
|
+
self.set_focus(target)
|
|
76
|
+
|
|
77
|
+
def _update_task_card_by_name(self, task_name: str) -> None:
|
|
78
|
+
"""Update a single task card's content without rebuilding the pane."""
|
|
79
|
+
try:
|
|
80
|
+
container = self.query_one("#task-pane", VerticalScroll)
|
|
81
|
+
except NoMatches:
|
|
82
|
+
return
|
|
83
|
+
for child in container.children:
|
|
84
|
+
if isinstance(child, TaskCard) and child.task_name == task_name:
|
|
85
|
+
execution = self._vm.task_execution(task_name)
|
|
86
|
+
task = next((t for t in self._vm.items("tasks") if t.get("name") == task_name), None)
|
|
87
|
+
if task is not None:
|
|
88
|
+
child.update(snippet_task_with_execution(task, execution))
|
|
89
|
+
return
|
|
90
|
+
# Card not found — structural change happened, full rebuild
|
|
91
|
+
self._rebuild_task_pane()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Focusable widget classes shared across panes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
|
|
9
|
+
# Pane IDs used for Tab-cycling between panes
|
|
10
|
+
PANE_IDS = ("config-pane", "task-pane", "result-pane")
|
|
11
|
+
|
|
12
|
+
_CONFIG_SECTIONS = [
|
|
13
|
+
("datasets", "Datasets"),
|
|
14
|
+
("selections", "Selections"),
|
|
15
|
+
("sources", "Sources"),
|
|
16
|
+
("preprocessors", "Preprocessors"),
|
|
17
|
+
("extractors", "Extractors"),
|
|
18
|
+
("workflows", "Workflows"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
SECTION_TITLES: dict[str, str] = dict(_CONFIG_SECTIONS)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Global unique-ID factory — avoids Textual DuplicateIds on rebuild
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
_next_uid: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def uid(prefix: str) -> str:
|
|
31
|
+
"""Return a globally unique widget ID like ``prefix-42``."""
|
|
32
|
+
global _next_uid
|
|
33
|
+
_next_uid += 1
|
|
34
|
+
return f"{prefix}-{_next_uid}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Focusable pane widgets
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PaneWidget(Static):
|
|
43
|
+
"""Base for focusable widgets that know which pane they live in."""
|
|
44
|
+
|
|
45
|
+
can_focus = True
|
|
46
|
+
pane: str = ""
|
|
47
|
+
|
|
48
|
+
def key_down(self) -> None:
|
|
49
|
+
self.app._focus_within_pane(self.pane, 1) # type: ignore[attr-defined] # noqa: SLF001
|
|
50
|
+
|
|
51
|
+
def key_up(self) -> None:
|
|
52
|
+
self.app._focus_within_pane(self.pane, -1) # type: ignore[attr-defined] # noqa: SLF001
|
|
53
|
+
|
|
54
|
+
def key_tab(self) -> None:
|
|
55
|
+
self.app._cycle_pane(1) # type: ignore[attr-defined] # noqa: SLF001
|
|
56
|
+
|
|
57
|
+
def key_shift_tab(self) -> None:
|
|
58
|
+
self.app._cycle_pane(-1) # type: ignore[attr-defined] # noqa: SLF001
|
|
59
|
+
|
|
60
|
+
async def _on_key(self, event: Any) -> None:
|
|
61
|
+
if event.key in ("tab", "shift+tab", "down", "up"):
|
|
62
|
+
event.stop()
|
|
63
|
+
event.prevent_default()
|
|
64
|
+
await super()._on_key(event)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CfgSectionHeader(PaneWidget):
|
|
68
|
+
pane = "config-pane"
|
|
69
|
+
|
|
70
|
+
def __init__(self, content: str, category: str, **kw: Any) -> None:
|
|
71
|
+
super().__init__(content, **kw)
|
|
72
|
+
self.fc_category = category
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CfgItem(PaneWidget):
|
|
76
|
+
pane = "config-pane"
|
|
77
|
+
|
|
78
|
+
def __init__(self, content: str, category: str, index: int, **kw: Any) -> None:
|
|
79
|
+
super().__init__(content, **kw)
|
|
80
|
+
self.fc_category = category
|
|
81
|
+
self.fc_index = index
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TaskPaneHeader(PaneWidget):
|
|
85
|
+
pane = "task-pane"
|
|
86
|
+
|
|
87
|
+
def __init__(self, content: str, **kw: Any) -> None:
|
|
88
|
+
super().__init__(content, **kw)
|
|
89
|
+
self.fc_category = "tasks"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TaskCard(PaneWidget):
|
|
93
|
+
pane = "task-pane"
|
|
94
|
+
|
|
95
|
+
def __init__(self, content: str, index: int, task_name: str = "", **kw: Any) -> None:
|
|
96
|
+
super().__init__(content, **kw)
|
|
97
|
+
self.fc_category = "tasks"
|
|
98
|
+
self.fc_index = index
|
|
99
|
+
self.task_name = task_name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ResultPaneHeader(PaneWidget):
|
|
103
|
+
pane = "result-pane"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ResultCard(PaneWidget):
|
|
107
|
+
pane = "result-pane"
|
|
108
|
+
|
|
109
|
+
def __init__(self, content: str, task_name: str, **kw: Any) -> None:
|
|
110
|
+
super().__init__(content, **kw)
|
|
111
|
+
self.task_name = task_name
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Screen and modal components for the TUI app."""
|
|
2
|
+
|
|
3
|
+
from dataeval_flow._app._screens._base import _MODAL_CSS, ComponentModal, _select_value
|
|
4
|
+
from dataeval_flow._app._screens._detail import ErrorDetailModal, ResultDetailModal
|
|
5
|
+
from dataeval_flow._app._screens._model import ModelModal
|
|
6
|
+
from dataeval_flow._app._screens._params import build_param_form, collect_param_form, validate_param_form
|
|
7
|
+
from dataeval_flow._app._screens._pathpicker import PathPickerScreen
|
|
8
|
+
from dataeval_flow._app._screens._section import SectionModal
|
|
9
|
+
from dataeval_flow._app._screens._settings import ExecutionSettings, SettingsModal
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ComponentModal",
|
|
13
|
+
"ErrorDetailModal",
|
|
14
|
+
"ExecutionSettings",
|
|
15
|
+
"ModelModal",
|
|
16
|
+
"PathPickerScreen",
|
|
17
|
+
"ResultDetailModal",
|
|
18
|
+
"SectionModal",
|
|
19
|
+
"SettingsModal",
|
|
20
|
+
"_MODAL_CSS",
|
|
21
|
+
"_select_value",
|
|
22
|
+
"build_param_form",
|
|
23
|
+
"collect_param_form",
|
|
24
|
+
"validate_param_form",
|
|
25
|
+
]
|