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.
- data_engine/__init__.py +37 -0
- data_engine/application/__init__.py +39 -0
- data_engine/application/actions.py +42 -0
- data_engine/application/catalog.py +151 -0
- data_engine/application/control.py +213 -0
- data_engine/application/details.py +73 -0
- data_engine/application/runtime.py +449 -0
- data_engine/application/workspace.py +62 -0
- data_engine/authoring/__init__.py +14 -0
- data_engine/authoring/builder.py +31 -0
- data_engine/authoring/execution/__init__.py +6 -0
- data_engine/authoring/execution/app.py +6 -0
- data_engine/authoring/execution/context.py +82 -0
- data_engine/authoring/execution/continuous.py +176 -0
- data_engine/authoring/execution/grouped.py +106 -0
- data_engine/authoring/execution/logging.py +83 -0
- data_engine/authoring/execution/polling.py +135 -0
- data_engine/authoring/execution/runner.py +210 -0
- data_engine/authoring/execution/single.py +171 -0
- data_engine/authoring/flow.py +361 -0
- data_engine/authoring/helpers.py +160 -0
- data_engine/authoring/model.py +59 -0
- data_engine/authoring/primitives.py +430 -0
- data_engine/authoring/services.py +42 -0
- data_engine/devtools/__init__.py +3 -0
- data_engine/devtools/project_ast_map.py +503 -0
- data_engine/docs/__init__.py +1 -0
- data_engine/docs/sphinx_source/_static/custom.css +13 -0
- data_engine/docs/sphinx_source/api.rst +42 -0
- data_engine/docs/sphinx_source/conf.py +37 -0
- data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
- data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
- data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
- data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
- data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
- data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
- data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
- data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
- data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
- data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
- data_engine/docs/sphinx_source/guides/project-map.md +118 -0
- data_engine/docs/sphinx_source/guides/recipes.md +268 -0
- data_engine/docs/sphinx_source/index.rst +22 -0
- data_engine/domain/__init__.py +92 -0
- data_engine/domain/actions.py +69 -0
- data_engine/domain/catalog.py +128 -0
- data_engine/domain/details.py +214 -0
- data_engine/domain/diagnostics.py +56 -0
- data_engine/domain/errors.py +104 -0
- data_engine/domain/inspection.py +99 -0
- data_engine/domain/logs.py +118 -0
- data_engine/domain/operations.py +172 -0
- data_engine/domain/operator.py +72 -0
- data_engine/domain/runs.py +155 -0
- data_engine/domain/runtime.py +279 -0
- data_engine/domain/source_state.py +17 -0
- data_engine/domain/support.py +54 -0
- data_engine/domain/time.py +23 -0
- data_engine/domain/workspace.py +159 -0
- data_engine/flow_modules/__init__.py +1 -0
- data_engine/flow_modules/flow_module_compiler.py +179 -0
- data_engine/flow_modules/flow_module_loader.py +201 -0
- data_engine/helpers/__init__.py +25 -0
- data_engine/helpers/duckdb.py +705 -0
- data_engine/hosts/__init__.py +1 -0
- data_engine/hosts/daemon/__init__.py +23 -0
- data_engine/hosts/daemon/app.py +221 -0
- data_engine/hosts/daemon/bootstrap.py +69 -0
- data_engine/hosts/daemon/client.py +465 -0
- data_engine/hosts/daemon/commands.py +64 -0
- data_engine/hosts/daemon/composition.py +310 -0
- data_engine/hosts/daemon/constants.py +15 -0
- data_engine/hosts/daemon/entrypoints.py +97 -0
- data_engine/hosts/daemon/lifecycle.py +191 -0
- data_engine/hosts/daemon/manager.py +272 -0
- data_engine/hosts/daemon/ownership.py +126 -0
- data_engine/hosts/daemon/runtime_commands.py +188 -0
- data_engine/hosts/daemon/runtime_control.py +31 -0
- data_engine/hosts/daemon/server.py +84 -0
- data_engine/hosts/daemon/shared_state.py +147 -0
- data_engine/hosts/daemon/state_sync.py +101 -0
- data_engine/platform/__init__.py +1 -0
- data_engine/platform/identity.py +35 -0
- data_engine/platform/local_settings.py +146 -0
- data_engine/platform/theme.py +259 -0
- data_engine/platform/workspace_models.py +190 -0
- data_engine/platform/workspace_policy.py +333 -0
- data_engine/runtime/__init__.py +1 -0
- data_engine/runtime/file_watch.py +185 -0
- data_engine/runtime/ledger_models.py +116 -0
- data_engine/runtime/runtime_db.py +938 -0
- data_engine/runtime/shared_state.py +523 -0
- data_engine/services/__init__.py +49 -0
- data_engine/services/daemon.py +64 -0
- data_engine/services/daemon_state.py +40 -0
- data_engine/services/flow_catalog.py +102 -0
- data_engine/services/flow_execution.py +48 -0
- data_engine/services/ledger.py +85 -0
- data_engine/services/logs.py +65 -0
- data_engine/services/runtime_binding.py +105 -0
- data_engine/services/runtime_execution.py +126 -0
- data_engine/services/runtime_history.py +62 -0
- data_engine/services/settings.py +58 -0
- data_engine/services/shared_state.py +28 -0
- data_engine/services/theme.py +59 -0
- data_engine/services/workspace_provisioning.py +224 -0
- data_engine/services/workspaces.py +74 -0
- data_engine/ui/__init__.py +3 -0
- data_engine/ui/cli/__init__.py +19 -0
- data_engine/ui/cli/app.py +161 -0
- data_engine/ui/cli/commands_doctor.py +178 -0
- data_engine/ui/cli/commands_run.py +80 -0
- data_engine/ui/cli/commands_start.py +100 -0
- data_engine/ui/cli/commands_workspace.py +97 -0
- data_engine/ui/cli/dependencies.py +44 -0
- data_engine/ui/cli/parser.py +56 -0
- data_engine/ui/gui/__init__.py +25 -0
- data_engine/ui/gui/app.py +116 -0
- data_engine/ui/gui/bootstrap.py +487 -0
- data_engine/ui/gui/bootstrapper.py +140 -0
- data_engine/ui/gui/cache_models.py +23 -0
- data_engine/ui/gui/control_support.py +185 -0
- data_engine/ui/gui/controllers/__init__.py +6 -0
- data_engine/ui/gui/controllers/flows.py +439 -0
- data_engine/ui/gui/controllers/runtime.py +245 -0
- data_engine/ui/gui/dialogs/__init__.py +12 -0
- data_engine/ui/gui/dialogs/messages.py +88 -0
- data_engine/ui/gui/dialogs/previews.py +222 -0
- data_engine/ui/gui/helpers/__init__.py +62 -0
- data_engine/ui/gui/helpers/inspection.py +81 -0
- data_engine/ui/gui/helpers/lifecycle.py +112 -0
- data_engine/ui/gui/helpers/scroll.py +28 -0
- data_engine/ui/gui/helpers/theming.py +87 -0
- data_engine/ui/gui/icons/dark_light.svg +12 -0
- data_engine/ui/gui/icons/documentation.svg +1 -0
- data_engine/ui/gui/icons/failed.svg +3 -0
- data_engine/ui/gui/icons/group.svg +4 -0
- data_engine/ui/gui/icons/home.svg +2 -0
- data_engine/ui/gui/icons/manual.svg +2 -0
- data_engine/ui/gui/icons/poll.svg +2 -0
- data_engine/ui/gui/icons/schedule.svg +4 -0
- data_engine/ui/gui/icons/settings.svg +2 -0
- data_engine/ui/gui/icons/started.svg +3 -0
- data_engine/ui/gui/icons/success.svg +3 -0
- data_engine/ui/gui/icons/view-log.svg +3 -0
- data_engine/ui/gui/icons.py +50 -0
- data_engine/ui/gui/launcher.py +48 -0
- data_engine/ui/gui/presenters/__init__.py +72 -0
- data_engine/ui/gui/presenters/docs.py +140 -0
- data_engine/ui/gui/presenters/logs.py +58 -0
- data_engine/ui/gui/presenters/runtime_projection.py +29 -0
- data_engine/ui/gui/presenters/sidebar.py +88 -0
- data_engine/ui/gui/presenters/steps.py +148 -0
- data_engine/ui/gui/presenters/workspace.py +39 -0
- data_engine/ui/gui/presenters/workspace_binding.py +75 -0
- data_engine/ui/gui/presenters/workspace_settings.py +182 -0
- data_engine/ui/gui/preview_models.py +37 -0
- data_engine/ui/gui/render_support.py +241 -0
- data_engine/ui/gui/rendering/__init__.py +12 -0
- data_engine/ui/gui/rendering/artifacts.py +95 -0
- data_engine/ui/gui/rendering/icons.py +50 -0
- data_engine/ui/gui/runtime.py +47 -0
- data_engine/ui/gui/state_support.py +193 -0
- data_engine/ui/gui/support.py +214 -0
- data_engine/ui/gui/surface.py +209 -0
- data_engine/ui/gui/theme.py +720 -0
- data_engine/ui/gui/widgets/__init__.py +34 -0
- data_engine/ui/gui/widgets/config.py +41 -0
- data_engine/ui/gui/widgets/logs.py +62 -0
- data_engine/ui/gui/widgets/panels.py +507 -0
- data_engine/ui/gui/widgets/sidebar.py +130 -0
- data_engine/ui/gui/widgets/steps.py +84 -0
- data_engine/ui/tui/__init__.py +5 -0
- data_engine/ui/tui/app.py +222 -0
- data_engine/ui/tui/bootstrap.py +475 -0
- data_engine/ui/tui/bootstrapper.py +117 -0
- data_engine/ui/tui/controllers/__init__.py +6 -0
- data_engine/ui/tui/controllers/flows.py +349 -0
- data_engine/ui/tui/controllers/runtime.py +167 -0
- data_engine/ui/tui/runtime.py +34 -0
- data_engine/ui/tui/state_support.py +141 -0
- data_engine/ui/tui/support.py +63 -0
- data_engine/ui/tui/theme.py +204 -0
- data_engine/ui/tui/widgets.py +123 -0
- data_engine/views/__init__.py +109 -0
- data_engine/views/actions.py +80 -0
- data_engine/views/artifacts.py +58 -0
- data_engine/views/flow_display.py +69 -0
- data_engine/views/logs.py +54 -0
- data_engine/views/models.py +96 -0
- data_engine/views/presentation.py +133 -0
- data_engine/views/runs.py +62 -0
- data_engine/views/state.py +39 -0
- data_engine/views/status.py +13 -0
- data_engine/views/text.py +109 -0
- py_data_engine-0.1.0.dist-info/METADATA +330 -0
- py_data_engine-0.1.0.dist-info/RECORD +200 -0
- py_data_engine-0.1.0.dist-info/WHEEL +5 -0
- py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|