flowyml 1.5.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.
@@ -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)
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
- self.api_url = os.getenv("FLOWYML_SERVER_URL", "http://localhost:8000")
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
- context_params: dict[str, Any],
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
- context_params: Parameters from context
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
- context_params: dict[str, Any],
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
- context_params: Parameters from context
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=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,