flowyml 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/__init__.py +2 -1
- flowyml/assets/featureset.py +30 -5
- flowyml/assets/metrics.py +47 -4
- flowyml/cli/main.py +21 -0
- flowyml/cli/models.py +444 -0
- flowyml/cli/rich_utils.py +95 -0
- flowyml/core/checkpoint.py +6 -1
- flowyml/core/conditional.py +104 -0
- flowyml/core/display.py +525 -0
- flowyml/core/execution_status.py +1 -0
- flowyml/core/executor.py +201 -8
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +301 -11
- flowyml/core/project.py +4 -1
- flowyml/core/scheduler.py +225 -81
- flowyml/core/versioning.py +13 -4
- flowyml/registry/model_registry.py +1 -1
- flowyml/storage/sql.py +53 -13
- flowyml/ui/backend/main.py +2 -0
- flowyml/ui/backend/routers/assets.py +36 -0
- flowyml/ui/backend/routers/execution.py +2 -2
- flowyml/ui/backend/routers/runs.py +211 -0
- flowyml/ui/backend/routers/stats.py +2 -2
- flowyml/ui/backend/routers/websocket.py +121 -0
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/package-lock.json +289 -0
- flowyml/ui/frontend/package.json +1 -0
- flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
- flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
- flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
- flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/server_manager.py +181 -0
- flowyml/ui/utils.py +63 -1
- flowyml/utils/config.py +7 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
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
|
flowyml/assets/featureset.py
CHANGED
|
@@ -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
|
-
|
|
70
|
+
# Initialize base asset first
|
|
71
|
+
super().__init__(
|
|
72
72
|
name=name,
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
149
|
+
data=all_metrics if all_metrics else None,
|
|
107
150
|
parent=parent,
|
|
108
|
-
tags=
|
|
109
|
-
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."""
|
flowyml/cli/models.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""CLI commands for model registry management."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from flowyml.registry.model_registry import ModelRegistry, ModelStage
|
|
6
|
+
from flowyml.utils.config import get_config
|
|
7
|
+
from flowyml.cli.rich_utils import get_console, print_rich_text, print_rich_panel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_registry() -> ModelRegistry:
|
|
11
|
+
"""Get ModelRegistry instance with default path."""
|
|
12
|
+
config = get_config()
|
|
13
|
+
registry_path = Path(config.flowyml_home) / "model_registry"
|
|
14
|
+
return ModelRegistry(str(registry_path))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.argument("model_name", required=False)
|
|
19
|
+
@click.option(
|
|
20
|
+
"--stage",
|
|
21
|
+
type=click.Choice(["development", "staging", "production", "archived"]),
|
|
22
|
+
help="Filter by stage",
|
|
23
|
+
)
|
|
24
|
+
def list_models(model_name: str | None, stage: str | None) -> None:
|
|
25
|
+
"""List all models or versions of a specific model.
|
|
26
|
+
|
|
27
|
+
If MODEL_NAME is provided, lists all versions of that model.
|
|
28
|
+
Otherwise, lists all registered models.
|
|
29
|
+
"""
|
|
30
|
+
console = get_console()
|
|
31
|
+
try:
|
|
32
|
+
registry = get_registry()
|
|
33
|
+
|
|
34
|
+
if model_name:
|
|
35
|
+
# List versions of a specific model
|
|
36
|
+
versions = registry.list_versions(model_name)
|
|
37
|
+
|
|
38
|
+
if not versions:
|
|
39
|
+
print_rich_text(
|
|
40
|
+
("❌ ", "red"),
|
|
41
|
+
(f"Model '{model_name}' not found or has no versions.", "red"),
|
|
42
|
+
console=console,
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Filter by stage if provided
|
|
47
|
+
if stage:
|
|
48
|
+
stage_enum = ModelStage(stage)
|
|
49
|
+
versions = [v for v in versions if v.stage == stage_enum]
|
|
50
|
+
|
|
51
|
+
if not versions:
|
|
52
|
+
print_rich_text(
|
|
53
|
+
("❌ ", "red"),
|
|
54
|
+
(f"No versions found for model '{model_name}' with stage '{stage}'.", "red"),
|
|
55
|
+
console=console,
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Use rich table for versions
|
|
60
|
+
if console:
|
|
61
|
+
from rich.table import Table
|
|
62
|
+
from rich import box
|
|
63
|
+
|
|
64
|
+
table = Table(
|
|
65
|
+
title=f"[bold cyan]📦 Model: {model_name}[/bold cyan]",
|
|
66
|
+
box=box.ROUNDED,
|
|
67
|
+
show_header=True,
|
|
68
|
+
header_style="bold cyan",
|
|
69
|
+
border_style="cyan",
|
|
70
|
+
)
|
|
71
|
+
table.add_column("Version", style="cyan", width=15)
|
|
72
|
+
table.add_column("Stage", justify="center", width=12)
|
|
73
|
+
table.add_column("Framework", width=12)
|
|
74
|
+
table.add_column("Created", width=20)
|
|
75
|
+
table.add_column("Metrics", style="yellow", width=30)
|
|
76
|
+
table.add_column("Tags", style="dim", width=30)
|
|
77
|
+
|
|
78
|
+
# Sort by created_at (newest first)
|
|
79
|
+
versions.sort(key=lambda v: v.created_at, reverse=True)
|
|
80
|
+
|
|
81
|
+
for version in versions:
|
|
82
|
+
stage_icon = {
|
|
83
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
84
|
+
ModelStage.STAGING: "🧪",
|
|
85
|
+
ModelStage.PRODUCTION: "✅",
|
|
86
|
+
ModelStage.ARCHIVED: "📦",
|
|
87
|
+
}.get(version.stage, "📌")
|
|
88
|
+
|
|
89
|
+
stage_text = f"{stage_icon} {version.stage.value}"
|
|
90
|
+
metrics_str = (
|
|
91
|
+
", ".join(f"{k}={v:.4f}" for k, v in version.metrics.items()) if version.metrics else "-"
|
|
92
|
+
)
|
|
93
|
+
tags_str = ", ".join(f"{k}={v}" for k, v in version.tags.items()) if version.tags else "-"
|
|
94
|
+
|
|
95
|
+
table.add_row(
|
|
96
|
+
version.version,
|
|
97
|
+
stage_text,
|
|
98
|
+
version.framework,
|
|
99
|
+
version.created_at,
|
|
100
|
+
metrics_str,
|
|
101
|
+
tags_str,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
if version.description:
|
|
106
|
+
console.print(f"[dim]Description: {version.description}[/dim]")
|
|
107
|
+
console.print()
|
|
108
|
+
else:
|
|
109
|
+
# Fallback to simple output
|
|
110
|
+
click.echo(f"\n📦 Model: {model_name}")
|
|
111
|
+
click.echo(f" Versions: {len(versions)}\n")
|
|
112
|
+
versions.sort(key=lambda v: v.created_at, reverse=True)
|
|
113
|
+
for version in versions:
|
|
114
|
+
stage_icon = {
|
|
115
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
116
|
+
ModelStage.STAGING: "🧪",
|
|
117
|
+
ModelStage.PRODUCTION: "✅",
|
|
118
|
+
ModelStage.ARCHIVED: "📦",
|
|
119
|
+
}.get(version.stage, "📌")
|
|
120
|
+
click.echo(f" {stage_icon} {version.version}")
|
|
121
|
+
click.echo(f" Stage: {version.stage.value}")
|
|
122
|
+
click.echo(f" Framework: {version.framework}")
|
|
123
|
+
click.echo(f" Created: {version.created_at}")
|
|
124
|
+
if version.metrics:
|
|
125
|
+
metrics_str = ", ".join(f"{k}={v:.4f}" for k, v in version.metrics.items())
|
|
126
|
+
click.echo(f" Metrics: {metrics_str}")
|
|
127
|
+
if version.tags:
|
|
128
|
+
tags_str = ", ".join(f"{k}={v}" for k, v in version.tags.items())
|
|
129
|
+
click.echo(f" Tags: {tags_str}")
|
|
130
|
+
if version.description:
|
|
131
|
+
click.echo(f" Description: {version.description}")
|
|
132
|
+
click.echo()
|
|
133
|
+
else:
|
|
134
|
+
# List all models
|
|
135
|
+
model_names = registry.list_models()
|
|
136
|
+
|
|
137
|
+
if not model_names:
|
|
138
|
+
print_rich_panel(
|
|
139
|
+
"Register a model using ModelRegistry.register() in your pipeline.",
|
|
140
|
+
title="📭 No models registered yet",
|
|
141
|
+
style="yellow",
|
|
142
|
+
console=console,
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Use rich table for all models
|
|
147
|
+
if console:
|
|
148
|
+
from rich.table import Table
|
|
149
|
+
from rich import box
|
|
150
|
+
|
|
151
|
+
table = Table(
|
|
152
|
+
title=f"[bold cyan]📦 Registered Models: {len(model_names)}[/bold cyan]",
|
|
153
|
+
box=box.ROUNDED,
|
|
154
|
+
show_header=True,
|
|
155
|
+
header_style="bold cyan",
|
|
156
|
+
border_style="cyan",
|
|
157
|
+
)
|
|
158
|
+
table.add_column("Model", style="cyan", width=30)
|
|
159
|
+
table.add_column("Versions", justify="center", width=10)
|
|
160
|
+
table.add_column("Latest Version", width=15)
|
|
161
|
+
table.add_column("Stage", justify="center", width=12)
|
|
162
|
+
table.add_column("Framework", width=12)
|
|
163
|
+
|
|
164
|
+
for name in sorted(model_names):
|
|
165
|
+
versions = registry.list_versions(name)
|
|
166
|
+
latest = registry.get_latest_version(name)
|
|
167
|
+
|
|
168
|
+
if latest:
|
|
169
|
+
stage_icon = {
|
|
170
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
171
|
+
ModelStage.STAGING: "🧪",
|
|
172
|
+
ModelStage.PRODUCTION: "✅",
|
|
173
|
+
ModelStage.ARCHIVED: "📦",
|
|
174
|
+
}.get(latest.stage, "📌")
|
|
175
|
+
|
|
176
|
+
stage_text = f"{stage_icon} {latest.stage.value}"
|
|
177
|
+
table.add_row(
|
|
178
|
+
name,
|
|
179
|
+
str(len(versions)),
|
|
180
|
+
latest.version,
|
|
181
|
+
stage_text,
|
|
182
|
+
latest.framework,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
table.add_row(name, "0", "-", "-", "-")
|
|
186
|
+
|
|
187
|
+
console.print(table)
|
|
188
|
+
console.print()
|
|
189
|
+
else:
|
|
190
|
+
# Fallback to simple output
|
|
191
|
+
click.echo(f"\n📦 Registered Models: {len(model_names)}\n")
|
|
192
|
+
for name in sorted(model_names):
|
|
193
|
+
versions = registry.list_versions(name)
|
|
194
|
+
latest = registry.get_latest_version(name)
|
|
195
|
+
if latest:
|
|
196
|
+
stage_icon = {
|
|
197
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
198
|
+
ModelStage.STAGING: "🧪",
|
|
199
|
+
ModelStage.PRODUCTION: "✅",
|
|
200
|
+
ModelStage.ARCHIVED: "📦",
|
|
201
|
+
}.get(latest.stage, "📌")
|
|
202
|
+
click.echo(f" {stage_icon} {name}")
|
|
203
|
+
click.echo(f" Versions: {len(versions)}")
|
|
204
|
+
click.echo(f" Latest: {latest.version} ({latest.stage.value})")
|
|
205
|
+
click.echo()
|
|
206
|
+
else:
|
|
207
|
+
click.echo(f" 📌 {name} (no versions)")
|
|
208
|
+
click.echo()
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
click.echo(f"✗ Error listing models: {e}", err=True)
|
|
212
|
+
raise click.Abort()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@click.command("promote")
|
|
216
|
+
@click.argument("model_name")
|
|
217
|
+
@click.argument("version")
|
|
218
|
+
@click.option(
|
|
219
|
+
"--to",
|
|
220
|
+
"to_stage",
|
|
221
|
+
required=True,
|
|
222
|
+
type=click.Choice(["development", "staging", "production", "archived"]),
|
|
223
|
+
help="Target stage to promote to",
|
|
224
|
+
)
|
|
225
|
+
def promote_model(model_name: str, version: str, to_stage: str) -> None:
|
|
226
|
+
"""Promote a model version to a different stage.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
flowyml models promote sentiment_classifier v1.0.0 --to production
|
|
230
|
+
"""
|
|
231
|
+
console = get_console()
|
|
232
|
+
try:
|
|
233
|
+
registry = get_registry()
|
|
234
|
+
|
|
235
|
+
# Check if model version exists
|
|
236
|
+
model_version = registry.get_version(model_name, version)
|
|
237
|
+
if not model_version:
|
|
238
|
+
print_rich_text(
|
|
239
|
+
("❌ ", "red"),
|
|
240
|
+
(f"Model '{model_name}' version '{version}' not found.", "red"),
|
|
241
|
+
console=console,
|
|
242
|
+
)
|
|
243
|
+
raise click.Abort()
|
|
244
|
+
|
|
245
|
+
# Convert string to ModelStage enum
|
|
246
|
+
target_stage = ModelStage(to_stage)
|
|
247
|
+
|
|
248
|
+
# Promote the model
|
|
249
|
+
updated_version = registry.promote(model_name, version, target_stage)
|
|
250
|
+
|
|
251
|
+
stage_icon = {
|
|
252
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
253
|
+
ModelStage.STAGING: "🧪",
|
|
254
|
+
ModelStage.PRODUCTION: "✅",
|
|
255
|
+
ModelStage.ARCHIVED: "📦",
|
|
256
|
+
}.get(target_stage, "📌")
|
|
257
|
+
|
|
258
|
+
# Use rich panel for promotion result
|
|
259
|
+
if console:
|
|
260
|
+
from rich.table import Table
|
|
261
|
+
from rich import box
|
|
262
|
+
|
|
263
|
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
264
|
+
table.add_column("", style="cyan", width=20)
|
|
265
|
+
table.add_column("", style="green")
|
|
266
|
+
|
|
267
|
+
table.add_row("Model", model_name)
|
|
268
|
+
table.add_row("Version", version)
|
|
269
|
+
table.add_row("Previous Stage", f"{model_version.stage.value}")
|
|
270
|
+
table.add_row("New Stage", f"{stage_icon} {updated_version.stage.value}")
|
|
271
|
+
|
|
272
|
+
content = "[bold green]✅ Promotion Successful[/bold green]\n\n"
|
|
273
|
+
console.print(content)
|
|
274
|
+
console.print(table)
|
|
275
|
+
else:
|
|
276
|
+
click.echo(f"✅ {stage_icon} Model '{model_name}' version '{version}' promoted to {to_stage}")
|
|
277
|
+
click.echo(f" Previous stage: {model_version.stage.value}")
|
|
278
|
+
click.echo(f" New stage: {updated_version.stage.value}")
|
|
279
|
+
|
|
280
|
+
except ValueError as e:
|
|
281
|
+
click.echo(f"❌ Error: {e}", err=True)
|
|
282
|
+
raise click.Abort()
|
|
283
|
+
except Exception as e:
|
|
284
|
+
click.echo(f"✗ Error promoting model: {e}", err=True)
|
|
285
|
+
raise click.Abort()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@click.command("show")
|
|
289
|
+
@click.argument("model_name")
|
|
290
|
+
@click.argument("version")
|
|
291
|
+
def show_model(model_name: str, version: str) -> None:
|
|
292
|
+
"""Show detailed information about a specific model version."""
|
|
293
|
+
console = get_console()
|
|
294
|
+
try:
|
|
295
|
+
registry = get_registry()
|
|
296
|
+
|
|
297
|
+
model_version = registry.get_version(model_name, version)
|
|
298
|
+
if not model_version:
|
|
299
|
+
print_rich_text(
|
|
300
|
+
("❌ ", "red"),
|
|
301
|
+
(f"Model '{model_name}' version '{version}' not found.", "red"),
|
|
302
|
+
console=console,
|
|
303
|
+
)
|
|
304
|
+
raise click.Abort()
|
|
305
|
+
|
|
306
|
+
stage_icon = {
|
|
307
|
+
ModelStage.DEVELOPMENT: "🔧",
|
|
308
|
+
ModelStage.STAGING: "🧪",
|
|
309
|
+
ModelStage.PRODUCTION: "✅",
|
|
310
|
+
ModelStage.ARCHIVED: "📦",
|
|
311
|
+
}.get(model_version.stage, "📌")
|
|
312
|
+
|
|
313
|
+
if console:
|
|
314
|
+
from rich.table import Table
|
|
315
|
+
from rich import box
|
|
316
|
+
|
|
317
|
+
# Main info table
|
|
318
|
+
table = Table(
|
|
319
|
+
title=f"[bold cyan]{stage_icon} Model: {model_name} v{version}[/bold cyan]",
|
|
320
|
+
box=box.ROUNDED,
|
|
321
|
+
show_header=False,
|
|
322
|
+
border_style="cyan",
|
|
323
|
+
)
|
|
324
|
+
table.add_column("Property", style="cyan", width=20)
|
|
325
|
+
table.add_column("Value", style="green")
|
|
326
|
+
|
|
327
|
+
table.add_row("Version", model_version.version)
|
|
328
|
+
table.add_row("Stage", f"{stage_icon} {model_version.stage.value}")
|
|
329
|
+
table.add_row("Framework", model_version.framework)
|
|
330
|
+
table.add_row("Created", model_version.created_at)
|
|
331
|
+
table.add_row("Updated", model_version.updated_at)
|
|
332
|
+
table.add_row("Path", f"[dim]{model_version.model_path}[/dim]")
|
|
333
|
+
|
|
334
|
+
if model_version.description:
|
|
335
|
+
table.add_row("Description", model_version.description)
|
|
336
|
+
if model_version.author:
|
|
337
|
+
table.add_row("Author", model_version.author)
|
|
338
|
+
if model_version.parent_version:
|
|
339
|
+
table.add_row("Parent Version", model_version.parent_version)
|
|
340
|
+
|
|
341
|
+
console.print(table)
|
|
342
|
+
console.print()
|
|
343
|
+
|
|
344
|
+
# Metrics table if available
|
|
345
|
+
if model_version.metrics:
|
|
346
|
+
metrics_table = Table(
|
|
347
|
+
title="[bold yellow]📊 Metrics[/bold yellow]",
|
|
348
|
+
box=box.ROUNDED,
|
|
349
|
+
show_header=True,
|
|
350
|
+
header_style="bold yellow",
|
|
351
|
+
border_style="yellow",
|
|
352
|
+
)
|
|
353
|
+
metrics_table.add_column("Metric", style="yellow", width=20)
|
|
354
|
+
metrics_table.add_column("Value", style="green", justify="right")
|
|
355
|
+
for key, value in sorted(model_version.metrics.items()):
|
|
356
|
+
metrics_table.add_row(key, f"{value:.6f}")
|
|
357
|
+
console.print(metrics_table)
|
|
358
|
+
console.print()
|
|
359
|
+
|
|
360
|
+
# Tags table if available
|
|
361
|
+
if model_version.tags:
|
|
362
|
+
tags_table = Table(
|
|
363
|
+
title="[bold dim]🏷️ Tags[/bold dim]",
|
|
364
|
+
box=box.SIMPLE,
|
|
365
|
+
show_header=False,
|
|
366
|
+
)
|
|
367
|
+
tags_table.add_column("Key", style="cyan", width=20)
|
|
368
|
+
tags_table.add_column("Value", style="dim")
|
|
369
|
+
for key, value in sorted(model_version.tags.items()):
|
|
370
|
+
tags_table.add_row(key, value)
|
|
371
|
+
console.print(tags_table)
|
|
372
|
+
console.print()
|
|
373
|
+
else:
|
|
374
|
+
# Fallback to simple output
|
|
375
|
+
click.echo(f"\n{stage_icon} Model: {model_name}")
|
|
376
|
+
click.echo(f" Version: {model_version.version}")
|
|
377
|
+
click.echo(f" Stage: {model_version.stage.value}")
|
|
378
|
+
click.echo(f" Framework: {model_version.framework}")
|
|
379
|
+
click.echo(f" Created: {model_version.created_at}")
|
|
380
|
+
click.echo(f" Updated: {model_version.updated_at}")
|
|
381
|
+
click.echo(f" Path: {model_version.model_path}")
|
|
382
|
+
if model_version.description:
|
|
383
|
+
click.echo(f" Description: {model_version.description}")
|
|
384
|
+
if model_version.author:
|
|
385
|
+
click.echo(f" Author: {model_version.author}")
|
|
386
|
+
if model_version.parent_version:
|
|
387
|
+
click.echo(f" Parent Version: {model_version.parent_version}")
|
|
388
|
+
if model_version.metrics:
|
|
389
|
+
click.echo("\n Metrics:")
|
|
390
|
+
for key, value in sorted(model_version.metrics.items()):
|
|
391
|
+
click.echo(f" {key}: {value:.6f}")
|
|
392
|
+
if model_version.tags:
|
|
393
|
+
click.echo("\n Tags:")
|
|
394
|
+
for key, value in sorted(model_version.tags.items()):
|
|
395
|
+
click.echo(f" {key}: {value}")
|
|
396
|
+
click.echo()
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
click.echo(f"✗ Error showing model: {e}", err=True)
|
|
400
|
+
raise click.Abort()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@click.command("delete")
|
|
404
|
+
@click.argument("model_name")
|
|
405
|
+
@click.argument("version")
|
|
406
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this model version?")
|
|
407
|
+
def delete_model(model_name: str, version: str) -> None:
|
|
408
|
+
"""Delete a specific model version.
|
|
409
|
+
|
|
410
|
+
WARNING: This will permanently delete the model version and its artifacts.
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
registry = get_registry()
|
|
414
|
+
|
|
415
|
+
model_version = registry.get_version(model_name, version)
|
|
416
|
+
if not model_version:
|
|
417
|
+
click.echo(f"❌ Model '{model_name}' version '{version}' not found.")
|
|
418
|
+
raise click.Abort()
|
|
419
|
+
|
|
420
|
+
# Delete model file
|
|
421
|
+
model_path = Path(model_version.model_path)
|
|
422
|
+
if model_path.exists():
|
|
423
|
+
if model_path.is_file():
|
|
424
|
+
model_path.unlink()
|
|
425
|
+
elif model_path.is_dir():
|
|
426
|
+
import shutil
|
|
427
|
+
|
|
428
|
+
shutil.rmtree(model_path)
|
|
429
|
+
|
|
430
|
+
# Remove from metadata
|
|
431
|
+
if model_name in registry._metadata:
|
|
432
|
+
registry._metadata[model_name] = [v for v in registry._metadata[model_name] if v["version"] != version]
|
|
433
|
+
|
|
434
|
+
# Remove model entry if no versions left
|
|
435
|
+
if not registry._metadata[model_name]:
|
|
436
|
+
del registry._metadata[model_name]
|
|
437
|
+
|
|
438
|
+
registry._save_metadata()
|
|
439
|
+
|
|
440
|
+
click.echo(f"✅ Deleted model '{model_name}' version '{version}'")
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
click.echo(f"✗ Error deleting model: {e}", err=True)
|
|
444
|
+
raise click.Abort()
|