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,163 @@
1
+ """Authentication and authorization for flowyml API."""
2
+
3
+ import secrets
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from datetime import datetime
9
+ from fastapi import HTTPException, Security
10
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
+
12
+ security = HTTPBearer(auto_error=False)
13
+
14
+
15
+ class TokenManager:
16
+ """Manage API tokens for authentication."""
17
+
18
+ def __init__(self, tokens_file: str = ".flowyml/api_tokens.json"):
19
+ self.tokens_file = Path(tokens_file)
20
+ self.tokens_file.parent.mkdir(parents=True, exist_ok=True)
21
+ self._load_tokens()
22
+
23
+ def _load_tokens(self) -> None:
24
+ """Load tokens from file."""
25
+ if self.tokens_file.exists():
26
+ with open(self.tokens_file) as f:
27
+ self.tokens = json.load(f)
28
+ else:
29
+ self.tokens = {}
30
+ self._save_tokens()
31
+
32
+ def _save_tokens(self) -> None:
33
+ """Save tokens to file."""
34
+ with open(self.tokens_file, "w") as f:
35
+ json.dump(self.tokens, f, indent=2)
36
+
37
+ def _hash_token(self, token: str) -> str:
38
+ """Hash a token for secure storage."""
39
+ return hashlib.sha256(token.encode()).hexdigest()
40
+
41
+ def create_token(
42
+ self,
43
+ name: str,
44
+ project: str | None = None,
45
+ permissions: list = None,
46
+ ) -> str:
47
+ """Create a new API token.
48
+
49
+ Args:
50
+ name: Token name/description
51
+ project: Optional project scope
52
+ permissions: List of permissions
53
+
54
+ Returns:
55
+ The generated token
56
+ """
57
+ token = f"uf_{secrets.token_urlsafe(32)}"
58
+ token_hash = self._hash_token(token)
59
+
60
+ self.tokens[token_hash] = {
61
+ "name": name,
62
+ "project": project,
63
+ "permissions": permissions or ["read", "write", "execute"],
64
+ "created_at": datetime.now().isoformat(),
65
+ "last_used": None,
66
+ }
67
+
68
+ self._save_tokens()
69
+ return token
70
+
71
+ def verify_token(self, token: str) -> dict[str, Any] | None:
72
+ """Verify a token and return its metadata.
73
+
74
+ Args:
75
+ token: The token to verify
76
+
77
+ Returns:
78
+ Token metadata if valid, None otherwise
79
+ """
80
+ token_hash = self._hash_token(token)
81
+ token_data = self.tokens.get(token_hash)
82
+
83
+ if token_data:
84
+ # Update last used timestamp
85
+ token_data["last_used"] = datetime.now().isoformat()
86
+ self.tokens[token_hash] = token_data
87
+ self._save_tokens()
88
+
89
+ return token_data
90
+
91
+ def revoke_token(self, token: str) -> bool:
92
+ """Revoke a token.
93
+
94
+ Args:
95
+ token: The token to revoke
96
+
97
+ Returns:
98
+ True if revoked, False if not found
99
+ """
100
+ token_hash = self._hash_token(token)
101
+ if token_hash in self.tokens:
102
+ del self.tokens[token_hash]
103
+ self._save_tokens()
104
+ return True
105
+ return False
106
+
107
+ def list_tokens(self) -> list:
108
+ """List all tokens (without revealing the actual token values)."""
109
+ return [
110
+ {
111
+ "name": data["name"],
112
+ "project": data["project"],
113
+ "permissions": data["permissions"],
114
+ "created_at": data["created_at"],
115
+ "last_used": data["last_used"],
116
+ }
117
+ for data in self.tokens.values()
118
+ ]
119
+
120
+
121
+ # Global token manager instance
122
+ token_manager = TokenManager()
123
+
124
+
125
+ async def verify_api_token(
126
+ credentials: HTTPAuthorizationCredentials = Security(security),
127
+ required_permission: str = "read",
128
+ ) -> dict[str, Any]:
129
+ """Verify API token from Authorization header.
130
+
131
+ Args:
132
+ credentials: HTTP authorization credentials
133
+ required_permission: Required permission level
134
+
135
+ Returns:
136
+ Token metadata
137
+
138
+ Raises:
139
+ HTTPException: If token is invalid or insufficient permissions
140
+ """
141
+ if not credentials:
142
+ raise HTTPException(
143
+ status_code=401,
144
+ detail="Not authenticated. Provide an API token in the Authorization header.",
145
+ )
146
+
147
+ token = credentials.credentials
148
+ token_data = token_manager.verify_token(token)
149
+
150
+ if not token_data:
151
+ raise HTTPException(
152
+ status_code=403,
153
+ detail="Invalid API token",
154
+ )
155
+
156
+ # Check permissions
157
+ if required_permission not in token_data["permissions"]:
158
+ raise HTTPException(
159
+ status_code=403,
160
+ detail=f"Insufficient permissions. Required: {required_permission}",
161
+ )
162
+
163
+ return token_data
@@ -0,0 +1,187 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
+ import os
6
+
7
+ # Include API routers
8
+ from flowyml.ui.backend.routers import (
9
+ pipelines,
10
+ runs,
11
+ assets,
12
+ experiments,
13
+ traces,
14
+ projects,
15
+ schedules,
16
+ notifications,
17
+ leaderboard,
18
+ execution,
19
+ plugins,
20
+ )
21
+
22
+ app = FastAPI(
23
+ title="flowyml UI",
24
+ description="Real-time UI for flowyml pipelines",
25
+ version="0.1.0",
26
+ )
27
+
28
+ # Configure CORS
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"], # For development
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+
38
+ # Health check endpoint
39
+ @app.get("/api/health")
40
+ async def health_check():
41
+ return {"status": "ok", "version": "0.1.0"}
42
+
43
+
44
+ @app.get("/api/config")
45
+ async def get_public_config():
46
+ """Get public configuration."""
47
+ from flowyml.utils.config import get_config
48
+
49
+ config = get_config()
50
+ return {
51
+ "execution_mode": config.execution_mode,
52
+ "remote_server_url": config.remote_server_url,
53
+ "remote_ui_url": config.remote_ui_url,
54
+ "enable_ui": config.enable_ui,
55
+ }
56
+
57
+
58
+ app.include_router(pipelines.router, prefix="/api/pipelines", tags=["pipelines"])
59
+ app.include_router(runs.router, prefix="/api/runs", tags=["runs"])
60
+ app.include_router(assets.router, prefix="/api/assets", tags=["assets"])
61
+ app.include_router(experiments.router, prefix="/api/experiments", tags=["experiments"])
62
+ app.include_router(traces.router, prefix="/api/traces", tags=["traces"])
63
+ app.include_router(projects.router, prefix="/api/projects", tags=["projects"])
64
+ app.include_router(schedules.router, prefix="/api/schedules", tags=["schedules"])
65
+ app.include_router(notifications.router, prefix="/api/notifications", tags=["notifications"])
66
+ app.include_router(leaderboard.router, prefix="/api/leaderboard", tags=["leaderboard"])
67
+ app.include_router(execution.router, prefix="/api/execution", tags=["execution"])
68
+ app.include_router(plugins.router, prefix="/api", tags=["plugins"])
69
+
70
+
71
+ # Stats endpoint for dashboard
72
+ @app.get("/api/stats")
73
+ async def get_stats(project: str = None):
74
+ """Get overall statistics for the dashboard, optionally filtered by project."""
75
+ try:
76
+ from flowyml.storage.metadata import SQLiteMetadataStore
77
+
78
+ store = SQLiteMetadataStore()
79
+
80
+ # Get base stats
81
+ stats = store.get_statistics()
82
+
83
+ # Get run status counts (not in get_statistics yet)
84
+ # We can add this to get_statistics later, but for now let's query efficiently
85
+ import sqlite3
86
+
87
+ conn = sqlite3.connect(store.db_path)
88
+ cursor = conn.cursor()
89
+
90
+ if project:
91
+ cursor.execute(
92
+ "SELECT COUNT(*) FROM runs WHERE project = ? AND status = 'completed'",
93
+ [project],
94
+ )
95
+ completed_runs = cursor.fetchone()[0]
96
+
97
+ cursor.execute(
98
+ "SELECT COUNT(*) FROM runs WHERE project = ? AND status = 'failed'",
99
+ [project],
100
+ )
101
+ failed_runs = cursor.fetchone()[0]
102
+
103
+ cursor.execute(
104
+ "SELECT AVG(duration) FROM runs WHERE project = ? AND duration IS NOT NULL",
105
+ [project],
106
+ )
107
+ avg_duration = cursor.fetchone()[0] or 0
108
+
109
+ cursor.execute(
110
+ "SELECT COUNT(*) FROM runs WHERE project = ?",
111
+ [project],
112
+ )
113
+ total_runs = cursor.fetchone()[0]
114
+ else:
115
+ cursor.execute("SELECT COUNT(*) FROM runs WHERE status = 'completed'")
116
+ completed_runs = cursor.fetchone()[0]
117
+
118
+ cursor.execute("SELECT COUNT(*) FROM runs WHERE status = 'failed'")
119
+ failed_runs = cursor.fetchone()[0]
120
+
121
+ cursor.execute("SELECT AVG(duration) FROM runs WHERE duration IS NOT NULL")
122
+ avg_duration = cursor.fetchone()[0] or 0
123
+
124
+ cursor.execute("SELECT COUNT(*) FROM runs")
125
+ total_runs = cursor.fetchone()[0]
126
+
127
+ conn.close()
128
+
129
+ return {
130
+ "runs": total_runs if project else stats.get("total_runs", 0),
131
+ "completed_runs": completed_runs,
132
+ "failed_runs": failed_runs,
133
+ "pipelines": stats.get("total_pipelines", 0), # TODO: filter by project
134
+ "artifacts": stats.get("total_artifacts", 0), # TODO: filter by project
135
+ "avg_duration": avg_duration,
136
+ }
137
+ except Exception as e:
138
+ # Return default stats if there's an error
139
+ return {
140
+ "runs": 0,
141
+ "completed_runs": 0,
142
+ "failed_runs": 0,
143
+ "pipelines": 0,
144
+ "artifacts": 0,
145
+ "avg_duration": 0,
146
+ "error": str(e),
147
+ }
148
+
149
+
150
+ # Static file serving for frontend
151
+ # Path to frontend build
152
+ frontend_dist = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
153
+
154
+ if os.path.exists(frontend_dist):
155
+ # Mount static assets
156
+ app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets")
157
+
158
+ # Serve index.html for root and other non-API routes
159
+ # Use a specific route for root
160
+ @app.get("/", include_in_schema=False)
161
+ async def serve_root():
162
+ return FileResponse(os.path.join(frontend_dist, "index.html"))
163
+
164
+ # For SPA routing, we need to serve index.html for common frontend routes
165
+ # But we can't use a catch-all because it interferes with API routes
166
+ # Instead, mount a StaticFiles handler for the root, but do it AFTER API routes
167
+ # Actually, let's try a different approach - use a custom middleware or exceptions
168
+
169
+ # The trick is to let FastAPI handle routes first, then catch 404s
170
+ from starlette.exceptions import HTTPException as StarletteHTTPException
171
+ from fastapi.exception_handlers import http_exception_handler
172
+
173
+ @app.exception_handler(StarletteHTTPException)
174
+ async def custom_http_exception_handler(request, exc):
175
+ # If it's a 404 and not an API route, serve the SPA
176
+ if exc.status_code == 404 and not request.url.path.startswith("/api"):
177
+ return FileResponse(os.path.join(frontend_dist, "index.html"))
178
+ # Otherwise, use the default handler
179
+ return await http_exception_handler(request, exc)
180
+ else:
181
+
182
+ @app.get("/")
183
+ async def root():
184
+ return {
185
+ "message": "flowyml API is running.",
186
+ "detail": "Frontend not built. Run 'npm run build' in flowyml/ui/frontend to enable the UI.",
187
+ }
File without changes
@@ -0,0 +1,45 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from flowyml.storage.metadata import SQLiteMetadataStore
3
+
4
+ router = APIRouter()
5
+
6
+
7
+ def get_store():
8
+ return SQLiteMetadataStore()
9
+
10
+
11
+ @router.get("/")
12
+ async def list_assets(limit: int = 50, asset_type: str = None, run_id: str = None, project: str = None):
13
+ """List all assets, optionally filtered by project."""
14
+ try:
15
+ store = get_store()
16
+
17
+ # Build filters
18
+ filters = {}
19
+ if asset_type:
20
+ filters["type"] = asset_type
21
+ if run_id:
22
+ filters["run_id"] = run_id
23
+
24
+ assets = store.list_assets(limit=limit, **filters)
25
+
26
+ # Filter by project if specified
27
+ if project:
28
+ # Get all runs for this project
29
+ all_runs = store.list_runs(limit=1000)
30
+ project_run_ids = {r.get("run_id") for r in all_runs if r.get("project") == project}
31
+ assets = [a for a in assets if a.get("run_id") in project_run_ids]
32
+
33
+ return {"assets": assets}
34
+ except Exception as e:
35
+ return {"assets": [], "error": str(e)}
36
+
37
+
38
+ @router.get("/{artifact_id}")
39
+ async def get_asset(artifact_id: str):
40
+ """Get details for a specific asset."""
41
+ store = get_store()
42
+ asset = store.load_artifact(artifact_id)
43
+ if not asset:
44
+ raise HTTPException(status_code=404, detail="Asset not found")
45
+ return asset
@@ -0,0 +1,179 @@
1
+ """Pipeline execution API endpoints."""
2
+
3
+ from fastapi import APIRouter, HTTPException, Depends, Security
4
+ from fastapi.security import HTTPAuthorizationCredentials
5
+ from pydantic import BaseModel
6
+ from typing import Any
7
+ from flowyml.ui.backend.auth import verify_api_token, security
8
+ import importlib
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ def require_permission(permission: str):
14
+ """Create a dependency for checking permissions."""
15
+
16
+ async def _verify(credentials: HTTPAuthorizationCredentials = Security(security)):
17
+ return await verify_api_token(credentials, required_permission=permission)
18
+
19
+ return _verify
20
+
21
+
22
+ class PipelineExecutionRequest(BaseModel):
23
+ """Pipeline execution request."""
24
+
25
+ pipeline_module: str # e.g., "my_pipelines.training"
26
+ pipeline_name: str # e.g., "training_pipeline"
27
+ parameters: dict[str, Any] = {}
28
+ project: str | None = None
29
+ dry_run: bool = False # If True, validate but don't execute
30
+
31
+
32
+ class TokenRequest(BaseModel):
33
+ """API token creation request."""
34
+
35
+ name: str
36
+ project: str | None = None
37
+ permissions: list = ["read", "write", "execute"]
38
+
39
+
40
+ @router.post("/execute")
41
+ async def execute_pipeline(
42
+ request: PipelineExecutionRequest,
43
+ token_data: dict = Depends(require_permission("execute")),
44
+ ):
45
+ """Execute a pipeline.
46
+
47
+ Requires 'execute' permission.
48
+
49
+ Example request:
50
+ ```json
51
+ {
52
+ "pipeline_module": "my_pipelines.training",
53
+ "pipeline_name": "training_pipeline",
54
+ "parameters": {"epochs": 10},
55
+ "project": "my_project",
56
+ "dry_run": false
57
+ }
58
+ ```
59
+ """
60
+ try:
61
+ # Check project scope if token is project-specific
62
+ if token_data.get("project") and token_data["project"] != request.project:
63
+ raise HTTPException(
64
+ status_code=403,
65
+ detail=f"Token is scoped to project '{token_data['project']}'",
66
+ )
67
+
68
+ if request.dry_run:
69
+ return {
70
+ "status": "validated",
71
+ "pipeline": request.pipeline_name,
72
+ "module": request.pipeline_module,
73
+ "parameters": request.parameters,
74
+ "message": "Pipeline configuration is valid (dry run)",
75
+ }
76
+
77
+ # Import the pipeline module
78
+ try:
79
+ module = importlib.import_module(request.pipeline_module)
80
+ except ImportError as e:
81
+ raise HTTPException(
82
+ status_code=404,
83
+ detail=f"Pipeline module not found: {request.pipeline_module}. Error: {str(e)}",
84
+ )
85
+
86
+ # Get the pipeline object
87
+ if not hasattr(module, request.pipeline_name):
88
+ raise HTTPException(
89
+ status_code=404,
90
+ detail=f"Pipeline '{request.pipeline_name}' not found in module '{request.pipeline_module}'",
91
+ )
92
+
93
+ pipeline = getattr(module, request.pipeline_name)
94
+
95
+ # Execute the pipeline
96
+ result = pipeline.run(**request.parameters)
97
+
98
+ return {
99
+ "status": "completed",
100
+ "run_id": result.run_id if hasattr(result, "run_id") else None,
101
+ "pipeline": request.pipeline_name,
102
+ "message": "Pipeline executed successfully",
103
+ }
104
+
105
+ except HTTPException:
106
+ raise
107
+ except Exception as e:
108
+ raise HTTPException(
109
+ status_code=500,
110
+ detail=f"Pipeline execution failed: {str(e)}",
111
+ )
112
+
113
+
114
+ @router.post("/tokens")
115
+ async def create_token(
116
+ request: TokenRequest,
117
+ token_data: dict = Depends(require_permission("admin")),
118
+ ):
119
+ """Create a new API token.
120
+
121
+ Requires 'admin' permission or can be called without auth for initial setup.
122
+ """
123
+ from flowyml.ui.backend.auth import token_manager
124
+
125
+ try:
126
+ token = token_manager.create_token(
127
+ name=request.name,
128
+ project=request.project,
129
+ permissions=request.permissions,
130
+ )
131
+
132
+ return {
133
+ "token": token,
134
+ "name": request.name,
135
+ "project": request.project,
136
+ "permissions": request.permissions,
137
+ "message": "Token created successfully. Save this token - it won't be shown again!",
138
+ }
139
+ except Exception as e:
140
+ raise HTTPException(
141
+ status_code=500,
142
+ detail=f"Failed to create token: {str(e)}",
143
+ )
144
+
145
+
146
+ @router.get("/tokens")
147
+ async def list_tokens():
148
+ """List all API tokens (without revealing token values)."""
149
+ from flowyml.ui.backend.auth import token_manager
150
+
151
+ # Allow listing tokens without auth (for UI to check if any exist)
152
+ return {"tokens": token_manager.list_tokens()}
153
+
154
+
155
+ @router.post("/tokens/init")
156
+ async def initialize_first_token():
157
+ """Create the first admin token (no auth required).
158
+
159
+ This endpoint can only be used if no tokens exist yet.
160
+ """
161
+ from flowyml.ui.backend.auth import token_manager
162
+
163
+ if token_manager.list_tokens():
164
+ raise HTTPException(
165
+ status_code=403,
166
+ detail="Tokens already exist. Use /api/execution/tokens with admin token to create more.",
167
+ )
168
+
169
+ token = token_manager.create_token(
170
+ name="Initial Admin Token",
171
+ project=None,
172
+ permissions=["read", "write", "execute", "admin"],
173
+ )
174
+
175
+ return {
176
+ "token": token,
177
+ "message": "Initial admin token created. Save this token - it won't be shown again!",
178
+ "next_steps": "Use this token to create additional tokens via /api/execution/tokens",
179
+ }
@@ -0,0 +1,49 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from flowyml.storage.metadata import SQLiteMetadataStore
3
+
4
+ router = APIRouter()
5
+
6
+
7
+ def get_store():
8
+ return SQLiteMetadataStore()
9
+
10
+
11
+ @router.get("/")
12
+ async def list_experiments(project: str = None):
13
+ """List all experiments, optionally filtered by project."""
14
+ try:
15
+ store = get_store()
16
+ experiments = store.list_experiments()
17
+
18
+ # Filter by project if specified
19
+ if project:
20
+ experiments = [e for e in experiments if e.get("project") == project]
21
+
22
+ return {"experiments": experiments}
23
+ except Exception as e:
24
+ return {"experiments": [], "error": str(e)}
25
+
26
+
27
+ @router.get("/{experiment_id}")
28
+ async def get_experiment(experiment_id: str):
29
+ """Get experiment details."""
30
+ store = get_store()
31
+ experiment = store.get_experiment(experiment_id)
32
+ if not experiment:
33
+ raise HTTPException(status_code=404, detail="Experiment not found")
34
+ return experiment
35
+
36
+
37
+ @router.put("/{experiment_name}/project")
38
+ async def update_experiment_project(experiment_name: str, project_update: dict):
39
+ """Update the project for an experiment."""
40
+ try:
41
+ store = get_store()
42
+ project_name = project_update.get("project_name")
43
+
44
+ # Update experiment project
45
+ store.update_experiment_project(experiment_name, project_name)
46
+
47
+ return {"message": f"Updated experiment {experiment_name} to project {project_name}"}
48
+ except Exception as e:
49
+ raise HTTPException(status_code=500, detail=str(e))