flowyml 1.1.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 +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from flowyml.tracking.leaderboard import ModelLeaderboard, compare_runs
|
|
3
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
4
|
+
|
|
5
|
+
router = APIRouter()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("/{metric}")
|
|
9
|
+
async def get_leaderboard(
|
|
10
|
+
metric: str,
|
|
11
|
+
higher_is_better: bool = True,
|
|
12
|
+
n: int = 10,
|
|
13
|
+
):
|
|
14
|
+
"""Get leaderboard for a metric."""
|
|
15
|
+
try:
|
|
16
|
+
store = SQLiteMetadataStore()
|
|
17
|
+
leaderboard = ModelLeaderboard(metric, higher_is_better, store)
|
|
18
|
+
|
|
19
|
+
top_models = leaderboard.get_top(n=n)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"metric": metric,
|
|
23
|
+
"higher_is_better": higher_is_better,
|
|
24
|
+
"models": [
|
|
25
|
+
{
|
|
26
|
+
"rank": i + 1,
|
|
27
|
+
"model_name": model.model_name,
|
|
28
|
+
"run_id": model.run_id,
|
|
29
|
+
"score": model.metric_value,
|
|
30
|
+
"timestamp": model.timestamp,
|
|
31
|
+
"metadata": model.metadata,
|
|
32
|
+
}
|
|
33
|
+
for i, model in enumerate(top_models)
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.post("/compare")
|
|
41
|
+
async def compare_model_runs(run_ids: list, metrics: list | None = None):
|
|
42
|
+
"""Compare multiple runs."""
|
|
43
|
+
comparison = compare_runs(run_ids, metrics)
|
|
44
|
+
return comparison
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/generate_sample_data")
|
|
48
|
+
async def generate_sample_data():
|
|
49
|
+
"""Generate sample data for the leaderboard."""
|
|
50
|
+
import random
|
|
51
|
+
from datetime import datetime, timedelta
|
|
52
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
store = SQLiteMetadataStore()
|
|
56
|
+
|
|
57
|
+
models = ["ResNet50", "BERT-Base", "YOLOv8", "EfficientNet", "GPT-2"]
|
|
58
|
+
|
|
59
|
+
for _i in range(10):
|
|
60
|
+
run_id = f"run_{random.randint(1000, 9999)}"
|
|
61
|
+
model_name = random.choice(models)
|
|
62
|
+
|
|
63
|
+
# Save run metadata
|
|
64
|
+
store.save_run(
|
|
65
|
+
run_id,
|
|
66
|
+
{
|
|
67
|
+
"run_id": run_id,
|
|
68
|
+
"pipeline_name": "training_pipeline",
|
|
69
|
+
"status": "completed",
|
|
70
|
+
"start_time": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
|
|
71
|
+
"end_time": datetime.now().isoformat(),
|
|
72
|
+
"duration": random.uniform(10, 100),
|
|
73
|
+
"success": True,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Save metrics
|
|
78
|
+
store.save_metric(run_id, "accuracy", random.uniform(0.7, 0.99))
|
|
79
|
+
store.save_metric(run_id, "loss", random.uniform(0.01, 0.5))
|
|
80
|
+
store.save_metric(run_id, "f1_score", random.uniform(0.6, 0.95))
|
|
81
|
+
store.save_metric(run_id, "latency", random.uniform(10, 200))
|
|
82
|
+
|
|
83
|
+
# Save model artifact with timestamp
|
|
84
|
+
store.save_artifact(
|
|
85
|
+
f"{run_id}_model",
|
|
86
|
+
{
|
|
87
|
+
"artifact_id": f"{run_id}_model",
|
|
88
|
+
"name": model_name,
|
|
89
|
+
"type": "Model",
|
|
90
|
+
"run_id": run_id,
|
|
91
|
+
"step": "train",
|
|
92
|
+
"value": f"Model: {model_name}",
|
|
93
|
+
"created_at": datetime.now().isoformat(),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Save leaderboard entry for this model
|
|
98
|
+
store.save_artifact(
|
|
99
|
+
f"{run_id}_leaderboard",
|
|
100
|
+
{
|
|
101
|
+
"artifact_id": f"{run_id}_leaderboard",
|
|
102
|
+
"name": model_name,
|
|
103
|
+
"type": "leaderboard_entry",
|
|
104
|
+
"run_id": run_id,
|
|
105
|
+
"value": str(
|
|
106
|
+
store.get_metrics(run_id, "accuracy")[0]["value"]
|
|
107
|
+
if store.get_metrics(run_id, "accuracy")
|
|
108
|
+
else 0,
|
|
109
|
+
),
|
|
110
|
+
"metric": "accuracy",
|
|
111
|
+
"created_at": datetime.now().isoformat(),
|
|
112
|
+
"metadata": {"model_type": model_name},
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {"success": True, "message": "Sample data generated"}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Any
|
|
4
|
+
from flowyml.monitoring.notifications import (
|
|
5
|
+
get_notifier,
|
|
6
|
+
configure_notifications,
|
|
7
|
+
ConsoleNotifier,
|
|
8
|
+
SlackNotifier,
|
|
9
|
+
EmailNotifier,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotificationConfig(BaseModel):
|
|
16
|
+
console: bool = True
|
|
17
|
+
slack_webhook: str | None = None
|
|
18
|
+
email_config: dict[str, Any] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/")
|
|
22
|
+
async def get_notification_config():
|
|
23
|
+
"""Get current notification configuration."""
|
|
24
|
+
notifier = get_notifier()
|
|
25
|
+
config = {
|
|
26
|
+
"console": False,
|
|
27
|
+
"slack": False,
|
|
28
|
+
"email": False,
|
|
29
|
+
"channels": [],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for channel in notifier.channels:
|
|
33
|
+
if isinstance(channel, ConsoleNotifier):
|
|
34
|
+
config["console"] = True
|
|
35
|
+
config["channels"].append("console")
|
|
36
|
+
elif isinstance(channel, SlackNotifier):
|
|
37
|
+
config["slack"] = True
|
|
38
|
+
config["channels"].append("slack")
|
|
39
|
+
elif isinstance(channel, EmailNotifier):
|
|
40
|
+
config["email"] = True
|
|
41
|
+
config["channels"].append("email")
|
|
42
|
+
|
|
43
|
+
return config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/")
|
|
47
|
+
async def update_notification_config(config: NotificationConfig):
|
|
48
|
+
"""Update notification configuration."""
|
|
49
|
+
try:
|
|
50
|
+
configure_notifications(
|
|
51
|
+
console=config.console,
|
|
52
|
+
slack_webhook=config.slack_webhook,
|
|
53
|
+
email_config=config.email_config,
|
|
54
|
+
)
|
|
55
|
+
return {"status": "success", "message": "Notification configuration updated"}
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.post("/test")
|
|
61
|
+
async def test_notification(channel: str = "console"):
|
|
62
|
+
"""Send a test notification."""
|
|
63
|
+
notifier = get_notifier()
|
|
64
|
+
try:
|
|
65
|
+
notifier.notify(
|
|
66
|
+
title="Test Notification",
|
|
67
|
+
message=f"This is a test notification sent to {channel}",
|
|
68
|
+
level="info",
|
|
69
|
+
)
|
|
70
|
+
return {"status": "success"}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
4
|
+
from flowyml.utils.config import get_config
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_store():
|
|
10
|
+
get_config()
|
|
11
|
+
# Assuming default path or from config
|
|
12
|
+
# The SQLiteMetadataStore defaults to .flowyml/metadata.db which is what we want for now
|
|
13
|
+
return SQLiteMetadataStore()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/")
|
|
17
|
+
async def list_pipelines(project: str = None):
|
|
18
|
+
"""List all unique pipelines, optionally filtered by project."""
|
|
19
|
+
try:
|
|
20
|
+
store = get_store()
|
|
21
|
+
pipelines = store.list_pipelines(project=project)
|
|
22
|
+
return {"pipelines": pipelines}
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get("/{pipeline_name}/runs")
|
|
28
|
+
async def list_pipeline_runs(pipeline_name: str, limit: int = 10):
|
|
29
|
+
"""List runs for a specific pipeline."""
|
|
30
|
+
store = get_store()
|
|
31
|
+
runs = store.query(pipeline_name=pipeline_name)
|
|
32
|
+
return {"runs": runs[:limit]}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/stats")
|
|
36
|
+
async def get_all_pipelines_stats():
|
|
37
|
+
"""Get statistics for all pipelines."""
|
|
38
|
+
try:
|
|
39
|
+
store = get_store()
|
|
40
|
+
pipelines = store.list_pipelines()
|
|
41
|
+
|
|
42
|
+
if not pipelines:
|
|
43
|
+
return {"total_pipelines": 0, "total_runs": 0, "pipelines": []}
|
|
44
|
+
|
|
45
|
+
stats = []
|
|
46
|
+
for pipeline_name in pipelines:
|
|
47
|
+
runs = store.query(pipeline_name=pipeline_name)
|
|
48
|
+
total = len(runs)
|
|
49
|
+
successful = len([r for r in runs if r.get("status") == "completed"])
|
|
50
|
+
stats.append(
|
|
51
|
+
{
|
|
52
|
+
"name": pipeline_name,
|
|
53
|
+
"total_runs": total,
|
|
54
|
+
"success_rate": successful / total if total > 0 else 0,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"total_pipelines": len(pipelines),
|
|
60
|
+
"total_runs": sum(s["total_runs"] for s in stats),
|
|
61
|
+
"pipelines": stats,
|
|
62
|
+
}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
# Database might not exist yet
|
|
65
|
+
return {"total_pipelines": 0, "total_runs": 0, "pipelines": [], "error": str(e)}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.get("/{pipeline_name}/stats")
|
|
69
|
+
async def get_pipeline_stats(pipeline_name: str):
|
|
70
|
+
"""Get statistics for a specific pipeline."""
|
|
71
|
+
try:
|
|
72
|
+
store = get_store()
|
|
73
|
+
runs = store.query(pipeline_name=pipeline_name)
|
|
74
|
+
|
|
75
|
+
total_runs = len(runs)
|
|
76
|
+
if total_runs == 0:
|
|
77
|
+
return {"total_runs": 0, "success_rate": 0, "avg_duration": 0}
|
|
78
|
+
|
|
79
|
+
successful_runs = [r for r in runs if r.get("status") == "completed"]
|
|
80
|
+
success_rate = len(successful_runs) / total_runs if total_runs > 0 else 0
|
|
81
|
+
|
|
82
|
+
durations = [r.get("duration", 0) for r in runs if r.get("duration") is not None]
|
|
83
|
+
avg_duration = sum(durations) / len(durations) if durations else 0
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"total_runs": total_runs,
|
|
87
|
+
"success_rate": success_rate,
|
|
88
|
+
"avg_duration": avg_duration,
|
|
89
|
+
"last_run": runs[0] if runs else None,
|
|
90
|
+
}
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return {"total_runs": 0, "success_rate": 0, "avg_duration": 0, "error": str(e)}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ProjectUpdate(BaseModel):
|
|
96
|
+
project_name: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.put("/{pipeline_name}/project")
|
|
100
|
+
async def update_pipeline_project(pipeline_name: str, update: ProjectUpdate):
|
|
101
|
+
"""Update the project for a pipeline."""
|
|
102
|
+
try:
|
|
103
|
+
store = get_store()
|
|
104
|
+
# This updates all runs for this pipeline to the new project
|
|
105
|
+
# In a real system, we might want to just tag the pipeline definition
|
|
106
|
+
# But since our "pipeline" concept is derived from runs, we update runs
|
|
107
|
+
store.update_pipeline_project(pipeline_name, update.project_name)
|
|
108
|
+
return {"status": "success", "project": update.project_name}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""API router for plugin management."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing import Any
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from flowyml.stacks.plugins import get_component_registry
|
|
10
|
+
from flowyml.stacks.migration import StackMigrator
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/plugins", tags=["plugins"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PluginInfo(BaseModel):
|
|
16
|
+
plugin_id: str
|
|
17
|
+
name: str
|
|
18
|
+
version: str
|
|
19
|
+
author: str
|
|
20
|
+
description: str
|
|
21
|
+
downloads: str
|
|
22
|
+
stars: str
|
|
23
|
+
tags: list[str]
|
|
24
|
+
installed: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InstallRequest(BaseModel):
|
|
28
|
+
plugin_id: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ImportStackRequest(BaseModel):
|
|
32
|
+
stack_name: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/available", response_model=list[PluginInfo])
|
|
36
|
+
async def get_available_plugins():
|
|
37
|
+
"""Get list of available plugins."""
|
|
38
|
+
import importlib.metadata
|
|
39
|
+
|
|
40
|
+
# Helper to check if package is installed
|
|
41
|
+
def is_installed(package_name: str) -> bool:
|
|
42
|
+
try:
|
|
43
|
+
importlib.metadata.distribution(package_name)
|
|
44
|
+
return True
|
|
45
|
+
except importlib.metadata.PackageNotFoundError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
# Mock data for now - in production this would query a plugin registry
|
|
49
|
+
plugins = [
|
|
50
|
+
PluginInfo(
|
|
51
|
+
plugin_id="zenml-kubernetes",
|
|
52
|
+
name="zenml-kubernetes",
|
|
53
|
+
version="0.45.0",
|
|
54
|
+
author="ZenML",
|
|
55
|
+
description="Kubernetes orchestrator integration from ZenML ecosystem.",
|
|
56
|
+
downloads="12k",
|
|
57
|
+
stars="450",
|
|
58
|
+
tags=["orchestrator", "kubernetes", "zenml"],
|
|
59
|
+
installed=is_installed("zenml-kubernetes"),
|
|
60
|
+
),
|
|
61
|
+
PluginInfo(
|
|
62
|
+
plugin_id="zenml-mlflow",
|
|
63
|
+
name="zenml-mlflow",
|
|
64
|
+
version="0.45.0",
|
|
65
|
+
author="ZenML",
|
|
66
|
+
description="MLflow integration for experiment tracking and model deployment.",
|
|
67
|
+
downloads="8.5k",
|
|
68
|
+
stars="320",
|
|
69
|
+
tags=["tracking", "mlflow", "zenml"],
|
|
70
|
+
installed=is_installed("zenml-mlflow"),
|
|
71
|
+
),
|
|
72
|
+
PluginInfo(
|
|
73
|
+
plugin_id="airflow-providers-google",
|
|
74
|
+
name="airflow-providers-google",
|
|
75
|
+
version="10.1.0",
|
|
76
|
+
author="Apache Airflow",
|
|
77
|
+
description="Google Cloud Platform providers for Airflow.",
|
|
78
|
+
downloads="50k",
|
|
79
|
+
stars="1.2k",
|
|
80
|
+
tags=["orchestrator", "gcp", "airflow"],
|
|
81
|
+
installed=is_installed("airflow-providers-google"),
|
|
82
|
+
),
|
|
83
|
+
PluginInfo(
|
|
84
|
+
plugin_id="aws-s3",
|
|
85
|
+
name="aws-s3",
|
|
86
|
+
version="1.0.0",
|
|
87
|
+
author="AWS",
|
|
88
|
+
description="S3 artifact store integration.",
|
|
89
|
+
downloads="15k",
|
|
90
|
+
stars="200",
|
|
91
|
+
tags=["artifact-store", "aws"],
|
|
92
|
+
installed=is_installed("aws-s3"),
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
return plugins
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.get("/installed", response_model=list[dict[str, Any]])
|
|
100
|
+
async def get_installed_plugins():
|
|
101
|
+
"""Get list of installed plugins."""
|
|
102
|
+
import importlib.metadata
|
|
103
|
+
|
|
104
|
+
# Get all installed packages that could be plugins
|
|
105
|
+
installed = []
|
|
106
|
+
|
|
107
|
+
# List of known plugin packages (you can expand this)
|
|
108
|
+
potential_plugins = [
|
|
109
|
+
"zenml",
|
|
110
|
+
"zenml-kubernetes",
|
|
111
|
+
"zenml-mlflow",
|
|
112
|
+
"zenml-s3",
|
|
113
|
+
"airflow",
|
|
114
|
+
"airflow-providers-google",
|
|
115
|
+
"airflow-providers-aws",
|
|
116
|
+
"aws-s3",
|
|
117
|
+
"boto3",
|
|
118
|
+
"kubernetes",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for package_name in potential_plugins:
|
|
122
|
+
try:
|
|
123
|
+
dist = importlib.metadata.distribution(package_name)
|
|
124
|
+
installed.append(
|
|
125
|
+
{
|
|
126
|
+
"id": package_name,
|
|
127
|
+
"name": package_name,
|
|
128
|
+
"version": dist.version,
|
|
129
|
+
"description": dist.metadata.get("Summary", ""),
|
|
130
|
+
"status": "active",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
except importlib.metadata.PackageNotFoundError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
return installed
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.post("/install")
|
|
140
|
+
async def install_plugin(request: InstallRequest):
|
|
141
|
+
"""Install a plugin."""
|
|
142
|
+
registry = get_component_registry()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
success = registry.install_plugin(request.plugin_id)
|
|
146
|
+
if success:
|
|
147
|
+
return {"success": True, "message": f"Plugin {request.plugin_id} installed successfully"}
|
|
148
|
+
else:
|
|
149
|
+
raise HTTPException(status_code=500, detail="Installation failed")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.post("/uninstall/{plugin_id}")
|
|
155
|
+
async def uninstall_plugin(plugin_id: str):
|
|
156
|
+
"""Uninstall a plugin."""
|
|
157
|
+
import asyncio
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Run subprocess in executor to avoid blocking
|
|
161
|
+
loop = asyncio.get_event_loop()
|
|
162
|
+
await loop.run_in_executor(
|
|
163
|
+
None,
|
|
164
|
+
subprocess.check_call,
|
|
165
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", plugin_id],
|
|
166
|
+
)
|
|
167
|
+
return {"success": True, "message": f"Plugin {plugin_id} uninstalled successfully"}
|
|
168
|
+
except subprocess.CalledProcessError as e:
|
|
169
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@router.post("/import-stack")
|
|
173
|
+
async def import_zenml_stack(request: ImportStackRequest):
|
|
174
|
+
"""Import a ZenML stack."""
|
|
175
|
+
migrator = StackMigrator()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
migration_data = migrator.migrate_zenml_stack(request.stack_name)
|
|
179
|
+
return {
|
|
180
|
+
"success": True,
|
|
181
|
+
"message": "Stack imported successfully",
|
|
182
|
+
"components": [
|
|
183
|
+
{"type": comp_type, "name": comp.name if hasattr(comp, "name") else str(comp)}
|
|
184
|
+
for comp_type, comp in migration_data["stack"]["components"].items()
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
except ImportError:
|
|
188
|
+
raise HTTPException(status_code=400, detail="ZenML is not installed")
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from flowyml.core.project import ProjectManager
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
router = APIRouter()
|
|
6
|
+
manager = ProjectManager()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/")
|
|
10
|
+
async def list_projects():
|
|
11
|
+
"""List all projects."""
|
|
12
|
+
try:
|
|
13
|
+
projects = manager.list_projects()
|
|
14
|
+
return projects
|
|
15
|
+
except Exception as e:
|
|
16
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProjectCreate(BaseModel):
|
|
20
|
+
name: str
|
|
21
|
+
description: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/")
|
|
25
|
+
async def create_project(project: ProjectCreate):
|
|
26
|
+
"""Create a new project."""
|
|
27
|
+
created_project = manager.create_project(project.name, project.description)
|
|
28
|
+
return {
|
|
29
|
+
"name": created_project.name,
|
|
30
|
+
"description": created_project.description,
|
|
31
|
+
"created": True,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/{project_name}")
|
|
36
|
+
async def get_project(project_name: str):
|
|
37
|
+
"""Get project details."""
|
|
38
|
+
project = manager.get_project(project_name)
|
|
39
|
+
if not project:
|
|
40
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"name": project.name,
|
|
44
|
+
"description": project.description,
|
|
45
|
+
"metadata": project.metadata,
|
|
46
|
+
"stats": project.get_stats(),
|
|
47
|
+
"pipelines": project.get_pipelines(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("/{project_name}/runs")
|
|
52
|
+
async def get_project_runs(
|
|
53
|
+
project_name: str,
|
|
54
|
+
pipeline_name: str | None = None,
|
|
55
|
+
limit: int = 100,
|
|
56
|
+
):
|
|
57
|
+
"""Get runs for a project."""
|
|
58
|
+
project = manager.get_project(project_name)
|
|
59
|
+
if not project:
|
|
60
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
61
|
+
|
|
62
|
+
runs = project.list_runs(pipeline_name=pipeline_name, limit=limit)
|
|
63
|
+
return runs
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.get("/{project_name}/artifacts")
|
|
67
|
+
async def get_project_artifacts(
|
|
68
|
+
project_name: str,
|
|
69
|
+
artifact_type: str | None = None,
|
|
70
|
+
limit: int = 100,
|
|
71
|
+
):
|
|
72
|
+
"""Get artifacts for a project."""
|
|
73
|
+
project = manager.get_project(project_name)
|
|
74
|
+
if not project:
|
|
75
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
76
|
+
|
|
77
|
+
artifacts = project.get_artifacts(artifact_type=artifact_type, limit=limit)
|
|
78
|
+
return artifacts
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.delete("/{project_name}")
|
|
82
|
+
async def delete_project(project_name: str):
|
|
83
|
+
"""Delete a project."""
|
|
84
|
+
manager.delete_project(project_name, confirm=True)
|
|
85
|
+
return {"deleted": True}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
4
|
+
|
|
5
|
+
router = APIRouter()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_store():
|
|
9
|
+
return SQLiteMetadataStore()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.get("/")
|
|
13
|
+
async def list_runs(limit: int = 20, project: str = None):
|
|
14
|
+
"""List all runs, optionally filtered by project."""
|
|
15
|
+
try:
|
|
16
|
+
store = get_store()
|
|
17
|
+
runs = store.list_runs(limit=limit)
|
|
18
|
+
|
|
19
|
+
# Filter by project if specified
|
|
20
|
+
if project:
|
|
21
|
+
runs = [r for r in runs if r.get("project") == project]
|
|
22
|
+
|
|
23
|
+
return {"runs": runs}
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return {"runs": [], "error": str(e)}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get("/{run_id}")
|
|
29
|
+
async def get_run(run_id: str):
|
|
30
|
+
"""Get details for a specific run."""
|
|
31
|
+
store = get_store()
|
|
32
|
+
run = store.load_run(run_id)
|
|
33
|
+
if not run:
|
|
34
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
35
|
+
return run
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/{run_id}/metrics")
|
|
39
|
+
async def get_run_metrics(run_id: str):
|
|
40
|
+
"""Get metrics for a specific run."""
|
|
41
|
+
store = get_store()
|
|
42
|
+
metrics = store.get_metrics(run_id)
|
|
43
|
+
return {"metrics": metrics}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("/{run_id}/artifacts")
|
|
47
|
+
async def get_run_artifacts(run_id: str):
|
|
48
|
+
"""Get artifacts for a specific run."""
|
|
49
|
+
store = get_store()
|
|
50
|
+
artifacts = store.list_assets(run_id=run_id)
|
|
51
|
+
return {"artifacts": artifacts}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProjectUpdate(BaseModel):
|
|
55
|
+
project_name: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.put("/{run_id}/project")
|
|
59
|
+
async def update_run_project(run_id: str, update: ProjectUpdate):
|
|
60
|
+
"""Update the project for a run."""
|
|
61
|
+
store = get_store()
|
|
62
|
+
try:
|
|
63
|
+
store.update_run_project(run_id, update.project_name)
|
|
64
|
+
return {"status": "success", "project": update.project_name}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise HTTPException(status_code=500, detail=str(e))
|