flowyml 1.4.0__py3-none-any.whl → 1.6.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 +21 -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 +525 -0
- flowyml/core/execution_status.py +1 -0
- flowyml/core/executor.py +201 -8
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +301 -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/storage/sql.py +53 -13
- flowyml/ui/backend/main.py +2 -0
- flowyml/ui/backend/routers/assets.py +36 -0
- flowyml/ui/backend/routers/execution.py +2 -2
- flowyml/ui/backend/routers/runs.py +211 -0
- flowyml/ui/backend/routers/stats.py +2 -2
- flowyml/ui/backend/routers/websocket.py +121 -0
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/package-lock.json +289 -0
- flowyml/ui/frontend/package.json +1 -0
- flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
- flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
- flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
- flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/server_manager.py +181 -0
- flowyml/ui/utils.py +63 -1
- flowyml/utils/config.py +7 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/display.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
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__(self, pipeline_name: str, steps: list[Any], dag: Any, verbose: bool = True):
|
|
25
|
+
"""Initialize display system.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
pipeline_name: Name of the pipeline
|
|
29
|
+
steps: List of step objects
|
|
30
|
+
dag: Pipeline DAG
|
|
31
|
+
verbose: Whether to show detailed output
|
|
32
|
+
"""
|
|
33
|
+
self.pipeline_name = pipeline_name
|
|
34
|
+
self.steps = steps
|
|
35
|
+
self.dag = dag
|
|
36
|
+
self.verbose = verbose
|
|
37
|
+
self.console = Console() if RICH_AVAILABLE else None
|
|
38
|
+
self.step_status = {step.name: "pending" for step in steps}
|
|
39
|
+
self.step_durations = {}
|
|
40
|
+
self.step_outputs = {}
|
|
41
|
+
self.step_errors = {}
|
|
42
|
+
self.step_cached = {}
|
|
43
|
+
self.start_time = None
|
|
44
|
+
self.progress = None
|
|
45
|
+
self.progress_tasks = {} # Track progress tasks for each step
|
|
46
|
+
if RICH_AVAILABLE:
|
|
47
|
+
self._init_progress()
|
|
48
|
+
|
|
49
|
+
def _init_progress(self) -> None:
|
|
50
|
+
"""Initialize progress display."""
|
|
51
|
+
if not RICH_AVAILABLE:
|
|
52
|
+
return
|
|
53
|
+
self.progress = Progress(
|
|
54
|
+
SpinnerColumn(),
|
|
55
|
+
TextColumn("[progress.description]{task.description}"),
|
|
56
|
+
BarColumn(),
|
|
57
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
58
|
+
TimeElapsedColumn(),
|
|
59
|
+
console=self.console,
|
|
60
|
+
transient=False,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def show_header(self) -> None:
|
|
64
|
+
"""Display pipeline header with DAG visualization."""
|
|
65
|
+
if not self.verbose:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if RICH_AVAILABLE:
|
|
69
|
+
# Rich header
|
|
70
|
+
header = Panel(
|
|
71
|
+
f"[bold cyan]🌊 flowyml Pipeline[/bold cyan]\n" f"[bold]{self.pipeline_name}[/bold]",
|
|
72
|
+
border_style="cyan",
|
|
73
|
+
box=box.ROUNDED,
|
|
74
|
+
)
|
|
75
|
+
self.console.print(header)
|
|
76
|
+
self.console.print()
|
|
77
|
+
|
|
78
|
+
# DAG visualization
|
|
79
|
+
self._show_dag_rich()
|
|
80
|
+
else:
|
|
81
|
+
# Fallback header
|
|
82
|
+
print("=" * 70)
|
|
83
|
+
print(f"🌊 flowyml Pipeline: {self.pipeline_name}")
|
|
84
|
+
print("=" * 70)
|
|
85
|
+
print()
|
|
86
|
+
self._show_dag_simple()
|
|
87
|
+
|
|
88
|
+
def _show_dag_rich(self) -> None:
|
|
89
|
+
"""Show DAG using rich."""
|
|
90
|
+
if not self.dag:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
tree = Tree("📊 Pipeline Execution Plan")
|
|
94
|
+
|
|
95
|
+
# Get topological order
|
|
96
|
+
try:
|
|
97
|
+
topo_order = self.dag.topological_sort()
|
|
98
|
+
except Exception:
|
|
99
|
+
topo_order = list(self.dag.nodes.values())
|
|
100
|
+
|
|
101
|
+
# Group steps by execution group
|
|
102
|
+
groups = defaultdict(list)
|
|
103
|
+
for step in self.steps:
|
|
104
|
+
if step.execution_group:
|
|
105
|
+
groups[step.execution_group].append(step)
|
|
106
|
+
else:
|
|
107
|
+
groups[None].append(step)
|
|
108
|
+
|
|
109
|
+
# Build tree
|
|
110
|
+
for i, node in enumerate(topo_order, 1):
|
|
111
|
+
step = next((s for s in self.steps if s.name == node.name), None)
|
|
112
|
+
if not step:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Check if in a group
|
|
116
|
+
group_name = step.execution_group
|
|
117
|
+
if group_name and group_name in groups:
|
|
118
|
+
# Add to group branch
|
|
119
|
+
if not hasattr(self, "_group_branches"):
|
|
120
|
+
self._group_branches = {}
|
|
121
|
+
if group_name not in self._group_branches:
|
|
122
|
+
group_branch = tree.add(f"📦 [cyan]{group_name}[/cyan]")
|
|
123
|
+
self._group_branches[group_name] = group_branch
|
|
124
|
+
branch = self._group_branches[group_name]
|
|
125
|
+
else:
|
|
126
|
+
branch = tree
|
|
127
|
+
|
|
128
|
+
# Step info
|
|
129
|
+
deps = self.dag.edges.get(node.name, [])
|
|
130
|
+
deps_str = f" → depends on: {', '.join(deps)}" if deps else ""
|
|
131
|
+
|
|
132
|
+
inputs_str = f"Inputs: {', '.join(step.inputs)}" if step.inputs else "No inputs"
|
|
133
|
+
outputs_str = f"Outputs: {', '.join(step.outputs)}" if step.outputs else "No outputs"
|
|
134
|
+
|
|
135
|
+
step_text = f"[bold]{i}. {step.name}[/bold]\n" f" {inputs_str}\n" f" {outputs_str}"
|
|
136
|
+
if deps_str:
|
|
137
|
+
step_text += f"\n [dim]{deps_str}[/dim]"
|
|
138
|
+
|
|
139
|
+
branch.add(step_text)
|
|
140
|
+
|
|
141
|
+
self.console.print(tree)
|
|
142
|
+
self.console.print()
|
|
143
|
+
|
|
144
|
+
def _show_dag_simple(self) -> None:
|
|
145
|
+
"""Show DAG using simple text."""
|
|
146
|
+
if not self.dag:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
print("Pipeline DAG:")
|
|
150
|
+
print("=" * 70)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
topo_order = self.dag.topological_sort()
|
|
154
|
+
except Exception:
|
|
155
|
+
topo_order = list(self.dag.nodes.values())
|
|
156
|
+
|
|
157
|
+
for i, node in enumerate(topo_order, 1):
|
|
158
|
+
step = next((s for s in self.steps if s.name == node.name), None)
|
|
159
|
+
if not step:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
deps = self.dag.edges.get(node.name, [])
|
|
163
|
+
deps_str = f"Dependencies: {', '.join(deps)}" if deps else "Dependencies: none"
|
|
164
|
+
|
|
165
|
+
inputs_str = f"Inputs: {step.inputs}" if step.inputs else "Inputs: []"
|
|
166
|
+
outputs_str = f"Outputs: {step.outputs}" if step.outputs else "Outputs: []"
|
|
167
|
+
|
|
168
|
+
group_str = f" [Group: {step.execution_group}]" if step.execution_group else ""
|
|
169
|
+
|
|
170
|
+
print(f"{i}. {step.name}{group_str}")
|
|
171
|
+
print(f" {inputs_str}")
|
|
172
|
+
print(f" {outputs_str}")
|
|
173
|
+
print(f" {deps_str}")
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
def show_execution_start(self) -> None:
|
|
177
|
+
"""Show execution start message."""
|
|
178
|
+
self.start_time = time.time()
|
|
179
|
+
if not self.verbose:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if RICH_AVAILABLE:
|
|
183
|
+
# Use Text for styled output
|
|
184
|
+
start_text = Text()
|
|
185
|
+
start_text.append("🚀 ", style="bold")
|
|
186
|
+
start_text.append("Starting pipeline execution...", style="bold green")
|
|
187
|
+
self.console.print(start_text)
|
|
188
|
+
self.console.print()
|
|
189
|
+
|
|
190
|
+
# Start progress display if available
|
|
191
|
+
if self.progress:
|
|
192
|
+
self.progress.start()
|
|
193
|
+
else:
|
|
194
|
+
print("🚀 Starting pipeline execution...")
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
def update_step_status(
|
|
198
|
+
self,
|
|
199
|
+
step_name: str,
|
|
200
|
+
status: str,
|
|
201
|
+
duration: float = None,
|
|
202
|
+
cached: bool = False,
|
|
203
|
+
error: str = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Update and display step status.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
step_name: Name of the step
|
|
209
|
+
status: Status (running, success, failed, cached)
|
|
210
|
+
duration: Execution duration in seconds
|
|
211
|
+
cached: Whether step was cached
|
|
212
|
+
error: Error message if failed
|
|
213
|
+
"""
|
|
214
|
+
self.step_status[step_name] = status
|
|
215
|
+
if duration is not None:
|
|
216
|
+
self.step_durations[step_name] = duration
|
|
217
|
+
if cached:
|
|
218
|
+
self.step_cached[step_name] = True
|
|
219
|
+
if error:
|
|
220
|
+
self.step_errors[step_name] = error
|
|
221
|
+
|
|
222
|
+
if not self.verbose:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if RICH_AVAILABLE:
|
|
226
|
+
self._update_step_rich(step_name, status, duration, cached, error)
|
|
227
|
+
else:
|
|
228
|
+
self._update_step_simple(step_name, status, duration, cached, error)
|
|
229
|
+
|
|
230
|
+
def _update_step_rich(
|
|
231
|
+
self,
|
|
232
|
+
step_name: str,
|
|
233
|
+
status: str,
|
|
234
|
+
duration: float = None,
|
|
235
|
+
cached: bool = False,
|
|
236
|
+
error: str = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Update step display using rich with progress tracking."""
|
|
239
|
+
if status == "running":
|
|
240
|
+
# Use Text for better formatting
|
|
241
|
+
icon = "⏳"
|
|
242
|
+
color = "yellow"
|
|
243
|
+
text_obj = Text()
|
|
244
|
+
text_obj.append(f"{icon} ", style=color)
|
|
245
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
246
|
+
text_obj.append(" running...", style="dim")
|
|
247
|
+
self.console.print(text_obj)
|
|
248
|
+
|
|
249
|
+
# Start progress tracking if available
|
|
250
|
+
if self.progress and step_name not in self.progress_tasks:
|
|
251
|
+
task_id = self.progress.add_task(
|
|
252
|
+
f"[yellow]⏳ {step_name}[/yellow]",
|
|
253
|
+
total=100,
|
|
254
|
+
)
|
|
255
|
+
self.progress_tasks[step_name] = task_id
|
|
256
|
+
self.progress.update(task_id, completed=50) # Show progress
|
|
257
|
+
|
|
258
|
+
elif status == "success":
|
|
259
|
+
icon = "✅"
|
|
260
|
+
color = "green"
|
|
261
|
+
text_obj = Text()
|
|
262
|
+
text_obj.append(f"{icon} ", style=color)
|
|
263
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
264
|
+
if cached:
|
|
265
|
+
text_obj.append(" (cached)", style="dim italic")
|
|
266
|
+
if duration:
|
|
267
|
+
text_obj.append(f" ({duration:.2f}s)", style="dim")
|
|
268
|
+
self.console.print(text_obj)
|
|
269
|
+
|
|
270
|
+
# Complete progress task
|
|
271
|
+
if self.progress and step_name in self.progress_tasks:
|
|
272
|
+
task_id = self.progress_tasks[step_name]
|
|
273
|
+
self.progress.update(task_id, completed=100)
|
|
274
|
+
self.progress.remove_task(task_id)
|
|
275
|
+
del self.progress_tasks[step_name]
|
|
276
|
+
|
|
277
|
+
elif status == "failed":
|
|
278
|
+
icon = "❌"
|
|
279
|
+
color = "red"
|
|
280
|
+
text_obj = Text()
|
|
281
|
+
text_obj.append(f"{icon} ", style=color)
|
|
282
|
+
text_obj.append(step_name, style=f"bold {color}")
|
|
283
|
+
if error:
|
|
284
|
+
text_obj.append(f" - {error[:100]}", style="red dim")
|
|
285
|
+
self.console.print(text_obj)
|
|
286
|
+
|
|
287
|
+
# Remove progress task on failure
|
|
288
|
+
if self.progress and step_name in self.progress_tasks:
|
|
289
|
+
task_id = self.progress_tasks[step_name]
|
|
290
|
+
self.progress.remove_task(task_id)
|
|
291
|
+
del self.progress_tasks[step_name]
|
|
292
|
+
else:
|
|
293
|
+
icon = "⏸️"
|
|
294
|
+
text_obj = Text()
|
|
295
|
+
text_obj.append(f"{icon} ", style="dim")
|
|
296
|
+
text_obj.append(step_name, style="dim")
|
|
297
|
+
self.console.print(text_obj)
|
|
298
|
+
|
|
299
|
+
def _update_step_simple(
|
|
300
|
+
self,
|
|
301
|
+
step_name: str,
|
|
302
|
+
status: str,
|
|
303
|
+
duration: float = None,
|
|
304
|
+
cached: bool = False,
|
|
305
|
+
error: str = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Update step display using simple text."""
|
|
308
|
+
if status == "running":
|
|
309
|
+
print(f"⏳ {step_name} running...")
|
|
310
|
+
elif status == "success":
|
|
311
|
+
cached_text = " (cached)" if cached else ""
|
|
312
|
+
duration_text = f" ({duration:.2f}s)" if duration else ""
|
|
313
|
+
print(f"✅ {step_name}{cached_text}{duration_text}")
|
|
314
|
+
elif status == "failed":
|
|
315
|
+
error_text = f" - {error}" if error else ""
|
|
316
|
+
print(f"❌ {step_name}{error_text}")
|
|
317
|
+
else:
|
|
318
|
+
print(f"⏸️ {step_name}")
|
|
319
|
+
|
|
320
|
+
def show_summary(self, result: Any, ui_url: str = None, run_url: str = None) -> None:
|
|
321
|
+
"""Show execution summary.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
result: PipelineResult object
|
|
325
|
+
ui_url: Optional UI server URL
|
|
326
|
+
run_url: Optional run-specific UI URL
|
|
327
|
+
"""
|
|
328
|
+
if not self.verbose:
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
total_duration = time.time() - self.start_time if self.start_time else result.duration_seconds
|
|
332
|
+
|
|
333
|
+
if RICH_AVAILABLE:
|
|
334
|
+
self._show_summary_rich(result, total_duration, ui_url, run_url)
|
|
335
|
+
else:
|
|
336
|
+
self._show_summary_simple(result, total_duration, ui_url, run_url)
|
|
337
|
+
|
|
338
|
+
def _show_summary_rich(self, result: Any, total_duration: float, ui_url: str = None, run_url: str = None) -> None:
|
|
339
|
+
"""Show summary using rich."""
|
|
340
|
+
# Stop progress display if running
|
|
341
|
+
if self.progress:
|
|
342
|
+
self.progress.stop()
|
|
343
|
+
self.console.print()
|
|
344
|
+
|
|
345
|
+
self.console.print()
|
|
346
|
+
|
|
347
|
+
# Summary table with enhanced styling
|
|
348
|
+
table = Table(
|
|
349
|
+
title="[bold cyan]📊 Execution Summary[/bold cyan]",
|
|
350
|
+
box=box.ROUNDED,
|
|
351
|
+
show_header=True,
|
|
352
|
+
header_style="bold cyan",
|
|
353
|
+
border_style="cyan",
|
|
354
|
+
)
|
|
355
|
+
table.add_column("Metric", style="cyan", no_wrap=True, width=20)
|
|
356
|
+
table.add_column("Value", style="green", width=30)
|
|
357
|
+
|
|
358
|
+
# Use Text for status with better formatting
|
|
359
|
+
status_text = Text()
|
|
360
|
+
if result.success:
|
|
361
|
+
status_text.append("✅ ", style="green")
|
|
362
|
+
status_text.append("SUCCESS", style="bold green")
|
|
363
|
+
else:
|
|
364
|
+
status_text.append("❌ ", style="red")
|
|
365
|
+
status_text.append("FAILED", style="bold red")
|
|
366
|
+
|
|
367
|
+
table.add_row("Pipeline", self.pipeline_name)
|
|
368
|
+
table.add_row("Run ID", result.run_id[:16] + "...")
|
|
369
|
+
table.add_row("Status", status_text)
|
|
370
|
+
table.add_row("Duration", f"[green]{total_duration:.2f}s[/green]")
|
|
371
|
+
table.add_row("Steps", f"[cyan]{len(result.step_results)}[/cyan]")
|
|
372
|
+
|
|
373
|
+
# Count cached steps
|
|
374
|
+
cached_count = sum(1 for r in result.step_results.values() if r.cached)
|
|
375
|
+
if cached_count > 0:
|
|
376
|
+
table.add_row("Cached Steps", f"[yellow]{cached_count}[/yellow]")
|
|
377
|
+
|
|
378
|
+
self.console.print(table)
|
|
379
|
+
self.console.print()
|
|
380
|
+
|
|
381
|
+
# Show UI URL if available
|
|
382
|
+
if run_url:
|
|
383
|
+
ui_panel = Panel(
|
|
384
|
+
f"[bold cyan]🌐 View in UI:[/bold cyan]\n[link={run_url}]{run_url}[/link]",
|
|
385
|
+
border_style="cyan",
|
|
386
|
+
box=box.ROUNDED,
|
|
387
|
+
)
|
|
388
|
+
self.console.print(ui_panel)
|
|
389
|
+
self.console.print()
|
|
390
|
+
elif ui_url:
|
|
391
|
+
ui_panel = Panel(
|
|
392
|
+
f"[bold cyan]🌐 UI Available:[/bold cyan]\n[link={ui_url}]{ui_url}[/link]",
|
|
393
|
+
border_style="cyan",
|
|
394
|
+
box=box.ROUNDED,
|
|
395
|
+
)
|
|
396
|
+
self.console.print(ui_panel)
|
|
397
|
+
self.console.print()
|
|
398
|
+
|
|
399
|
+
# Step results table
|
|
400
|
+
step_table = Table(title="📋 Step Results", box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
|
401
|
+
step_table.add_column("Step", style="cyan")
|
|
402
|
+
step_table.add_column("Status", justify="center")
|
|
403
|
+
step_table.add_column("Duration", justify="right")
|
|
404
|
+
step_table.add_column("Details")
|
|
405
|
+
|
|
406
|
+
for step_name, step_result in result.step_results.items():
|
|
407
|
+
if step_result.success:
|
|
408
|
+
status = "[green]✅[/green]"
|
|
409
|
+
details = "[dim]Cached[/dim]" if step_result.cached else ""
|
|
410
|
+
else:
|
|
411
|
+
status = "[red]❌[/red]"
|
|
412
|
+
details = f"[red]{step_result.error[:50]}...[/red]" if step_result.error else ""
|
|
413
|
+
|
|
414
|
+
duration = f"{step_result.duration_seconds:.2f}s" if step_result.duration_seconds else "N/A"
|
|
415
|
+
step_table.add_row(step_name, status, duration, details)
|
|
416
|
+
|
|
417
|
+
self.console.print(step_table)
|
|
418
|
+
self.console.print()
|
|
419
|
+
|
|
420
|
+
# Outputs summary
|
|
421
|
+
if result.outputs:
|
|
422
|
+
outputs_panel = Panel(
|
|
423
|
+
self._format_outputs_rich(result.outputs),
|
|
424
|
+
title="📦 Outputs",
|
|
425
|
+
border_style="cyan",
|
|
426
|
+
box=box.ROUNDED,
|
|
427
|
+
)
|
|
428
|
+
self.console.print(outputs_panel)
|
|
429
|
+
self.console.print()
|
|
430
|
+
|
|
431
|
+
def _format_outputs_rich(self, outputs: dict) -> str:
|
|
432
|
+
"""Format outputs for rich display."""
|
|
433
|
+
lines = []
|
|
434
|
+
for key, value in outputs.items():
|
|
435
|
+
# Skip internal/duplicate outputs (step names vs output names)
|
|
436
|
+
if key in [s.name for s in self.steps] and any(
|
|
437
|
+
out_name in outputs for s in self.steps for out_name in (s.outputs or []) if s.name == key
|
|
438
|
+
):
|
|
439
|
+
continue # Skip step name if we have the actual output name
|
|
440
|
+
|
|
441
|
+
if hasattr(value, "__class__"):
|
|
442
|
+
value_str = f"{value.__class__.__name__}"
|
|
443
|
+
if hasattr(value, "name"):
|
|
444
|
+
value_str += f" (name: {value.name})"
|
|
445
|
+
if hasattr(value, "version"):
|
|
446
|
+
value_str += f" (version: {value.version})"
|
|
447
|
+
# Try to get more info for Asset types
|
|
448
|
+
if hasattr(value, "__dict__"):
|
|
449
|
+
attrs = {
|
|
450
|
+
k: v
|
|
451
|
+
for k, v in value.__dict__.items()
|
|
452
|
+
if not k.startswith("_") and k not in ["name", "version"]
|
|
453
|
+
}
|
|
454
|
+
if attrs:
|
|
455
|
+
# Show first few attributes
|
|
456
|
+
attr_str = ", ".join(f"{k}={v}" for k, v in list(attrs.items())[:3])
|
|
457
|
+
if len(attrs) > 3:
|
|
458
|
+
attr_str += "..."
|
|
459
|
+
value_str += f" [{attr_str}]"
|
|
460
|
+
else:
|
|
461
|
+
# For simple types, show a preview
|
|
462
|
+
value_str = str(value)
|
|
463
|
+
if len(value_str) > 80:
|
|
464
|
+
value_str = value_str[:80] + "..."
|
|
465
|
+
lines.append(f"[cyan]{key}:[/cyan] {value_str}")
|
|
466
|
+
return "\n".join(lines) if lines else "[dim]No outputs[/dim]"
|
|
467
|
+
|
|
468
|
+
def _show_summary_simple(self, result: Any, total_duration: float, ui_url: str = None, run_url: str = None) -> None:
|
|
469
|
+
"""Show summary using simple text."""
|
|
470
|
+
print()
|
|
471
|
+
print("=" * 70)
|
|
472
|
+
status_icon = "✅" if result.success else "❌"
|
|
473
|
+
status_text = "SUCCESS" if result.success else "FAILED"
|
|
474
|
+
print(f"{status_icon} Pipeline {status_text}!")
|
|
475
|
+
print("=" * 70)
|
|
476
|
+
print()
|
|
477
|
+
print(f"Pipeline: {self.pipeline_name}")
|
|
478
|
+
print(f"Run ID: {result.run_id}")
|
|
479
|
+
print(f"Duration: {total_duration:.2f}s")
|
|
480
|
+
print(f"Steps: {len(result.step_results)}")
|
|
481
|
+
|
|
482
|
+
# Show UI URL if available
|
|
483
|
+
if run_url:
|
|
484
|
+
print()
|
|
485
|
+
print(f"🌐 View in UI: {run_url}")
|
|
486
|
+
elif ui_url:
|
|
487
|
+
print()
|
|
488
|
+
print(f"🌐 UI Available: {ui_url}")
|
|
489
|
+
print()
|
|
490
|
+
|
|
491
|
+
print("Step Results:")
|
|
492
|
+
print("-" * 70)
|
|
493
|
+
for step_name, step_result in result.step_results.items():
|
|
494
|
+
icon = "✅" if step_result.success else "❌"
|
|
495
|
+
cached = " (cached)" if step_result.cached else ""
|
|
496
|
+
duration = f" ({step_result.duration_seconds:.2f}s)" if step_result.duration_seconds else ""
|
|
497
|
+
print(f" {icon} {step_name}{cached}{duration}")
|
|
498
|
+
if step_result.error:
|
|
499
|
+
print(f" Error: {step_result.error[:100]}")
|
|
500
|
+
print()
|
|
501
|
+
|
|
502
|
+
if result.outputs:
|
|
503
|
+
print("Outputs:")
|
|
504
|
+
print("-" * 70)
|
|
505
|
+
for key, value in result.outputs.items():
|
|
506
|
+
# Skip internal/duplicate outputs
|
|
507
|
+
if key in [s.name for s in self.steps] and any(
|
|
508
|
+
out_name in result.outputs for s in self.steps for out_name in (s.outputs or []) if s.name == key
|
|
509
|
+
):
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
if hasattr(value, "__class__"):
|
|
513
|
+
value_str = f"{value.__class__.__name__}"
|
|
514
|
+
if hasattr(value, "name"):
|
|
515
|
+
value_str += f" (name: {value.name})"
|
|
516
|
+
if hasattr(value, "version"):
|
|
517
|
+
value_str += f" (version: {value.version})"
|
|
518
|
+
else:
|
|
519
|
+
value_str = str(value)
|
|
520
|
+
if len(value_str) > 100:
|
|
521
|
+
value_str = value_str[:100] + "..."
|
|
522
|
+
print(f" {key}: {value_str}")
|
|
523
|
+
print()
|
|
524
|
+
|
|
525
|
+
print("=" * 70)
|