py-data-engine 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.
- data_engine/__init__.py +37 -0
- data_engine/application/__init__.py +39 -0
- data_engine/application/actions.py +42 -0
- data_engine/application/catalog.py +151 -0
- data_engine/application/control.py +213 -0
- data_engine/application/details.py +73 -0
- data_engine/application/runtime.py +449 -0
- data_engine/application/workspace.py +62 -0
- data_engine/authoring/__init__.py +14 -0
- data_engine/authoring/builder.py +31 -0
- data_engine/authoring/execution/__init__.py +6 -0
- data_engine/authoring/execution/app.py +6 -0
- data_engine/authoring/execution/context.py +82 -0
- data_engine/authoring/execution/continuous.py +176 -0
- data_engine/authoring/execution/grouped.py +106 -0
- data_engine/authoring/execution/logging.py +83 -0
- data_engine/authoring/execution/polling.py +135 -0
- data_engine/authoring/execution/runner.py +210 -0
- data_engine/authoring/execution/single.py +171 -0
- data_engine/authoring/flow.py +361 -0
- data_engine/authoring/helpers.py +160 -0
- data_engine/authoring/model.py +59 -0
- data_engine/authoring/primitives.py +430 -0
- data_engine/authoring/services.py +42 -0
- data_engine/devtools/__init__.py +3 -0
- data_engine/devtools/project_ast_map.py +503 -0
- data_engine/docs/__init__.py +1 -0
- data_engine/docs/sphinx_source/_static/custom.css +13 -0
- data_engine/docs/sphinx_source/api.rst +42 -0
- data_engine/docs/sphinx_source/conf.py +37 -0
- data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
- data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
- data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
- data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
- data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
- data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
- data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
- data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
- data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
- data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
- data_engine/docs/sphinx_source/guides/project-map.md +118 -0
- data_engine/docs/sphinx_source/guides/recipes.md +268 -0
- data_engine/docs/sphinx_source/index.rst +22 -0
- data_engine/domain/__init__.py +92 -0
- data_engine/domain/actions.py +69 -0
- data_engine/domain/catalog.py +128 -0
- data_engine/domain/details.py +214 -0
- data_engine/domain/diagnostics.py +56 -0
- data_engine/domain/errors.py +104 -0
- data_engine/domain/inspection.py +99 -0
- data_engine/domain/logs.py +118 -0
- data_engine/domain/operations.py +172 -0
- data_engine/domain/operator.py +72 -0
- data_engine/domain/runs.py +155 -0
- data_engine/domain/runtime.py +279 -0
- data_engine/domain/source_state.py +17 -0
- data_engine/domain/support.py +54 -0
- data_engine/domain/time.py +23 -0
- data_engine/domain/workspace.py +159 -0
- data_engine/flow_modules/__init__.py +1 -0
- data_engine/flow_modules/flow_module_compiler.py +179 -0
- data_engine/flow_modules/flow_module_loader.py +201 -0
- data_engine/helpers/__init__.py +25 -0
- data_engine/helpers/duckdb.py +705 -0
- data_engine/hosts/__init__.py +1 -0
- data_engine/hosts/daemon/__init__.py +23 -0
- data_engine/hosts/daemon/app.py +221 -0
- data_engine/hosts/daemon/bootstrap.py +69 -0
- data_engine/hosts/daemon/client.py +465 -0
- data_engine/hosts/daemon/commands.py +64 -0
- data_engine/hosts/daemon/composition.py +310 -0
- data_engine/hosts/daemon/constants.py +15 -0
- data_engine/hosts/daemon/entrypoints.py +97 -0
- data_engine/hosts/daemon/lifecycle.py +191 -0
- data_engine/hosts/daemon/manager.py +272 -0
- data_engine/hosts/daemon/ownership.py +126 -0
- data_engine/hosts/daemon/runtime_commands.py +188 -0
- data_engine/hosts/daemon/runtime_control.py +31 -0
- data_engine/hosts/daemon/server.py +84 -0
- data_engine/hosts/daemon/shared_state.py +147 -0
- data_engine/hosts/daemon/state_sync.py +101 -0
- data_engine/platform/__init__.py +1 -0
- data_engine/platform/identity.py +35 -0
- data_engine/platform/local_settings.py +146 -0
- data_engine/platform/theme.py +259 -0
- data_engine/platform/workspace_models.py +190 -0
- data_engine/platform/workspace_policy.py +333 -0
- data_engine/runtime/__init__.py +1 -0
- data_engine/runtime/file_watch.py +185 -0
- data_engine/runtime/ledger_models.py +116 -0
- data_engine/runtime/runtime_db.py +938 -0
- data_engine/runtime/shared_state.py +523 -0
- data_engine/services/__init__.py +49 -0
- data_engine/services/daemon.py +64 -0
- data_engine/services/daemon_state.py +40 -0
- data_engine/services/flow_catalog.py +102 -0
- data_engine/services/flow_execution.py +48 -0
- data_engine/services/ledger.py +85 -0
- data_engine/services/logs.py +65 -0
- data_engine/services/runtime_binding.py +105 -0
- data_engine/services/runtime_execution.py +126 -0
- data_engine/services/runtime_history.py +62 -0
- data_engine/services/settings.py +58 -0
- data_engine/services/shared_state.py +28 -0
- data_engine/services/theme.py +59 -0
- data_engine/services/workspace_provisioning.py +224 -0
- data_engine/services/workspaces.py +74 -0
- data_engine/ui/__init__.py +3 -0
- data_engine/ui/cli/__init__.py +19 -0
- data_engine/ui/cli/app.py +161 -0
- data_engine/ui/cli/commands_doctor.py +178 -0
- data_engine/ui/cli/commands_run.py +80 -0
- data_engine/ui/cli/commands_start.py +100 -0
- data_engine/ui/cli/commands_workspace.py +97 -0
- data_engine/ui/cli/dependencies.py +44 -0
- data_engine/ui/cli/parser.py +56 -0
- data_engine/ui/gui/__init__.py +25 -0
- data_engine/ui/gui/app.py +116 -0
- data_engine/ui/gui/bootstrap.py +487 -0
- data_engine/ui/gui/bootstrapper.py +140 -0
- data_engine/ui/gui/cache_models.py +23 -0
- data_engine/ui/gui/control_support.py +185 -0
- data_engine/ui/gui/controllers/__init__.py +6 -0
- data_engine/ui/gui/controllers/flows.py +439 -0
- data_engine/ui/gui/controllers/runtime.py +245 -0
- data_engine/ui/gui/dialogs/__init__.py +12 -0
- data_engine/ui/gui/dialogs/messages.py +88 -0
- data_engine/ui/gui/dialogs/previews.py +222 -0
- data_engine/ui/gui/helpers/__init__.py +62 -0
- data_engine/ui/gui/helpers/inspection.py +81 -0
- data_engine/ui/gui/helpers/lifecycle.py +112 -0
- data_engine/ui/gui/helpers/scroll.py +28 -0
- data_engine/ui/gui/helpers/theming.py +87 -0
- data_engine/ui/gui/icons/dark_light.svg +12 -0
- data_engine/ui/gui/icons/documentation.svg +1 -0
- data_engine/ui/gui/icons/failed.svg +3 -0
- data_engine/ui/gui/icons/group.svg +4 -0
- data_engine/ui/gui/icons/home.svg +2 -0
- data_engine/ui/gui/icons/manual.svg +2 -0
- data_engine/ui/gui/icons/poll.svg +2 -0
- data_engine/ui/gui/icons/schedule.svg +4 -0
- data_engine/ui/gui/icons/settings.svg +2 -0
- data_engine/ui/gui/icons/started.svg +3 -0
- data_engine/ui/gui/icons/success.svg +3 -0
- data_engine/ui/gui/icons/view-log.svg +3 -0
- data_engine/ui/gui/icons.py +50 -0
- data_engine/ui/gui/launcher.py +48 -0
- data_engine/ui/gui/presenters/__init__.py +72 -0
- data_engine/ui/gui/presenters/docs.py +140 -0
- data_engine/ui/gui/presenters/logs.py +58 -0
- data_engine/ui/gui/presenters/runtime_projection.py +29 -0
- data_engine/ui/gui/presenters/sidebar.py +88 -0
- data_engine/ui/gui/presenters/steps.py +148 -0
- data_engine/ui/gui/presenters/workspace.py +39 -0
- data_engine/ui/gui/presenters/workspace_binding.py +75 -0
- data_engine/ui/gui/presenters/workspace_settings.py +182 -0
- data_engine/ui/gui/preview_models.py +37 -0
- data_engine/ui/gui/render_support.py +241 -0
- data_engine/ui/gui/rendering/__init__.py +12 -0
- data_engine/ui/gui/rendering/artifacts.py +95 -0
- data_engine/ui/gui/rendering/icons.py +50 -0
- data_engine/ui/gui/runtime.py +47 -0
- data_engine/ui/gui/state_support.py +193 -0
- data_engine/ui/gui/support.py +214 -0
- data_engine/ui/gui/surface.py +209 -0
- data_engine/ui/gui/theme.py +720 -0
- data_engine/ui/gui/widgets/__init__.py +34 -0
- data_engine/ui/gui/widgets/config.py +41 -0
- data_engine/ui/gui/widgets/logs.py +62 -0
- data_engine/ui/gui/widgets/panels.py +507 -0
- data_engine/ui/gui/widgets/sidebar.py +130 -0
- data_engine/ui/gui/widgets/steps.py +84 -0
- data_engine/ui/tui/__init__.py +5 -0
- data_engine/ui/tui/app.py +222 -0
- data_engine/ui/tui/bootstrap.py +475 -0
- data_engine/ui/tui/bootstrapper.py +117 -0
- data_engine/ui/tui/controllers/__init__.py +6 -0
- data_engine/ui/tui/controllers/flows.py +349 -0
- data_engine/ui/tui/controllers/runtime.py +167 -0
- data_engine/ui/tui/runtime.py +34 -0
- data_engine/ui/tui/state_support.py +141 -0
- data_engine/ui/tui/support.py +63 -0
- data_engine/ui/tui/theme.py +204 -0
- data_engine/ui/tui/widgets.py +123 -0
- data_engine/views/__init__.py +109 -0
- data_engine/views/actions.py +80 -0
- data_engine/views/artifacts.py +58 -0
- data_engine/views/flow_display.py +69 -0
- data_engine/views/logs.py +54 -0
- data_engine/views/models.py +96 -0
- data_engine/views/presentation.py +133 -0
- data_engine/views/runs.py +62 -0
- data_engine/views/state.py +39 -0
- data_engine/views/status.py +13 -0
- data_engine/views/text.py +109 -0
- py_data_engine-0.1.0.dist-info/METADATA +330 -0
- py_data_engine-0.1.0.dist-info/RECORD +200 -0
- py_data_engine-0.1.0.dist-info/WHEEL +5 -0
- py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
- py_data_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Sidebar row builders for the desktop UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtGui import QIcon, QPixmap
|
|
9
|
+
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QSizePolicy, QVBoxLayout
|
|
10
|
+
|
|
11
|
+
from data_engine.views.flow_display import FlowRowDisplay, GroupRowDisplay
|
|
12
|
+
from data_engine.views.presentation import status_color_name as shared_status_color_name
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
16
|
+
from data_engine.views.models import QtFlowCard
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def group_secondary_text(window: "DataEngineWindow", group_name: str, entries: list["QtFlowCard"]) -> str:
|
|
20
|
+
return GroupRowDisplay.from_group(group_name, entries, window.flow_states).secondary
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def flow_secondary_text(window: "DataEngineWindow", card: "QtFlowCard") -> str:
|
|
24
|
+
state = window.flow_states.get(card.name, card.state)
|
|
25
|
+
return FlowRowDisplay.from_card(card, state, primary="name").secondary
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def flow_primary_text(card: "QtFlowCard") -> str:
|
|
29
|
+
return card.name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def status_color_name(state: str) -> str:
|
|
33
|
+
return shared_status_color_name(state)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def icon_label(icon: QIcon, size: int = 18) -> QLabel:
|
|
37
|
+
label = QLabel()
|
|
38
|
+
label.setObjectName("sidebarIcon")
|
|
39
|
+
pixmap = icon.pixmap(size, size)
|
|
40
|
+
label.setPixmap(QPixmap(pixmap))
|
|
41
|
+
label.setFixedSize(size + 8, size + 8)
|
|
42
|
+
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
43
|
+
return label
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_group_row_widget(window: "DataEngineWindow", group_name: str, entries: list["QtFlowCard"]) -> QFrame:
|
|
47
|
+
group_display = GroupRowDisplay.from_group(group_name, entries, window.flow_states)
|
|
48
|
+
frame = QFrame()
|
|
49
|
+
frame.setObjectName("sidebarGroupRow")
|
|
50
|
+
frame.setProperty("groupName", group_name)
|
|
51
|
+
frame.setProperty("hovered", False)
|
|
52
|
+
frame.setFixedHeight(44)
|
|
53
|
+
row = QHBoxLayout(frame)
|
|
54
|
+
row.setContentsMargins(0, 8, 0, 2)
|
|
55
|
+
row.setSpacing(8)
|
|
56
|
+
icon = QLabel()
|
|
57
|
+
icon.setObjectName("sidebarIcon")
|
|
58
|
+
icon.setPixmap(window._render_group_icon_pixmap(group_name, 16))
|
|
59
|
+
icon.setFixedSize(24, 24)
|
|
60
|
+
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
61
|
+
row.addWidget(icon)
|
|
62
|
+
|
|
63
|
+
text_col = QVBoxLayout()
|
|
64
|
+
text_col.setContentsMargins(0, 0, 0, 0)
|
|
65
|
+
text_col.setSpacing(1)
|
|
66
|
+
title = QLabel(group_display.title)
|
|
67
|
+
title.setObjectName("sidebarGroupTitle")
|
|
68
|
+
subtitle = QLabel(group_display.secondary)
|
|
69
|
+
subtitle.setObjectName("sidebarGroupMeta")
|
|
70
|
+
text_col.addWidget(title)
|
|
71
|
+
text_col.addWidget(subtitle)
|
|
72
|
+
row.addLayout(text_col, 1)
|
|
73
|
+
frame.enterEvent = lambda event, widget=frame: window._set_hovered(widget, True)
|
|
74
|
+
frame.leaveEvent = lambda event, widget=frame: window._set_hovered(widget, False)
|
|
75
|
+
return frame
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_flow_row_widget(window: "DataEngineWindow", card: "QtFlowCard") -> QFrame:
|
|
79
|
+
flow_display = FlowRowDisplay.from_card(card, window.flow_states.get(card.name, card.state), primary="name")
|
|
80
|
+
frame = QFrame()
|
|
81
|
+
frame.setObjectName("sidebarFlowRow")
|
|
82
|
+
frame.setProperty("selected", False)
|
|
83
|
+
frame.setProperty("hovered", False)
|
|
84
|
+
frame.setFixedHeight(42)
|
|
85
|
+
row = QHBoxLayout(frame)
|
|
86
|
+
row.setContentsMargins(12, 4, 8, 4)
|
|
87
|
+
row.setSpacing(10)
|
|
88
|
+
number = QLabel("00")
|
|
89
|
+
number.setObjectName("sidebarFlowNumber")
|
|
90
|
+
row.addWidget(number)
|
|
91
|
+
|
|
92
|
+
text_col = QVBoxLayout()
|
|
93
|
+
text_col.setContentsMargins(0, 0, 0, 0)
|
|
94
|
+
text_col.setSpacing(1)
|
|
95
|
+
title = QLabel(flow_primary_text(card))
|
|
96
|
+
title.setObjectName("sidebarFlowCode")
|
|
97
|
+
subtitle = QLabel(flow_display.secondary)
|
|
98
|
+
subtitle.setObjectName("sidebarFlowMeta")
|
|
99
|
+
subtitle.setProperty("stateColor", flow_display.state_color)
|
|
100
|
+
text_col.addWidget(title)
|
|
101
|
+
text_col.addWidget(subtitle)
|
|
102
|
+
row.addLayout(text_col, 1)
|
|
103
|
+
|
|
104
|
+
state_dot = QLabel("\u25cf")
|
|
105
|
+
state_dot.setObjectName("sidebarStateDot")
|
|
106
|
+
state_dot.setProperty("stateColor", flow_display.state_color)
|
|
107
|
+
state_dot.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
108
|
+
state_dot.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
|
109
|
+
row.addWidget(state_dot)
|
|
110
|
+
frame.setToolTip(flow_display.tooltip)
|
|
111
|
+
frame.mousePressEvent = lambda event, flow_name=card.name: window._select_flow(flow_name)
|
|
112
|
+
frame.enterEvent = lambda event, widget=frame: window._set_hovered(widget, True)
|
|
113
|
+
frame.leaveEvent = lambda event, widget=frame: window._set_hovered(widget, False)
|
|
114
|
+
return frame
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def group_label(group_name: str) -> str:
|
|
118
|
+
return GroupRowDisplay.from_group(group_name, [], {}).title
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"build_flow_row_widget",
|
|
123
|
+
"build_group_row_widget",
|
|
124
|
+
"flow_primary_text",
|
|
125
|
+
"flow_secondary_text",
|
|
126
|
+
"group_label",
|
|
127
|
+
"group_secondary_text",
|
|
128
|
+
"icon_label",
|
|
129
|
+
"status_color_name",
|
|
130
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Step-list view helpers for the desktop UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton
|
|
9
|
+
from data_engine.ui.gui.cache_models import OperationRowWidgets
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from data_engine.ui.gui.app import DataEngineWindow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_operation_cards(window: "DataEngineWindow", operation_items: tuple[str, ...]) -> None:
|
|
16
|
+
for timer in list(window.operation_flash_timers):
|
|
17
|
+
timer.stop()
|
|
18
|
+
window.operation_flash_timers.clear()
|
|
19
|
+
|
|
20
|
+
while window.operation_layout.count() > 1:
|
|
21
|
+
item = window.operation_layout.takeAt(0)
|
|
22
|
+
widget = item.widget()
|
|
23
|
+
if widget is not None:
|
|
24
|
+
widget.deleteLater()
|
|
25
|
+
window.operation_row_widgets = []
|
|
26
|
+
|
|
27
|
+
if not operation_items:
|
|
28
|
+
empty = QFrame()
|
|
29
|
+
empty.setObjectName("operationCard")
|
|
30
|
+
row = QHBoxLayout(empty)
|
|
31
|
+
row.setContentsMargins(12, 9, 12, 9)
|
|
32
|
+
label = QLabel("No steps configured.")
|
|
33
|
+
label.setObjectName("bodyText")
|
|
34
|
+
row.addWidget(label)
|
|
35
|
+
window.operation_layout.insertWidget(0, empty)
|
|
36
|
+
window._update_operation_scroll_cues()
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
for index, name in enumerate(operation_items, start=1):
|
|
40
|
+
card = QFrame()
|
|
41
|
+
card.setObjectName("operationCard")
|
|
42
|
+
row = QHBoxLayout(card)
|
|
43
|
+
row.setContentsMargins(12, 9, 12, 9)
|
|
44
|
+
row.setSpacing(10)
|
|
45
|
+
step = QLabel(f"{index:02d}")
|
|
46
|
+
step.setObjectName("operationStep")
|
|
47
|
+
title = QLabel(format_operation_title(name))
|
|
48
|
+
title.setObjectName("operationTitle")
|
|
49
|
+
title.setTextFormat(Qt.TextFormat.RichText)
|
|
50
|
+
duration = QLabel("")
|
|
51
|
+
duration.setObjectName("operationDuration")
|
|
52
|
+
duration.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
53
|
+
inspect_button: QPushButton | None = None
|
|
54
|
+
row.addWidget(step)
|
|
55
|
+
row.addWidget(title, 1)
|
|
56
|
+
if window._is_inspectable_operation(name):
|
|
57
|
+
inspect_button = QPushButton("Inspect")
|
|
58
|
+
inspect_button.setObjectName("inspectOutputButton")
|
|
59
|
+
inspect_button.clicked.connect(lambda _checked=False, operation_name=name: window._inspect_step_output(operation_name))
|
|
60
|
+
row.addWidget(inspect_button)
|
|
61
|
+
row.addWidget(duration)
|
|
62
|
+
window.operation_layout.insertWidget(index - 1, card)
|
|
63
|
+
window.operation_row_widgets.append(
|
|
64
|
+
OperationRowWidgets(
|
|
65
|
+
row_card=card,
|
|
66
|
+
title_label=title,
|
|
67
|
+
duration_label=duration,
|
|
68
|
+
inspect_button=inspect_button,
|
|
69
|
+
operation_name=name,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if window.selected_flow_name is not None:
|
|
73
|
+
window._refresh_operation_buttons(window.selected_flow_name)
|
|
74
|
+
window._update_operation_scroll_cues()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def format_operation_title(operation_name: str) -> str:
|
|
78
|
+
head, separator, tail = operation_name.partition(":")
|
|
79
|
+
if not separator:
|
|
80
|
+
return f"<b>{head}</b>"
|
|
81
|
+
return f"<b>{head}</b><span style='font-weight: 400;'> </span><i><span style='font-weight: 400;'>{tail}</span></i>"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["format_operation_title", "set_operation_cards"]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Textual-based terminal UI for Data Engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from queue import Empty
|
|
7
|
+
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import Container, Grid, Horizontal, Vertical
|
|
11
|
+
from textual.widgets import Button, Footer, ListView, Select, Static
|
|
12
|
+
|
|
13
|
+
from data_engine.domain import (
|
|
14
|
+
FlowRunState,
|
|
15
|
+
RunDetailState,
|
|
16
|
+
)
|
|
17
|
+
from data_engine.authoring.model import FlowValidationError
|
|
18
|
+
from data_engine.views.text import render_run_group_lines
|
|
19
|
+
from data_engine.ui.tui.bootstrap import TuiServices
|
|
20
|
+
from data_engine.ui.tui.bootstrapper import bootstrap_tui_app
|
|
21
|
+
from data_engine.ui.tui.theme import DEFAULT_THEME, stylesheet as tui_stylesheet
|
|
22
|
+
from data_engine.ui.tui.state_support import TuiStateMixin
|
|
23
|
+
from data_engine.ui.tui.support import TuiWindowSupportMixin
|
|
24
|
+
from data_engine.ui.tui.widgets import FlowListItem, InfoModal, RunGroupListItem
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DataEngineTui(TuiWindowSupportMixin, TuiStateMixin, App[None]):
|
|
28
|
+
"""Full-screen terminal UI for headless Data Engine operation."""
|
|
29
|
+
|
|
30
|
+
CSS = tui_stylesheet(DEFAULT_THEME)
|
|
31
|
+
_ACTIVE_FLOW_STATES = {"running", "polling", "scheduled", "stopping flow", "stopping runtime"}
|
|
32
|
+
|
|
33
|
+
BINDINGS = [
|
|
34
|
+
Binding("q", "quit", "Quit"),
|
|
35
|
+
Binding("r", "refresh_flows", "Refresh"),
|
|
36
|
+
Binding("enter", "run_selected", "Run"),
|
|
37
|
+
Binding("e", "start_engine", "Start Engine"),
|
|
38
|
+
Binding("s", "stop_engine", "Stop"),
|
|
39
|
+
Binding("v", "view_log", "View Log"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, theme_name: str = DEFAULT_THEME, services: TuiServices | None = None) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
bootstrap_tui_app(self, theme_name=theme_name, services=services)
|
|
45
|
+
|
|
46
|
+
def compose(self) -> ComposeResult:
|
|
47
|
+
with Horizontal(id="header"):
|
|
48
|
+
with Vertical(id="header-copy"):
|
|
49
|
+
yield Static("Flow Control", id="screen-title")
|
|
50
|
+
yield Static("Monitor and operate one workspace daemon from the terminal.", id="screen-subtitle")
|
|
51
|
+
yield Static("Workspace runtime is idle.", id="status-line")
|
|
52
|
+
yield Static("", id="control-status")
|
|
53
|
+
with Horizontal(id="header-actions"):
|
|
54
|
+
with Horizontal(id="header-controls"):
|
|
55
|
+
yield Button("Start Engine", id="start-engine")
|
|
56
|
+
yield Button("Stop", id="stop-engine")
|
|
57
|
+
yield Button("Refresh", id="refresh")
|
|
58
|
+
yield Select([], prompt="Workspace", allow_blank=True, id="workspace-select")
|
|
59
|
+
with Grid(id="body"):
|
|
60
|
+
with Container(id="flow-list-pane"):
|
|
61
|
+
yield Static("CONFIGURED FLOWS", classes="pane-title")
|
|
62
|
+
yield ListView(id="flow-list")
|
|
63
|
+
with Container(id="detail-pane"):
|
|
64
|
+
yield Static("STEPS", classes="pane-title")
|
|
65
|
+
with Horizontal(classes="pane-toolbar"):
|
|
66
|
+
yield Button("Run Once", id="run-once")
|
|
67
|
+
yield Button("View Config", id="view-config")
|
|
68
|
+
yield Static("", id="detail-view")
|
|
69
|
+
with Container(id="log-pane"):
|
|
70
|
+
yield Static("LOGS", classes="pane-title")
|
|
71
|
+
with Horizontal(classes="pane-toolbar"):
|
|
72
|
+
yield Button("View Log", id="view-log")
|
|
73
|
+
yield Button("Clear Flow Log", id="clear-flow-log")
|
|
74
|
+
yield ListView(id="log-run-list")
|
|
75
|
+
yield Footer()
|
|
76
|
+
|
|
77
|
+
def on_mount(self) -> None:
|
|
78
|
+
logger = logging.getLogger("data_engine")
|
|
79
|
+
logger.setLevel(logging.INFO)
|
|
80
|
+
logger.addHandler(self.log_handler)
|
|
81
|
+
self._register_client_session()
|
|
82
|
+
self._reload_workspace_options()
|
|
83
|
+
self._load_flows()
|
|
84
|
+
if self._has_authored_workspace():
|
|
85
|
+
self._ensure_daemon_started()
|
|
86
|
+
self._sync_daemon_state()
|
|
87
|
+
self.set_interval(0.15, self._poll_ui)
|
|
88
|
+
self._refresh_buttons()
|
|
89
|
+
|
|
90
|
+
def on_unmount(self) -> None:
|
|
91
|
+
logging.getLogger("data_engine").removeHandler(self.log_handler)
|
|
92
|
+
if self._unregister_client_session_and_check_for_shutdown():
|
|
93
|
+
self._shutdown_daemon_on_close()
|
|
94
|
+
self.runtime_binding_service.close_binding(self.runtime_binding)
|
|
95
|
+
|
|
96
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
97
|
+
if isinstance(event.item, FlowListItem):
|
|
98
|
+
self.selected_flow_name = event.item.card.name
|
|
99
|
+
self._render_selected_flow()
|
|
100
|
+
elif isinstance(event.item, RunGroupListItem):
|
|
101
|
+
self.selected_run_key = event.item.run_group.key
|
|
102
|
+
self._show_run_group_modal(event.item.run_group)
|
|
103
|
+
|
|
104
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
105
|
+
button_id = event.button.id
|
|
106
|
+
if button_id == "refresh":
|
|
107
|
+
self.action_refresh_flows()
|
|
108
|
+
elif button_id == "run-once":
|
|
109
|
+
self.action_run_selected()
|
|
110
|
+
elif button_id == "start-engine":
|
|
111
|
+
self.action_start_engine()
|
|
112
|
+
elif button_id == "stop-engine":
|
|
113
|
+
self.action_stop_engine()
|
|
114
|
+
elif button_id == "view-config":
|
|
115
|
+
self.action_view_config()
|
|
116
|
+
elif button_id == "view-log":
|
|
117
|
+
self.action_view_log()
|
|
118
|
+
elif button_id == "clear-flow-log":
|
|
119
|
+
self.action_clear_flow_log()
|
|
120
|
+
|
|
121
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
122
|
+
if event.select.id != "workspace-select":
|
|
123
|
+
return
|
|
124
|
+
if self._workspace_switch_suppressed or not self.is_mounted:
|
|
125
|
+
return
|
|
126
|
+
if event.value in {Select.NULL, Select.BLANK}:
|
|
127
|
+
return
|
|
128
|
+
workspace_id = str(event.value or "").strip()
|
|
129
|
+
if not workspace_id or workspace_id == self.workspace_paths.workspace_id:
|
|
130
|
+
return
|
|
131
|
+
self._switch_workspace(workspace_id)
|
|
132
|
+
|
|
133
|
+
def action_refresh_flows(self) -> None:
|
|
134
|
+
self.flow_controller.action_refresh_flows(self)
|
|
135
|
+
|
|
136
|
+
def action_run_selected(self) -> None:
|
|
137
|
+
self.flow_controller.action_run_selected(self)
|
|
138
|
+
|
|
139
|
+
def action_start_engine(self) -> None:
|
|
140
|
+
self.flow_controller.action_start_engine(self)
|
|
141
|
+
|
|
142
|
+
def action_stop_engine(self) -> None:
|
|
143
|
+
self.flow_controller.action_stop_engine(self)
|
|
144
|
+
|
|
145
|
+
def action_view_config(self) -> None:
|
|
146
|
+
self.flow_controller.action_view_config(self)
|
|
147
|
+
|
|
148
|
+
def action_clear_flow_log(self) -> None:
|
|
149
|
+
self.flow_controller.action_clear_flow_log(self)
|
|
150
|
+
|
|
151
|
+
def action_view_log(self) -> None:
|
|
152
|
+
self.flow_controller.action_view_log(self)
|
|
153
|
+
|
|
154
|
+
def _load_flows(self) -> None:
|
|
155
|
+
self.flow_controller.load_flows(self)
|
|
156
|
+
|
|
157
|
+
def _reload_workspace_options(self) -> None:
|
|
158
|
+
self.flow_controller.reload_workspace_options(self)
|
|
159
|
+
|
|
160
|
+
def _switch_workspace(self, workspace_id: str) -> None:
|
|
161
|
+
self.flow_controller.switch_workspace(self, workspace_id)
|
|
162
|
+
|
|
163
|
+
def _render_selected_flow(self) -> None:
|
|
164
|
+
self.flow_controller.render_selected_flow(self)
|
|
165
|
+
|
|
166
|
+
def _selected_run_group(self) -> FlowRunState | None:
|
|
167
|
+
return self.flow_controller.selected_run_group(self)
|
|
168
|
+
|
|
169
|
+
def _show_run_group_modal(self, run_group: FlowRunState) -> None:
|
|
170
|
+
detail = RunDetailState.from_run(run_group)
|
|
171
|
+
lines = render_run_group_lines(run_group)
|
|
172
|
+
self.push_screen(InfoModal(title=f"Run Details · {detail.source_label}", body="\n".join(lines)))
|
|
173
|
+
|
|
174
|
+
def _poll_ui(self) -> None:
|
|
175
|
+
while True:
|
|
176
|
+
try:
|
|
177
|
+
entry = self.log_queue.get_nowait()
|
|
178
|
+
except Empty:
|
|
179
|
+
break
|
|
180
|
+
del entry
|
|
181
|
+
self._sync_daemon_state()
|
|
182
|
+
|
|
183
|
+
def _refresh_flow_list_items(self) -> None:
|
|
184
|
+
self.runtime_controller.refresh_flow_list_items(self)
|
|
185
|
+
|
|
186
|
+
def _refresh_buttons(self) -> None:
|
|
187
|
+
self.runtime_controller.refresh_buttons(self)
|
|
188
|
+
|
|
189
|
+
def _set_status(self, message: str) -> None:
|
|
190
|
+
self.query_one("#status-line", Static).update(message)
|
|
191
|
+
|
|
192
|
+
def _sync_daemon_state(self) -> None:
|
|
193
|
+
self.runtime_controller.sync_daemon_state(self)
|
|
194
|
+
|
|
195
|
+
def _ensure_daemon_started(self) -> bool:
|
|
196
|
+
return self.runtime_controller.ensure_daemon_started(self)
|
|
197
|
+
|
|
198
|
+
def _start_daemon_worker(self) -> None:
|
|
199
|
+
self.runtime_controller.start_daemon_worker(self)
|
|
200
|
+
|
|
201
|
+
def _finish_daemon_startup(self, success: bool, error_text: str) -> None:
|
|
202
|
+
self.runtime_controller.finish_daemon_startup(self, success, error_text)
|
|
203
|
+
|
|
204
|
+
def _rebuild_runtime_snapshot(self) -> None:
|
|
205
|
+
self.runtime_controller.rebuild_runtime_snapshot(self)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
"""Launch the Textual terminal UI."""
|
|
210
|
+
try:
|
|
211
|
+
app = DataEngineTui()
|
|
212
|
+
app.run()
|
|
213
|
+
except ModuleNotFoundError as exc: # pragma: no cover - import-time dependency guard
|
|
214
|
+
raise SystemExit(
|
|
215
|
+
"The terminal UI requires the 'textual' package. Reinstall Data Engine after updating dependencies."
|
|
216
|
+
) from exc
|
|
217
|
+
except FlowValidationError as exc:
|
|
218
|
+
raise SystemExit(str(exc)) from exc
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = ["DataEngineTui", "main"]
|
|
222
|
+
__all__ += ["FlowListItem", "GroupHeaderListItem", "RunGroupListItem", "InfoModal", "QueueLogHandler"]
|