flowyml 1.5.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/__init__.py CHANGED
@@ -14,7 +14,7 @@ from flowyml.core.step import step, Step
14
14
  from flowyml.core.pipeline import Pipeline
15
15
  from flowyml.core.executor import Executor, LocalExecutor
16
16
  from flowyml.core.cache import CacheStrategy
17
- from flowyml.core.conditional import Condition, ConditionalBranch, Switch, when, unless
17
+ from flowyml.core.conditional import Condition, ConditionalBranch, Switch, when, unless, If
18
18
  from flowyml.core.parallel import ParallelExecutor, DataParallelExecutor, BatchExecutor, parallel_map
19
19
  from flowyml.core.error_handling import (
20
20
  CircuitBreaker,
@@ -115,6 +115,7 @@ __all__ = [
115
115
  "Condition",
116
116
  "ConditionalBranch",
117
117
  "Switch",
118
+ "If",
118
119
  "when",
119
120
  "unless",
120
121
  # Parallel Execution
@@ -67,19 +67,36 @@ class FeatureSet(Asset):
67
67
  source_dataset: Name of source dataset
68
68
  **kwargs: Additional metadata
69
69
  """
70
- # Initialize base asset
71
- metadata = FeatureSetMetadata(
70
+ # Initialize base asset first
71
+ super().__init__(
72
72
  name=name,
73
- type="featureset",
73
+ version=kwargs.get("version"),
74
+ data=None,
75
+ parent=kwargs.get("parent"),
76
+ tags=kwargs.get("tags"),
77
+ properties=kwargs.get("properties"),
78
+ )
79
+
80
+ # Create FeatureSet-specific metadata
81
+ feature_set_metadata = FeatureSetMetadata(
82
+ asset_id=self.asset_id,
83
+ name=name,
84
+ version=self.version,
85
+ asset_type=self.__class__.__name__,
86
+ created_at=self.metadata.created_at,
87
+ created_by=self.metadata.created_by,
88
+ parent_ids=self.metadata.parent_ids,
89
+ tags=self.metadata.tags,
90
+ properties=self.metadata.properties,
74
91
  feature_names=feature_names or [],
75
92
  num_features=len(feature_names) if feature_names else 0,
76
93
  num_samples=num_samples,
77
94
  transformations=transformations or [],
78
95
  source_dataset=source_dataset,
79
- **kwargs,
80
96
  )
81
97
 
82
- super().__init__(name=name, type="featureset", metadata=metadata)
98
+ # Replace metadata with FeatureSet-specific metadata
99
+ self.metadata = feature_set_metadata
83
100
  self._data = data
84
101
 
85
102
  # Extract feature metadata if data provided
@@ -147,6 +164,11 @@ class FeatureSet(Asset):
147
164
  """Get the feature data."""
148
165
  return self._data
149
166
 
167
+ @data.setter
168
+ def data(self, value: Any) -> None:
169
+ """Set the feature data."""
170
+ self._data = value
171
+
150
172
  @property
151
173
  def feature_names(self) -> list[str]:
152
174
  """Get feature names."""
@@ -188,6 +210,7 @@ class FeatureSet(Asset):
188
210
  data: Any,
189
211
  name: str | None = None,
190
212
  feature_names: list[str] | None = None,
213
+ num_samples: int = 0,
191
214
  transformations: list[str] | None = None,
192
215
  source_dataset: str | None = None,
193
216
  **kwargs,
@@ -198,6 +221,7 @@ class FeatureSet(Asset):
198
221
  data: The feature matrix
199
222
  name: Name of the feature set (auto-generated if not provided)
200
223
  feature_names: List of feature names
224
+ num_samples: Number of samples in the feature set
201
225
  transformations: List of transformations applied
202
226
  source_dataset: Name of source dataset
203
227
  **kwargs: Additional metadata
@@ -213,6 +237,7 @@ class FeatureSet(Asset):
213
237
  name=name,
214
238
  data=data,
215
239
  feature_names=feature_names,
240
+ num_samples=num_samples,
216
241
  transformations=transformations,
217
242
  source_dataset=source_dataset,
218
243
  **kwargs,
flowyml/assets/metrics.py CHANGED
@@ -93,18 +93,61 @@ class Metrics(Asset):
93
93
  parent: Asset | None = None,
94
94
  tags: dict[str, str] | None = None,
95
95
  properties: dict[str, Any] | None = None,
96
- **metrics,
96
+ metadata: dict[str, Any] | None = None,
97
+ metrics: dict[str, Any] | None = None,
98
+ **kwargs,
97
99
  ) -> "Metrics":
98
100
  """Factory method to create metrics.
99
101
 
102
+ Supports multiple ways to provide metrics:
103
+ 1. As keyword arguments: Metrics.create(accuracy=0.95, loss=0.05)
104
+ 2. As a dict: Metrics.create(metrics={"accuracy": 0.95, "loss": 0.05})
105
+ 3. Mixed: Metrics.create(metrics={"accuracy": 0.95}, loss=0.05)
106
+
107
+ Args:
108
+ name: Name of the metrics asset
109
+ version: Version string
110
+ parent: Parent asset for lineage
111
+ tags: Tags dictionary (or use metadata for convenience)
112
+ properties: Properties dictionary
113
+ metadata: Metadata dictionary (merged into tags and properties)
114
+ metrics: Metrics as a dictionary (alternative to **kwargs)
115
+ **kwargs: Additional metrics as keyword arguments
116
+
100
117
  Example:
118
+ >>> # Using keyword arguments
101
119
  >>> metrics = Metrics.create(accuracy=0.95, loss=0.05, training_time="2h 15m")
120
+
121
+ >>> # Using metrics dict
122
+ >>> metrics = Metrics.create(
123
+ ... name="example_metrics",
124
+ ... metrics={"test_accuracy": 0.93, "test_loss": 0.07},
125
+ ... metadata={"source": "example"},
126
+ ... )
102
127
  """
128
+ # Merge metrics dict with kwargs
129
+ all_metrics = {}
130
+ if metrics:
131
+ all_metrics.update(metrics)
132
+ all_metrics.update(kwargs)
133
+
134
+ # Handle metadata - merge into tags and properties
135
+ final_tags = tags or {}
136
+ final_properties = properties or {}
137
+ if metadata:
138
+ # If metadata contains string values, treat as tags
139
+ # Otherwise, merge into properties
140
+ for key, value in metadata.items():
141
+ if isinstance(value, str):
142
+ final_tags[key] = value
143
+ else:
144
+ final_properties[key] = value
145
+
103
146
  return cls(
104
147
  name=name or "metrics",
105
148
  version=version,
106
- data=metrics,
149
+ data=all_metrics if all_metrics else None,
107
150
  parent=parent,
108
- tags=tags,
109
- properties=properties,
151
+ tags=final_tags,
152
+ properties=final_properties,
110
153
  )
flowyml/cli/main.py CHANGED
@@ -4,6 +4,14 @@ import click
4
4
  from pathlib import Path
5
5
  from flowyml.utils.config import get_config
6
6
 
7
+ # Import model commands early to avoid E402 error
8
+ from flowyml.cli.models import (
9
+ list_models,
10
+ promote_model,
11
+ show_model,
12
+ delete_model,
13
+ )
14
+
7
15
 
8
16
  @click.group()
9
17
  @click.version_option(version="0.1.0", prog_name="flowyml")
@@ -362,6 +370,19 @@ def clear() -> None:
362
370
  click.echo(f"✗ Error clearing cache: {e}", err=True)
363
371
 
364
372
 
373
+ @cli.group()
374
+ def models() -> None:
375
+ """Model registry management commands."""
376
+ pass
377
+
378
+
379
+ # Register model commands
380
+ models.add_command(list_models)
381
+ models.add_command(promote_model)
382
+ models.add_command(show_model)
383
+ models.add_command(delete_model)
384
+
385
+
365
386
  @cli.group()
366
387
  def config() -> None:
367
388
  """Configuration management commands."""
@@ -429,5 +450,381 @@ def logs(run_id: str, step: str, tail: int) -> None:
429
450
  click.echo(" [Log output would appear here]")
430
451
 
431
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
+
432
829
  if __name__ == "__main__":
433
830
  cli()