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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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))