flowyml 1.2.0__py3-none-any.whl → 1.4.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 +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +34 -17
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/scheduler.py +9 -9
- flowyml/core/scheduler_config.py +2 -3
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/bridge.py +9 -9
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/stacks/plugins.py +2 -2
- flowyml/stacks/registry.py +21 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/base.py +33 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +3 -881
- flowyml/storage/remote.py +590 -0
- flowyml/storage/sql.py +911 -0
- flowyml/ui/backend/dependencies.py +28 -0
- flowyml/ui/backend/main.py +43 -80
- flowyml/ui/backend/routers/assets.py +483 -17
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +97 -14
- flowyml/ui/backend/routers/metrics.py +168 -0
- flowyml/ui/backend/routers/pipelines.py +77 -12
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +221 -12
- flowyml/ui/backend/routers/schedules.py +5 -21
- flowyml/ui/backend/routers/stats.py +14 -0
- flowyml/ui/backend/routers/traces.py +37 -53
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
- flowyml-1.4.0.dist-info/RECORD +200 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- flowyml-1.2.0.dist-info/RECORD +0 -159
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,25 +1,54 @@
|
|
|
1
1
|
from fastapi import APIRouter, HTTPException
|
|
2
2
|
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
3
|
+
from flowyml.core.project import ProjectManager
|
|
4
|
+
from flowyml.ui.backend.dependencies import get_store
|
|
5
|
+
from pydantic import BaseModel
|
|
3
6
|
|
|
4
7
|
router = APIRouter()
|
|
5
8
|
|
|
6
9
|
|
|
7
|
-
def
|
|
8
|
-
|
|
10
|
+
def _iter_metadata_stores():
|
|
11
|
+
"""Yield tuples of (project_name, store) including global and project stores."""
|
|
12
|
+
stores = [(None, SQLiteMetadataStore())]
|
|
13
|
+
try:
|
|
14
|
+
manager = ProjectManager()
|
|
15
|
+
for project_meta in manager.list_projects():
|
|
16
|
+
name = project_meta.get("name")
|
|
17
|
+
if not name:
|
|
18
|
+
continue
|
|
19
|
+
project = manager.get_project(name)
|
|
20
|
+
if project:
|
|
21
|
+
stores.append((name, project.metadata_store))
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
return stores
|
|
9
25
|
|
|
10
26
|
|
|
11
27
|
@router.get("/")
|
|
12
|
-
async def list_experiments(project: str = None):
|
|
28
|
+
async def list_experiments(project: str | None = None):
|
|
13
29
|
"""List all experiments, optionally filtered by project."""
|
|
14
30
|
try:
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
combined_experiments = []
|
|
32
|
+
|
|
33
|
+
for project_name, store in _iter_metadata_stores():
|
|
34
|
+
# Skip other projects if filtering
|
|
35
|
+
if project and project_name and project != project_name:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
store_experiments = store.list_experiments()
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
for exp in store_experiments:
|
|
41
|
+
# Add project info if not already present
|
|
42
|
+
if project_name and not exp.get("project"):
|
|
43
|
+
exp["project"] = project_name
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
# Skip if filtering by project and doesn't match
|
|
46
|
+
if project and exp.get("project") != project:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
combined_experiments.append(exp)
|
|
50
|
+
|
|
51
|
+
return {"experiments": combined_experiments}
|
|
23
52
|
except Exception as e:
|
|
24
53
|
return {"experiments": [], "error": str(e)}
|
|
25
54
|
|
|
@@ -27,11 +56,15 @@ async def list_experiments(project: str = None):
|
|
|
27
56
|
@router.get("/{experiment_id}")
|
|
28
57
|
async def get_experiment(experiment_id: str):
|
|
29
58
|
"""Get experiment details."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
# Try to find experiment in any store
|
|
60
|
+
for project_name, store in _iter_metadata_stores():
|
|
61
|
+
experiment = store.get_experiment(experiment_id)
|
|
62
|
+
if experiment:
|
|
63
|
+
if project_name and not experiment.get("project"):
|
|
64
|
+
experiment["project"] = project_name
|
|
65
|
+
return experiment
|
|
66
|
+
|
|
67
|
+
raise HTTPException(status_code=404, detail="Experiment not found")
|
|
35
68
|
|
|
36
69
|
|
|
37
70
|
@router.put("/{experiment_name}/project")
|
|
@@ -47,3 +80,53 @@ async def update_experiment_project(experiment_name: str, project_update: dict):
|
|
|
47
80
|
return {"message": f"Updated experiment {experiment_name} to project {project_name}"}
|
|
48
81
|
except Exception as e:
|
|
49
82
|
raise HTTPException(status_code=500, detail=str(e))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ExperimentCreate(BaseModel):
|
|
86
|
+
experiment_id: str
|
|
87
|
+
name: str
|
|
88
|
+
description: str = ""
|
|
89
|
+
tags: dict = {}
|
|
90
|
+
project: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.post("/")
|
|
94
|
+
async def create_experiment(experiment: ExperimentCreate):
|
|
95
|
+
"""Create or update an experiment."""
|
|
96
|
+
try:
|
|
97
|
+
store = get_store()
|
|
98
|
+
store.save_experiment(
|
|
99
|
+
experiment_id=experiment.experiment_id,
|
|
100
|
+
name=experiment.name,
|
|
101
|
+
description=experiment.description,
|
|
102
|
+
tags=experiment.tags,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if experiment.project:
|
|
106
|
+
store.update_experiment_project(experiment.name, experiment.project)
|
|
107
|
+
|
|
108
|
+
return {"status": "success", "experiment_id": experiment.experiment_id}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ExperimentRunLog(BaseModel):
|
|
114
|
+
run_id: str
|
|
115
|
+
metrics: dict | None = None
|
|
116
|
+
parameters: dict | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.post("/{experiment_id}/runs")
|
|
120
|
+
async def log_experiment_run(experiment_id: str, log: ExperimentRunLog):
|
|
121
|
+
"""Log a run to an experiment."""
|
|
122
|
+
try:
|
|
123
|
+
store = get_store()
|
|
124
|
+
store.log_experiment_run(
|
|
125
|
+
experiment_id=experiment_id,
|
|
126
|
+
run_id=log.run_id,
|
|
127
|
+
metrics=log.metrics,
|
|
128
|
+
parameters=log.parameters,
|
|
129
|
+
)
|
|
130
|
+
return {"status": "success"}
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Model metrics logging API endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Depends, Security, Query
|
|
4
|
+
from fastapi.security import HTTPAuthorizationCredentials
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowyml.ui.backend.auth import verify_api_token, security
|
|
9
|
+
from flowyml.ui.backend.dependencies import get_store
|
|
10
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
11
|
+
from flowyml.core.project import ProjectManager
|
|
12
|
+
from flowyml.utils.config import get_config
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def require_permission(permission: str):
|
|
18
|
+
"""Create dependency enforcing a given permission."""
|
|
19
|
+
|
|
20
|
+
async def _verify(credentials: HTTPAuthorizationCredentials = Security(security)):
|
|
21
|
+
return await verify_api_token(credentials, required_permission=permission)
|
|
22
|
+
|
|
23
|
+
return _verify
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_project_manager() -> ProjectManager:
|
|
27
|
+
"""Get a project manager rooted at configured projects dir."""
|
|
28
|
+
config = get_config()
|
|
29
|
+
return ProjectManager(str(config.projects_dir))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_global_store() -> SQLiteMetadataStore:
|
|
33
|
+
"""Metadata store for shared metrics."""
|
|
34
|
+
config = get_config()
|
|
35
|
+
return SQLiteMetadataStore(str(config.metadata_db))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MetricsLogRequest(BaseModel):
|
|
39
|
+
"""Payload for logging production model metrics."""
|
|
40
|
+
|
|
41
|
+
project: str = Field(..., description="Project identifier")
|
|
42
|
+
model_name: str = Field(..., description="Name of the model emitting metrics")
|
|
43
|
+
metrics: dict[str, float] = Field(..., description="Dictionary of metric_name -> value")
|
|
44
|
+
run_id: str | None = Field(None, description="Related run identifier (optional)")
|
|
45
|
+
environment: str | None = Field(None, description="Environment label (e.g., prod, staging)")
|
|
46
|
+
tags: dict[str, Any] | None = Field(default_factory=dict, description="Optional metadata tags")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MetricLog(BaseModel):
|
|
50
|
+
run_id: str
|
|
51
|
+
name: str
|
|
52
|
+
value: float
|
|
53
|
+
step: int = 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/")
|
|
57
|
+
async def log_metric(metric: MetricLog):
|
|
58
|
+
"""Log a single metric."""
|
|
59
|
+
try:
|
|
60
|
+
store = get_global_store()
|
|
61
|
+
store.save_metric(metric.run_id, metric.name, metric.value, metric.step)
|
|
62
|
+
return {"status": "success"}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.post("/log")
|
|
68
|
+
async def log_model_metrics(
|
|
69
|
+
payload: MetricsLogRequest,
|
|
70
|
+
token_data: dict = Depends(require_permission("write")),
|
|
71
|
+
):
|
|
72
|
+
"""Log production metrics for a model.
|
|
73
|
+
|
|
74
|
+
Requires tokens with the `write` permission. Project-scoped tokens
|
|
75
|
+
may only submit metrics for their project.
|
|
76
|
+
"""
|
|
77
|
+
if token_data.get("project") and token_data["project"] != payload.project:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=403,
|
|
80
|
+
detail=f"Token is scoped to project '{token_data['project']}'",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not payload.metrics:
|
|
84
|
+
raise HTTPException(status_code=400, detail="metrics dictionary cannot be empty")
|
|
85
|
+
|
|
86
|
+
numeric_metrics = {}
|
|
87
|
+
for name, value in payload.metrics.items():
|
|
88
|
+
try:
|
|
89
|
+
numeric_metrics[name] = float(value)
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
raise HTTPException(
|
|
92
|
+
status_code=400,
|
|
93
|
+
detail=f"Metric '{name}' must be numeric.",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
project_manager = get_project_manager()
|
|
97
|
+
project = project_manager.get_project(payload.project)
|
|
98
|
+
if not project:
|
|
99
|
+
project = project_manager.create_project(payload.project)
|
|
100
|
+
|
|
101
|
+
shared_store = get_global_store()
|
|
102
|
+
shared_store.log_model_metrics(
|
|
103
|
+
project=payload.project,
|
|
104
|
+
model_name=payload.model_name,
|
|
105
|
+
metrics=numeric_metrics,
|
|
106
|
+
run_id=payload.run_id,
|
|
107
|
+
environment=payload.environment,
|
|
108
|
+
tags=payload.tags,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
project.log_model_metrics(
|
|
112
|
+
model_name=payload.model_name,
|
|
113
|
+
metrics=numeric_metrics,
|
|
114
|
+
run_id=payload.run_id,
|
|
115
|
+
environment=payload.environment,
|
|
116
|
+
tags=payload.tags,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"project": payload.project,
|
|
121
|
+
"model_name": payload.model_name,
|
|
122
|
+
"logged_metrics": list(numeric_metrics.keys()),
|
|
123
|
+
"message": "Metrics logged successfully",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get("")
|
|
128
|
+
async def list_model_metrics(
|
|
129
|
+
project: str | None = Query(default=None, description="Filter by project"),
|
|
130
|
+
model_name: str | None = Query(default=None, description="Filter by model"),
|
|
131
|
+
limit: int = Query(default=100, ge=1, le=500),
|
|
132
|
+
token_data: dict = Depends(require_permission("read")),
|
|
133
|
+
):
|
|
134
|
+
"""Retrieve the latest logged model metrics."""
|
|
135
|
+
if token_data.get("project"):
|
|
136
|
+
if project and token_data["project"] != project:
|
|
137
|
+
raise HTTPException(
|
|
138
|
+
status_code=403,
|
|
139
|
+
detail=f"Token is scoped to project '{token_data['project']}'",
|
|
140
|
+
)
|
|
141
|
+
project = token_data["project"]
|
|
142
|
+
|
|
143
|
+
store: SQLiteMetadataStore
|
|
144
|
+
if project:
|
|
145
|
+
project_manager = get_project_manager()
|
|
146
|
+
project_obj = project_manager.get_project(project)
|
|
147
|
+
if not project_obj:
|
|
148
|
+
raise HTTPException(status_code=404, detail=f"Project '{project}' not found")
|
|
149
|
+
store = project_obj.metadata_store
|
|
150
|
+
else:
|
|
151
|
+
store = get_global_store()
|
|
152
|
+
|
|
153
|
+
records = store.list_model_metrics(project=project, model_name=model_name, limit=limit)
|
|
154
|
+
return {"metrics": records}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.get("/observability/orchestrator")
|
|
158
|
+
async def get_orchestrator_metrics():
|
|
159
|
+
"""Get orchestrator-level performance metrics."""
|
|
160
|
+
store = get_store()
|
|
161
|
+
return store.get_orchestrator_metrics(days=30)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.get("/observability/cache")
|
|
165
|
+
async def get_cache_metrics():
|
|
166
|
+
"""Get cache performance metrics."""
|
|
167
|
+
store = get_store()
|
|
168
|
+
return store.get_cache_metrics(days=30)
|
|
@@ -1,25 +1,74 @@
|
|
|
1
1
|
from fastapi import APIRouter, HTTPException
|
|
2
2
|
from pydantic import BaseModel
|
|
3
|
-
from flowyml.
|
|
4
|
-
from flowyml.
|
|
3
|
+
from flowyml.ui.backend.dependencies import get_store
|
|
4
|
+
from flowyml.core.project import ProjectManager
|
|
5
5
|
|
|
6
6
|
router = APIRouter()
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
def _iter_metadata_stores():
|
|
10
|
+
"""Yield tuples of (project_name, store) including global and project stores."""
|
|
11
|
+
stores = [(None, get_store())]
|
|
12
|
+
try:
|
|
13
|
+
manager = ProjectManager()
|
|
14
|
+
for project_meta in manager.list_projects():
|
|
15
|
+
name = project_meta.get("name")
|
|
16
|
+
if not name:
|
|
17
|
+
continue
|
|
18
|
+
project = manager.get_project(name)
|
|
19
|
+
if project:
|
|
20
|
+
stores.append((name, project.metadata_store))
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
return stores
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
@router.get("/")
|
|
17
|
-
async def list_pipelines(project: str = None):
|
|
18
|
-
"""List all unique pipelines, optionally filtered by project."""
|
|
27
|
+
async def list_pipelines(project: str | None = None, limit: int = 100):
|
|
28
|
+
"""List all unique pipelines with details, optionally filtered by project."""
|
|
19
29
|
try:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
pipeline_map = {} # pipeline_name -> data
|
|
31
|
+
|
|
32
|
+
for project_name, store in _iter_metadata_stores():
|
|
33
|
+
# Skip other projects if filtering
|
|
34
|
+
if project and project_name and project != project_name:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# Get pipeline names from this store
|
|
38
|
+
store_pipeline_names = store.list_pipelines()
|
|
39
|
+
|
|
40
|
+
for name in store_pipeline_names:
|
|
41
|
+
# Get runs for this pipeline
|
|
42
|
+
filters = {"pipeline_name": name}
|
|
43
|
+
runs = store.query(**filters)
|
|
44
|
+
|
|
45
|
+
if not runs:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
last_run = runs[0]
|
|
49
|
+
run_project = last_run.get("project") or project_name
|
|
50
|
+
|
|
51
|
+
# Skip if filtering by project and doesn't match
|
|
52
|
+
if project and run_project != project:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# Use composite key if we already have this pipeline from another project
|
|
56
|
+
key = f"{name}:{run_project}" if run_project else name
|
|
57
|
+
|
|
58
|
+
if key not in pipeline_map:
|
|
59
|
+
pipeline_map[key] = {
|
|
60
|
+
"name": name,
|
|
61
|
+
"created": last_run.get("start_time"),
|
|
62
|
+
"version": last_run.get("git_sha", "latest")[:7] if last_run.get("git_sha") else "1.0",
|
|
63
|
+
"status": last_run.get("status", "unknown"),
|
|
64
|
+
"run_count": len(runs),
|
|
65
|
+
"last_run_id": last_run.get("run_id"),
|
|
66
|
+
"project": run_project,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Return list of pipelines
|
|
70
|
+
enriched_pipelines = list(pipeline_map.values())[:limit]
|
|
71
|
+
return {"pipelines": enriched_pipelines}
|
|
23
72
|
except Exception as e:
|
|
24
73
|
raise HTTPException(status_code=500, detail=str(e))
|
|
25
74
|
|
|
@@ -108,3 +157,19 @@ async def update_pipeline_project(pipeline_name: str, update: ProjectUpdate):
|
|
|
108
157
|
return {"status": "success", "project": update.project_name}
|
|
109
158
|
except Exception as e:
|
|
110
159
|
raise HTTPException(status_code=500, detail=str(e))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PipelineDefinitionCreate(BaseModel):
|
|
163
|
+
pipeline_name: str
|
|
164
|
+
definition: dict
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.post("/")
|
|
168
|
+
async def save_pipeline_definition(data: PipelineDefinitionCreate):
|
|
169
|
+
"""Save a pipeline definition."""
|
|
170
|
+
try:
|
|
171
|
+
store = get_store()
|
|
172
|
+
store.save_pipeline_definition(data.pipeline_name, data.definition)
|
|
173
|
+
return {"status": "success", "pipeline_name": data.pipeline_name}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
from fastapi import APIRouter, HTTPException
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Depends
|
|
2
2
|
from flowyml.core.project import ProjectManager
|
|
3
|
+
from flowyml.utils.config import get_config
|
|
3
4
|
from pydantic import BaseModel
|
|
4
5
|
|
|
5
6
|
router = APIRouter()
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_projects_manager() -> ProjectManager:
|
|
10
|
+
"""Instantiate a ProjectManager bound to the current config."""
|
|
11
|
+
config = get_config()
|
|
12
|
+
return ProjectManager(str(config.projects_dir))
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
@router.get("/")
|
|
10
|
-
async def list_projects():
|
|
16
|
+
async def list_projects(manager: ProjectManager = Depends(get_projects_manager)):
|
|
11
17
|
"""List all projects."""
|
|
12
18
|
try:
|
|
13
19
|
projects = manager.list_projects()
|
|
14
|
-
return projects
|
|
20
|
+
return {"projects": projects}
|
|
15
21
|
except Exception as e:
|
|
16
22
|
raise HTTPException(status_code=500, detail=str(e))
|
|
17
23
|
|
|
@@ -22,7 +28,7 @@ class ProjectCreate(BaseModel):
|
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
@router.post("/")
|
|
25
|
-
async def create_project(project: ProjectCreate):
|
|
31
|
+
async def create_project(project: ProjectCreate, manager: ProjectManager = Depends(get_projects_manager)):
|
|
26
32
|
"""Create a new project."""
|
|
27
33
|
created_project = manager.create_project(project.name, project.description)
|
|
28
34
|
return {
|
|
@@ -33,7 +39,7 @@ async def create_project(project: ProjectCreate):
|
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
@router.get("/{project_name}")
|
|
36
|
-
async def get_project(project_name: str):
|
|
42
|
+
async def get_project(project_name: str, manager: ProjectManager = Depends(get_projects_manager)):
|
|
37
43
|
"""Get project details."""
|
|
38
44
|
project = manager.get_project(project_name)
|
|
39
45
|
if not project:
|
|
@@ -53,6 +59,7 @@ async def get_project_runs(
|
|
|
53
59
|
project_name: str,
|
|
54
60
|
pipeline_name: str | None = None,
|
|
55
61
|
limit: int = 100,
|
|
62
|
+
manager: ProjectManager = Depends(get_projects_manager),
|
|
56
63
|
):
|
|
57
64
|
"""Get runs for a project."""
|
|
58
65
|
project = manager.get_project(project_name)
|
|
@@ -68,6 +75,7 @@ async def get_project_artifacts(
|
|
|
68
75
|
project_name: str,
|
|
69
76
|
artifact_type: str | None = None,
|
|
70
77
|
limit: int = 100,
|
|
78
|
+
manager: ProjectManager = Depends(get_projects_manager),
|
|
71
79
|
):
|
|
72
80
|
"""Get artifacts for a project."""
|
|
73
81
|
project = manager.get_project(project_name)
|
|
@@ -78,8 +86,26 @@ async def get_project_artifacts(
|
|
|
78
86
|
return artifacts
|
|
79
87
|
|
|
80
88
|
|
|
89
|
+
@router.get("/{project_name}/metrics")
|
|
90
|
+
async def get_project_metrics(
|
|
91
|
+
project_name: str,
|
|
92
|
+
model_name: str | None = None,
|
|
93
|
+
limit: int = 100,
|
|
94
|
+
manager: ProjectManager = Depends(get_projects_manager),
|
|
95
|
+
):
|
|
96
|
+
"""Get logged production metrics for a project."""
|
|
97
|
+
project = manager.get_project(project_name)
|
|
98
|
+
if not project:
|
|
99
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"project": project_name,
|
|
103
|
+
"metrics": project.list_model_metrics(model_name=model_name, limit=limit),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
81
107
|
@router.delete("/{project_name}")
|
|
82
|
-
async def delete_project(project_name: str):
|
|
108
|
+
async def delete_project(project_name: str, manager: ProjectManager = Depends(get_projects_manager)):
|
|
83
109
|
"""Delete a project."""
|
|
84
110
|
manager.delete_project(project_name, confirm=True)
|
|
85
111
|
return {"deleted": True}
|