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 +2 -1
- flowyml/assets/featureset.py +30 -5
- flowyml/assets/metrics.py +47 -4
- flowyml/cli/main.py +397 -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 +595 -0
- flowyml/core/executor.py +27 -6
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +447 -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/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/{index-DF8dJaFL.js → index-CX5RV2C9.js} +118 -117
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +43 -4
- flowyml/ui/server_manager.py +189 -0
- flowyml/ui/utils.py +66 -2
- flowyml/utils/config.py +7 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/METADATA +5 -3
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/RECORD +28 -24
- flowyml/ui/frontend/dist/assets/index-CBUXOWze.css +0 -1
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/WHEEL +0 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.5.0.dist-info → flowyml-1.7.0.dist-info}/licenses/LICENSE +0 -0
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()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Rich utilities for CLI commands."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich import box
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
RICH_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
RICH_AVAILABLE = False
|
|
14
|
+
Console = None
|
|
15
|
+
Table = None
|
|
16
|
+
Panel = None
|
|
17
|
+
Text = None
|
|
18
|
+
box = None
|
|
19
|
+
Tree = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_console() -> Console | None:
|
|
23
|
+
"""Get Rich console instance if available."""
|
|
24
|
+
return Console() if RICH_AVAILABLE else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_rich_table(title: str, headers: list[str], rows: list[list[str]], console: Console | None = None) -> None:
|
|
28
|
+
"""Print a rich table with fallback to simple output."""
|
|
29
|
+
if not console:
|
|
30
|
+
console = get_console()
|
|
31
|
+
|
|
32
|
+
if RICH_AVAILABLE and console:
|
|
33
|
+
table = Table(
|
|
34
|
+
title=f"[bold cyan]{title}[/bold cyan]",
|
|
35
|
+
box=box.ROUNDED,
|
|
36
|
+
show_header=True,
|
|
37
|
+
header_style="bold cyan",
|
|
38
|
+
border_style="cyan",
|
|
39
|
+
)
|
|
40
|
+
for header in headers:
|
|
41
|
+
table.add_column(header, style="cyan")
|
|
42
|
+
for row in rows:
|
|
43
|
+
table.add_row(*row)
|
|
44
|
+
console.print(table)
|
|
45
|
+
else:
|
|
46
|
+
# Fallback to simple output
|
|
47
|
+
print(f"\n{title}")
|
|
48
|
+
print("=" * 70)
|
|
49
|
+
print(" | ".join(headers))
|
|
50
|
+
print("-" * 70)
|
|
51
|
+
for row in rows:
|
|
52
|
+
print(" | ".join(str(cell) for cell in row))
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_rich_panel(content: str, title: str = "", style: str = "cyan", console: Console | None = None) -> None:
|
|
57
|
+
"""Print a rich panel with fallback to simple output."""
|
|
58
|
+
if not console:
|
|
59
|
+
console = get_console()
|
|
60
|
+
|
|
61
|
+
if RICH_AVAILABLE and console:
|
|
62
|
+
panel = Panel(
|
|
63
|
+
content,
|
|
64
|
+
title=title,
|
|
65
|
+
border_style=style,
|
|
66
|
+
box=box.ROUNDED,
|
|
67
|
+
)
|
|
68
|
+
console.print(panel)
|
|
69
|
+
else:
|
|
70
|
+
# Fallback to simple output
|
|
71
|
+
if title:
|
|
72
|
+
print(f"\n{title}")
|
|
73
|
+
print("=" * 70)
|
|
74
|
+
print(content)
|
|
75
|
+
print()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def print_rich_text(*parts: tuple[str, str], console: Console | None = None) -> None:
|
|
79
|
+
"""Print rich text with styles, fallback to simple output.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
*parts: Tuples of (text, style) to print
|
|
83
|
+
console: Optional console instance
|
|
84
|
+
"""
|
|
85
|
+
if not console:
|
|
86
|
+
console = get_console()
|
|
87
|
+
|
|
88
|
+
if RICH_AVAILABLE and console and Text:
|
|
89
|
+
text_obj = Text()
|
|
90
|
+
for text, style in parts:
|
|
91
|
+
text_obj.append(text, style=style)
|
|
92
|
+
console.print(text_obj)
|
|
93
|
+
else:
|
|
94
|
+
# Fallback to simple output
|
|
95
|
+
print("".join(text for text, _ in parts))
|
flowyml/core/checkpoint.py
CHANGED
|
@@ -49,12 +49,17 @@ class PipelineCheckpoint:
|
|
|
49
49
|
# Update checkpoint metadata
|
|
50
50
|
checkpoint_data = self.load() if self.checkpoint_file.exists() else {}
|
|
51
51
|
|
|
52
|
+
# Get existing completed steps (avoid duplicates)
|
|
53
|
+
completed_steps = checkpoint_data.get("completed_steps", [])
|
|
54
|
+
if step_name not in completed_steps:
|
|
55
|
+
completed_steps.append(step_name)
|
|
56
|
+
|
|
52
57
|
checkpoint_data.update(
|
|
53
58
|
{
|
|
54
59
|
"run_id": self.run_id,
|
|
55
60
|
"last_completed_step": step_name,
|
|
56
61
|
"last_update": datetime.now().isoformat(),
|
|
57
|
-
"completed_steps":
|
|
62
|
+
"completed_steps": completed_steps,
|
|
58
63
|
"step_metadata": checkpoint_data.get("step_metadata", {}),
|
|
59
64
|
},
|
|
60
65
|
)
|
flowyml/core/conditional.py
CHANGED
|
@@ -371,3 +371,107 @@ def unless(condition_func: Callable[[Any], bool]) -> Callable:
|
|
|
371
371
|
return wrapper
|
|
372
372
|
|
|
373
373
|
return decorator
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class If:
|
|
377
|
+
"""If-else conditional control flow for pipelines.
|
|
378
|
+
|
|
379
|
+
Supports both constructor and fluent API styles.
|
|
380
|
+
|
|
381
|
+
Example (constructor style):
|
|
382
|
+
```python
|
|
383
|
+
from flowyml import Pipeline, step, If
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@step(outputs=["accuracy"])
|
|
387
|
+
def evaluate_model():
|
|
388
|
+
return 0.95
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@step
|
|
392
|
+
def deploy_model():
|
|
393
|
+
print("Deploying...")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@step
|
|
397
|
+
def retrain_model():
|
|
398
|
+
print("Retraining...")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
pipeline = Pipeline("conditional_deploy")
|
|
402
|
+
pipeline.add_step(evaluate_model)
|
|
403
|
+
pipeline.add_control_flow(
|
|
404
|
+
If(
|
|
405
|
+
condition=lambda ctx: ctx.steps["evaluate_model"].outputs["accuracy"] > 0.9,
|
|
406
|
+
then_step=deploy_model,
|
|
407
|
+
else_step=retrain_model,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Example (fluent style):
|
|
413
|
+
```python
|
|
414
|
+
pipeline.add_control_flow(
|
|
415
|
+
If(condition=lambda ctx: ctx["accuracy"] > 0.95).then(deploy_to_prod).else_(notify_slack_failure)
|
|
416
|
+
)
|
|
417
|
+
```
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
def __init__(
|
|
421
|
+
self,
|
|
422
|
+
condition: Callable[[Any], bool],
|
|
423
|
+
then_step: Callable | None = None,
|
|
424
|
+
else_step: Callable | None = None,
|
|
425
|
+
):
|
|
426
|
+
"""Initialize If condition.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
condition: Function that takes a context object and returns bool
|
|
430
|
+
then_step: Step to execute if condition is True (optional, can use .then() instead)
|
|
431
|
+
else_step: Step to execute if condition is False (optional, can use .else_() instead)
|
|
432
|
+
"""
|
|
433
|
+
self.condition = condition
|
|
434
|
+
self.then_step = then_step
|
|
435
|
+
self.else_step = else_step
|
|
436
|
+
|
|
437
|
+
def then(self, step: Callable) -> "If":
|
|
438
|
+
"""Set the step to execute if condition is True (fluent API).
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
step: Step function to execute
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Self for method chaining
|
|
445
|
+
"""
|
|
446
|
+
self.then_step = step
|
|
447
|
+
return self
|
|
448
|
+
|
|
449
|
+
def else_(self, step: Callable) -> "If":
|
|
450
|
+
"""Set the step to execute if condition is False (fluent API).
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
step: Step function to execute
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Self for method chaining
|
|
457
|
+
"""
|
|
458
|
+
self.else_step = step
|
|
459
|
+
return self
|
|
460
|
+
|
|
461
|
+
def evaluate(self, context: Any) -> Callable | None:
|
|
462
|
+
"""Evaluate condition and return the step to execute.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
context: Context object with step outputs and other data
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Step function to execute, or None if no step should execute
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
if self.condition(context):
|
|
472
|
+
return self.then_step
|
|
473
|
+
else:
|
|
474
|
+
return self.else_step
|
|
475
|
+
except Exception:
|
|
476
|
+
# If condition evaluation fails, return else_step as fallback
|
|
477
|
+
return self.else_step
|