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,12 @@
1
+ """Dialog-layer helpers for the desktop UI."""
2
+
3
+ from data_engine.ui.gui.dialogs.messages import show_message_box, structured_error_content
4
+ from data_engine.ui.gui.dialogs.previews import show_config_preview, show_output_preview, show_run_log_preview
5
+
6
+ __all__ = [
7
+ "show_config_preview",
8
+ "show_message_box",
9
+ "show_output_preview",
10
+ "show_run_log_preview",
11
+ "structured_error_content",
12
+ ]
@@ -0,0 +1,88 @@
1
+ """Message dialog helpers for the desktop UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QVBoxLayout
8
+ from data_engine.domain import StructuredErrorState
9
+ from data_engine.ui.gui.widgets import make_label_selectable
10
+
11
+ if TYPE_CHECKING:
12
+ from data_engine.ui.gui.app import DataEngineWindow
13
+
14
+
15
+ def structured_error_content(text: str) -> StructuredErrorState | None:
16
+ """Parse one developer-facing flow-module error into dialog sections when possible."""
17
+ return StructuredErrorState.parse(text)
18
+
19
+
20
+ def show_message_box(window: "DataEngineWindow", *, title: str, text: str, tone: str) -> None:
21
+ """Show one simple application dialog for info/error messages."""
22
+ dialog = QDialog(window)
23
+ dialog.setWindowTitle(title)
24
+ dialog.setModal(True)
25
+ dialog.resize(520, 220)
26
+ layout = QVBoxLayout(dialog)
27
+ layout.setContentsMargins(20, 18, 20, 18)
28
+ layout.setSpacing(14)
29
+
30
+ structured = structured_error_content(text) if tone == "error" else None
31
+ title_label = QLabel(structured.title if structured is not None else title)
32
+ title_label.setObjectName("sectionTitle")
33
+ layout.addWidget(title_label)
34
+
35
+ if structured is None:
36
+ body_label = QLabel(text)
37
+ body_label.setWordWrap(True)
38
+ body_label.setObjectName("errorText" if tone == "error" else "bodyText")
39
+ make_label_selectable(body_label)
40
+ layout.addWidget(body_label, 1)
41
+ else:
42
+ summary_grid = QGridLayout()
43
+ summary_grid.setContentsMargins(0, 0, 0, 0)
44
+ summary_grid.setHorizontalSpacing(10)
45
+ summary_grid.setVerticalSpacing(6)
46
+ for row_index, field in enumerate(structured.fields):
47
+ label = QLabel(field.label)
48
+ label.setObjectName("fieldLabel")
49
+ value = QLabel(field.value)
50
+ value.setObjectName("fieldValue")
51
+ value.setWordWrap(True)
52
+ make_label_selectable(value)
53
+ summary_grid.addWidget(label, row_index, 0)
54
+ summary_grid.addWidget(value, row_index, 1)
55
+ layout.addLayout(summary_grid)
56
+
57
+ detail_label = QLabel("Error")
58
+ detail_label.setObjectName("fieldLabel")
59
+ layout.addWidget(detail_label)
60
+
61
+ detail_body = QTextEdit()
62
+ detail_body.setObjectName("outputPreviewText")
63
+ detail_body.setReadOnly(True)
64
+ detail_body.setPlainText(structured.detail)
65
+ detail_body.setMinimumHeight(88)
66
+ layout.addWidget(detail_body, 1)
67
+
68
+ raw_label = QLabel("Details")
69
+ raw_label.setObjectName("fieldLabel")
70
+ layout.addWidget(raw_label)
71
+
72
+ raw_body = QTextEdit()
73
+ raw_body.setObjectName("outputPreviewText")
74
+ raw_body.setReadOnly(True)
75
+ raw_body.setPlainText(structured.raw_text)
76
+ raw_body.setMinimumHeight(88)
77
+ layout.addWidget(raw_body, 1)
78
+
79
+ action_row = QHBoxLayout()
80
+ action_row.addStretch(1)
81
+ close_button = QPushButton("OK")
82
+ close_button.clicked.connect(dialog.accept)
83
+ action_row.addWidget(close_button)
84
+ layout.addLayout(action_row)
85
+ dialog.exec()
86
+
87
+
88
+ __all__ = ["show_message_box", "structured_error_content"]
@@ -0,0 +1,222 @@
1
+ """Preview dialog helpers for the desktop UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import Qt
9
+ from PySide6.QtWidgets import (
10
+ QDialog,
11
+ QFrame,
12
+ QHBoxLayout,
13
+ QLabel,
14
+ QListWidget,
15
+ QListWidgetItem,
16
+ QPushButton,
17
+ QVBoxLayout,
18
+ QWidget,
19
+ )
20
+
21
+ from data_engine.ui.gui.preview_models import ConfigPreviewRequest, OutputPreviewRequest, RunLogPreviewRequest
22
+ from data_engine.ui.gui.rendering import populate_output_preview, render_svg_icon_pixmap
23
+ from data_engine.ui.gui.widgets import build_config_value, make_label_selectable
24
+
25
+ if TYPE_CHECKING:
26
+ from data_engine.domain import FlowLogEntry, FlowRunState
27
+ from data_engine.views.models import QtFlowCard
28
+ from data_engine.ui.gui.app import DataEngineWindow
29
+
30
+
31
+ def show_run_log_preview(window: "DataEngineWindow", request: RunLogPreviewRequest) -> QDialog:
32
+ detail = request.detail
33
+ dialog = QDialog(window)
34
+ dialog.setWindowTitle("Run Log")
35
+ dialog.setObjectName("outputPreviewDialog")
36
+ dialog.resize(760, 520)
37
+ layout = QVBoxLayout(dialog)
38
+ layout.setContentsMargins(18, 18, 18, 18)
39
+ layout.setSpacing(12)
40
+
41
+ header = QFrame()
42
+ header.setObjectName("outputPreviewHeader")
43
+ header_layout = QVBoxLayout(header)
44
+ header_layout.setContentsMargins(14, 14, 14, 14)
45
+ header_layout.setSpacing(4)
46
+
47
+ title_label = QLabel(detail.display_label)
48
+ title_label.setObjectName("heroTitle")
49
+ header_layout.addWidget(title_label)
50
+
51
+ summary_parts = [detail.status.title()]
52
+ if detail.elapsed_seconds is not None:
53
+ summary_parts.append(window._format_seconds(detail.elapsed_seconds))
54
+ summary_label = QLabel(" • ".join(summary_parts))
55
+ summary_label.setObjectName("sectionMeta")
56
+ header_layout.addWidget(summary_label)
57
+ layout.addWidget(header)
58
+
59
+ log_list = QListWidget()
60
+ log_list.setObjectName("runLogList")
61
+ log_list.setSpacing(6)
62
+ for entry in request.run_group.entries:
63
+ item = QListWidgetItem(entry.line)
64
+ widget = _build_raw_log_entry_widget(window, entry, run_group=request.run_group)
65
+ item.setSizeHint(widget.sizeHint())
66
+ log_list.addItem(item)
67
+ log_list.setItemWidget(item, widget)
68
+ layout.addWidget(log_list, 1)
69
+
70
+ _present_dialog(dialog)
71
+ return dialog
72
+
73
+
74
+ def _build_raw_log_entry_widget(window: "DataEngineWindow", entry: "FlowLogEntry", *, run_group: "FlowRunState") -> QFrame:
75
+ frame = QFrame()
76
+ frame.setObjectName("rawLogRow")
77
+ frame.setMinimumHeight(40)
78
+ layout = QHBoxLayout(frame)
79
+ layout.setContentsMargins(8, 6, 8, 6)
80
+ layout.setSpacing(10)
81
+
82
+ timestamp = QLabel(entry.created_at_utc.astimezone().strftime("%I:%M:%S %p"))
83
+ timestamp.setObjectName("rawLogTimestamp")
84
+ make_label_selectable(timestamp)
85
+ layout.addWidget(timestamp, 0, Qt.AlignmentFlag.AlignVCenter)
86
+
87
+ message = QLabel(window._format_raw_log_message(entry))
88
+ message.setObjectName("rawLogMessage")
89
+ message.setTextFormat(Qt.TextFormat.RichText)
90
+ message.setWordWrap(False)
91
+ make_label_selectable(message)
92
+ layout.addWidget(message, 1, Qt.AlignmentFlag.AlignVCenter)
93
+
94
+ event = entry.event
95
+ layout.addStretch(0)
96
+ inspect_slot = QWidget()
97
+ inspect_slot.setObjectName("rawLogInspectSlot")
98
+ inspect_slot.setFixedWidth(108)
99
+ inspect_slot_layout = QHBoxLayout(inspect_slot)
100
+ inspect_slot_layout.setContentsMargins(0, 0, 0, 0)
101
+ inspect_slot_layout.setSpacing(0)
102
+ inspect_slot_layout.addStretch(1)
103
+ inspect_button = QPushButton("Inspect")
104
+ inspect_button.setObjectName("inspectOutputButton")
105
+ inspect_button.setFixedWidth(96)
106
+ inspect_button.setEnabled(False)
107
+ inspect_slot_layout.addWidget(inspect_button, 0, Qt.AlignmentFlag.AlignVCenter)
108
+
109
+ icon_slot = QWidget()
110
+ icon_slot.setObjectName("rawLogIconSlot")
111
+ icon_slot.setFixedWidth(20)
112
+ icon_slot_layout = QHBoxLayout(icon_slot)
113
+ icon_slot_layout.setContentsMargins(0, 0, 0, 0)
114
+ icon_slot_layout.setSpacing(0)
115
+ icon_slot_layout.addStretch(1)
116
+
117
+ if event is not None:
118
+ if event.status == "failed":
119
+ inspect_button.setEnabled(True)
120
+ inspect_button.clicked.connect(
121
+ lambda _checked=False, group=run_group, failed_entry=entry: window._show_run_error_details(group, failed_entry)
122
+ )
123
+ if event.status in {"started", "failed", "success"}:
124
+ status_name = "failed" if event.status == "failed" else "started" if event.status == "started" else "finished"
125
+ status_icon = QLabel()
126
+ status_icon.setObjectName("rawLogStatusIcon")
127
+ fill = window._LOG_ICON_COLORS.get(status_name, window._group_icon_color().name())
128
+ status_icon.setPixmap(
129
+ render_svg_icon_pixmap(
130
+ icon_name=window._LOG_ICON_NAMES[status_name],
131
+ size=14,
132
+ device_pixel_ratio=window.devicePixelRatioF(),
133
+ fill_color=fill,
134
+ default_fill_color=window._group_icon_color(),
135
+ )
136
+ )
137
+ status_icon.setToolTip(event.status.title())
138
+ icon_slot_layout.addWidget(status_icon, 0, Qt.AlignmentFlag.AlignVCenter)
139
+
140
+ layout.addWidget(inspect_slot, 0, Qt.AlignmentFlag.AlignVCenter)
141
+ layout.addWidget(icon_slot, 0, Qt.AlignmentFlag.AlignVCenter)
142
+ return frame
143
+
144
+
145
+ def show_output_preview(window: "DataEngineWindow", request: OutputPreviewRequest) -> QDialog:
146
+ dialog = QDialog(window)
147
+ dialog.setWindowTitle("Inspect Output")
148
+ dialog.setObjectName("outputPreviewDialog")
149
+ dialog.resize(860, 520)
150
+ layout = QVBoxLayout(dialog)
151
+ layout.setContentsMargins(18, 18, 18, 18)
152
+ layout.setSpacing(12)
153
+
154
+ header = QFrame()
155
+ header.setObjectName("outputPreviewHeader")
156
+ header_layout = QVBoxLayout(header)
157
+ header_layout.setContentsMargins(14, 14, 14, 14)
158
+ header_layout.setSpacing(0)
159
+
160
+ path_label = QLabel(str(request.output_path))
161
+ path_label.setObjectName("outputPreviewPath")
162
+ path_label.setWordWrap(True)
163
+ make_label_selectable(path_label)
164
+ header_layout.addWidget(path_label)
165
+ layout.addWidget(header)
166
+
167
+ populate_output_preview(layout, request.output_path)
168
+
169
+ _present_dialog(dialog)
170
+ return dialog
171
+
172
+
173
+ def show_config_preview(window: "DataEngineWindow", request: ConfigPreviewRequest) -> QDialog:
174
+ preview_state = request.preview
175
+ dialog = QDialog(window)
176
+ dialog.setWindowTitle(preview_state.title)
177
+ dialog.setObjectName("outputPreviewDialog")
178
+ dialog.resize(560, 420)
179
+ layout = QVBoxLayout(dialog)
180
+ layout.setContentsMargins(18, 18, 18, 18)
181
+ layout.setSpacing(12)
182
+
183
+ header = QFrame()
184
+ header.setObjectName("outputPreviewHeader")
185
+ header_layout = QVBoxLayout(header)
186
+ header_layout.setContentsMargins(14, 14, 14, 14)
187
+ header_layout.setSpacing(4)
188
+
189
+ title_label = QLabel(preview_state.title)
190
+ title_label.setObjectName("heroTitle")
191
+ title_label.setWordWrap(True)
192
+ header_layout.addWidget(title_label)
193
+ if preview_state.description:
194
+ description_label = QLabel(preview_state.description)
195
+ description_label.setObjectName("bodyText")
196
+ description_label.setWordWrap(True)
197
+ header_layout.addWidget(description_label)
198
+ layout.addWidget(header)
199
+
200
+ body = QFrame()
201
+ body.setObjectName("configPreviewBody")
202
+ body_layout = QVBoxLayout(body)
203
+ body_layout.setContentsMargins(14, 14, 14, 14)
204
+ body_layout.setSpacing(2)
205
+ for row in preview_state.summary.rows:
206
+ row_value = build_config_value(body_layout, row.label)
207
+ row_value.setText(row.value)
208
+ body_layout.addStretch(1)
209
+ layout.addWidget(body, 1)
210
+
211
+ _present_dialog(dialog)
212
+ return dialog
213
+
214
+
215
+ def _present_dialog(dialog: QDialog) -> None:
216
+ dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
217
+ dialog.show()
218
+ dialog.raise_()
219
+ dialog.activateWindow()
220
+
221
+
222
+ __all__ = ["show_config_preview", "show_output_preview", "show_run_log_preview"]
@@ -0,0 +1,62 @@
1
+ """Low-level helper functions for the desktop GUI shell."""
2
+
3
+ from data_engine.ui.gui.helpers.inspection import (
4
+ artifact_key_for_operation,
5
+ capture_step_outputs,
6
+ inspect_step_output,
7
+ is_inspectable_operation,
8
+ refresh_operation_buttons,
9
+ rehydrate_step_outputs_from_ledger,
10
+ show_config_preview,
11
+ show_output_preview,
12
+ )
13
+ from data_engine.ui.gui.helpers.lifecycle import (
14
+ is_last_process_ui_window,
15
+ register_client_session,
16
+ shutdown_daemon_on_close,
17
+ start_worker_thread,
18
+ unregister_client_session_and_check_for_shutdown,
19
+ wait_for_worker_threads,
20
+ )
21
+ from data_engine.ui.gui.helpers.scroll import update_operation_scroll_cues, update_sidebar_scroll_cues
22
+ from data_engine.ui.gui.helpers.theming import (
23
+ action_bar_icon,
24
+ apply_theme,
25
+ group_icon,
26
+ group_icon_color,
27
+ log_icon,
28
+ render_group_icon_pixmap,
29
+ render_svg_icon_pixmap,
30
+ sync_theme_to_system,
31
+ toggle_theme,
32
+ view_rail_icon,
33
+ )
34
+
35
+ __all__ = [
36
+ "action_bar_icon",
37
+ "apply_theme",
38
+ "artifact_key_for_operation",
39
+ "capture_step_outputs",
40
+ "group_icon",
41
+ "group_icon_color",
42
+ "is_last_process_ui_window",
43
+ "inspect_step_output",
44
+ "is_inspectable_operation",
45
+ "log_icon",
46
+ "register_client_session",
47
+ "refresh_operation_buttons",
48
+ "rehydrate_step_outputs_from_ledger",
49
+ "render_group_icon_pixmap",
50
+ "render_svg_icon_pixmap",
51
+ "show_config_preview",
52
+ "show_output_preview",
53
+ "shutdown_daemon_on_close",
54
+ "start_worker_thread",
55
+ "sync_theme_to_system",
56
+ "toggle_theme",
57
+ "unregister_client_session_and_check_for_shutdown",
58
+ "update_operation_scroll_cues",
59
+ "update_sidebar_scroll_cues",
60
+ "view_rail_icon",
61
+ "wait_for_worker_threads",
62
+ ]
@@ -0,0 +1,81 @@
1
+ """Output inspection helper functions for the desktop GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from data_engine.views.state import artifact_key_for_operation as artifact_key_for_operation_helper
9
+ from data_engine.views.state import capture_step_outputs as capture_step_outputs_helper
10
+ from data_engine.views.state import is_inspectable_operation as is_inspectable_operation_helper
11
+ from data_engine.domain import ConfigPreviewState
12
+ from data_engine.ui.gui.dialogs import show_config_preview as show_config_preview_dialog
13
+ from data_engine.ui.gui.dialogs import show_output_preview as show_output_preview_dialog
14
+ from data_engine.ui.gui.preview_models import ConfigPreviewRequest, OutputPreviewRequest
15
+
16
+ if TYPE_CHECKING:
17
+ from data_engine.ui.gui.app import DataEngineWindow
18
+
19
+
20
+ def is_inspectable_operation(operation_name: str) -> bool:
21
+ return is_inspectable_operation_helper(operation_name)
22
+
23
+
24
+ def artifact_key_for_operation(operation_name: str) -> str | None:
25
+ return artifact_key_for_operation_helper(operation_name)
26
+
27
+
28
+ def capture_step_outputs(window: "DataEngineWindow", flow_name: str, results: object) -> None:
29
+ updated = capture_step_outputs_helper(
30
+ window.flow_cards[flow_name],
31
+ window.step_output_index.outputs_for(flow_name).outputs,
32
+ results,
33
+ )
34
+ window.step_output_index = window.step_output_index.with_flow_outputs(flow_name, updated)
35
+
36
+
37
+ def rehydrate_step_outputs_from_ledger(window: "DataEngineWindow") -> None:
38
+ window.step_output_index = window.runtime_history_service.rebuild_step_outputs(
39
+ window.runtime_binding.runtime_ledger,
40
+ window.flow_cards,
41
+ )
42
+
43
+
44
+ def refresh_operation_buttons(window: "DataEngineWindow", flow_name: str) -> None:
45
+ for row_widgets in window.operation_row_widgets:
46
+ if row_widgets.inspect_button is None:
47
+ continue
48
+ row_widgets.inspect_button.setEnabled(window.step_output_index.has_output(flow_name, row_widgets.operation_name))
49
+
50
+
51
+ def inspect_step_output(window: "DataEngineWindow", operation_name: str) -> None:
52
+ request = build_output_preview_request(window, operation_name)
53
+ if request is None:
54
+ window._show_message_box(
55
+ title="Inspect Output",
56
+ text="No output is available for this step yet.",
57
+ tone="info",
58
+ )
59
+ return
60
+ window._show_output_preview(request.operation_name, request.output_path)
61
+
62
+
63
+ def show_output_preview(window: "DataEngineWindow", operation_name: str, output_path: Path) -> None:
64
+ request = OutputPreviewRequest(operation_name=operation_name, output_path=output_path)
65
+ window.output_preview_dialog = show_output_preview_dialog(window, request)
66
+
67
+
68
+ def show_config_preview(window: "DataEngineWindow") -> None:
69
+ card = window.flow_cards.get(window.selected_flow_name or "")
70
+ preview_state = ConfigPreviewState.from_flow(card, window.flow_states)
71
+ window.config_preview_dialog = show_config_preview_dialog(window, ConfigPreviewRequest(preview=preview_state))
72
+
73
+
74
+ def build_output_preview_request(window: "DataEngineWindow", operation_name: str) -> OutputPreviewRequest | None:
75
+ """Build one explicit output-preview request for the selected flow."""
76
+ if window.selected_flow_name is None:
77
+ return None
78
+ output_path = window.step_output_index.output_path(window.selected_flow_name, operation_name)
79
+ if output_path is None or not output_path.exists():
80
+ return None
81
+ return OutputPreviewRequest(operation_name=operation_name, output_path=output_path)
@@ -0,0 +1,112 @@
1
+ """Lifecycle and worker helpers for the desktop GUI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import threading
7
+ import time
8
+ from time import monotonic
9
+ from typing import TYPE_CHECKING
10
+
11
+ from PySide6.QtWidgets import QApplication
12
+
13
+ if TYPE_CHECKING:
14
+ from data_engine.ui.gui.app import DataEngineWindow
15
+
16
+
17
+ def register_client_session(window: "DataEngineWindow") -> None:
18
+ """Register this UI process as one active local client for the workspace."""
19
+ window.runtime_binding_service.register_client_session(
20
+ window.runtime_binding,
21
+ client_id=window.client_session_id,
22
+ client_kind="ui",
23
+ pid=os.getpid(),
24
+ )
25
+
26
+
27
+ def is_last_process_ui_window(window: "DataEngineWindow") -> bool:
28
+ """Return whether this is the last Data Engine window still open in this process."""
29
+ for widget in QApplication.topLevelWidgets():
30
+ if widget is window:
31
+ continue
32
+ if isinstance(widget, type(window)) and not getattr(widget, "ui_closing", False):
33
+ return False
34
+ return True
35
+
36
+
37
+ def unregister_client_session_and_check_for_shutdown(
38
+ window: "DataEngineWindow",
39
+ *,
40
+ purge_process_ui_sessions: bool = False,
41
+ ) -> bool:
42
+ """Remove this UI session and return whether no local clients remain."""
43
+ try:
44
+ window.runtime_binding_service.remove_client_session(window.runtime_binding, window.client_session_id)
45
+ if purge_process_ui_sessions:
46
+ window.runtime_binding_service.purge_process_client_sessions(
47
+ window.runtime_binding,
48
+ client_kind="ui",
49
+ pid=os.getpid(),
50
+ )
51
+ remaining = window.runtime_binding_service.count_live_client_sessions(window.runtime_binding)
52
+ return remaining == 0
53
+ except Exception:
54
+ return False
55
+
56
+
57
+ def shutdown_daemon_on_close(window: "DataEngineWindow") -> None:
58
+ """Best-effort local daemon shutdown when the last local client closes."""
59
+ client_error_type = window._daemon_client_error_type
60
+ try:
61
+ if not window._is_daemon_live(window.workspace_paths):
62
+ return
63
+ window._daemon_request(window.workspace_paths, {"command": "shutdown_daemon"}, timeout=1.5)
64
+ deadline = monotonic() + 2.0
65
+ while monotonic() < deadline:
66
+ if not window._is_daemon_live(window.workspace_paths):
67
+ break
68
+ time.sleep(0.05)
69
+ except client_error_type:
70
+ pass
71
+ except Exception:
72
+ pass
73
+
74
+
75
+ def start_worker_thread(window: "DataEngineWindow", *, target, args=()) -> None:
76
+ """Start one tracked daemon worker thread for background UI tasks."""
77
+ thread = threading.Thread(target=run_tracked_worker, args=(window, target, args), daemon=True)
78
+ window._register_worker_thread(thread)
79
+ thread.start()
80
+
81
+
82
+ def run_tracked_worker(window: "DataEngineWindow", target, args) -> None:
83
+ """Execute one worker target and remove its thread from the tracked set."""
84
+ current = threading.current_thread()
85
+ try:
86
+ target(*args)
87
+ finally:
88
+ window._discard_worker_thread(current)
89
+
90
+
91
+ def wait_for_worker_threads(window: "DataEngineWindow", *, timeout_seconds: float) -> None:
92
+ """Wait briefly for tracked workers to honor stop requests before exit."""
93
+ deadline = time.monotonic() + max(timeout_seconds, 0.0)
94
+ for thread in window._worker_threads_snapshot():
95
+ remaining = deadline - time.monotonic()
96
+ if remaining <= 0:
97
+ break
98
+ is_alive = getattr(thread, "is_alive", None)
99
+ join = getattr(thread, "join", None)
100
+ alive = bool(is_alive()) if callable(is_alive) else False
101
+ if alive and callable(join):
102
+ join(timeout=min(remaining, 0.3))
103
+
104
+
105
+ __all__ = [
106
+ "is_last_process_ui_window",
107
+ "register_client_session",
108
+ "shutdown_daemon_on_close",
109
+ "start_worker_thread",
110
+ "unregister_client_session_and_check_for_shutdown",
111
+ "wait_for_worker_threads",
112
+ ]
@@ -0,0 +1,28 @@
1
+ """Scroll-surface helper functions for the desktop GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from data_engine.ui.gui.app import DataEngineWindow
9
+
10
+
11
+ def update_operation_scroll_cues(window: "DataEngineWindow", *args) -> None:
12
+ del args
13
+ scrollbar = window.operation_scroll.verticalScrollBar()
14
+ maximum = scrollbar.maximum()
15
+ value = scrollbar.value()
16
+ has_overflow = maximum > 0
17
+ window.operation_top_cue.setVisible(has_overflow and value > 0)
18
+ window.operation_bottom_cue.setVisible(has_overflow and value < maximum)
19
+
20
+
21
+ def update_sidebar_scroll_cues(window: "DataEngineWindow", *args) -> None:
22
+ del args
23
+ scrollbar = window.sidebar_scroll.verticalScrollBar()
24
+ maximum = scrollbar.maximum()
25
+ value = scrollbar.value()
26
+ has_overflow = maximum > 0
27
+ window.sidebar_top_cue.setVisible(has_overflow and value > 0)
28
+ window.sidebar_bottom_cue.setVisible(has_overflow and value < maximum)