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,80 @@
1
+ """Shared operator action-state view models across GUI and TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from data_engine.domain import OperatorActionContext
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class GuiActionState:
12
+ """Button and control state for the desktop GUI surface."""
13
+
14
+ flow_run_label: str
15
+ flow_run_enabled: bool
16
+ flow_config_enabled: bool
17
+ engine_enabled: bool
18
+ engine_label: str
19
+ engine_state: str
20
+ refresh_enabled: bool
21
+ clear_flow_log_enabled: bool
22
+ request_control_visible: bool
23
+ request_control_enabled: bool
24
+
25
+ @classmethod
26
+ def from_context(cls, context: OperatorActionContext) -> "GuiActionState":
27
+ """Return the GUI action state derived from one operator action context."""
28
+ session = context.runtime_session
29
+ selected = context.selected_flow
30
+ active = session.runtime_active or session.runtime_stopping
31
+ return cls(
32
+ flow_run_label="Running..." if selected.running else "Run Once",
33
+ flow_run_enabled=selected.valid and not selected.group_active and session.control_available and context.workspace_available,
34
+ flow_config_enabled=selected.present,
35
+ engine_enabled=(
36
+ session.runtime_active
37
+ or (context.has_automated_flows and session.control_available and context.workspace_available)
38
+ ) and not session.runtime_stopping,
39
+ engine_label="Stopping..." if session.runtime_stopping else "Stop Engine" if active else "Start Engine",
40
+ engine_state="running" if active else "stopped",
41
+ refresh_enabled=not session.runtime_active and not session.manual_run_active,
42
+ clear_flow_log_enabled=selected.present and selected.has_logs,
43
+ request_control_visible=True,
44
+ request_control_enabled=not session.workspace_owned,
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class TuiActionState:
50
+ """Button and control state for the terminal UI surface."""
51
+
52
+ refresh_disabled: bool
53
+ run_once_disabled: bool
54
+ start_engine_disabled: bool
55
+ stop_engine_disabled: bool
56
+ view_config_disabled: bool
57
+ view_log_disabled: bool
58
+ clear_flow_log_disabled: bool
59
+ workspace_select_disabled: bool
60
+ @classmethod
61
+ def from_context(cls, context: OperatorActionContext) -> "TuiActionState":
62
+ """Return the TUI action state derived from one operator action context."""
63
+ session = context.runtime_session
64
+ busy = session.runtime_active or session.manual_run_active or session.runtime_stopping
65
+ return cls(
66
+ refresh_disabled=busy,
67
+ run_once_disabled=busy or not session.control_available or not context.workspace_available,
68
+ start_engine_disabled=busy or not session.control_available or not context.workspace_available,
69
+ stop_engine_disabled=not busy,
70
+ view_config_disabled=not context.selected_flow.present,
71
+ view_log_disabled=not context.selected_run_group_present,
72
+ clear_flow_log_disabled=not context.selected_flow.present,
73
+ workspace_select_disabled=False,
74
+ )
75
+
76
+
77
+ __all__ = [
78
+ "GuiActionState",
79
+ "TuiActionState",
80
+ ]
@@ -0,0 +1,58 @@
1
+ """Shared artifact-preview presentation decisions across operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import mimetypes
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ArtifactPreviewSpec:
12
+ """Describe how one artifact should be previewed."""
13
+
14
+ kind: str
15
+ label: str
16
+ previewable: bool
17
+ placeholder_message: str | None = None
18
+
19
+
20
+ def classify_artifact_preview(path: Path) -> ArtifactPreviewSpec:
21
+ """Return the preview strategy for one output artifact."""
22
+ suffix = path.suffix.lower()
23
+ if suffix in {".parquet"}:
24
+ return ArtifactPreviewSpec(kind="parquet", label="Parquet table preview", previewable=True)
25
+ if suffix in {".xlsx", ".xls", ".xlsm", ".xlsb"}:
26
+ return ArtifactPreviewSpec(kind="excel", label="Excel table preview", previewable=True)
27
+ if suffix in {".pdf"}:
28
+ return ArtifactPreviewSpec(
29
+ kind="pdf",
30
+ label="PDF inspection",
31
+ previewable=False,
32
+ placeholder_message="PDF artifacts are recognized, but in-app PDF text inspection is not available yet.",
33
+ )
34
+ if is_text_artifact(path):
35
+ return ArtifactPreviewSpec(kind="text", label="Text preview", previewable=True)
36
+ return ArtifactPreviewSpec(
37
+ kind="unsupported",
38
+ label="Artifact inspection",
39
+ previewable=False,
40
+ placeholder_message="This artifact type is not previewable in the UI yet.",
41
+ )
42
+
43
+
44
+ def is_text_artifact(path: Path) -> bool:
45
+ """Return whether one artifact should use text-preview treatment."""
46
+ suffix = path.suffix.lower()
47
+ if suffix in {
48
+ ".txt", ".log", ".md", ".json", ".csv", ".tsv", ".yaml", ".yml",
49
+ ".xml", ".html", ".htm", ".sql", ".py", ".toml", ".ini", ".cfg",
50
+ }:
51
+ return True
52
+ guessed_type, _encoding = mimetypes.guess_type(path.name)
53
+ if guessed_type is None:
54
+ return False
55
+ return guessed_type.startswith("text/") or guessed_type in {"application/json", "application/xml", "application/x-yaml"}
56
+
57
+
58
+ __all__ = ["ArtifactPreviewSpec", "classify_artifact_preview", "is_text_artifact"]
@@ -0,0 +1,69 @@
1
+ """Shared flow/group row display models across GUI and TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from data_engine.views.models import QtFlowCard
8
+ from data_engine.views.presentation import FlowGroupBucket, flow_secondary_text, group_label, group_secondary_text, state_dot, status_color_name
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class FlowRowDisplay:
13
+ """Display metadata for one flow row in a list/tree surface."""
14
+
15
+ primary: str
16
+ secondary: str
17
+ state_color: str
18
+ dot: str
19
+ tooltip: str
20
+
21
+ @classmethod
22
+ def from_card(cls, card: QtFlowCard, state: str, *, primary: str = "title") -> "FlowRowDisplay":
23
+ """Return display metadata for one flow row."""
24
+ primary_text = card.title if primary == "title" else card.name
25
+ resolved_state = state if card.valid else "failed"
26
+ tooltip = f"{card.name} | {card.title} | {state}"
27
+ if card.group:
28
+ tooltip = f"{tooltip} | group={card.group}"
29
+ return cls(
30
+ primary=primary_text,
31
+ secondary=flow_secondary_text(card.mode, state),
32
+ state_color=status_color_name(state),
33
+ dot=state_dot(resolved_state),
34
+ tooltip=tooltip,
35
+ )
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class GroupRowDisplay:
40
+ """Display metadata for one grouped flow header."""
41
+
42
+ title: str
43
+ secondary: str
44
+ uppercase_title: str
45
+ @classmethod
46
+ def from_group(
47
+ cls,
48
+ group_name: str,
49
+ entries: list[QtFlowCard] | tuple[QtFlowCard, ...],
50
+ flow_states: dict[str, str],
51
+ ) -> "GroupRowDisplay":
52
+ """Return display metadata for one flow group header."""
53
+ title = group_label(group_name)
54
+ return cls(
55
+ title=title,
56
+ secondary=group_secondary_text(list(entries), flow_states),
57
+ uppercase_title=title.upper(),
58
+ )
59
+
60
+ @classmethod
61
+ def from_bucket(cls, bucket: FlowGroupBucket, flow_states: dict[str, str]) -> "GroupRowDisplay":
62
+ """Return display metadata for one grouped flow bucket."""
63
+ return cls.from_group(bucket.group_name, bucket.entries, flow_states)
64
+
65
+
66
+ __all__ = [
67
+ "FlowRowDisplay",
68
+ "GroupRowDisplay",
69
+ ]
@@ -0,0 +1,54 @@
1
+ """Shared log storage helpers for Data Engine operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from data_engine.domain import FlowLogEntry, FlowRunState, LogKind, RuntimeStepEvent
6
+
7
+ CollapsedLogKey = tuple[str, str, str]
8
+
9
+
10
+ class FlowLogStore:
11
+ """Keep operator log history and expose per-flow filtered views."""
12
+
13
+ def __init__(self, entries: tuple[FlowLogEntry, ...] = ()) -> None:
14
+ self._entries: list[FlowLogEntry] = list(entries)
15
+
16
+ def append_entry(self, entry: FlowLogEntry) -> None:
17
+ self._entries.append(entry)
18
+
19
+ def append_line(self, line: str, *, kind: LogKind, flow_name: str | None = None) -> FlowLogEntry:
20
+ entry = FlowLogEntry(line=line, kind=kind, flow_name=flow_name)
21
+ self.append_entry(entry)
22
+ return entry
23
+
24
+ def clear(self) -> None:
25
+ self._entries.clear()
26
+
27
+ def clear_flow(self, flow_name: str | None) -> None:
28
+ if flow_name is None:
29
+ return
30
+ self._entries = [
31
+ entry
32
+ for entry in self._entries
33
+ if not (entry.kind == "flow" and entry.flow_name == flow_name)
34
+ ]
35
+
36
+ def entries_for_flow(self, flow_name: str | None) -> tuple[FlowLogEntry, ...]:
37
+ if flow_name is None:
38
+ return ()
39
+ return tuple(entry for entry in self._entries if entry.kind == "flow" and entry.flow_name == flow_name)
40
+
41
+ def runs_for_flow(self, flow_name: str | None) -> tuple[FlowRunState, ...]:
42
+ entries = self.entries_for_flow(flow_name)
43
+ if not entries:
44
+ return ()
45
+ return FlowRunState.group_entries(entries)
46
+
47
+ __all__ = [
48
+ "CollapsedLogKey",
49
+ "FlowLogStore",
50
+ "FlowLogEntry",
51
+ "FlowRunState",
52
+ "LogKind",
53
+ "RuntimeStepEvent",
54
+ ]
@@ -0,0 +1,96 @@
1
+ """Shared UI-facing card models and small display helpers across Data Engine surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from data_engine.domain import FlowCatalogEntry, default_flow_state, flow_category
10
+
11
+ if TYPE_CHECKING:
12
+ from data_engine.services.flow_catalog import FlowCatalogService
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class QtFlowCard:
17
+ """Display model for one discovered flow."""
18
+
19
+ name: str
20
+ group: str | None
21
+ title: str
22
+ description: str
23
+ source_root: str
24
+ target_root: str
25
+ mode: str
26
+ interval: str
27
+ operations: str
28
+ operation_items: tuple[str, ...]
29
+ state: str
30
+ valid: bool
31
+ category: str
32
+ error: str = ""
33
+
34
+ def qt_flow_card_from_entry(entry: FlowCatalogEntry) -> QtFlowCard:
35
+ """Map one catalog entry into a shared surface card."""
36
+ return QtFlowCard(
37
+ name=entry.name,
38
+ group=entry.group,
39
+ title=entry.title,
40
+ description=entry.description,
41
+ source_root=entry.source_root,
42
+ target_root=entry.target_root,
43
+ mode=entry.mode,
44
+ interval=entry.interval,
45
+ operations=entry.operations,
46
+ operation_items=entry.operation_items,
47
+ state=entry.state,
48
+ valid=entry.valid,
49
+ category=entry.category,
50
+ error=entry.error,
51
+ )
52
+
53
+
54
+ def flow_catalog_entry_from_qt_card(card: QtFlowCard) -> FlowCatalogEntry:
55
+ """Map one shared surface card back into a catalog entry."""
56
+ return FlowCatalogEntry(
57
+ name=card.name,
58
+ group=card.group,
59
+ title=card.title,
60
+ description=card.description,
61
+ source_root=card.source_root,
62
+ target_root=card.target_root,
63
+ mode=card.mode,
64
+ interval=card.interval,
65
+ operations=card.operations,
66
+ operation_items=card.operation_items,
67
+ state=card.state,
68
+ valid=card.valid,
69
+ category=card.category,
70
+ error=card.error,
71
+ )
72
+
73
+
74
+ def qt_flow_cards_from_entries(entries: tuple[FlowCatalogEntry, ...] | list[FlowCatalogEntry]) -> tuple[QtFlowCard, ...]:
75
+ """Map discovered catalog entries into shared surface cards."""
76
+ return tuple(qt_flow_card_from_entry(entry) for entry in entries)
77
+
78
+
79
+ def load_qt_flow_cards(
80
+ flow_catalog_service: "FlowCatalogService",
81
+ *,
82
+ workspace_root: Path | None = None,
83
+ ) -> tuple[QtFlowCard, ...]:
84
+ """Load discovered catalog entries and map them into shared surface cards."""
85
+ return qt_flow_cards_from_entries(flow_catalog_service.load_entries(workspace_root=workspace_root))
86
+
87
+
88
+ __all__ = [
89
+ "QtFlowCard",
90
+ "default_flow_state",
91
+ "flow_category",
92
+ "flow_catalog_entry_from_qt_card",
93
+ "load_qt_flow_cards",
94
+ "qt_flow_card_from_entry",
95
+ "qt_flow_cards_from_entries",
96
+ ]
@@ -0,0 +1,133 @@
1
+ """Shared presentation helpers across GUI and TUI surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import math
7
+
8
+ from data_engine.domain.catalog import FlowCatalogLike
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class FlowGroupBucket:
13
+ """One grouped bucket of flow cards in shared surface order."""
14
+
15
+ group_name: str
16
+ entries: tuple[FlowCatalogLike, ...]
17
+
18
+ @property
19
+ def title(self) -> str:
20
+ """Return the user-facing label for this grouped flow section."""
21
+ return group_label(self.group_name)
22
+
23
+
24
+ def flow_group_name(card: FlowCatalogLike) -> str:
25
+ """Return the display/runtime group bucket for one flow card."""
26
+ return card.group or card.mode
27
+
28
+
29
+ def group_label(group_name: str) -> str:
30
+ """Return the user-facing label for one grouped flow section."""
31
+ if group_name in {"poll", "schedule", "manual"}:
32
+ return group_name.title()
33
+ return group_name
34
+
35
+
36
+ def group_cards(cards: tuple[FlowCatalogLike, ...] | list[FlowCatalogLike]) -> tuple[FlowGroupBucket, ...]:
37
+ """Group cards by display bucket in the shared surface order."""
38
+ grouped: dict[str, list[FlowCatalogLike]] = {}
39
+ for card in cards:
40
+ grouped.setdefault(flow_group_name(card), []).append(card)
41
+ priority = {"manual": 0, "poll": 1, "schedule": 2}
42
+ return tuple(
43
+ FlowGroupBucket(group_name=group_name, entries=tuple(entries))
44
+ for group_name, entries in sorted(grouped.items(), key=lambda item: (priority.get(item[0], 10), item[0].lower()))
45
+ )
46
+
47
+
48
+ def flow_secondary_text(mode: str, state: str) -> str:
49
+ """Return the secondary status line for one flow card."""
50
+ if mode == "poll":
51
+ return "Polling" if state in {"poll ready", "polling"} else f"Polling {state}"
52
+ if mode == "schedule":
53
+ return "Scheduled" if state in {"schedule ready", "scheduled"} else f"Scheduled {state}"
54
+ return "Manual" if state == "manual" else f"Manual {state}"
55
+
56
+
57
+ def group_secondary_text(entries: list[FlowCatalogLike], flow_states: dict[str, str]) -> str:
58
+ """Return one compact group summary line for sidebar/list displays."""
59
+ total = len(entries)
60
+ active = sum(1 for card in entries if flow_states.get(card.name, card.state) in {"running", "polling", "scheduled"})
61
+ failed = sum(1 for card in entries if flow_states.get(card.name, card.state) == "failed")
62
+ if failed:
63
+ return f"{total} flow(s) Error: {failed}"
64
+ if active:
65
+ return f"{total} flow(s) Running: {active}"
66
+ return f"{total} flow(s)"
67
+
68
+
69
+ def status_color_name(state: str) -> str:
70
+ """Return the named status color token for one flow state."""
71
+ if state == "failed":
72
+ return "error"
73
+ if state == "started":
74
+ return "started"
75
+ if state in {"running", "polling", "scheduled", "success", "finished"}:
76
+ return "success"
77
+ if state in {"stopping flow", "stopping runtime"}:
78
+ return "warning"
79
+ return "idle"
80
+
81
+
82
+ def state_dot(state: str) -> str:
83
+ """Return one small textual state marker for compact terminal displays."""
84
+ if state == "failed":
85
+ return "!"
86
+ if state in {"running", "polling", "scheduled", "success", "finished"}:
87
+ return "*"
88
+ if state in {"stopping flow", "stopping runtime"}:
89
+ return "~"
90
+ return "·"
91
+
92
+
93
+ def operation_marker(status: str) -> str:
94
+ """Return one small textual marker for operation-level progress."""
95
+ if status == "running":
96
+ return ">"
97
+ if status == "success":
98
+ return "+"
99
+ if status == "failed":
100
+ return "!"
101
+ return "·"
102
+
103
+
104
+ def format_seconds(seconds: float) -> str:
105
+ """Render elapsed seconds into the compact duration text used across surfaces."""
106
+
107
+ def truncate(value: float, decimals: int = 1) -> float:
108
+ factor = 10**decimals
109
+ return math.trunc(value * factor) / factor
110
+
111
+ if seconds < 0.001:
112
+ return "<1ms"
113
+ if seconds < 1:
114
+ return f"{math.trunc(seconds * 1000)}ms"
115
+ if seconds < 60:
116
+ return f"{truncate(seconds):.1f}s"
117
+ if seconds < 3600:
118
+ return f"{truncate(seconds / 60):.1f}m"
119
+ return f"{truncate(seconds / 3600):.1f}h"
120
+
121
+
122
+ __all__ = [
123
+ "FlowGroupBucket",
124
+ "flow_secondary_text",
125
+ "flow_group_name",
126
+ "format_seconds",
127
+ "group_cards",
128
+ "group_label",
129
+ "group_secondary_text",
130
+ "operation_marker",
131
+ "state_dot",
132
+ "status_color_name",
133
+ ]
@@ -0,0 +1,62 @@
1
+ """Shared run-group presentation helpers across operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from data_engine.domain import FlowLogEntry, FlowRunState, RunDetailState
8
+ from data_engine.views.presentation import format_seconds
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RunGroupDisplay:
13
+ """Canonical GUI-first presentation state for one grouped run."""
14
+
15
+ primary_label: str
16
+ source_label: str
17
+ status_text: str
18
+ status_visual_state: str
19
+ duration_text: str | None
20
+
21
+ @classmethod
22
+ def from_run(cls, run_state: FlowRunState) -> "RunGroupDisplay":
23
+ detail = RunDetailState.from_run(run_state)
24
+ return cls(
25
+ primary_label=detail.display_label,
26
+ source_label=detail.source_label,
27
+ status_text=detail.status.title(),
28
+ status_visual_state=_status_visual_state(detail.status),
29
+ duration_text=format_seconds(detail.elapsed_seconds) if detail.elapsed_seconds is not None else None,
30
+ )
31
+
32
+
33
+ def format_raw_log_message(entry: FlowLogEntry) -> str:
34
+ """Return canonical user-facing log text for one raw runtime/log entry."""
35
+ from html import escape
36
+
37
+ event = entry.event
38
+ if event is None:
39
+ return escape(entry.line)
40
+ flow_name = escape(event.flow_name)
41
+ source_label = escape(event.source_label)
42
+ status = escape(event.status)
43
+ has_source = event.source_label not in {"", "-"}
44
+ if event.step_name is None:
45
+ if has_source:
46
+ return f"{flow_name} &gt; {source_label} &gt; <i>{status}</i>"
47
+ return f"{flow_name} &gt; <i>{status}</i>"
48
+ step_name = escape(event.step_name.replace(":", "::", 1))
49
+ if has_source:
50
+ return f"{flow_name} &gt; {source_label} &gt; <b>{step_name}</b> - <i>{status}</i>"
51
+ return f"{flow_name} &gt; <b>{step_name}</b> - <i>{status}</i>"
52
+
53
+
54
+ def _status_visual_state(status: str) -> str:
55
+ if status in {"failed", "stopped"}:
56
+ return "failed"
57
+ if status == "started":
58
+ return "started"
59
+ return "finished"
60
+
61
+
62
+ __all__ = ["RunGroupDisplay", "format_raw_log_message"]
@@ -0,0 +1,39 @@
1
+ """Shared state and presentation helpers for Data Engine operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from data_engine.domain.details import FlowSummaryState, OperationArtifactState
6
+ from data_engine.domain.operations import OperationFlowState, OperationRowState, OperationSessionState
7
+ from data_engine.views.models import QtFlowCard
8
+
9
+ def build_flow_summary(card: QtFlowCard | None, flow_states: dict[str, str]) -> FlowSummaryState:
10
+ """Return summary rows for the selected flow."""
11
+ return FlowSummaryState.from_flow(card, flow_states)
12
+
13
+
14
+ def is_inspectable_operation(operation_name: str) -> bool:
15
+ """Return whether an operation can surface a previewable output path."""
16
+ return OperationArtifactState(operation_name).inspectable
17
+
18
+
19
+ def artifact_key_for_operation(operation_name: str) -> str | None:
20
+ """Return the runtime metadata key produced by one operation."""
21
+ return OperationArtifactState(operation_name).artifact_key
22
+
23
+
24
+ def capture_step_outputs(flow_card: QtFlowCard, existing: dict[str, "Path"], results: object) -> dict[str, "Path"]:
25
+ """Return updated output-path mappings extracted from completed flow results."""
26
+ return OperationArtifactState.capture_outputs(flow_card, existing, results)
27
+
28
+
29
+ OperationDisplayState = OperationFlowState
30
+
31
+
32
+ __all__ = [
33
+ "OperationDisplayState",
34
+ "OperationRowState",
35
+ "artifact_key_for_operation",
36
+ "build_flow_summary",
37
+ "capture_step_outputs",
38
+ "is_inspectable_operation",
39
+ ]
@@ -0,0 +1,13 @@
1
+ """Shared surface-facing status-copy helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ WORKSPACE_UNAVAILABLE_TEXT = "Workspace root is no longer available."
6
+
7
+
8
+ def surface_control_status_text(control_status_text: str | None, *, empty_flow_message: str = "") -> str:
9
+ """Return the shared control/status line text shown by operator surfaces."""
10
+ return control_status_text or empty_flow_message
11
+
12
+
13
+ __all__ = ["WORKSPACE_UNAVAILABLE_TEXT", "surface_control_status_text"]