flowyml 1.5.0__py3-none-any.whl → 1.7.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.
- flowyml/__init__.py +2 -1
- flowyml/assets/featureset.py +30 -5
- flowyml/assets/metrics.py +47 -4
- flowyml/cli/main.py +397 -0
- flowyml/cli/models.py +444 -0
- flowyml/cli/rich_utils.py +95 -0
- flowyml/core/checkpoint.py +6 -1
- flowyml/core/conditional.py +104 -0
- flowyml/core/display.py +595 -0
- flowyml/core/executor.py +27 -6
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +447 -11
- flowyml/core/project.py +4 -1
- flowyml/core/scheduler.py +225 -81
- flowyml/core/versioning.py +13 -4
- flowyml/registry/model_registry.py +1 -1
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/{index-DF8dJaFL.js → index-CX5RV2C9.js} +118 -117
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +43 -4
- flowyml/ui/server_manager.py +189 -0
- flowyml/ui/utils.py +66 -2
- flowyml/utils/config.py +7 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/METADATA +5 -3
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/RECORD +28 -24
- flowyml/ui/frontend/dist/assets/index-CBUXOWze.css +0 -1
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/WHEEL +0 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/display.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""Rich display system for pipeline execution with beautiful CLI output."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
|
12
|
+
from rich.tree import Tree
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich import box
|
|
15
|
+
|
|
16
|
+
RICH_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
RICH_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PipelineDisplay:
|
|
22
|
+
"""Beautiful CLI display for pipeline execution."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
pipeline_name: str,
|
|
27
|
+
steps: list[Any],
|
|
28
|
+
dag: Any,
|
|
29
|
+
verbose: bool = True,
|
|
30
|
+
ui_url: str | None = None,
|
|
31
|
+
run_url: str | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize display system.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pipeline_name: Name of the pipeline
|
|
37
|
+
steps: List of step objects
|
|
38
|
+
dag: Pipeline DAG
|
|
39
|
+
verbose: Whether to show detailed output
|
|
40
|
+
ui_url: Optional base URL for the UI dashboard
|
|
41
|
+
run_url: Optional URL to view this specific run in the UI
|
|
42
|
+
"""
|
|
43
|
+
self.pipeline_name = pipeline_name
|
|
44
|
+
self.steps = steps
|
|
45
|
+
self.dag = dag
|
|
46
|
+
self.verbose = verbose
|
|
47
|
+
self.ui_url = ui_url
|
|
48
|
+
self.run_url = run_url
|
|
49
|
+
self.console = Console() if RICH_AVAILABLE else None
|
|
50
|
+
self.step_status = {step.name: "pending" for step in steps}
|
|
51
|
+
self.step_durations = {}
|
|
52
|
+
self.step_outputs = {}
|
|
53
|
+
self.step_errors = {}
|
|
54
|
+
self.step_cached = {}
|
|
55
|
+
self.start_time = None
|
|
56
|
+
self.progress = None
|
|
57
|
+
self.progress_tasks = {} # Track progress tasks for each step
|
|
58
|
+
if RICH_AVAILABLE:
|
|
59
|
+
self._init_progress()
|
|
60
|
+
|
|
61
|
+
def _init_progress(self) -> None:
|
|
62
|
+
"""Initialize progress display."""
|
|
63
|
+
if not RICH_AVAILABLE:
|
|
64
|
+
return
|
|
65
|
+
self.progress = Progress(
|
|
66
|
+
SpinnerColumn(),
|
|
67
|
+
TextColumn("[progress.description]{task.description}"),
|
|
68
|
+
BarColumn(),
|
|
69
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
70
|
+
TimeElapsedColumn(),
|
|
71
|
+
console=self.console,
|
|
72
|
+
transient=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def show_header(self) -> None:
|
|
76
|
+
"""Display pipeline header with DAG visualization."""
|
|
77
|
+
if not self.verbose:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if RICH_AVAILABLE:
|
|
81
|
+
# Rich header
|
|
82
|
+
header = Panel(
|
|
83
|
+
f"[bold cyan]🌊 flowyml Pipeline[/bold cyan]\n" f"[bold]{self.pipeline_name}[/bold]",
|
|
84
|
+
border_style="cyan",
|
|
85
|
+
box=box.ROUNDED,
|
|
86
|
+
)
|
|
87
|
+
self.console.print(header)
|
|
88
|
+
self.console.print()
|
|
89
|
+
|
|
90
|
+
# Show prominent UI URL if available (so users can click to follow execution)
|
|
91
|
+
self._show_ui_url_banner()
|
|
92
|
+
|
|
93
|
+
# DAG visualization
|
|
94
|
+
self._show_dag_rich()
|
|
95
|
+
else:
|
|
96
|
+
# Fallback header
|
|
97
|
+
print("=" * 70)
|
|
98
|
+
print(f"🌊 flowyml Pipeline: {self.pipeline_name}")
|
|
99
|
+
print("=" * 70)
|
|
100
|
+
print()
|
|
101
|
+
# Show UI URL in simple mode too
|
|
102
|
+
self._show_ui_url_simple()
|
|
103
|
+
self._show_dag_simple()
|
|
104
|
+
|
|
105
|
+
def _show_ui_url_banner(self) -> None:
|
|
106
|
+
"""Show a prominent UI URL banner with clickable link (Rich mode)."""
|
|
107
|
+
if not RICH_AVAILABLE or not self.console:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if self.run_url:
|
|
111
|
+
# Create a prominent banner with the run URL
|
|
112
|
+
url_content = Text()
|
|
113
|
+
url_content.append("🌐 ", style="bold cyan")
|
|
114
|
+
url_content.append("Dashboard: ", style="bold white")
|
|
115
|
+
url_content.append(self.run_url, style="bold cyan underline link " + self.run_url)
|
|
116
|
+
url_content.append("\n", style="")
|
|
117
|
+
url_content.append(" ", style="")
|
|
118
|
+
url_content.append("↑ Click to follow pipeline execution in real-time", style="dim italic")
|
|
119
|
+
|
|
120
|
+
ui_panel = Panel(
|
|
121
|
+
url_content,
|
|
122
|
+
border_style="green",
|
|
123
|
+
box=box.DOUBLE,
|
|
124
|
+
title="[bold green]✨ Live Dashboard[/bold green]",
|
|
125
|
+
title_align="left",
|
|
126
|
+
)
|
|
127
|
+
self.console.print(ui_panel)
|
|
128
|
+
self.console.print()
|
|
129
|
+
elif self.ui_url:
|
|
130
|
+
# Show base UI URL if no specific run URL
|
|
131
|
+
url_content = Text()
|
|
132
|
+
url_content.append("🌐 ", style="bold cyan")
|
|
133
|
+
url_content.append("Dashboard: ", style="bold white")
|
|
134
|
+
url_content.append(self.ui_url, style="bold cyan underline link " + self.ui_url)
|
|
135
|
+
|
|
136
|
+
ui_panel = Panel(
|
|
137
|
+
url_content,
|
|
138
|
+
border_style="cyan",
|
|
139
|
+
box=box.ROUNDED,
|
|
140
|
+
title="[bold cyan]UI Available[/bold cyan]",
|
|
141
|
+
title_align="left",
|
|
142
|
+
)
|
|
143
|
+
self.console.print(ui_panel)
|
|
144
|
+
self.console.print()
|
|
145
|
+
|
|
146
|
+
def _show_ui_url_simple(self) -> None:
|
|
147
|
+
"""Show UI URL in simple text mode."""
|
|
148
|
+
if self.run_url:
|
|
149
|
+
print("=" * 70)
|
|
150
|
+
print(f"🌐 Dashboard: {self.run_url}")
|
|
151
|
+
print(" ↑ Open this URL to follow pipeline execution in real-time")
|
|
152
|
+
print("=" * 70)
|
|
153
|
+
print()
|
|
154
|
+
elif self.ui_url:
|
|
155
|
+
print(f"🌐 UI Available: {self.ui_url}")
|
|
156
|
+
print()
|
|
157
|
+
|
|
158
|
+
def _show_dag_rich(self) -> None:
|
|
159
|
+
"""Show DAG using rich."""
|
|
160
|
+
if not self.dag:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
tree = Tree("📊 Pipeline Execution Plan")
|
|
164
|
+
|
|
165
|
+
# Get topological order
|
|
166
|
+
try:
|
|
167
|
+
topo_order = self.dag.topological_sort()
|
|
168
|
+
except Exception:
|
|
169
|
+
topo_order = list(self.dag.nodes.values())
|
|
170
|
+
|
|
171
|
+
# Group steps by execution group
|
|
172
|
+
groups = defaultdict(list)
|
|
173
|
+
for step in self.steps:
|
|
174
|
+
if step.execution_group:
|
|
175
|
+
groups[step.execution_group].append(step)
|
|
176
|
+
else:
|
|
177
|
+
groups[None].append(step)
|
|
178
|
+
|
|
179
|
+
# Build tree
|
|
180
|
+
for i, node in enumerate(topo_order, 1):
|
|
181
|
+
step = next((s for s in self.steps if s.name == node.name), None)
|
|
182
|
+
if not step:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Check if in a group
|
|
186
|
+
group_name = step.execution_group
|
|
187
|
+
if group_name and group_name in groups:
|
|
188
|
+
# Add to group branch
|
|
189
|
+
if not hasattr(self, "_group_branches"):
|
|
190
|
+
self._group_branches = {}
|
|
191
|
+
if group_name not in self._group_branches:
|
|
192
|
+
group_branch = tree.add(f"📦 [cyan]{group_name}[/cyan]")
|
|
193
|
+
self._group_branches[group_name] = group_branch
|
|
194
|
+
branch = self._group_branches[group_name]
|
|
195
|
+
else:
|
|
196
|
+
branch = tree
|
|
197
|
+
|
|
198
|
+
# Step info
|
|
199
|
+
deps = self.dag.edges.get(node.name, [])
|
|
200
|
+
deps_str = f" → depends on: {', '.join(deps)}" if deps else ""
|
|
201
|
+
|
|
202
|
+
inputs_str = f"Inputs: {', '.join(step.inputs)}" if step.inputs else "No inputs"
|
|
203
|
+
outputs_str = f"Outputs: {', '.join(step.outputs)}" if step.outputs else "No outputs"
|
|
204
|
+
|
|
205
|
+
step_text = f"[bold]{i}. {step.name}[/bold]\n" f" {inputs_str}\n" f" {outputs_str}"
|
|
206
|
+
if deps_str:
|
|
207
|
+
step_text += f"\n [dim]{deps_str}[/dim]"
|
|
208
|
+
|
|
209
|
+
branch.add(step_text)
|
|
210
|
+
|
|
211
|
+
self.console.print(tree)
|
|
212
|
+
self.console.print()
|
|
213
|
+
|
|
214
|
+
def _show_dag_simple(self) -> None:
|
|
215
|
+
"""Show DAG using simple text."""
|
|
216
|
+
if not self.dag:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
print("Pipeline DAG:")
|
|
220
|
+
print("=" * 70)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
topo_order = self.dag.topological_sort()
|
|
224
|
+
except Exception:
|
|
225
|
+
topo_order = list(self.dag.nodes.values())
|
|
226
|
+
|
|
227
|
+
for i, node in enumerate(topo_order, 1):
|
|
228
|
+
step = next((s for s in self.steps if s.name == node.name), None)
|
|
229
|
+
if not step:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
deps = self.dag.edges.get(node.name, [])
|
|
233
|
+
deps_str = f"Dependencies: {', '.join(deps)}" if deps else "Dependencies: none"
|
|
234
|
+
|
|
235
|
+
inputs_str = f"Inputs: {step.inputs}" if step.inputs else "Inputs: []"
|
|
236
|
+
outputs_str = f"Outputs: {step.outputs}" if step.outputs else "Outputs: []"
|
|
237
|
+
|
|
238
|
+
group_str = f" [Group: {step.execution_group}]" if step.execution_group else ""
|
|
239
|
+
|
|
240
|
+
print(f"{i}. {step.name}{group_str}")
|
|
241
|
+
print(f" {inputs_str}")
|
|
242
|
+
print(f" {outputs_str}")
|
|
243
|
+
print(f" {deps_str}")
|
|
244
|
+
print()
|
|
245
|
+
|
|
246
|
+
def show_execution_start(self) -> None:
|
|
247
|
+
"""Show execution start message."""
|
|
248
|
+
self.start_time = time.time()
|
|
249
|
+
if not self.verbose:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if RICH_AVAILABLE:
|
|
253
|
+
# Use Text for styled output
|
|
254
|
+
start_text = Text()
|
|
255
|
+
start_text.append("🚀 ", style="bold")
|
|
256
|
+
start_text.append("Starting pipeline execution...", style="bold green")
|
|
257
|
+
self.console.print(start_text)
|
|
258
|
+
self.console.print()
|
|
259
|
+
|
|
260
|
+
# Start progress display if available
|
|
261
|
+
if self.progress:
|
|
262
|
+
self.progress.start()
|
|
263
|
+
else:
|
|
264
|
+
print("🚀 Starting pipeline execution...")
|
|
265
|
+
print()
|
|
266
|
+
|
|
267
|
+
def update_step_status(
|
|
268
|
+
self,
|
|
269
|
+
step_name: str,
|
|
270
|
+
status: str,
|
|
271
|
+
duration: float = None,
|
|
272
|
+
cached: bool = False,
|
|
273
|
+
error: str = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Update and display step status.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
step_name: Name of the step
|
|
279
|
+
status: Status (running, success, failed, cached)
|
|
280
|
+
duration: Execution duration in seconds
|
|
281
|
+
cached: Whether step was cached
|
|
282
|
+
error: Error message if failed
|
|
283
|
+
"""
|
|
284
|
+
self.step_status[step_name] = status
|
|
285
|
+
if duration is not None:
|
|
286
|
+
self.step_durations[step_name] = duration
|
|
287
|
+
if cached:
|
|
288
|
+
self.step_cached[step_name] = True
|
|
289
|
+
if error:
|
|
290
|
+
self.step_errors[step_name] = error
|
|
291
|
+
|
|
292
|
+
if not self.verbose:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if RICH_AVAILABLE:
|
|
296
|
+
self._update_step_rich(step_name, status, duration, cached, error)
|
|
297
|
+
else:
|
|
298
|
+
self._update_step_simple(step_name, status, duration, cached, error)
|
|
299
|
+
|
|
300
|
+
def _update_step_rich(
|
|
301
|
+
self,
|
|
302
|
+
step_name: str,
|
|
303
|
+
status: str,
|
|
304
|
+
duration: float = None,
|
|
305
|
+
cached: bool = False,
|
|
306
|
+
error: str = None,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Update step display using rich with progress tracking."""
|
|
309
|
+
if status == "running":
|
|
310
|
+
# Use Text for better formatting
|
|
311
|
+
icon = "⏳"
|
|
312
|
+
color = "yellow"
|
|
313
|
+
text_obj = Text()
|
|
314
|
+
text_obj.append(f"{icon} ", style=color)
|
|
315
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
316
|
+
text_obj.append(" running...", style="dim")
|
|
317
|
+
self.console.print(text_obj)
|
|
318
|
+
|
|
319
|
+
# Start progress tracking if available
|
|
320
|
+
if self.progress and step_name not in self.progress_tasks:
|
|
321
|
+
task_id = self.progress.add_task(
|
|
322
|
+
f"[yellow]⏳ {step_name}[/yellow]",
|
|
323
|
+
total=100,
|
|
324
|
+
)
|
|
325
|
+
self.progress_tasks[step_name] = task_id
|
|
326
|
+
self.progress.update(task_id, completed=50) # Show progress
|
|
327
|
+
|
|
328
|
+
elif status == "success":
|
|
329
|
+
icon = "✅"
|
|
330
|
+
color = "green"
|
|
331
|
+
text_obj = Text()
|
|
332
|
+
text_obj.append(f"{icon} ", style=color)
|
|
333
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
334
|
+
if cached:
|
|
335
|
+
text_obj.append(" (cached)", style="dim italic")
|
|
336
|
+
if duration:
|
|
337
|
+
text_obj.append(f" ({duration:.2f}s)", style="dim")
|
|
338
|
+
self.console.print(text_obj)
|
|
339
|
+
|
|
340
|
+
# Complete progress task
|
|
341
|
+
if self.progress and step_name in self.progress_tasks:
|
|
342
|
+
task_id = self.progress_tasks[step_name]
|
|
343
|
+
self.progress.update(task_id, completed=100)
|
|
344
|
+
self.progress.remove_task(task_id)
|
|
345
|
+
del self.progress_tasks[step_name]
|
|
346
|
+
|
|
347
|
+
elif status == "failed":
|
|
348
|
+
icon = "❌"
|
|
349
|
+
color = "red"
|
|
350
|
+
text_obj = Text()
|
|
351
|
+
text_obj.append(f"{icon} ", style=color)
|
|
352
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
353
|
+
if error:
|
|
354
|
+
text_obj.append(f" - {error[:100]}", style="red dim")
|
|
355
|
+
self.console.print(text_obj)
|
|
356
|
+
|
|
357
|
+
# Remove progress task on failure
|
|
358
|
+
if self.progress and step_name in self.progress_tasks:
|
|
359
|
+
task_id = self.progress_tasks[step_name]
|
|
360
|
+
self.progress.remove_task(task_id)
|
|
361
|
+
del self.progress_tasks[step_name]
|
|
362
|
+
else:
|
|
363
|
+
icon = "⏸️"
|
|
364
|
+
text_obj = Text()
|
|
365
|
+
text_obj.append(f"{icon} ", style="dim")
|
|
366
|
+
text_obj.append(step_name, style="dim")
|
|
367
|
+
self.console.print(text_obj)
|
|
368
|
+
|
|
369
|
+
def _update_step_simple(
|
|
370
|
+
self,
|
|
371
|
+
step_name: str,
|
|
372
|
+
status: str,
|
|
373
|
+
duration: float = None,
|
|
374
|
+
cached: bool = False,
|
|
375
|
+
error: str = None,
|
|
376
|
+
) -> None:
|
|
377
|
+
"""Update step display using simple text."""
|
|
378
|
+
if status == "running":
|
|
379
|
+
print(f"⏳ {step_name} running...")
|
|
380
|
+
elif status == "success":
|
|
381
|
+
cached_text = " (cached)" if cached else ""
|
|
382
|
+
duration_text = f" ({duration:.2f}s)" if duration else ""
|
|
383
|
+
print(f"✅ {step_name}{cached_text}{duration_text}")
|
|
384
|
+
elif status == "failed":
|
|
385
|
+
error_text = f" - {error}" if error else ""
|
|
386
|
+
print(f"❌ {step_name}{error_text}")
|
|
387
|
+
else:
|
|
388
|
+
print(f"⏸️ {step_name}")
|
|
389
|
+
|
|
390
|
+
def show_summary(self, result: Any, ui_url: str = None, run_url: str = None) -> None:
|
|
391
|
+
"""Show execution summary.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
result: PipelineResult object
|
|
395
|
+
ui_url: Optional UI server URL
|
|
396
|
+
run_url: Optional run-specific UI URL
|
|
397
|
+
"""
|
|
398
|
+
if not self.verbose:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
total_duration = time.time() - self.start_time if self.start_time else result.duration_seconds
|
|
402
|
+
|
|
403
|
+
if RICH_AVAILABLE:
|
|
404
|
+
self._show_summary_rich(result, total_duration, ui_url, run_url)
|
|
405
|
+
else:
|
|
406
|
+
self._show_summary_simple(result, total_duration, ui_url, run_url)
|
|
407
|
+
|
|
408
|
+
def _show_summary_rich(self, result: Any, total_duration: float, ui_url: str = None, run_url: str = None) -> None:
|
|
409
|
+
"""Show summary using rich."""
|
|
410
|
+
# Stop progress display if running
|
|
411
|
+
if self.progress:
|
|
412
|
+
self.progress.stop()
|
|
413
|
+
self.console.print()
|
|
414
|
+
|
|
415
|
+
self.console.print()
|
|
416
|
+
|
|
417
|
+
# Summary table with enhanced styling
|
|
418
|
+
table = Table(
|
|
419
|
+
title="[bold cyan]📊 Execution Summary[/bold cyan]",
|
|
420
|
+
box=box.ROUNDED,
|
|
421
|
+
show_header=True,
|
|
422
|
+
header_style="bold cyan",
|
|
423
|
+
border_style="cyan",
|
|
424
|
+
)
|
|
425
|
+
table.add_column("Metric", style="cyan", no_wrap=True, width=20)
|
|
426
|
+
table.add_column("Value", style="green", width=30)
|
|
427
|
+
|
|
428
|
+
# Use Text for status with better formatting
|
|
429
|
+
status_text = Text()
|
|
430
|
+
if result.success:
|
|
431
|
+
status_text.append("✅ ", style="green")
|
|
432
|
+
status_text.append("SUCCESS", style="bold green")
|
|
433
|
+
else:
|
|
434
|
+
status_text.append("❌ ", style="red")
|
|
435
|
+
status_text.append("FAILED", style="bold red")
|
|
436
|
+
|
|
437
|
+
table.add_row("Pipeline", self.pipeline_name)
|
|
438
|
+
table.add_row("Run ID", result.run_id[:16] + "...")
|
|
439
|
+
table.add_row("Status", status_text)
|
|
440
|
+
table.add_row("Duration", f"[green]{total_duration:.2f}s[/green]")
|
|
441
|
+
table.add_row("Steps", f"[cyan]{len(result.step_results)}[/cyan]")
|
|
442
|
+
|
|
443
|
+
# Count cached steps
|
|
444
|
+
cached_count = sum(1 for r in result.step_results.values() if r.cached)
|
|
445
|
+
if cached_count > 0:
|
|
446
|
+
table.add_row("Cached Steps", f"[yellow]{cached_count}[/yellow]")
|
|
447
|
+
|
|
448
|
+
self.console.print(table)
|
|
449
|
+
self.console.print()
|
|
450
|
+
|
|
451
|
+
# Show UI URL if available
|
|
452
|
+
if run_url:
|
|
453
|
+
ui_panel = Panel(
|
|
454
|
+
f"[bold cyan]🌐 View in UI:[/bold cyan]\n[link={run_url}]{run_url}[/link]",
|
|
455
|
+
border_style="cyan",
|
|
456
|
+
box=box.ROUNDED,
|
|
457
|
+
)
|
|
458
|
+
self.console.print(ui_panel)
|
|
459
|
+
self.console.print()
|
|
460
|
+
elif ui_url:
|
|
461
|
+
ui_panel = Panel(
|
|
462
|
+
f"[bold cyan]🌐 UI Available:[/bold cyan]\n[link={ui_url}]{ui_url}[/link]",
|
|
463
|
+
border_style="cyan",
|
|
464
|
+
box=box.ROUNDED,
|
|
465
|
+
)
|
|
466
|
+
self.console.print(ui_panel)
|
|
467
|
+
self.console.print()
|
|
468
|
+
|
|
469
|
+
# Step results table
|
|
470
|
+
step_table = Table(title="📋 Step Results", box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
|
471
|
+
step_table.add_column("Step", style="cyan")
|
|
472
|
+
step_table.add_column("Status", justify="center")
|
|
473
|
+
step_table.add_column("Duration", justify="right")
|
|
474
|
+
step_table.add_column("Details")
|
|
475
|
+
|
|
476
|
+
for step_name, step_result in result.step_results.items():
|
|
477
|
+
if step_result.success:
|
|
478
|
+
status = "[green]✅[/green]"
|
|
479
|
+
details = "[dim]Cached[/dim]" if step_result.cached else ""
|
|
480
|
+
else:
|
|
481
|
+
status = "[red]❌[/red]"
|
|
482
|
+
details = f"[red]{step_result.error[:50]}...[/red]" if step_result.error else ""
|
|
483
|
+
|
|
484
|
+
duration = f"{step_result.duration_seconds:.2f}s" if step_result.duration_seconds else "N/A"
|
|
485
|
+
step_table.add_row(step_name, status, duration, details)
|
|
486
|
+
|
|
487
|
+
self.console.print(step_table)
|
|
488
|
+
self.console.print()
|
|
489
|
+
|
|
490
|
+
# Outputs summary
|
|
491
|
+
if result.outputs:
|
|
492
|
+
outputs_panel = Panel(
|
|
493
|
+
self._format_outputs_rich(result.outputs),
|
|
494
|
+
title="📦 Outputs",
|
|
495
|
+
border_style="cyan",
|
|
496
|
+
box=box.ROUNDED,
|
|
497
|
+
)
|
|
498
|
+
self.console.print(outputs_panel)
|
|
499
|
+
self.console.print()
|
|
500
|
+
|
|
501
|
+
def _format_outputs_rich(self, outputs: dict) -> str:
|
|
502
|
+
"""Format outputs for rich display."""
|
|
503
|
+
lines = []
|
|
504
|
+
for key, value in outputs.items():
|
|
505
|
+
# Skip internal/duplicate outputs (step names vs output names)
|
|
506
|
+
if key in [s.name for s in self.steps] and any(
|
|
507
|
+
out_name in outputs for s in self.steps for out_name in (s.outputs or []) if s.name == key
|
|
508
|
+
):
|
|
509
|
+
continue # Skip step name if we have the actual output name
|
|
510
|
+
|
|
511
|
+
if hasattr(value, "__class__"):
|
|
512
|
+
value_str = f"{value.__class__.__name__}"
|
|
513
|
+
if hasattr(value, "name"):
|
|
514
|
+
value_str += f" (name: {value.name})"
|
|
515
|
+
if hasattr(value, "version"):
|
|
516
|
+
value_str += f" (version: {value.version})"
|
|
517
|
+
# Try to get more info for Asset types
|
|
518
|
+
if hasattr(value, "__dict__"):
|
|
519
|
+
attrs = {
|
|
520
|
+
k: v
|
|
521
|
+
for k, v in value.__dict__.items()
|
|
522
|
+
if not k.startswith("_") and k not in ["name", "version"]
|
|
523
|
+
}
|
|
524
|
+
if attrs:
|
|
525
|
+
# Show first few attributes
|
|
526
|
+
attr_str = ", ".join(f"{k}={v}" for k, v in list(attrs.items())[:3])
|
|
527
|
+
if len(attrs) > 3:
|
|
528
|
+
attr_str += "..."
|
|
529
|
+
value_str += f" [{attr_str}]"
|
|
530
|
+
else:
|
|
531
|
+
# For simple types, show a preview
|
|
532
|
+
value_str = str(value)
|
|
533
|
+
if len(value_str) > 80:
|
|
534
|
+
value_str = value_str[:80] + "..."
|
|
535
|
+
lines.append(f"[cyan]{key}:[/cyan] {value_str}")
|
|
536
|
+
return "\n".join(lines) if lines else "[dim]No outputs[/dim]"
|
|
537
|
+
|
|
538
|
+
def _show_summary_simple(self, result: Any, total_duration: float, ui_url: str = None, run_url: str = None) -> None:
|
|
539
|
+
"""Show summary using simple text."""
|
|
540
|
+
print()
|
|
541
|
+
print("=" * 70)
|
|
542
|
+
status_icon = "✅" if result.success else "❌"
|
|
543
|
+
status_text = "SUCCESS" if result.success else "FAILED"
|
|
544
|
+
print(f"{status_icon} Pipeline {status_text}!")
|
|
545
|
+
print("=" * 70)
|
|
546
|
+
print()
|
|
547
|
+
print(f"Pipeline: {self.pipeline_name}")
|
|
548
|
+
print(f"Run ID: {result.run_id}")
|
|
549
|
+
print(f"Duration: {total_duration:.2f}s")
|
|
550
|
+
print(f"Steps: {len(result.step_results)}")
|
|
551
|
+
|
|
552
|
+
# Show UI URL if available
|
|
553
|
+
if run_url:
|
|
554
|
+
print()
|
|
555
|
+
print(f"🌐 View in UI: {run_url}")
|
|
556
|
+
elif ui_url:
|
|
557
|
+
print()
|
|
558
|
+
print(f"🌐 UI Available: {ui_url}")
|
|
559
|
+
print()
|
|
560
|
+
|
|
561
|
+
print("Step Results:")
|
|
562
|
+
print("-" * 70)
|
|
563
|
+
for step_name, step_result in result.step_results.items():
|
|
564
|
+
icon = "✅" if step_result.success else "❌"
|
|
565
|
+
cached = " (cached)" if step_result.cached else ""
|
|
566
|
+
duration = f" ({step_result.duration_seconds:.2f}s)" if step_result.duration_seconds else ""
|
|
567
|
+
print(f" {icon} {step_name}{cached}{duration}")
|
|
568
|
+
if step_result.error:
|
|
569
|
+
print(f" Error: {step_result.error[:100]}")
|
|
570
|
+
print()
|
|
571
|
+
|
|
572
|
+
if result.outputs:
|
|
573
|
+
print("Outputs:")
|
|
574
|
+
print("-" * 70)
|
|
575
|
+
for key, value in result.outputs.items():
|
|
576
|
+
# Skip internal/duplicate outputs
|
|
577
|
+
if key in [s.name for s in self.steps] and any(
|
|
578
|
+
out_name in result.outputs for s in self.steps for out_name in (s.outputs or []) if s.name == key
|
|
579
|
+
):
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
if hasattr(value, "__class__"):
|
|
583
|
+
value_str = f"{value.__class__.__name__}"
|
|
584
|
+
if hasattr(value, "name"):
|
|
585
|
+
value_str += f" (name: {value.name})"
|
|
586
|
+
if hasattr(value, "version"):
|
|
587
|
+
value_str += f" (version: {value.version})"
|
|
588
|
+
else:
|
|
589
|
+
value_str = str(value)
|
|
590
|
+
if len(value_str) > 100:
|
|
591
|
+
value_str = value_str[:100] + "..."
|
|
592
|
+
print(f" {key}: {value_str}")
|
|
593
|
+
print()
|
|
594
|
+
|
|
595
|
+
print("=" * 70)
|
flowyml/core/executor.py
CHANGED
|
@@ -78,7 +78,14 @@ class MonitorThread(threading.Thread):
|
|
|
78
78
|
self.log_capture = log_capture
|
|
79
79
|
self.interval = interval
|
|
80
80
|
self._stop_event = threading.Event()
|
|
81
|
-
|
|
81
|
+
# Get UI server URL from configuration (supports env vars, config, centralized deployments)
|
|
82
|
+
try:
|
|
83
|
+
from flowyml.ui.utils import get_ui_server_url
|
|
84
|
+
|
|
85
|
+
self.api_url = get_ui_server_url()
|
|
86
|
+
except Exception:
|
|
87
|
+
# Fallback to environment variable or default
|
|
88
|
+
self.api_url = os.getenv("FLOWYML_SERVER_URL", "http://localhost:8080")
|
|
82
89
|
|
|
83
90
|
def stop(self):
|
|
84
91
|
self._stop_event.set()
|
|
@@ -182,7 +189,8 @@ class Executor:
|
|
|
182
189
|
self,
|
|
183
190
|
step_group, # StepGroup
|
|
184
191
|
inputs: dict[str, Any],
|
|
185
|
-
|
|
192
|
+
context: Any | None = None, # Context object for per-step injection
|
|
193
|
+
context_params: dict[str, Any] | None = None, # Deprecated: use context instead
|
|
186
194
|
cache_store: Any | None = None,
|
|
187
195
|
artifact_store: Any | None = None,
|
|
188
196
|
run_id: str | None = None,
|
|
@@ -193,7 +201,8 @@ class Executor:
|
|
|
193
201
|
Args:
|
|
194
202
|
step_group: StepGroup to execute
|
|
195
203
|
inputs: Input data available to the group
|
|
196
|
-
|
|
204
|
+
context: Context object for per-step parameter injection (preferred)
|
|
205
|
+
context_params: Parameters from context (deprecated, use context instead)
|
|
197
206
|
cache_store: Cache store for caching
|
|
198
207
|
artifact_store: Artifact store for materialization
|
|
199
208
|
run_id: Run identifier
|
|
@@ -397,7 +406,8 @@ class LocalExecutor(Executor):
|
|
|
397
406
|
self,
|
|
398
407
|
step_group, # StepGroup from step_grouping module
|
|
399
408
|
inputs: dict[str, Any],
|
|
400
|
-
|
|
409
|
+
context: Any | None = None, # Context object for per-step injection
|
|
410
|
+
context_params: dict[str, Any] | None = None, # Deprecated: use context instead
|
|
401
411
|
cache_store: Any | None = None,
|
|
402
412
|
artifact_store: Any | None = None,
|
|
403
413
|
run_id: str | None = None,
|
|
@@ -410,7 +420,8 @@ class LocalExecutor(Executor):
|
|
|
410
420
|
Args:
|
|
411
421
|
step_group: StepGroup containing steps to execute
|
|
412
422
|
inputs: Input data available to the group
|
|
413
|
-
|
|
423
|
+
context: Context object for per-step parameter injection (preferred)
|
|
424
|
+
context_params: Parameters from context (deprecated, use context instead)
|
|
414
425
|
cache_store: Cache store for caching
|
|
415
426
|
artifact_store: Artifact store for materialization
|
|
416
427
|
run_id: Run identifier
|
|
@@ -433,11 +444,21 @@ class LocalExecutor(Executor):
|
|
|
433
444
|
if input_name in step_outputs:
|
|
434
445
|
step_inputs[input_name] = step_outputs[input_name]
|
|
435
446
|
|
|
447
|
+
# Inject context parameters for this specific step
|
|
448
|
+
if context is not None:
|
|
449
|
+
# Use context object to inject params per step
|
|
450
|
+
step_context_params = context.inject_params(step.func)
|
|
451
|
+
elif context_params is not None:
|
|
452
|
+
# Fallback to provided context_params (backward compatibility)
|
|
453
|
+
step_context_params = context_params
|
|
454
|
+
else:
|
|
455
|
+
step_context_params = {}
|
|
456
|
+
|
|
436
457
|
# Execute this step
|
|
437
458
|
result = self.execute_step(
|
|
438
459
|
step=step,
|
|
439
460
|
inputs=step_inputs,
|
|
440
|
-
context_params=
|
|
461
|
+
context_params=step_context_params,
|
|
441
462
|
cache_store=cache_store,
|
|
442
463
|
artifact_store=artifact_store,
|
|
443
464
|
run_id=run_id,
|