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 +376 -0
- flowyml/core/display.py +71 -1
- flowyml/core/pipeline.py +161 -15
- flowyml/ui/server_manager.py +20 -12
- flowyml/ui/utils.py +3 -1
- {flowyml-1.6.0.dist-info → flowyml-1.7.0.dist-info}/METADATA +1 -1
- {flowyml-1.6.0.dist-info → flowyml-1.7.0.dist-info}/RECORD +10 -10
- {flowyml-1.6.0.dist-info → flowyml-1.7.0.dist-info}/WHEEL +0 -0
- {flowyml-1.6.0.dist-info → flowyml-1.7.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.6.0.dist-info → flowyml-1.7.0.dist-info}/licenses/LICENSE +0 -0
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__(
|
|
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
|
-
|
|
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:
|
flowyml/ui/server_manager.py
CHANGED
|
@@ -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=
|
|
104
|
-
port=
|
|
105
|
-
log_level="warning", #
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
194
|
-
flowyml/ui/utils.py,sha256=
|
|
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.
|
|
205
|
-
flowyml-1.
|
|
206
|
-
flowyml-1.
|
|
207
|
-
flowyml-1.
|
|
208
|
-
flowyml-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|