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.
Files changed (200) hide show
  1. data_engine/__init__.py +37 -0
  2. data_engine/application/__init__.py +39 -0
  3. data_engine/application/actions.py +42 -0
  4. data_engine/application/catalog.py +151 -0
  5. data_engine/application/control.py +213 -0
  6. data_engine/application/details.py +73 -0
  7. data_engine/application/runtime.py +449 -0
  8. data_engine/application/workspace.py +62 -0
  9. data_engine/authoring/__init__.py +14 -0
  10. data_engine/authoring/builder.py +31 -0
  11. data_engine/authoring/execution/__init__.py +6 -0
  12. data_engine/authoring/execution/app.py +6 -0
  13. data_engine/authoring/execution/context.py +82 -0
  14. data_engine/authoring/execution/continuous.py +176 -0
  15. data_engine/authoring/execution/grouped.py +106 -0
  16. data_engine/authoring/execution/logging.py +83 -0
  17. data_engine/authoring/execution/polling.py +135 -0
  18. data_engine/authoring/execution/runner.py +210 -0
  19. data_engine/authoring/execution/single.py +171 -0
  20. data_engine/authoring/flow.py +361 -0
  21. data_engine/authoring/helpers.py +160 -0
  22. data_engine/authoring/model.py +59 -0
  23. data_engine/authoring/primitives.py +430 -0
  24. data_engine/authoring/services.py +42 -0
  25. data_engine/devtools/__init__.py +3 -0
  26. data_engine/devtools/project_ast_map.py +503 -0
  27. data_engine/docs/__init__.py +1 -0
  28. data_engine/docs/sphinx_source/_static/custom.css +13 -0
  29. data_engine/docs/sphinx_source/api.rst +42 -0
  30. data_engine/docs/sphinx_source/conf.py +37 -0
  31. data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
  32. data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
  33. data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
  34. data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
  35. data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
  36. data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
  37. data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
  38. data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
  39. data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
  40. data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
  41. data_engine/docs/sphinx_source/guides/project-map.md +118 -0
  42. data_engine/docs/sphinx_source/guides/recipes.md +268 -0
  43. data_engine/docs/sphinx_source/index.rst +22 -0
  44. data_engine/domain/__init__.py +92 -0
  45. data_engine/domain/actions.py +69 -0
  46. data_engine/domain/catalog.py +128 -0
  47. data_engine/domain/details.py +214 -0
  48. data_engine/domain/diagnostics.py +56 -0
  49. data_engine/domain/errors.py +104 -0
  50. data_engine/domain/inspection.py +99 -0
  51. data_engine/domain/logs.py +118 -0
  52. data_engine/domain/operations.py +172 -0
  53. data_engine/domain/operator.py +72 -0
  54. data_engine/domain/runs.py +155 -0
  55. data_engine/domain/runtime.py +279 -0
  56. data_engine/domain/source_state.py +17 -0
  57. data_engine/domain/support.py +54 -0
  58. data_engine/domain/time.py +23 -0
  59. data_engine/domain/workspace.py +159 -0
  60. data_engine/flow_modules/__init__.py +1 -0
  61. data_engine/flow_modules/flow_module_compiler.py +179 -0
  62. data_engine/flow_modules/flow_module_loader.py +201 -0
  63. data_engine/helpers/__init__.py +25 -0
  64. data_engine/helpers/duckdb.py +705 -0
  65. data_engine/hosts/__init__.py +1 -0
  66. data_engine/hosts/daemon/__init__.py +23 -0
  67. data_engine/hosts/daemon/app.py +221 -0
  68. data_engine/hosts/daemon/bootstrap.py +69 -0
  69. data_engine/hosts/daemon/client.py +465 -0
  70. data_engine/hosts/daemon/commands.py +64 -0
  71. data_engine/hosts/daemon/composition.py +310 -0
  72. data_engine/hosts/daemon/constants.py +15 -0
  73. data_engine/hosts/daemon/entrypoints.py +97 -0
  74. data_engine/hosts/daemon/lifecycle.py +191 -0
  75. data_engine/hosts/daemon/manager.py +272 -0
  76. data_engine/hosts/daemon/ownership.py +126 -0
  77. data_engine/hosts/daemon/runtime_commands.py +188 -0
  78. data_engine/hosts/daemon/runtime_control.py +31 -0
  79. data_engine/hosts/daemon/server.py +84 -0
  80. data_engine/hosts/daemon/shared_state.py +147 -0
  81. data_engine/hosts/daemon/state_sync.py +101 -0
  82. data_engine/platform/__init__.py +1 -0
  83. data_engine/platform/identity.py +35 -0
  84. data_engine/platform/local_settings.py +146 -0
  85. data_engine/platform/theme.py +259 -0
  86. data_engine/platform/workspace_models.py +190 -0
  87. data_engine/platform/workspace_policy.py +333 -0
  88. data_engine/runtime/__init__.py +1 -0
  89. data_engine/runtime/file_watch.py +185 -0
  90. data_engine/runtime/ledger_models.py +116 -0
  91. data_engine/runtime/runtime_db.py +938 -0
  92. data_engine/runtime/shared_state.py +523 -0
  93. data_engine/services/__init__.py +49 -0
  94. data_engine/services/daemon.py +64 -0
  95. data_engine/services/daemon_state.py +40 -0
  96. data_engine/services/flow_catalog.py +102 -0
  97. data_engine/services/flow_execution.py +48 -0
  98. data_engine/services/ledger.py +85 -0
  99. data_engine/services/logs.py +65 -0
  100. data_engine/services/runtime_binding.py +105 -0
  101. data_engine/services/runtime_execution.py +126 -0
  102. data_engine/services/runtime_history.py +62 -0
  103. data_engine/services/settings.py +58 -0
  104. data_engine/services/shared_state.py +28 -0
  105. data_engine/services/theme.py +59 -0
  106. data_engine/services/workspace_provisioning.py +224 -0
  107. data_engine/services/workspaces.py +74 -0
  108. data_engine/ui/__init__.py +3 -0
  109. data_engine/ui/cli/__init__.py +19 -0
  110. data_engine/ui/cli/app.py +161 -0
  111. data_engine/ui/cli/commands_doctor.py +178 -0
  112. data_engine/ui/cli/commands_run.py +80 -0
  113. data_engine/ui/cli/commands_start.py +100 -0
  114. data_engine/ui/cli/commands_workspace.py +97 -0
  115. data_engine/ui/cli/dependencies.py +44 -0
  116. data_engine/ui/cli/parser.py +56 -0
  117. data_engine/ui/gui/__init__.py +25 -0
  118. data_engine/ui/gui/app.py +116 -0
  119. data_engine/ui/gui/bootstrap.py +487 -0
  120. data_engine/ui/gui/bootstrapper.py +140 -0
  121. data_engine/ui/gui/cache_models.py +23 -0
  122. data_engine/ui/gui/control_support.py +185 -0
  123. data_engine/ui/gui/controllers/__init__.py +6 -0
  124. data_engine/ui/gui/controllers/flows.py +439 -0
  125. data_engine/ui/gui/controllers/runtime.py +245 -0
  126. data_engine/ui/gui/dialogs/__init__.py +12 -0
  127. data_engine/ui/gui/dialogs/messages.py +88 -0
  128. data_engine/ui/gui/dialogs/previews.py +222 -0
  129. data_engine/ui/gui/helpers/__init__.py +62 -0
  130. data_engine/ui/gui/helpers/inspection.py +81 -0
  131. data_engine/ui/gui/helpers/lifecycle.py +112 -0
  132. data_engine/ui/gui/helpers/scroll.py +28 -0
  133. data_engine/ui/gui/helpers/theming.py +87 -0
  134. data_engine/ui/gui/icons/dark_light.svg +12 -0
  135. data_engine/ui/gui/icons/documentation.svg +1 -0
  136. data_engine/ui/gui/icons/failed.svg +3 -0
  137. data_engine/ui/gui/icons/group.svg +4 -0
  138. data_engine/ui/gui/icons/home.svg +2 -0
  139. data_engine/ui/gui/icons/manual.svg +2 -0
  140. data_engine/ui/gui/icons/poll.svg +2 -0
  141. data_engine/ui/gui/icons/schedule.svg +4 -0
  142. data_engine/ui/gui/icons/settings.svg +2 -0
  143. data_engine/ui/gui/icons/started.svg +3 -0
  144. data_engine/ui/gui/icons/success.svg +3 -0
  145. data_engine/ui/gui/icons/view-log.svg +3 -0
  146. data_engine/ui/gui/icons.py +50 -0
  147. data_engine/ui/gui/launcher.py +48 -0
  148. data_engine/ui/gui/presenters/__init__.py +72 -0
  149. data_engine/ui/gui/presenters/docs.py +140 -0
  150. data_engine/ui/gui/presenters/logs.py +58 -0
  151. data_engine/ui/gui/presenters/runtime_projection.py +29 -0
  152. data_engine/ui/gui/presenters/sidebar.py +88 -0
  153. data_engine/ui/gui/presenters/steps.py +148 -0
  154. data_engine/ui/gui/presenters/workspace.py +39 -0
  155. data_engine/ui/gui/presenters/workspace_binding.py +75 -0
  156. data_engine/ui/gui/presenters/workspace_settings.py +182 -0
  157. data_engine/ui/gui/preview_models.py +37 -0
  158. data_engine/ui/gui/render_support.py +241 -0
  159. data_engine/ui/gui/rendering/__init__.py +12 -0
  160. data_engine/ui/gui/rendering/artifacts.py +95 -0
  161. data_engine/ui/gui/rendering/icons.py +50 -0
  162. data_engine/ui/gui/runtime.py +47 -0
  163. data_engine/ui/gui/state_support.py +193 -0
  164. data_engine/ui/gui/support.py +214 -0
  165. data_engine/ui/gui/surface.py +209 -0
  166. data_engine/ui/gui/theme.py +720 -0
  167. data_engine/ui/gui/widgets/__init__.py +34 -0
  168. data_engine/ui/gui/widgets/config.py +41 -0
  169. data_engine/ui/gui/widgets/logs.py +62 -0
  170. data_engine/ui/gui/widgets/panels.py +507 -0
  171. data_engine/ui/gui/widgets/sidebar.py +130 -0
  172. data_engine/ui/gui/widgets/steps.py +84 -0
  173. data_engine/ui/tui/__init__.py +5 -0
  174. data_engine/ui/tui/app.py +222 -0
  175. data_engine/ui/tui/bootstrap.py +475 -0
  176. data_engine/ui/tui/bootstrapper.py +117 -0
  177. data_engine/ui/tui/controllers/__init__.py +6 -0
  178. data_engine/ui/tui/controllers/flows.py +349 -0
  179. data_engine/ui/tui/controllers/runtime.py +167 -0
  180. data_engine/ui/tui/runtime.py +34 -0
  181. data_engine/ui/tui/state_support.py +141 -0
  182. data_engine/ui/tui/support.py +63 -0
  183. data_engine/ui/tui/theme.py +204 -0
  184. data_engine/ui/tui/widgets.py +123 -0
  185. data_engine/views/__init__.py +109 -0
  186. data_engine/views/actions.py +80 -0
  187. data_engine/views/artifacts.py +58 -0
  188. data_engine/views/flow_display.py +69 -0
  189. data_engine/views/logs.py +54 -0
  190. data_engine/views/models.py +96 -0
  191. data_engine/views/presentation.py +133 -0
  192. data_engine/views/runs.py +62 -0
  193. data_engine/views/state.py +39 -0
  194. data_engine/views/status.py +13 -0
  195. data_engine/views/text.py +109 -0
  196. py_data_engine-0.1.0.dist-info/METADATA +330 -0
  197. py_data_engine-0.1.0.dist-info/RECORD +200 -0
  198. py_data_engine-0.1.0.dist-info/WHEEL +5 -0
  199. py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
  200. 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,5 @@
1
+ """Textual TUI surface for Data Engine."""
2
+
3
+ from data_engine.ui.tui.app import DataEngineTui, main
4
+
5
+ __all__ = ["DataEngineTui", "main"]
@@ -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"]