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.
Files changed (51) hide show
  1. flowyml/__init__.py +2 -1
  2. flowyml/assets/featureset.py +30 -5
  3. flowyml/assets/metrics.py +47 -4
  4. flowyml/cli/main.py +21 -0
  5. flowyml/cli/models.py +444 -0
  6. flowyml/cli/rich_utils.py +95 -0
  7. flowyml/core/checkpoint.py +6 -1
  8. flowyml/core/conditional.py +104 -0
  9. flowyml/core/display.py +525 -0
  10. flowyml/core/execution_status.py +1 -0
  11. flowyml/core/executor.py +201 -8
  12. flowyml/core/orchestrator.py +500 -7
  13. flowyml/core/pipeline.py +301 -11
  14. flowyml/core/project.py +4 -1
  15. flowyml/core/scheduler.py +225 -81
  16. flowyml/core/versioning.py +13 -4
  17. flowyml/registry/model_registry.py +1 -1
  18. flowyml/storage/sql.py +53 -13
  19. flowyml/ui/backend/main.py +2 -0
  20. flowyml/ui/backend/routers/assets.py +36 -0
  21. flowyml/ui/backend/routers/execution.py +2 -2
  22. flowyml/ui/backend/routers/runs.py +211 -0
  23. flowyml/ui/backend/routers/stats.py +2 -2
  24. flowyml/ui/backend/routers/websocket.py +121 -0
  25. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
  26. flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
  27. flowyml/ui/frontend/dist/index.html +2 -2
  28. flowyml/ui/frontend/package-lock.json +289 -0
  29. flowyml/ui/frontend/package.json +1 -0
  30. flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
  31. flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
  32. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
  33. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
  34. flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
  35. flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
  36. flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
  37. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
  38. flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
  39. flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
  40. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
  41. flowyml/ui/frontend/src/router/index.jsx +4 -0
  42. flowyml/ui/server_manager.py +181 -0
  43. flowyml/ui/utils.py +63 -1
  44. flowyml/utils/config.py +7 -0
  45. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
  46. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
  47. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
  48. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
  49. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
  50. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
  51. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -25,6 +25,7 @@ class ExecutionStatus(str, Enum):
25
25
  # Intermediate states
26
26
  STOPPING = "stopping"
27
27
  CANCELLING = "cancelling"
28
+ DEAD = "dead"
28
29
 
29
30
  @property
30
31
  def is_finished(self) -> bool: