flowyml 1.6.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/cli/main.py CHANGED
@@ -450,5 +450,381 @@ def logs(run_id: str, step: str, tail: int) -> None:
450
450
  click.echo(" [Log output would appear here]")
451
451
 
452
452
 
453
+ # ============================================================================
454
+ # Quick Commands: flowyml go / stop / status
455
+ # ============================================================================
456
+
457
+
458
+ @cli.command()
459
+ @click.option("--host", default="localhost", help="Host to bind to")
460
+ @click.option("--port", default=8080, type=int, help="Port to bind to")
461
+ @click.option("--open-browser", "-o", is_flag=True, help="Open browser automatically")
462
+ def go(host: str, port: int, open_browser: bool) -> None:
463
+ r"""🚀 Start flowyml - Initialize UI dashboard and show welcome message.
464
+
465
+ This is the quickest way to get started with flowyml. It starts the UI
466
+ dashboard server in the background and displays the URL to access it.
467
+
468
+ \b
469
+ Examples:
470
+ flowyml go # Start on default port 8080
471
+ flowyml go -o # Start and open browser
472
+ flowyml go --port 9000 # Start on custom port
473
+ """
474
+ import subprocess
475
+ import sys
476
+ import time
477
+ from flowyml.ui.utils import is_ui_running
478
+
479
+ try:
480
+ from rich.console import Console
481
+ from rich.panel import Panel
482
+ from rich.text import Text
483
+ from rich import box
484
+
485
+ console = Console()
486
+ rich_available = True
487
+ except ImportError:
488
+ rich_available = False
489
+
490
+ url = f"http://{host}:{port}"
491
+
492
+ # Check if already running
493
+ if is_ui_running(host, port):
494
+ if rich_available:
495
+ panel_content = Text()
496
+ panel_content.append("✅ ", style="green")
497
+ panel_content.append("flowyml is already running!\n\n", style="bold green")
498
+ panel_content.append("🌐 Dashboard: ", style="bold")
499
+ panel_content.append(url, style="cyan underline link " + url)
500
+ panel_content.append("\n\n", style="")
501
+ panel_content.append("Run ", style="dim")
502
+ panel_content.append("flowyml stop", style="bold yellow")
503
+ panel_content.append(" to stop the server.", style="dim")
504
+
505
+ console.print(
506
+ Panel(
507
+ panel_content,
508
+ title="[bold cyan]🌊 flowyml[/bold cyan]",
509
+ border_style="cyan",
510
+ box=box.DOUBLE,
511
+ ),
512
+ )
513
+ else:
514
+ click.echo("✅ flowyml is already running!")
515
+ click.echo(f"🌐 Dashboard: {url}")
516
+ click.echo("\nRun 'flowyml stop' to stop the server.")
517
+
518
+ if open_browser:
519
+ import webbrowser
520
+
521
+ webbrowser.open(url)
522
+ return
523
+
524
+ # Start the UI server as a background subprocess
525
+ if rich_available:
526
+ console.print("[bold cyan]🌊 flowyml[/bold cyan] - Starting up...\n")
527
+ else:
528
+ click.echo("🌊 flowyml - Starting up...")
529
+
530
+ try:
531
+ # Start uvicorn as a background process
532
+ # Using subprocess with nohup-like behavior
533
+ cmd = [
534
+ sys.executable,
535
+ "-m",
536
+ "uvicorn",
537
+ "flowyml.ui.backend.main:app",
538
+ "--host",
539
+ host,
540
+ "--port",
541
+ str(port),
542
+ "--log-level",
543
+ "warning",
544
+ ]
545
+
546
+ # Start as detached background process
547
+ if sys.platform == "win32":
548
+ # Windows: use CREATE_NEW_PROCESS_GROUP
549
+ process = subprocess.Popen(
550
+ cmd,
551
+ stdout=subprocess.DEVNULL,
552
+ stderr=subprocess.DEVNULL,
553
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
554
+ )
555
+ else:
556
+ # Unix: use start_new_session
557
+ process = subprocess.Popen(
558
+ cmd,
559
+ stdout=subprocess.DEVNULL,
560
+ stderr=subprocess.DEVNULL,
561
+ start_new_session=True,
562
+ )
563
+
564
+ # Wait for server to start (up to 8 seconds)
565
+ started = False
566
+ for _ in range(80):
567
+ time.sleep(0.1)
568
+ if is_ui_running(host, port):
569
+ started = True
570
+ break
571
+
572
+ if started:
573
+ # Save PID for later stop command
574
+ pid_file = Path.home() / ".flowyml" / "ui_server.pid"
575
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
576
+ pid_file.write_text(f"{process.pid}\n{host}\n{port}")
577
+
578
+ if rich_available:
579
+ panel_content = Text()
580
+ panel_content.append("✅ ", style="green")
581
+ panel_content.append("flowyml is ready!\n\n", style="bold green")
582
+ panel_content.append("🌐 Dashboard: ", style="bold")
583
+ panel_content.append(url, style="cyan underline link " + url)
584
+ panel_content.append("\n\n", style="")
585
+ panel_content.append("📊 View pipelines: ", style="")
586
+ panel_content.append(f"{url}/pipelines", style="cyan")
587
+ panel_content.append("\n", style="")
588
+ panel_content.append("📜 View runs: ", style="")
589
+ panel_content.append(f"{url}/runs", style="cyan")
590
+ panel_content.append("\n\n", style="")
591
+ panel_content.append("Run ", style="dim")
592
+ panel_content.append("flowyml stop", style="bold yellow")
593
+ panel_content.append(" to stop the server.", style="dim")
594
+
595
+ console.print(
596
+ Panel(
597
+ panel_content,
598
+ title="[bold cyan]🌊 flowyml[/bold cyan]",
599
+ border_style="green",
600
+ box=box.DOUBLE,
601
+ ),
602
+ )
603
+
604
+ console.print()
605
+ console.print("[dim]Tip: The dashboard runs in the background. Your pipelines will[/dim]")
606
+ console.print("[dim]automatically show a clickable URL when they run.[/dim]")
607
+ else:
608
+ click.echo("✅ flowyml is ready!")
609
+ click.echo(f"🌐 Dashboard: {url}")
610
+ click.echo(f"📊 View pipelines: {url}/pipelines")
611
+ click.echo(f"📜 View runs: {url}/runs")
612
+ click.echo("\nRun 'flowyml stop' to stop the server.")
613
+ click.echo("\nTip: The dashboard runs in the background. Your pipelines will")
614
+ click.echo("automatically show a clickable URL when they run.")
615
+
616
+ if open_browser:
617
+ import webbrowser
618
+
619
+ webbrowser.open(url)
620
+ else:
621
+ # Server didn't start, kill the process
622
+ process.terminate()
623
+ raise RuntimeError("Server failed to start within timeout")
624
+
625
+ except Exception as e:
626
+ if rich_available:
627
+ panel_content = Text()
628
+ panel_content.append("❌ ", style="red")
629
+ panel_content.append("Failed to start flowyml UI server.\n\n", style="bold red")
630
+ panel_content.append(f"Error: {str(e)[:100]}\n\n", style="dim red")
631
+ panel_content.append("Possible issues:\n", style="")
632
+ panel_content.append(f" • Port {port} might be in use\n", style="dim")
633
+ panel_content.append(" • Missing dependencies (uvicorn, fastapi)\n", style="dim")
634
+ panel_content.append("\n", style="")
635
+ panel_content.append("Try:\n", style="")
636
+ panel_content.append(f" flowyml go --port {port + 1}", style="bold yellow")
637
+ panel_content.append(" (use different port)\n", style="dim")
638
+ panel_content.append(" flowyml ui start", style="bold yellow")
639
+ panel_content.append(" (for verbose output)", style="dim")
640
+
641
+ console.print(
642
+ Panel(
643
+ panel_content,
644
+ title="[bold red]Error[/bold red]",
645
+ border_style="red",
646
+ box=box.ROUNDED,
647
+ ),
648
+ )
649
+ else:
650
+ click.echo(f"❌ Failed to start flowyml UI server: {e}")
651
+ click.echo("Possible issues:")
652
+ click.echo(f" • Port {port} might be in use")
653
+ click.echo(" • Missing dependencies (uvicorn, fastapi)")
654
+ click.echo(f"\nTry: flowyml go --port {port + 1}")
655
+ click.echo("Or run 'flowyml ui start' for verbose output.")
656
+
657
+
658
+ @cli.command("stop")
659
+ @click.option("--host", default="localhost", help="Host of the server")
660
+ @click.option("--port", default=8080, type=int, help="Port of the server")
661
+ def stop_server(host: str, port: int) -> None:
662
+ r"""🛑 Stop flowyml - Shutdown the UI dashboard server.
663
+
664
+ Stops the flowyml UI server if it's running.
665
+
666
+ \b
667
+ Examples:
668
+ flowyml stop # Stop server on default port
669
+ flowyml stop --port 9000 # Stop server on custom port
670
+ """
671
+ import os
672
+ import signal
673
+ import time
674
+ from flowyml.ui.utils import is_ui_running
675
+
676
+ try:
677
+ from rich.console import Console
678
+ from rich.panel import Panel
679
+ from rich.text import Text
680
+ from rich import box
681
+
682
+ console = Console()
683
+ rich_available = True
684
+ except ImportError:
685
+ rich_available = False
686
+
687
+ pid_file = Path.home() / ".flowyml" / "ui_server.pid"
688
+
689
+ # First check if we have a PID file from 'flowyml go'
690
+ if pid_file.exists():
691
+ try:
692
+ content = pid_file.read_text().strip().split("\n")
693
+ pid = int(content[0])
694
+ # Note: saved_host and saved_port are in the file but we use the CLI args
695
+ # to allow stopping a server on a different port if needed
696
+
697
+ # Try to kill the process
698
+ try:
699
+ os.kill(pid, signal.SIGTERM)
700
+ time.sleep(0.5)
701
+
702
+ # Clean up PID file
703
+ pid_file.unlink(missing_ok=True)
704
+
705
+ if rich_available:
706
+ console.print(f"[green]✅ flowyml server (PID {pid}) stopped successfully.[/green]")
707
+ else:
708
+ click.echo(f"✅ flowyml server (PID {pid}) stopped successfully.")
709
+ return
710
+ except ProcessLookupError:
711
+ # Process already dead, clean up PID file
712
+ pid_file.unlink(missing_ok=True)
713
+ except PermissionError:
714
+ if rich_available:
715
+ console.print(f"[red]❌ Permission denied to stop process {pid}[/red]")
716
+ else:
717
+ click.echo(f"❌ Permission denied to stop process {pid}")
718
+ return
719
+ except (ValueError, IndexError):
720
+ # Invalid PID file, remove it
721
+ pid_file.unlink(missing_ok=True)
722
+
723
+ # Check if server is running
724
+ if not is_ui_running(host, port):
725
+ if rich_available:
726
+ console.print(f"[yellow]ℹ️ No flowyml server running on {host}:{port}[/yellow]")
727
+ else:
728
+ click.echo(f"ℹ️ No flowyml server running on {host}:{port}")
729
+ return
730
+
731
+ # Server is running but we don't have a PID file - must be from 'flowyml ui start'
732
+ if rich_available:
733
+ panel_content = Text()
734
+ panel_content.append("ℹ️ ", style="yellow")
735
+ panel_content.append("Server running but not started with 'flowyml go'.\n\n", style="")
736
+ panel_content.append("To stop it:\n", style="")
737
+ panel_content.append(" • If running in foreground: ", style="dim")
738
+ panel_content.append("Press Ctrl+C\n", style="bold")
739
+ panel_content.append(" • Find and kill: ", style="dim")
740
+ panel_content.append(f"pkill -f 'uvicorn.*:{port}'\n", style="bold")
741
+ panel_content.append(" • Or find PID: ", style="dim")
742
+ panel_content.append(f"lsof -i :{port}", style="bold")
743
+
744
+ console.print(
745
+ Panel(
746
+ panel_content,
747
+ title="[bold yellow]Manual Stop Required[/bold yellow]",
748
+ border_style="yellow",
749
+ box=box.ROUNDED,
750
+ ),
751
+ )
752
+ else:
753
+ click.echo("ℹ️ Server running but not started with 'flowyml go'.")
754
+ click.echo("To stop it:")
755
+ click.echo(" • If running in foreground: Press Ctrl+C")
756
+ click.echo(f" • Find and kill: pkill -f 'uvicorn.*:{port}'")
757
+ click.echo(f" • Or find PID: lsof -i :{port}")
758
+
759
+
760
+ @cli.command("status")
761
+ @click.option("--host", default="localhost", help="Host to check")
762
+ @click.option("--port", default=8080, type=int, help="Port to check")
763
+ def server_status(host: str, port: int) -> None:
764
+ r"""📊 Check flowyml status - Show if the UI server is running.
765
+
766
+ \b
767
+ Examples:
768
+ flowyml status # Check default port
769
+ flowyml status --port 9000 # Check custom port
770
+ """
771
+ from flowyml.ui.utils import is_ui_running
772
+
773
+ try:
774
+ from rich.console import Console
775
+ from rich.panel import Panel
776
+ from rich.text import Text
777
+ from rich import box
778
+
779
+ console = Console()
780
+ rich_available = True
781
+ except ImportError:
782
+ rich_available = False
783
+
784
+ if is_ui_running(host, port):
785
+ url = f"http://{host}:{port}"
786
+ if rich_available:
787
+ panel_content = Text()
788
+ panel_content.append("✅ ", style="green")
789
+ panel_content.append("flowyml is running\n\n", style="bold green")
790
+ panel_content.append("🌐 Dashboard: ", style="bold")
791
+ panel_content.append(url, style="cyan underline link " + url)
792
+ panel_content.append("\n", style="")
793
+ panel_content.append("💚 Health: ", style="")
794
+ panel_content.append(f"{url}/api/health", style="dim")
795
+
796
+ console.print(
797
+ Panel(
798
+ panel_content,
799
+ title="[bold cyan]🌊 flowyml Status[/bold cyan]",
800
+ border_style="green",
801
+ box=box.ROUNDED,
802
+ ),
803
+ )
804
+ else:
805
+ click.echo("✅ flowyml is running")
806
+ click.echo(f"🌐 Dashboard: {url}")
807
+ click.echo(f"💚 Health: {url}/api/health")
808
+ else:
809
+ if rich_available:
810
+ panel_content = Text()
811
+ panel_content.append("❌ ", style="red")
812
+ panel_content.append(f"flowyml is not running on {host}:{port}\n\n", style="")
813
+ panel_content.append("Start with: ", style="dim")
814
+ panel_content.append("flowyml go", style="bold cyan")
815
+
816
+ console.print(
817
+ Panel(
818
+ panel_content,
819
+ title="[bold cyan]🌊 flowyml Status[/bold cyan]",
820
+ border_style="red",
821
+ box=box.ROUNDED,
822
+ ),
823
+ )
824
+ else:
825
+ click.echo(f"❌ flowyml is not running on {host}:{port}")
826
+ click.echo("Start with: flowyml go")
827
+
828
+
453
829
  if __name__ == "__main__":
454
830
  cli()
flowyml/core/display.py CHANGED
@@ -21,7 +21,15 @@ except ImportError:
21
21
  class PipelineDisplay:
22
22
  """Beautiful CLI display for pipeline execution."""
23
23
 
24
- def __init__(self, pipeline_name: str, steps: list[Any], dag: Any, verbose: bool = True):
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
+ ):
25
33
  """Initialize display system.
26
34
 
27
35
  Args:
@@ -29,11 +37,15 @@ class PipelineDisplay:
29
37
  steps: List of step objects
30
38
  dag: Pipeline DAG
31
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
32
42
  """
33
43
  self.pipeline_name = pipeline_name
34
44
  self.steps = steps
35
45
  self.dag = dag
36
46
  self.verbose = verbose
47
+ self.ui_url = ui_url
48
+ self.run_url = run_url
37
49
  self.console = Console() if RICH_AVAILABLE else None
38
50
  self.step_status = {step.name: "pending" for step in steps}
39
51
  self.step_durations = {}
@@ -75,6 +87,9 @@ class PipelineDisplay:
75
87
  self.console.print(header)
76
88
  self.console.print()
77
89
 
90
+ # Show prominent UI URL if available (so users can click to follow execution)
91
+ self._show_ui_url_banner()
92
+
78
93
  # DAG visualization
79
94
  self._show_dag_rich()
80
95
  else:
@@ -83,8 +98,63 @@ class PipelineDisplay:
83
98
  print(f"🌊 flowyml Pipeline: {self.pipeline_name}")
84
99
  print("=" * 70)
85
100
  print()
101
+ # Show UI URL in simple mode too
102
+ self._show_ui_url_simple()
86
103
  self._show_dag_simple()
87
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
+
88
158
  def _show_dag_rich(self) -> None:
89
159
  """Show DAG using rich."""
90
160
  if not self.dag:
flowyml/core/pipeline.py CHANGED
@@ -451,22 +451,9 @@ class Pipeline:
451
451
  # Auto-start UI server if requested
452
452
  ui_url = None
453
453
  run_url = None
454
+ ui_start_failed = False
454
455
  if auto_start_ui:
455
- try:
456
- from flowyml.ui.server_manager import UIServerManager
457
- from flowyml.ui.utils import get_ui_host_port
458
-
459
- ui_manager = UIServerManager.get_instance()
460
- # Use config values for host/port
461
- host, port = get_ui_host_port()
462
- if ui_manager.ensure_running(host=host, port=port, auto_start=True):
463
- ui_url = ui_manager.get_url()
464
- run_url = ui_manager.get_run_url(run_id)
465
-
466
- # UI URL will be shown in summary, no need to print here
467
- except Exception:
468
- # Silently fail if UI is not available
469
- pass
456
+ ui_url, run_url, ui_start_failed = self._ensure_ui_server(run_id)
470
457
 
471
458
  # Determine stack for this run
472
459
  if stack is not None:
@@ -511,6 +498,8 @@ class Pipeline:
511
498
  steps=self.steps,
512
499
  dag=self.dag,
513
500
  verbose=True,
501
+ ui_url=ui_url, # Pass UI URL for prominent display at start
502
+ run_url=run_url, # Pass run-specific URL for clickable link
514
503
  )
515
504
  display.show_header()
516
505
  display.show_execution_start()
@@ -594,6 +583,163 @@ class Pipeline:
594
583
  # Don't fail the run if definition saving fails
595
584
  print(f"Warning: Failed to save pipeline definition: {e}")
596
585
 
586
+ def _ensure_ui_server(self, run_id: str) -> tuple[str | None, str | None, bool]:
587
+ """Ensure UI server is running, start it if needed, or show guidance.
588
+
589
+ Args:
590
+ run_id: The run ID for generating the run URL
591
+
592
+ Returns:
593
+ Tuple of (ui_url, run_url, start_failed)
594
+ - ui_url: Base URL of the UI server if running
595
+ - run_url: URL to view this specific run if server is running
596
+ - start_failed: True if we tried to start and failed (show guidance)
597
+ """
598
+ import subprocess
599
+ import sys
600
+ import time
601
+ from pathlib import Path
602
+
603
+ try:
604
+ from flowyml.ui.utils import is_ui_running, get_ui_host_port
605
+ except ImportError:
606
+ return None, None, False
607
+
608
+ host, port = get_ui_host_port()
609
+ url = f"http://{host}:{port}"
610
+
611
+ # Check if already running
612
+ if is_ui_running(host, port):
613
+ return url, f"{url}/runs/{run_id}", False
614
+
615
+ # Try to start the UI server as a background subprocess
616
+ try:
617
+ # Check if uvicorn is available
618
+ try:
619
+ import uvicorn # noqa: F401
620
+ except ImportError:
621
+ # uvicorn not installed, show guidance but don't fail
622
+ self._show_ui_guidance(host, port, reason="missing_deps")
623
+ return None, None, True
624
+
625
+ # Start uvicorn as a background process
626
+ cmd = [
627
+ sys.executable,
628
+ "-m",
629
+ "uvicorn",
630
+ "flowyml.ui.backend.main:app",
631
+ "--host",
632
+ host,
633
+ "--port",
634
+ str(port),
635
+ "--log-level",
636
+ "warning",
637
+ ]
638
+
639
+ # Start as detached background process
640
+ if sys.platform == "win32":
641
+ process = subprocess.Popen(
642
+ cmd,
643
+ stdout=subprocess.DEVNULL,
644
+ stderr=subprocess.DEVNULL,
645
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
646
+ )
647
+ else:
648
+ process = subprocess.Popen(
649
+ cmd,
650
+ stdout=subprocess.DEVNULL,
651
+ stderr=subprocess.DEVNULL,
652
+ start_new_session=True,
653
+ )
654
+
655
+ # Wait for server to start (up to 8 seconds)
656
+ started = False
657
+ for _ in range(80):
658
+ time.sleep(0.1)
659
+ if is_ui_running(host, port):
660
+ started = True
661
+ break
662
+
663
+ if started:
664
+ # Save PID for later stop command
665
+ pid_file = Path.home() / ".flowyml" / "ui_server.pid"
666
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
667
+ pid_file.write_text(f"{process.pid}\n{host}\n{port}")
668
+
669
+ return url, f"{url}/runs/{run_id}", False
670
+ else:
671
+ # Server didn't start, kill the process and show guidance
672
+ process.terminate()
673
+ self._show_ui_guidance(host, port, reason="start_failed")
674
+ return None, None, True
675
+
676
+ except Exception:
677
+ # Show guidance on failure
678
+ self._show_ui_guidance(host, port, reason="error")
679
+ return None, None, True
680
+
681
+ def _show_ui_guidance(self, host: str, port: int, reason: str = "not_running") -> None:
682
+ """Show a helpful message guiding the user to start the UI server.
683
+
684
+ Args:
685
+ host: Host the server should run on
686
+ port: Port the server should run on
687
+ reason: Why we're showing guidance (not_running, missing_deps, start_failed, error)
688
+ """
689
+ try:
690
+ from rich.console import Console
691
+ from rich.panel import Panel
692
+ from rich.text import Text
693
+ from rich import box
694
+
695
+ console = Console()
696
+
697
+ content = Text()
698
+ content.append("💡 ", style="yellow")
699
+ content.append("Want to see your pipeline run in a live dashboard?\n\n", style="bold")
700
+
701
+ if reason == "missing_deps":
702
+ content.append("UI dependencies not installed. ", style="dim")
703
+ content.append("Install with:\n", style="")
704
+ content.append(" pip install uvicorn fastapi\n\n", style="bold cyan")
705
+
706
+ content.append("Start the dashboard with:\n", style="")
707
+ content.append(" flowyml go", style="bold green")
708
+
709
+ if port != 8080:
710
+ content.append(f" --port {port}", style="bold green")
711
+
712
+ content.append("\n\n", style="")
713
+ content.append("Then run your pipeline again to see it in the UI!", style="dim")
714
+
715
+ console.print()
716
+ console.print(
717
+ Panel(
718
+ content,
719
+ title="[bold cyan]🌐 Dashboard Available[/bold cyan]",
720
+ border_style="yellow",
721
+ box=box.ROUNDED,
722
+ ),
723
+ )
724
+ console.print()
725
+
726
+ except ImportError:
727
+ # Fallback to simple print
728
+ print()
729
+ print("=" * 60)
730
+ print("💡 Want to see your pipeline run in a live dashboard?")
731
+ print()
732
+ if reason == "missing_deps":
733
+ print(" UI dependencies not installed. Install with:")
734
+ print(" pip install uvicorn fastapi")
735
+ print()
736
+ print(" Start the dashboard with:")
737
+ print(" flowyml go" + (f" --port {port}" if port != 8080 else ""))
738
+ print()
739
+ print(" Then run your pipeline again to see it in the UI!")
740
+ print("=" * 60)
741
+ print()
742
+
597
743
  def _coerce_resource_config(self, resources: Any | None):
598
744
  """Convert resources input to ResourceConfig if necessary."""
599
745
  if resources is None:
@@ -86,12 +86,15 @@ class UIServerManager:
86
86
  try:
87
87
  # Check if UI dependencies are available
88
88
  try:
89
- import uvicorn
90
-
91
- print(f"uvicorn {uvicorn.__version__}")
89
+ import uvicorn # noqa: F401 - just check import
92
90
  except ImportError:
93
91
  return False
94
92
 
93
+ # Capture host/port for closure
94
+ server_host = self._host
95
+ server_port = self._port
96
+ startup_error = {"error": None}
97
+
95
98
  # Start server in a daemon thread
96
99
  def run_server():
97
100
  try:
@@ -100,13 +103,13 @@ class UIServerManager:
100
103
  # Run uvicorn server (blocking call, but in daemon thread)
101
104
  uvicorn.run(
102
105
  "flowyml.ui.backend.main:app",
103
- host=host,
104
- port=port,
105
- log_level="warning", # Reduce noise in background
106
+ host=server_host,
107
+ port=server_port,
108
+ log_level="warning", # Show startup issues
106
109
  access_log=False,
107
110
  )
108
- except Exception:
109
- pass # Server will be stopped
111
+ except Exception as e:
112
+ startup_error["error"] = str(e)
110
113
 
111
114
  # Start in daemon thread
112
115
  self._server_thread = threading.Thread(
@@ -118,14 +121,19 @@ class UIServerManager:
118
121
  self._running = True
119
122
  self._started = True
120
123
 
121
- # Wait a bit for server to start
122
- max_wait = 5
124
+ # Wait a bit for server to start (up to 8 seconds)
125
+ max_wait = 8
123
126
  for _ in range(max_wait * 10): # Check every 100ms
124
127
  time.sleep(0.1)
125
- if is_ui_running(host, port):
128
+ # Check if server started successfully
129
+ if is_ui_running(server_host, server_port):
126
130
  return True
131
+ # Check if we have an error
132
+ if startup_error["error"]:
133
+ self._running = False
134
+ return False
127
135
 
128
- # If we get here, server didn't start
136
+ # If we get here, server didn't start in time
129
137
  self._running = False
130
138
  return False
131
139
 
flowyml/ui/utils.py CHANGED
@@ -78,12 +78,14 @@ def is_ui_running(host: str = "localhost", port: int = 8080) -> bool:
78
78
  conn = http.client.HTTPConnection(host, port, timeout=2)
79
79
  conn.request("GET", "/api/health")
80
80
  response = conn.getresponse()
81
- conn.close()
82
81
 
83
82
  # Check if response is successful and from flowyml
83
+ # Note: must read data BEFORE closing connection
84
84
  if response.status == 200:
85
85
  data = response.read().decode("utf-8")
86
+ conn.close()
86
87
  return "flowyml" in data.lower() or "ok" in data.lower()
88
+ conn.close()
87
89
  return False
88
90
  except Exception:
89
91
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowyml
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Next-Generation ML Pipeline Framework
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ flowyml/assets/report.py,sha256=CR1aI_08GereO-qsVwvy4JdG5_Du5rMcTfoqJ_1jmu8,9207
11
11
  flowyml/cli/__init__.py,sha256=bMA7grr-wiy3LeAjGFSeSG2WQwlXDnQKIeFP7X4-HhM,83
12
12
  flowyml/cli/experiment.py,sha256=ryPzfOlPKtCksjW9E7umJEVmE_5lNjAZqacbYX-azos,6284
13
13
  flowyml/cli/init.py,sha256=FAlk_xhiCYYI_HoiVBqPfiZVC4mGJmDWxqY7R77E7UY,6204
14
- flowyml/cli/main.py,sha256=nwRVRqGraZoDnk2L93bItShExOj7ETucACbPkXX4G2o,14934
14
+ flowyml/cli/main.py,sha256=mltcYGsH-xdvGE5itDoORDyCjyuiNvnS9Ui9-J_qETU,29613
15
15
  flowyml/cli/models.py,sha256=fg5Ry3J_FaPpoHtnm4Ou-wNGMTrNS9ufYZhdonIZhKU,17772
16
16
  flowyml/cli/rich_utils.py,sha256=-jUZ71oNYVc9O3doCuw0Tx6EBuiNb-erIVXhOFJTEEc,2645
17
17
  flowyml/cli/run.py,sha256=bkpEhE9LOqP3baNE0_inDGw29eYfToyb-8pCUS8084A,2293
@@ -24,7 +24,7 @@ flowyml/core/cache.py,sha256=rIzIQ-GX2sVO7qAckpZMfmxDLKQU34XJKJp38zywIpk,6197
24
24
  flowyml/core/checkpoint.py,sha256=EBKAi0UqWkCOOiz5wyXY_SBDBWJH4POwH_E6-AI_h4s,4794
25
25
  flowyml/core/conditional.py,sha256=Huwb_dt6ZRr_OGjmA826IaXxwGQkUchGX8pnT5pHJ0g,11991
26
26
  flowyml/core/context.py,sha256=M0_K_REzIByJlF-2INCgHqDWFDKXmNQb-2HSmPt_WIY,4816
27
- flowyml/core/display.py,sha256=JviIXNwxQzGuO9EcSymBe6NmbkGYVmgnUW26tHYNqBI,19489
27
+ flowyml/core/display.py,sha256=_zVGZAlqG_0n2NxVZPavbtMpFGsV2SKFhzY5s07YIms,22138
28
28
  flowyml/core/error_handling.py,sha256=TovzbOFzQYBHMMM8NjsR8WcbmbdtVjXuW8_fsdbgsPA,11952
29
29
  flowyml/core/execution_status.py,sha256=vlQOOzglpSea4z9_csqnPkFP27jJuXjAke6EQbPMq3g,1347
30
30
  flowyml/core/executor.py,sha256=ljBV472nJyp_06meeeHmD_aomm_JdJD6y03OiPvMtEA,19168
@@ -33,7 +33,7 @@ flowyml/core/hooks.py,sha256=UOqrNY1m3lmVql1FRls6AXDNAV3dxxZl-zO6ijEeRW8,4022
33
33
  flowyml/core/observability.py,sha256=NW05m8zki1TwKeRBBCtAP1UwguMcJasGz8HHgMVbjAU,7011
34
34
  flowyml/core/orchestrator.py,sha256=8duXRaVNJD6V7s7LBTRESJC7a8dBfQau-UQIk7UFo2Y,37733
35
35
  flowyml/core/parallel.py,sha256=KGstDu32i9FFKZV0bBRrm1kl3bKjcHmL2js48dVjWlk,12492
36
- flowyml/core/pipeline.py,sha256=IY-3zK4hCpfEdDpekIMFGoGmm2RJk7GFZ4wwwXTZSus,40692
36
+ flowyml/core/pipeline.py,sha256=4Up-3Iz5_jy578l3-B8jvKkHYxLo6bmieut5jS6JNQ4,46076
37
37
  flowyml/core/project.py,sha256=7Cv_MUnXrD2e69Pzwo4ThXhGyjvP45De6CqL3Z2BtiA,8997
38
38
  flowyml/core/remote_orchestrator.py,sha256=LpHlNQslq14OliNZogBGf0fpu6c5n9T7f3BpftahM8Q,3527
39
39
  flowyml/core/resources.py,sha256=5TimjjSOEedALgNVVjRrvU-cHujKebyFjU4YxImvqZE,14402
@@ -190,8 +190,8 @@ flowyml/ui/frontend/src/utils/date.js,sha256=8tYLT-TspihDCbziiYCNaBjvMa5ez155kBx
190
190
  flowyml/ui/frontend/src/utils/downloads.js,sha256=2w3uSOiAktiCWAj56bTSZUx3eNA9QZt1qkVCzX3YrdY,305
191
191
  flowyml/ui/frontend/tailwind.config.js,sha256=__nxDJC93bzcg8Ro9uxt4c2DiErpUCJfi4B-zNRooYg,813
192
192
  flowyml/ui/frontend/vite.config.js,sha256=b4JAsNo2yU4wRdTKf7ppBKsaw6WW447LrS0V4AbXkbk,401
193
- flowyml/ui/server_manager.py,sha256=vUUt_vFIBPdVH9_wuphWZrKHj1lFo1T16CitqimJj1M,5864
194
- flowyml/ui/utils.py,sha256=Hi1YtCan56nYKeruE5zrlNWUHRw7RLnrZP6jNHfjigA,4434
193
+ flowyml/ui/server_manager.py,sha256=Se37U4aXnzZsG4oh5RmKnGmmisXQ-8lOhaod0QbzSzc,6274
194
+ flowyml/ui/utils.py,sha256=FeM5zga3Dbbn_HRKRRdjLrt0HbnCrxXk0NN7NqPeJII,4516
195
195
  flowyml/utils/__init__.py,sha256=eA_YZEZCnCvWRdcqH8IzwWIO-LSUI5-8sbA9mU6xBto,1490
196
196
  flowyml/utils/config.py,sha256=Oeywfo2vptI0-yF28AwmtIbf3fb-2SME-zbkfKfQgqs,10280
197
197
  flowyml/utils/debug.py,sha256=zcHZxGLbuSImLdcfn1V7CwfaDzc3SunXdV-pWR_UW90,6536
@@ -201,8 +201,8 @@ flowyml/utils/logging.py,sha256=PBJDFlGdp1mePS6A3g08dnGAB-v8jTlcNxEsYs9WSBo,1371
201
201
  flowyml/utils/performance.py,sha256=-ne9v9ddEltiKRPk-AerM1R3Gwwd_oCRKtNyHARWd4k,8655
202
202
  flowyml/utils/stack_config.py,sha256=STX1niArJzvu0YsqUQmrNJ0WTeMVW_setYNH36BlbVI,10826
203
203
  flowyml/utils/validation.py,sha256=mClumVro0bl_XXxT1zWPlRI6M_iZa3z2SZ0QUdmTOqs,10199
204
- flowyml-1.6.0.dist-info/METADATA,sha256=5S8xZHHAj9G9lAI8us_hQxUNUlDNLz2QfDz3NJhRGrw,15536
205
- flowyml-1.6.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
206
- flowyml-1.6.0.dist-info/entry_points.txt,sha256=yuF-dOC4rbyJ2Aqi4CMRBxFhqIRoKO6Mhh6jfiQEVjI,48
207
- flowyml-1.6.0.dist-info/licenses/LICENSE,sha256=DRBRWOEjKZQBvy1WZwxyvp2NmnC1whW9Ef7v0Oo-p_g,626
208
- flowyml-1.6.0.dist-info/RECORD,,
204
+ flowyml-1.7.0.dist-info/METADATA,sha256=ocS0ENtzA4OxxIyBctONIvO8VLiJnOaFjCww9WGuKcY,15536
205
+ flowyml-1.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
206
+ flowyml-1.7.0.dist-info/entry_points.txt,sha256=yuF-dOC4rbyJ2Aqi4CMRBxFhqIRoKO6Mhh6jfiQEVjI,48
207
+ flowyml-1.7.0.dist-info/licenses/LICENSE,sha256=DRBRWOEjKZQBvy1WZwxyvp2NmnC1whW9Ef7v0Oo-p_g,626
208
+ flowyml-1.7.0.dist-info/RECORD,,