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,148 @@
1
+ """Step-list presentation helpers for the desktop UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QTimer
8
+ from PySide6.QtWidgets import QFrame
9
+
10
+ from data_engine.domain import RuntimeStepEvent
11
+ from data_engine.views.models import default_flow_state
12
+ from data_engine.views.presentation import format_seconds
13
+
14
+ if TYPE_CHECKING:
15
+ from data_engine.ui.gui.app import DataEngineWindow
16
+
17
+
18
+ def reset_operation_state(window: "DataEngineWindow", flow_name: str) -> None:
19
+ window.operation_tracker = window.operation_tracker.reset_flow(flow_name, window.flow_cards[flow_name].operation_items)
20
+ if window.selected_flow_name == flow_name:
21
+ render_operation_durations(window, flow_name)
22
+
23
+
24
+ def apply_runtime_event(window: "DataEngineWindow", event: RuntimeStepEvent) -> None:
25
+ if event.flow_name not in window.flow_cards:
26
+ return
27
+ flow_name = event.flow_name
28
+ card = window.flow_cards[flow_name]
29
+ if event.step_name is None:
30
+ if event.status == "failed":
31
+ window._set_flow_state(flow_name, "failed")
32
+ elif event.status in {"started", "success", "stopped"}:
33
+ if window.runtime_session.runtime_active and flow_name in window.runtime_session.active_runtime_flow_names:
34
+ window._set_flow_state(
35
+ flow_name,
36
+ "polling" if card.mode == "poll" else "scheduled" if card.mode == "schedule" else default_flow_state(card.mode),
37
+ )
38
+ elif window.flow_states.get(flow_name) == "failed":
39
+ window._set_flow_state(flow_name, default_flow_state(card.mode))
40
+ return
41
+ window.operation_tracker, flash_index = window.operation_tracker.apply_event(
42
+ flow_name,
43
+ card.operation_items,
44
+ event,
45
+ now=window._monotonic(),
46
+ )
47
+ if flash_index is not None and window.selected_flow_name == flow_name:
48
+ flash_operation_row(window, flash_index)
49
+ if window.selected_flow_name == flow_name:
50
+ render_operation_durations(window, flow_name)
51
+
52
+
53
+ def render_operation_durations(window: "DataEngineWindow", flow_name: str) -> None:
54
+ card = window.flow_cards.get(flow_name)
55
+ state = window.operation_tracker.state_for(flow_name)
56
+ if card is None or state is None:
57
+ for row_widgets in window.operation_row_widgets:
58
+ row_widgets.duration_label.setText("")
59
+ return
60
+ for index, operation_name in enumerate(card.operation_items):
61
+ if index >= len(window.operation_row_widgets):
62
+ break
63
+ row_widgets = window.operation_row_widgets[index]
64
+ row_card = row_widgets.row_card
65
+ duration_label = row_widgets.duration_label
66
+ row_state = window.operation_tracker.row_state(flow_name, operation_name)
67
+ apply_operation_row_state(row_card, row_state.status if row_state is not None else "idle")
68
+ duration_label.setText(duration_text(window, flow_name, operation_name))
69
+ window._refresh_operation_buttons(flow_name)
70
+
71
+
72
+ def duration_text(window: "DataEngineWindow", flow_name: str, operation_name: str) -> str:
73
+ return window.operation_tracker.duration_text(flow_name, operation_name, now=window._monotonic(), formatter=format_seconds)
74
+
75
+
76
+ def refresh_live_operation_durations(window: "DataEngineWindow") -> None:
77
+ if window.selected_flow_name is None:
78
+ return
79
+ card = window.flow_cards.get(window.selected_flow_name)
80
+ state = window.operation_tracker.state_for(window.selected_flow_name)
81
+ if card is None or state is None:
82
+ return
83
+ for index, operation_name in enumerate(card.operation_items):
84
+ if index >= len(window.operation_row_widgets):
85
+ break
86
+ row_state = window.operation_tracker.row_state(window.selected_flow_name, operation_name)
87
+ if row_state is None or row_state.status != "running":
88
+ continue
89
+ row_widgets = window.operation_row_widgets[index]
90
+ row_widgets.duration_label.setText(duration_text(window, window.selected_flow_name, operation_name))
91
+
92
+
93
+ def apply_operation_row_state(row_card: QFrame, status: str) -> None:
94
+ if row_card.property("stepState") == status:
95
+ return
96
+ row_card.setProperty("stepState", status)
97
+ style = row_card.style()
98
+ style.unpolish(row_card)
99
+ style.polish(row_card)
100
+ row_card.update()
101
+
102
+
103
+ def flash_operation_row(window: "DataEngineWindow", index: int) -> None:
104
+ if index >= len(window.operation_row_widgets):
105
+ return
106
+ row_card = window.operation_row_widgets[index].row_card
107
+ row_card.setProperty("flashState", "complete")
108
+ style = row_card.style()
109
+ style.unpolish(row_card)
110
+ style.polish(row_card)
111
+ row_card.update()
112
+
113
+ timer = QTimer(window)
114
+ timer.setSingleShot(True)
115
+
116
+ def clear_flash() -> None:
117
+ try:
118
+ row_card.setProperty("flashState", "")
119
+ style = row_card.style()
120
+ style.unpolish(row_card)
121
+ style.polish(row_card)
122
+ row_card.update()
123
+ except RuntimeError:
124
+ pass
125
+ if timer in window.operation_flash_timers:
126
+ window.operation_flash_timers.remove(timer)
127
+
128
+ timer.timeout.connect(clear_flash)
129
+ window.operation_flash_timers.append(timer)
130
+ timer.start(140)
131
+
132
+
133
+ def normalize_completed_operation_rows(window: "DataEngineWindow", flow_name: str) -> None:
134
+ window.operation_tracker = window.operation_tracker.normalize_completed(flow_name)
135
+ if window.selected_flow_name == flow_name:
136
+ render_operation_durations(window, flow_name)
137
+
138
+
139
+ __all__ = [
140
+ "apply_runtime_event",
141
+ "duration_text",
142
+ "flash_operation_row",
143
+ "format_seconds",
144
+ "normalize_completed_operation_rows",
145
+ "refresh_live_operation_durations",
146
+ "render_operation_durations",
147
+ "reset_operation_state",
148
+ ]
@@ -0,0 +1,39 @@
1
+ """Compatibility re-exports for the split workspace presenter helpers."""
2
+
3
+ from data_engine.ui.gui.presenters.docs import (
4
+ create_docs_browser,
5
+ docs_build_dir,
6
+ finish_docs_build,
7
+ initialize_docs_view,
8
+ load_docs_page,
9
+ run_docs_build_worker,
10
+ start_docs_build,
11
+ )
12
+ from data_engine.ui.gui.presenters.runtime_projection import (
13
+ apply_daemon_snapshot,
14
+ finish_daemon_startup,
15
+ )
16
+ from data_engine.ui.gui.presenters.workspace_binding import rebind_workspace_context
17
+ from data_engine.ui.gui.presenters.workspace_settings import (
18
+ browse_workspace_collection_root_override,
19
+ refresh_workspace_root_controls,
20
+ reset_workspace_collection_root_override,
21
+ save_workspace_collection_root_override,
22
+ )
23
+
24
+ __all__ = [
25
+ "apply_daemon_snapshot",
26
+ "browse_workspace_collection_root_override",
27
+ "create_docs_browser",
28
+ "docs_build_dir",
29
+ "finish_daemon_startup",
30
+ "finish_docs_build",
31
+ "initialize_docs_view",
32
+ "load_docs_page",
33
+ "rebind_workspace_context",
34
+ "refresh_workspace_root_controls",
35
+ "reset_workspace_collection_root_override",
36
+ "run_docs_build_worker",
37
+ "save_workspace_collection_root_override",
38
+ "start_docs_build",
39
+ ]
@@ -0,0 +1,75 @@
1
+ """Workspace rebinding 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 data_engine.domain import StepOutputIndex, WorkspaceControlState
9
+ from data_engine.ui.gui.helpers import register_client_session
10
+ from data_engine.ui.gui.presenters.workspace_settings import refresh_workspace_root_controls
11
+
12
+ if TYPE_CHECKING:
13
+ from data_engine.ui.gui.app import DataEngineWindow
14
+
15
+
16
+ def _close_workspace_scoped_dialogs(window: "DataEngineWindow") -> None:
17
+ for attr_name in ("output_preview_dialog", "config_preview_dialog", "run_log_preview_dialog"):
18
+ dialog = getattr(window, attr_name, None)
19
+ if dialog is None:
20
+ continue
21
+ try:
22
+ dialog.close()
23
+ except Exception:
24
+ pass
25
+ setattr(window, attr_name, None)
26
+
27
+
28
+ def rebind_workspace_context(
29
+ window: "DataEngineWindow",
30
+ *,
31
+ workspace_id: str | None = None,
32
+ override_root: Path | None | object = ...,
33
+ ) -> None:
34
+ from data_engine.ui.gui.presenters.docs import initialize_docs_view
35
+
36
+ window._message_box_generation += 1
37
+ window._pending_message_box = None
38
+ window._message_box_scheduled = False
39
+ _close_workspace_scoped_dialogs(window)
40
+
41
+ try:
42
+ window.runtime_binding_service.remove_client_session(window.runtime_binding, window.client_session_id)
43
+ except Exception:
44
+ pass
45
+ window.runtime_binding_service.close_binding(window.runtime_binding)
46
+ if override_root is ...:
47
+ override_root = window.workspace_collection_root_override
48
+ window.workspace_paths = window._resolve_workspace_paths(
49
+ workspace_id=workspace_id,
50
+ workspace_collection_root=override_root,
51
+ )
52
+ binding = window.workspace_session_application.bind_workspace(
53
+ workspace_paths=window.workspace_paths,
54
+ override_root=override_root,
55
+ )
56
+ window._operator_session_state = binding.operator_session
57
+ window.runtime_binding = window.runtime_binding_service.open_binding(window.workspace_paths)
58
+ register_client_session(window)
59
+ window.daemon_status = window.daemon_status.empty()
60
+ window.workspace_control_state = WorkspaceControlState.empty()
61
+ window._daemon_startup_in_progress = False
62
+ window._last_daemon_spawn_attempt = 0.0
63
+ window.manual_flow_stop_events = {}
64
+ window.step_output_index = StepOutputIndex.empty()
65
+ window._reload_workspace_options()
66
+ refresh_workspace_root_controls(window)
67
+ window._load_flows()
68
+ window._refresh_log_view(force_scroll_to_bottom=True)
69
+ window._refresh_action_buttons()
70
+ initialize_docs_view(window)
71
+ if window._auto_daemon_enabled:
72
+ window._sync_from_daemon()
73
+
74
+
75
+ __all__ = ["rebind_workspace_context"]
@@ -0,0 +1,182 @@
1
+ """Workspace-folder settings presentation helpers for the desktop UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+ from pathlib import Path
7
+ import sys
8
+ from typing import TYPE_CHECKING
9
+
10
+ from PySide6.QtWidgets import QFileDialog
11
+
12
+ from data_engine.domain.time import parse_utc_text
13
+
14
+ if TYPE_CHECKING:
15
+ from data_engine.ui.gui.app import DataEngineWindow
16
+
17
+
18
+ def refresh_workspace_root_controls(window: "DataEngineWindow") -> None:
19
+ window.workspace_root_input.setText(window.workspace_session_state.root.input_text)
20
+ window.workspace_root_status_label.setText(window.workspace_session_state.root.status_text)
21
+ refresh_workspace_provisioning_controls(window)
22
+ refresh_workspace_visibility_panel(window)
23
+
24
+
25
+ def save_workspace_collection_root_override(window: "DataEngineWindow") -> None:
26
+ from data_engine.ui.gui.presenters.workspace_binding import rebind_workspace_context
27
+
28
+ raw_value = window.workspace_root_input.text().strip()
29
+ if not raw_value:
30
+ reset_workspace_collection_root_override(window)
31
+ return
32
+ target_root = Path(raw_value).expanduser().resolve()
33
+ window.settings_service.set_workspace_collection_root(target_root)
34
+ rebind_workspace_context(window, override_root=target_root)
35
+ window.workspace_root_status_label.setText(f"Workspace folder: {target_root}")
36
+
37
+
38
+ def reset_workspace_collection_root_override(window: "DataEngineWindow") -> None:
39
+ from data_engine.ui.gui.presenters.workspace_binding import rebind_workspace_context
40
+
41
+ window.settings_service.set_workspace_collection_root(None)
42
+ rebind_workspace_context(window, override_root=None)
43
+ window.workspace_root_status_label.setText(window.workspace_session_state.root.status_text)
44
+
45
+
46
+ def browse_workspace_collection_root_override(window: "DataEngineWindow") -> None:
47
+ current_text = window.workspace_root_input.text().strip()
48
+ if current_text:
49
+ start_dir = str(Path(current_text).expanduser())
50
+ elif window.workspace_paths.workspace_configured:
51
+ start_dir = str(window.workspace_paths.workspace_collection_root)
52
+ else:
53
+ start_dir = str(Path.home())
54
+ selected = QFileDialog.getExistingDirectory(window, "Select Workspace Folder", start_dir)
55
+ if selected:
56
+ window.workspace_root_input.setText(str(Path(selected).expanduser().resolve()))
57
+ save_workspace_collection_root_override(window)
58
+
59
+
60
+ def provision_selected_workspace(window: "DataEngineWindow") -> None:
61
+ if not window.workspace_paths.workspace_configured:
62
+ window._show_message_box(
63
+ title="Workspace Folder Required",
64
+ text="Choose a workspace folder before provisioning a workspace.",
65
+ tone="error",
66
+ )
67
+ return
68
+ try:
69
+ result = window.workspace_provisioning_service.provision_workspace(
70
+ window.workspace_paths,
71
+ interpreter_path=Path(sys.executable).expanduser(),
72
+ )
73
+ except Exception as exc:
74
+ window.workspace_provision_status_label.setText(f"Provisioning failed: {exc}")
75
+ window._show_message_box(
76
+ title="Provisioning Failed",
77
+ text=str(exc),
78
+ tone="error",
79
+ )
80
+ return
81
+ created_names = ", ".join(path.name for path in result.created_paths) if result.created_paths else "nothing new"
82
+ window._rebind_workspace_context(workspace_id=window.workspace_paths.workspace_id)
83
+ window.workspace_provision_status_label.setText(
84
+ f"Provisioned {result.workspace_root.name}: created {created_names}."
85
+ )
86
+
87
+
88
+ def force_shutdown_daemon(window: "DataEngineWindow") -> None:
89
+ result = window.runtime_application.force_shutdown_daemon(window.workspace_paths, timeout=0.5)
90
+ if not result.ok:
91
+ window.force_shutdown_daemon_status_label.setText(f"Force stop failed: {result.error}")
92
+ window._show_message_box(
93
+ title="Force Stop Failed",
94
+ text=result.error,
95
+ tone="error",
96
+ )
97
+ return
98
+ window.force_shutdown_daemon_status_label.setText(
99
+ "Local daemon force-stopped. Any active engine or manual runs were terminated."
100
+ )
101
+ window._sync_from_daemon()
102
+
103
+
104
+ def refresh_workspace_provisioning_controls(window: "DataEngineWindow") -> None:
105
+ if not window.workspace_paths.workspace_configured:
106
+ window.workspace_target_label.setText("Selected workspace: choose a workspace folder first.")
107
+ window.provision_workspace_button.setEnabled(False)
108
+ if not window.workspace_provision_status_label.text().strip():
109
+ window.workspace_provision_status_label.setText(
110
+ "Provisioning creates a workspace folder, flow_modules, and VS Code settings without overwriting existing files."
111
+ )
112
+ return
113
+ workspace_root = window.workspace_paths.workspace_root
114
+ workspace_ready = window.workspace_paths.flow_modules_dir.is_dir()
115
+ window.workspace_target_label.setText(f"Selected workspace: {workspace_root}")
116
+ window.provision_workspace_button.setEnabled(True)
117
+ if workspace_ready:
118
+ window.workspace_provision_status_label.setText(
119
+ "Workspace already has flow modules. Provisioning will only add missing folders or VS Code settings."
120
+ )
121
+ else:
122
+ window.workspace_provision_status_label.setText(
123
+ "Provision the selected workspace to create flow_modules and local VS Code settings."
124
+ )
125
+
126
+
127
+ def refresh_workspace_visibility_panel(window: "DataEngineWindow") -> None:
128
+ interpreter_path = Path(sys.executable).expanduser()
129
+ interpreter_mode = "virtual environment" if sys.prefix != getattr(sys, "base_prefix", sys.prefix) else "system/global"
130
+ window.force_shutdown_daemon_button.setEnabled(window.workspace_paths.workspace_configured)
131
+ if not window.force_shutdown_daemon_status_label.text().strip():
132
+ window.force_shutdown_daemon_status_label.setText(
133
+ "Use only when normal stop does not return control."
134
+ )
135
+ window.visibility_interpreter_value.setText(str(interpreter_path))
136
+ window.visibility_interpreter_mode_value.setText(interpreter_mode.title())
137
+ window.workspace_counts_footer_label.setText(_workspace_counts_footer_text(window))
138
+
139
+
140
+ def _workspace_module_count(flow_modules_dir: Path) -> int:
141
+ if not flow_modules_dir.is_dir():
142
+ return 0
143
+ return sum(
144
+ 1
145
+ for path in flow_modules_dir.iterdir()
146
+ if path.is_file() and path.suffix in {".py", ".ipynb"} and path.name != "__init__.py"
147
+ )
148
+
149
+
150
+ def _workspace_counts_footer_text(window: "DataEngineWindow") -> str:
151
+ cards = tuple(window.flow_cards.values())
152
+ module_count = _workspace_module_count(window.workspace_paths.flow_modules_dir)
153
+ group_count = len({card.group for card in cards})
154
+ flow_count = len(cards)
155
+ recent_runs_count = _recent_workspace_run_count(window, days=30)
156
+ return f"{module_count} modules - {group_count} groups - {flow_count} flows - {recent_runs_count} runs last 30 days"
157
+
158
+
159
+ def _recent_workspace_run_count(window: "DataEngineWindow", *, days: int) -> int:
160
+ cutoff = datetime.now(UTC) - timedelta(days=days)
161
+ count = 0
162
+ try:
163
+ runs = window.runtime_binding.runtime_ledger.list_runs()
164
+ except Exception:
165
+ return 0
166
+ for run in runs:
167
+ started_at = parse_utc_text(run.started_at_utc)
168
+ if started_at is not None and started_at >= cutoff:
169
+ count += 1
170
+ return count
171
+
172
+
173
+ __all__ = [
174
+ "browse_workspace_collection_root_override",
175
+ "force_shutdown_daemon",
176
+ "provision_selected_workspace",
177
+ "refresh_workspace_provisioning_controls",
178
+ "refresh_workspace_visibility_panel",
179
+ "refresh_workspace_root_controls",
180
+ "reset_workspace_collection_root_override",
181
+ "save_workspace_collection_root_override",
182
+ ]
@@ -0,0 +1,37 @@
1
+ """Explicit request/state models for GUI preview dialogs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from data_engine.domain import ConfigPreviewState, FlowRunState, RunDetailState
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class OutputPreviewRequest:
13
+ """Request state for one output-preview dialog."""
14
+
15
+ operation_name: str
16
+ output_path: Path
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RunLogPreviewRequest:
21
+ """Request state for one run-log preview dialog."""
22
+
23
+ run_group: FlowRunState
24
+ detail: RunDetailState
25
+
26
+ @classmethod
27
+ def from_run(cls, run_group: FlowRunState) -> "RunLogPreviewRequest":
28
+ """Build one run-log preview request from a grouped run."""
29
+ return cls(run_group=run_group, detail=RunDetailState.from_run(run_group))
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ConfigPreviewRequest:
34
+ """Request state for one config-preview dialog."""
35
+
36
+ preview: ConfigPreviewState
37
+