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,214 @@
1
+ """Domain models for selected-flow and run-detail state."""
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.catalog import FlowCatalogLike
10
+
11
+ if TYPE_CHECKING:
12
+ from data_engine.domain.runs import FlowRunState
13
+ from data_engine.domain.operations import OperationSessionState
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FlowSummaryRow:
18
+ """One labeled row in a flow summary/config display."""
19
+
20
+ label: str
21
+ value: str
22
+
23
+ @classmethod
24
+ def rows_for_flow(
25
+ cls,
26
+ card: FlowCatalogLike | None,
27
+ flow_states: dict[str, str],
28
+ ) -> tuple["FlowSummaryRow", ...]:
29
+ """Build summary/config rows for one flow card."""
30
+ if card is None:
31
+ return (
32
+ cls("Flow", "-"),
33
+ cls("Mode", "-"),
34
+ cls("Interval", "-"),
35
+ cls("State", "-"),
36
+ cls("Source", "-"),
37
+ cls("Target", "-"),
38
+ )
39
+ state = flow_states.get(card.name, card.state)
40
+ return (
41
+ cls("Flow", card.name),
42
+ cls("Mode", card.mode),
43
+ cls("Interval", card.interval),
44
+ cls("State", state),
45
+ cls("Source", card.source_root),
46
+ cls("Target", card.target_root),
47
+ )
48
+
49
+ @classmethod
50
+ def pairs_for_flow(
51
+ cls,
52
+ card: FlowCatalogLike | None,
53
+ flow_states: dict[str, str],
54
+ ) -> tuple[tuple[str, str], ...]:
55
+ """Build tuple pairs for display surfaces that only need labels and values."""
56
+ return tuple((row.label, row.value) for row in cls.rows_for_flow(card, flow_states))
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class FlowSummaryState:
61
+ """Explicit summary state for one selected flow."""
62
+
63
+ rows: tuple[FlowSummaryRow, ...]
64
+
65
+ @classmethod
66
+ def from_flow(
67
+ cls,
68
+ card: FlowCatalogLike | None,
69
+ flow_states: dict[str, str],
70
+ ) -> "FlowSummaryState":
71
+ """Build one summary-state bundle for a selected flow."""
72
+ return cls(rows=FlowSummaryRow.rows_for_flow(card, flow_states))
73
+
74
+ @property
75
+ def pairs(self) -> tuple[tuple[str, str], ...]:
76
+ """Return the legacy label/value pair projection for simple surfaces."""
77
+ return tuple((row.label, row.value) for row in self.rows)
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class OperationArtifactState:
82
+ """Artifact/inspection rules for one operation row."""
83
+
84
+ operation_name: str
85
+
86
+ @property
87
+ def inspectable(self) -> bool:
88
+ """Return whether the operation can surface an inspectable artifact."""
89
+ return bool(self.operation_name)
90
+
91
+ @property
92
+ def artifact_key(self) -> str | None:
93
+ """Return the runtime metadata key produced by this operation."""
94
+ return self.operation_name or None
95
+
96
+ @classmethod
97
+ def capture_outputs(
98
+ cls,
99
+ card: FlowCatalogLike,
100
+ existing: dict[str, Path],
101
+ results: object,
102
+ ) -> dict[str, Path]:
103
+ """Return updated output-path mappings extracted from completed flow results."""
104
+ if not isinstance(results, list):
105
+ return existing.copy()
106
+ captured = existing.copy()
107
+ for context in results:
108
+ metadata = getattr(context, "metadata", None)
109
+ if not isinstance(metadata, dict):
110
+ continue
111
+ step_outputs = metadata.get("step_outputs")
112
+ if not isinstance(step_outputs, dict):
113
+ continue
114
+ for operation_name in card.operation_items:
115
+ value = step_outputs.get(cls(operation_name).artifact_key)
116
+ if isinstance(value, Path):
117
+ captured[operation_name] = value
118
+ return captured
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class OperationDetailRow:
123
+ """One operation row in selected-flow detail state."""
124
+
125
+ name: str
126
+ status: str
127
+ elapsed_seconds: float | None
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class SelectedFlowDetailState:
132
+ """Surface-agnostic detail state for one selected flow."""
133
+
134
+ title: str
135
+ description: str
136
+ error: str
137
+ summary_rows: tuple[FlowSummaryRow, ...]
138
+ operation_rows: tuple[OperationDetailRow, ...]
139
+
140
+ @classmethod
141
+ def from_flow(
142
+ cls,
143
+ card: FlowCatalogLike,
144
+ tracker: "OperationSessionState",
145
+ *,
146
+ flow_states: dict[str, str] | None = None,
147
+ ) -> "SelectedFlowDetailState":
148
+ """Build the selected-flow detail state for one card."""
149
+ summary_rows = FlowSummaryRow.rows_for_flow(card, flow_states or {})
150
+ operation_rows = tuple(
151
+ OperationDetailRow(
152
+ name=operation_name,
153
+ status=(row_state.status if row_state is not None else "idle"),
154
+ elapsed_seconds=(row_state.elapsed_seconds if row_state is not None else None),
155
+ )
156
+ for operation_name in card.operation_items
157
+ for row_state in (tracker.row_state(card.name, operation_name),)
158
+ )
159
+ return cls(
160
+ title=card.title,
161
+ description=card.description or "",
162
+ error=card.error or "",
163
+ summary_rows=summary_rows,
164
+ operation_rows=operation_rows,
165
+ )
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class RunStepDetailRow:
170
+ """One step row inside a grouped run detail."""
171
+
172
+ step_name: str
173
+ status: str
174
+ elapsed_seconds: float | None
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class RunDetailState:
179
+ """Surface-agnostic detail state for one grouped run."""
180
+
181
+ display_label: str
182
+ source_label: str
183
+ status: str
184
+ elapsed_seconds: float | None
185
+ step_rows: tuple[RunStepDetailRow, ...]
186
+
187
+ @classmethod
188
+ def from_run(cls, run_state: "FlowRunState") -> "RunDetailState":
189
+ """Build the grouped-run detail state used by operator surfaces."""
190
+ step_rows = tuple(
191
+ RunStepDetailRow(
192
+ step_name=step.step_name,
193
+ status=step.status,
194
+ elapsed_seconds=step.elapsed_seconds,
195
+ )
196
+ for step in run_state.steps
197
+ )
198
+ return cls(
199
+ display_label=run_state.display_label,
200
+ source_label=run_state.source_label,
201
+ status=run_state.status,
202
+ elapsed_seconds=run_state.elapsed_seconds,
203
+ step_rows=step_rows,
204
+ )
205
+
206
+ __all__ = [
207
+ "FlowSummaryState",
208
+ "FlowSummaryRow",
209
+ "OperationArtifactState",
210
+ "OperationDetailRow",
211
+ "RunDetailState",
212
+ "RunStepDetailRow",
213
+ "SelectedFlowDetailState",
214
+ ]
@@ -0,0 +1,56 @@
1
+ """Diagnostic state models shared across operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class DoctorCheck:
10
+ """One doctor check row with status and message."""
11
+
12
+ status: str
13
+ message: str
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ProcessInfo:
18
+ """One relevant local process row."""
19
+
20
+ pid: int
21
+ ppid: int
22
+ status: str
23
+ command: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ClassifiedProcessInfo:
28
+ """One local process row with Data Engine role classification."""
29
+
30
+ pid: int
31
+ ppid: int
32
+ status: str
33
+ command: str
34
+ kind: str
35
+
36
+ @property
37
+ def is_defunct(self) -> bool:
38
+ """Return whether this process row represents a zombie/defunct process."""
39
+ return self.status.startswith("Z")
40
+
41
+ @property
42
+ def is_orphaned(self) -> bool:
43
+ """Return whether this process row is now parented by init/launchd."""
44
+ return self.ppid == 1
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class WorkspaceLeaseDiagnostic:
49
+ """One workspace lease health row for CLI diagnostics."""
50
+
51
+ workspace_id: str
52
+ lease_pid: int | None
53
+ state: str
54
+ stale: bool
55
+ local_owner: bool
56
+
@@ -0,0 +1,104 @@
1
+ """Explicit parsed error models shared across operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import re
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class StructuredErrorField:
11
+ """One labeled field in a parsed operator error."""
12
+
13
+ label: str
14
+ value: str
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class StructuredErrorState:
19
+ """Structured presentation state for one operator-facing error."""
20
+
21
+ title: str
22
+ fields: tuple[StructuredErrorField, ...]
23
+ detail: str
24
+ raw_text: str
25
+
26
+ @classmethod
27
+ def parse(cls, text: str) -> "StructuredErrorState | None":
28
+ """Parse one known verbose error string into structured fields when possible."""
29
+ step_match = re.fullmatch(
30
+ r'Flow "(?P<flow>[^"]+)" failed in step "(?P<step>[^"]+)"'
31
+ r'(?: \(function (?P<function>[^)]+)\))?'
32
+ r'(?: for source "(?P<source>[^"]+)")?: (?P<detail>.+)',
33
+ text,
34
+ )
35
+ if step_match is not None:
36
+ fields = [
37
+ StructuredErrorField("Flow", step_match.group("flow")),
38
+ StructuredErrorField("Phase", "step"),
39
+ StructuredErrorField("Step", step_match.group("step")),
40
+ ]
41
+ function_name = step_match.group("function")
42
+ source_name = step_match.group("source")
43
+ if function_name:
44
+ fields.append(StructuredErrorField("Function", function_name))
45
+ if source_name:
46
+ fields.append(StructuredErrorField("Source", source_name))
47
+ return cls(
48
+ title="Flow Failed",
49
+ fields=tuple(fields),
50
+ detail=step_match.group("detail"),
51
+ raw_text=text,
52
+ )
53
+
54
+ build_match = re.fullmatch(
55
+ r'Flow module "(?P<flow_module>[^"]+)" failed during build\(\)'
56
+ r'(?: in (?P<function>[^:]+))?: (?P<detail>.+)',
57
+ text,
58
+ )
59
+ if build_match is not None:
60
+ fields = [
61
+ StructuredErrorField("Flow Module", build_match.group("flow_module")),
62
+ StructuredErrorField("Phase", "build"),
63
+ ]
64
+ function_name = build_match.group("function")
65
+ if function_name:
66
+ fields.append(StructuredErrorField("Function", function_name))
67
+ return cls(
68
+ title="Flow Module Failed",
69
+ fields=tuple(fields),
70
+ detail=build_match.group("detail"),
71
+ raw_text=text,
72
+ )
73
+
74
+ import_match = re.fullmatch(r'Flow module "(?P<flow_module>[^"]+)" failed during import: (?P<detail>.+)', text)
75
+ if import_match is not None:
76
+ return cls(
77
+ title="Flow Module Failed",
78
+ fields=(
79
+ StructuredErrorField("Flow Module", import_match.group("flow_module")),
80
+ StructuredErrorField("Phase", "import"),
81
+ ),
82
+ detail=import_match.group("detail"),
83
+ raw_text=text,
84
+ )
85
+
86
+ missing_match = re.fullmatch(
87
+ r"Flow module '(?P<flow_module>[^']+)' is not available in (?P<path>.+?)\. Available flow modules: (?P<available>.+)\.",
88
+ text,
89
+ )
90
+ if missing_match is not None:
91
+ return cls(
92
+ title="Flow Module Not Found",
93
+ fields=(
94
+ StructuredErrorField("Flow Module", missing_match.group("flow_module")),
95
+ StructuredErrorField("Workspace", missing_match.group("path")),
96
+ StructuredErrorField("Available", missing_match.group("available")),
97
+ ),
98
+ detail=text,
99
+ raw_text=text,
100
+ )
101
+ return None
102
+
103
+
104
+ __all__ = ["StructuredErrorField", "StructuredErrorState"]
@@ -0,0 +1,99 @@
1
+ """Inspection and preview state models shared across operator surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from data_engine.domain.catalog import FlowCatalogLike
9
+ from data_engine.domain.details import FlowSummaryState
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FlowStepOutputsState:
14
+ """Latest known inspectable outputs for one flow."""
15
+
16
+ outputs: dict[str, Path] = field(default_factory=dict)
17
+
18
+ def get(self, operation_name: str) -> Path | None:
19
+ """Return the last known output path for one operation."""
20
+ return self.outputs.get(operation_name)
21
+
22
+ def has(self, operation_name: str) -> bool:
23
+ """Return whether one operation currently has an inspectable output."""
24
+ return operation_name in self.outputs
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class StepOutputIndex:
29
+ """Latest known inspectable outputs keyed by flow and operation."""
30
+
31
+ flow_outputs: dict[str, FlowStepOutputsState] = field(default_factory=dict)
32
+
33
+ @classmethod
34
+ def empty(cls) -> "StepOutputIndex":
35
+ """Return an empty output index."""
36
+ return cls()
37
+
38
+ @classmethod
39
+ def from_mapping(cls, mapping: dict[str, dict[str, Path]]) -> "StepOutputIndex":
40
+ """Build one output index from legacy nested flow/output mappings."""
41
+ return cls(
42
+ flow_outputs={
43
+ flow_name: FlowStepOutputsState(outputs=dict(outputs))
44
+ for flow_name, outputs in mapping.items()
45
+ }
46
+ )
47
+
48
+ def outputs_for(self, flow_name: str) -> FlowStepOutputsState:
49
+ """Return output state for one flow."""
50
+ return self.flow_outputs.get(flow_name, FlowStepOutputsState())
51
+
52
+ def output_path(self, flow_name: str, operation_name: str) -> Path | None:
53
+ """Return the last known output path for one flow operation."""
54
+ return self.outputs_for(flow_name).get(operation_name)
55
+
56
+ def has_output(self, flow_name: str, operation_name: str) -> bool:
57
+ """Return whether one flow operation has an inspectable output."""
58
+ return self.outputs_for(flow_name).has(operation_name)
59
+
60
+ def with_flow_outputs(self, flow_name: str, outputs: dict[str, Path]) -> "StepOutputIndex":
61
+ """Return a copy with one flow's outputs replaced."""
62
+ next_flow_outputs = dict(self.flow_outputs)
63
+ next_flow_outputs[flow_name] = FlowStepOutputsState(outputs=dict(outputs))
64
+ return type(self)(flow_outputs=next_flow_outputs)
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class ConfigPreviewState:
69
+ """Surface-agnostic state for one flow config/summary preview."""
70
+
71
+ title: str
72
+ description: str
73
+ summary: FlowSummaryState
74
+
75
+ @classmethod
76
+ def from_flow(
77
+ cls,
78
+ card: FlowCatalogLike | None,
79
+ flow_states: dict[str, str],
80
+ ) -> "ConfigPreviewState":
81
+ """Build one config-preview state bundle for a selected flow."""
82
+ if card is None:
83
+ return cls(
84
+ title="No flow selected",
85
+ description="",
86
+ summary=FlowSummaryState.from_flow(None, flow_states),
87
+ )
88
+ return cls(
89
+ title=card.title,
90
+ description=card.description or "No flow description provided.",
91
+ summary=FlowSummaryState.from_flow(card, flow_states),
92
+ )
93
+
94
+
95
+ __all__ = [
96
+ "ConfigPreviewState",
97
+ "FlowStepOutputsState",
98
+ "StepOutputIndex",
99
+ ]
@@ -0,0 +1,118 @@
1
+ """Domain models and parsing helpers for runtime log-entry state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import UTC, datetime
7
+ import logging
8
+ from pathlib import Path
9
+ import re
10
+ from typing import Literal
11
+
12
+ LogKind = Literal["flow", "system"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class RuntimeStepEvent:
17
+ """Parsed runtime event derived from one builder log record."""
18
+
19
+ flow_name: str
20
+ step_name: str | None
21
+ source_label: str
22
+ status: str
23
+ elapsed_seconds: float | None = None
24
+ run_id: str | None = None
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class FlowLogEntry:
29
+ """One runtime log entry captured for operator surfaces."""
30
+
31
+ line: str
32
+ kind: LogKind
33
+ event: RuntimeStepEvent | None = None
34
+ flow_name: str | None = None
35
+ created_at_utc: datetime = field(default_factory=lambda: datetime.now(UTC))
36
+
37
+ @staticmethod
38
+ def format_runtime_message(message: str) -> str:
39
+ """Render a runtime message into a compact operator-facing single line."""
40
+ return format_runtime_message(message)
41
+
42
+
43
+ def short_source_label(value: str | None) -> str:
44
+ """Collapse a source path down to a filename-style label."""
45
+ if value in (None, "None", ""):
46
+ return "-"
47
+ return Path(str(value)).name
48
+
49
+
50
+ def format_runtime_message(message: str) -> str:
51
+ """Render a runtime message into a compact operator-facing single line."""
52
+ step_match = re.search(r"flow=(?P<flow>\S+) step=(?P<step>.+?) source=(?P<source>.+?) status=(?P<status>\S+)", message)
53
+ if step_match is not None:
54
+ source = short_source_label(step_match.group("source"))
55
+ return f"{step_match.group('flow')} {step_match.group('step')} {step_match.group('status')} {source}"
56
+
57
+ flow_match = re.search(r"flow=(?P<flow>\S+) source=(?P<source>.+?) status=(?P<status>\S+)", message)
58
+ if flow_match is not None:
59
+ source = short_source_label(flow_match.group("source"))
60
+ return f"{flow_match.group('flow')} {flow_match.group('status')} {source}"
61
+
62
+ return re.sub(r"/[^ ]+", lambda match: Path(match.group(0)).name, message)
63
+
64
+
65
+ def format_log_line(record: logging.LogRecord) -> str:
66
+ """Render runtime logs into a compact operator-facing single line."""
67
+ return format_runtime_message(record.getMessage())
68
+
69
+
70
+ def parse_runtime_message(message: str) -> RuntimeStepEvent | None:
71
+ """Parse one runtime message into structured flow/step event data when possible."""
72
+ step_match = re.search(
73
+ r"run=(?P<run>\S+) flow=(?P<flow>\S+) step=(?P<step>.+?) source=(?P<source>.+?) status=(?P<status>\S+)(?: elapsed=(?P<elapsed>\S+))?",
74
+ message,
75
+ )
76
+ if step_match is not None:
77
+ elapsed = step_match.group("elapsed")
78
+ return RuntimeStepEvent(
79
+ run_id=step_match.group("run"),
80
+ flow_name=step_match.group("flow"),
81
+ step_name=step_match.group("step"),
82
+ source_label=short_source_label(step_match.group("source")),
83
+ status=step_match.group("status"),
84
+ elapsed_seconds=float(elapsed) if elapsed is not None else None,
85
+ )
86
+
87
+ flow_match = re.search(
88
+ r"run=(?P<run>\S+) flow=(?P<flow>\S+) source=(?P<source>.+?) status=(?P<status>\S+)(?: elapsed=(?P<elapsed>\S+))?",
89
+ message,
90
+ )
91
+ if flow_match is not None:
92
+ elapsed = flow_match.group("elapsed")
93
+ return RuntimeStepEvent(
94
+ run_id=flow_match.group("run"),
95
+ flow_name=flow_match.group("flow"),
96
+ step_name=None,
97
+ source_label=short_source_label(flow_match.group("source")),
98
+ status=flow_match.group("status"),
99
+ elapsed_seconds=float(elapsed) if elapsed is not None else None,
100
+ )
101
+ return None
102
+
103
+
104
+ def parse_runtime_event(record: logging.LogRecord) -> RuntimeStepEvent | None:
105
+ """Parse one runtime log record into structured flow/step event data when possible."""
106
+ return parse_runtime_message(record.getMessage())
107
+
108
+
109
+ __all__ = [
110
+ "FlowLogEntry",
111
+ "LogKind",
112
+ "RuntimeStepEvent",
113
+ "format_log_line",
114
+ "format_runtime_message",
115
+ "parse_runtime_event",
116
+ "parse_runtime_message",
117
+ "short_source_label",
118
+ ]