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,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))
|