flowyml 1.7.1__py3-none-any.whl → 1.8.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/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
flowyml/storage/sql.py
CHANGED
|
@@ -2,32 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
# Python 3.11+ has UTC, but Python 3.10 doesn't
|
|
9
9
|
try:
|
|
10
10
|
from datetime import UTC
|
|
11
11
|
except ImportError:
|
|
12
|
-
|
|
12
|
+
from datetime import timezone
|
|
13
|
+
|
|
14
|
+
UTC = timezone.utc
|
|
13
15
|
|
|
14
16
|
from sqlalchemy import (
|
|
15
|
-
create_engine,
|
|
16
|
-
MetaData,
|
|
17
|
-
Table,
|
|
18
17
|
Column,
|
|
19
|
-
String,
|
|
20
|
-
Integer,
|
|
21
18
|
Float,
|
|
22
|
-
Text,
|
|
23
19
|
ForeignKey,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
Integer,
|
|
21
|
+
MetaData,
|
|
22
|
+
String,
|
|
23
|
+
Table,
|
|
24
|
+
Text,
|
|
25
|
+
create_engine,
|
|
27
26
|
delete,
|
|
28
27
|
func,
|
|
29
|
-
|
|
28
|
+
insert,
|
|
30
29
|
inspect,
|
|
30
|
+
select,
|
|
31
|
+
text,
|
|
32
|
+
update,
|
|
31
33
|
)
|
|
32
34
|
from sqlalchemy.pool import StaticPool
|
|
33
35
|
|
|
@@ -204,6 +206,53 @@ class SQLMetadataStore(MetadataStore):
|
|
|
204
206
|
Column("updated_at", String, nullable=False),
|
|
205
207
|
)
|
|
206
208
|
|
|
209
|
+
# Projects table for proper project storage
|
|
210
|
+
self.projects = Table(
|
|
211
|
+
"projects",
|
|
212
|
+
self.metadata,
|
|
213
|
+
Column("name", String, primary_key=True),
|
|
214
|
+
Column("description", Text),
|
|
215
|
+
Column("tags", Text), # JSON
|
|
216
|
+
Column("created_at", String, server_default=func.current_timestamp()),
|
|
217
|
+
Column("updated_at", String),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Model registry table for versioned model management
|
|
221
|
+
self.model_versions = Table(
|
|
222
|
+
"model_versions",
|
|
223
|
+
self.metadata,
|
|
224
|
+
Column("id", Integer, primary_key=True, autoincrement=True),
|
|
225
|
+
Column("name", String, nullable=False),
|
|
226
|
+
Column("version", String, nullable=False),
|
|
227
|
+
Column("stage", String, nullable=False), # development, staging, production, archived
|
|
228
|
+
Column("framework", String, nullable=False),
|
|
229
|
+
Column("model_path", String, nullable=False),
|
|
230
|
+
Column("metrics", Text), # JSON
|
|
231
|
+
Column("tags", Text), # JSON
|
|
232
|
+
Column("schema", Text), # JSON - model input/output schema
|
|
233
|
+
Column("description", Text),
|
|
234
|
+
Column("author", String),
|
|
235
|
+
Column("parent_version", String),
|
|
236
|
+
Column("created_at", String, server_default=func.current_timestamp()),
|
|
237
|
+
Column("updated_at", String),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Pipeline templates for reusable pipeline configurations
|
|
241
|
+
self.pipeline_templates = Table(
|
|
242
|
+
"pipeline_templates",
|
|
243
|
+
self.metadata,
|
|
244
|
+
Column("template_id", String, primary_key=True),
|
|
245
|
+
Column("name", String, nullable=False),
|
|
246
|
+
Column("description", Text),
|
|
247
|
+
Column("definition", Text, nullable=False), # JSON pipeline config
|
|
248
|
+
Column("tags", Text), # JSON
|
|
249
|
+
Column("author", String),
|
|
250
|
+
Column("category", String), # e.g., "training", "inference", "data-processing"
|
|
251
|
+
Column("is_public", Integer, default=0), # 0=private, 1=public
|
|
252
|
+
Column("created_at", String, server_default=func.current_timestamp()),
|
|
253
|
+
Column("updated_at", String),
|
|
254
|
+
)
|
|
255
|
+
|
|
207
256
|
# Create tables
|
|
208
257
|
self.metadata.create_all(self.engine)
|
|
209
258
|
|
|
@@ -639,6 +688,33 @@ class SQLMetadataStore(MetadataStore):
|
|
|
639
688
|
)
|
|
640
689
|
return experiments
|
|
641
690
|
|
|
691
|
+
def list_experiment_runs(self, experiment_id: str) -> list[dict]:
|
|
692
|
+
"""List all runs in an experiment."""
|
|
693
|
+
with self.engine.connect() as conn:
|
|
694
|
+
stmt = (
|
|
695
|
+
select(
|
|
696
|
+
self.experiment_runs.c.run_id,
|
|
697
|
+
self.experiment_runs.c.metrics,
|
|
698
|
+
self.experiment_runs.c.parameters,
|
|
699
|
+
self.experiment_runs.c.timestamp,
|
|
700
|
+
)
|
|
701
|
+
.where(
|
|
702
|
+
self.experiment_runs.c.experiment_id == experiment_id,
|
|
703
|
+
)
|
|
704
|
+
.order_by(self.experiment_runs.c.timestamp.desc())
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
rows = conn.execute(stmt).fetchall()
|
|
708
|
+
return [
|
|
709
|
+
{
|
|
710
|
+
"run_id": row[0],
|
|
711
|
+
"metrics": json.loads(row[1]) if row[1] else {},
|
|
712
|
+
"parameters": json.loads(row[2]) if row[2] else {},
|
|
713
|
+
"created_at": str(row[3]),
|
|
714
|
+
}
|
|
715
|
+
for row in rows
|
|
716
|
+
]
|
|
717
|
+
|
|
642
718
|
def update_experiment_project(self, experiment_name: str, project_name: str) -> None:
|
|
643
719
|
"""Update the project for an experiment."""
|
|
644
720
|
with self.engine.connect() as conn:
|
|
@@ -967,3 +1043,316 @@ class SQLMetadataStore(MetadataStore):
|
|
|
967
1043
|
"total_experiments": total_experiments,
|
|
968
1044
|
"total_models": total_models,
|
|
969
1045
|
}
|
|
1046
|
+
|
|
1047
|
+
# ==================== Project CRUD ====================
|
|
1048
|
+
|
|
1049
|
+
def save_project(self, name: str, description: str = "", tags: dict = None) -> None:
|
|
1050
|
+
"""Save or update a project."""
|
|
1051
|
+
now = datetime.now(UTC).isoformat()
|
|
1052
|
+
with self.engine.connect() as conn:
|
|
1053
|
+
stmt = select(self.projects).where(self.projects.c.name == name)
|
|
1054
|
+
existing = conn.execute(stmt).fetchone()
|
|
1055
|
+
|
|
1056
|
+
values = {
|
|
1057
|
+
"name": name,
|
|
1058
|
+
"description": description,
|
|
1059
|
+
"tags": json.dumps(tags or {}),
|
|
1060
|
+
"updated_at": now,
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if existing:
|
|
1064
|
+
conn.execute(
|
|
1065
|
+
update(self.projects).where(self.projects.c.name == name).values(**values),
|
|
1066
|
+
)
|
|
1067
|
+
else:
|
|
1068
|
+
values["created_at"] = now
|
|
1069
|
+
conn.execute(insert(self.projects).values(**values))
|
|
1070
|
+
|
|
1071
|
+
conn.commit()
|
|
1072
|
+
|
|
1073
|
+
def get_project(self, name: str) -> dict | None:
|
|
1074
|
+
"""Get a project by name."""
|
|
1075
|
+
with self.engine.connect() as conn:
|
|
1076
|
+
stmt = select(self.projects).where(self.projects.c.name == name)
|
|
1077
|
+
row = conn.execute(stmt).fetchone()
|
|
1078
|
+
if row:
|
|
1079
|
+
return {
|
|
1080
|
+
"name": row.name,
|
|
1081
|
+
"description": row.description,
|
|
1082
|
+
"tags": json.loads(row.tags) if row.tags else {},
|
|
1083
|
+
"created_at": row.created_at,
|
|
1084
|
+
"updated_at": row.updated_at,
|
|
1085
|
+
}
|
|
1086
|
+
return None
|
|
1087
|
+
|
|
1088
|
+
def list_projects(self) -> list[dict]:
|
|
1089
|
+
"""List all projects."""
|
|
1090
|
+
with self.engine.connect() as conn:
|
|
1091
|
+
stmt = select(self.projects).order_by(self.projects.c.created_at.desc())
|
|
1092
|
+
rows = conn.execute(stmt).fetchall()
|
|
1093
|
+
return [
|
|
1094
|
+
{
|
|
1095
|
+
"name": row.name,
|
|
1096
|
+
"description": row.description,
|
|
1097
|
+
"tags": json.loads(row.tags) if row.tags else {},
|
|
1098
|
+
"created_at": row.created_at,
|
|
1099
|
+
"updated_at": row.updated_at,
|
|
1100
|
+
}
|
|
1101
|
+
for row in rows
|
|
1102
|
+
]
|
|
1103
|
+
|
|
1104
|
+
def delete_project(self, name: str) -> None:
|
|
1105
|
+
"""Delete a project."""
|
|
1106
|
+
with self.engine.connect() as conn:
|
|
1107
|
+
conn.execute(delete(self.projects).where(self.projects.c.name == name))
|
|
1108
|
+
conn.commit()
|
|
1109
|
+
|
|
1110
|
+
# ========== Model Registry Methods ==========
|
|
1111
|
+
|
|
1112
|
+
def save_model_version(
|
|
1113
|
+
self,
|
|
1114
|
+
name: str,
|
|
1115
|
+
version: str,
|
|
1116
|
+
stage: str,
|
|
1117
|
+
framework: str,
|
|
1118
|
+
model_path: str,
|
|
1119
|
+
metrics: dict | None = None,
|
|
1120
|
+
tags: dict | None = None,
|
|
1121
|
+
schema: dict | None = None,
|
|
1122
|
+
description: str = "",
|
|
1123
|
+
author: str | None = None,
|
|
1124
|
+
parent_version: str | None = None,
|
|
1125
|
+
) -> int:
|
|
1126
|
+
"""Save a model version to the registry.
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
The ID of the saved model version.
|
|
1130
|
+
"""
|
|
1131
|
+
now = datetime.now(UTC).isoformat()
|
|
1132
|
+
with self.engine.connect() as conn:
|
|
1133
|
+
# Check if version already exists
|
|
1134
|
+
stmt = select(self.model_versions).where(
|
|
1135
|
+
(self.model_versions.c.name == name) & (self.model_versions.c.version == version),
|
|
1136
|
+
)
|
|
1137
|
+
existing = conn.execute(stmt).fetchone()
|
|
1138
|
+
|
|
1139
|
+
values = {
|
|
1140
|
+
"name": name,
|
|
1141
|
+
"version": version,
|
|
1142
|
+
"stage": stage,
|
|
1143
|
+
"framework": framework,
|
|
1144
|
+
"model_path": model_path,
|
|
1145
|
+
"metrics": json.dumps(metrics or {}),
|
|
1146
|
+
"tags": json.dumps(tags or {}),
|
|
1147
|
+
"schema": json.dumps(schema or {}),
|
|
1148
|
+
"description": description,
|
|
1149
|
+
"author": author,
|
|
1150
|
+
"parent_version": parent_version,
|
|
1151
|
+
"updated_at": now,
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if existing:
|
|
1155
|
+
conn.execute(
|
|
1156
|
+
update(self.model_versions)
|
|
1157
|
+
.where(
|
|
1158
|
+
(self.model_versions.c.name == name) & (self.model_versions.c.version == version),
|
|
1159
|
+
)
|
|
1160
|
+
.values(**values),
|
|
1161
|
+
)
|
|
1162
|
+
conn.commit()
|
|
1163
|
+
return existing.id
|
|
1164
|
+
else:
|
|
1165
|
+
result = conn.execute(insert(self.model_versions).values(**values))
|
|
1166
|
+
conn.commit()
|
|
1167
|
+
return result.lastrowid
|
|
1168
|
+
|
|
1169
|
+
def get_model_version(self, name: str, version: str) -> dict | None:
|
|
1170
|
+
"""Get a specific model version."""
|
|
1171
|
+
with self.engine.connect() as conn:
|
|
1172
|
+
stmt = select(self.model_versions).where(
|
|
1173
|
+
(self.model_versions.c.name == name) & (self.model_versions.c.version == version),
|
|
1174
|
+
)
|
|
1175
|
+
row = conn.execute(stmt).fetchone()
|
|
1176
|
+
if row:
|
|
1177
|
+
return self._row_to_model_version(row)
|
|
1178
|
+
return None
|
|
1179
|
+
|
|
1180
|
+
def _row_to_model_version(self, row) -> dict:
|
|
1181
|
+
"""Convert a row to a model version dict."""
|
|
1182
|
+
return {
|
|
1183
|
+
"id": row.id,
|
|
1184
|
+
"name": row.name,
|
|
1185
|
+
"version": row.version,
|
|
1186
|
+
"stage": row.stage,
|
|
1187
|
+
"framework": row.framework,
|
|
1188
|
+
"model_path": row.model_path,
|
|
1189
|
+
"metrics": json.loads(row.metrics) if row.metrics else {},
|
|
1190
|
+
"tags": json.loads(row.tags) if row.tags else {},
|
|
1191
|
+
"schema": json.loads(row.schema) if row.schema else {},
|
|
1192
|
+
"description": row.description,
|
|
1193
|
+
"author": row.author,
|
|
1194
|
+
"parent_version": row.parent_version,
|
|
1195
|
+
"created_at": row.created_at,
|
|
1196
|
+
"updated_at": row.updated_at,
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
def list_model_versions(self, name: str | None = None, stage: str | None = None) -> list[dict]:
|
|
1200
|
+
"""List model versions with optional filters."""
|
|
1201
|
+
with self.engine.connect() as conn:
|
|
1202
|
+
stmt = select(self.model_versions)
|
|
1203
|
+
|
|
1204
|
+
if name:
|
|
1205
|
+
stmt = stmt.where(self.model_versions.c.name == name)
|
|
1206
|
+
if stage:
|
|
1207
|
+
stmt = stmt.where(self.model_versions.c.stage == stage)
|
|
1208
|
+
|
|
1209
|
+
stmt = stmt.order_by(self.model_versions.c.created_at.desc())
|
|
1210
|
+
rows = conn.execute(stmt).fetchall()
|
|
1211
|
+
return [self._row_to_model_version(row) for row in rows]
|
|
1212
|
+
|
|
1213
|
+
def list_registered_models(self) -> list[str]:
|
|
1214
|
+
"""List all unique model names in the registry."""
|
|
1215
|
+
with self.engine.connect() as conn:
|
|
1216
|
+
stmt = select(self.model_versions.c.name).distinct()
|
|
1217
|
+
rows = conn.execute(stmt).fetchall()
|
|
1218
|
+
return [row[0] for row in rows]
|
|
1219
|
+
|
|
1220
|
+
def promote_model(self, name: str, version: str, to_stage: str) -> bool:
|
|
1221
|
+
"""Promote a model version to a different stage."""
|
|
1222
|
+
now = datetime.now(UTC).isoformat()
|
|
1223
|
+
with self.engine.connect() as conn:
|
|
1224
|
+
stmt = (
|
|
1225
|
+
update(self.model_versions)
|
|
1226
|
+
.where(
|
|
1227
|
+
(self.model_versions.c.name == name) & (self.model_versions.c.version == version),
|
|
1228
|
+
)
|
|
1229
|
+
.values(stage=to_stage, updated_at=now)
|
|
1230
|
+
)
|
|
1231
|
+
result = conn.execute(stmt)
|
|
1232
|
+
conn.commit()
|
|
1233
|
+
return result.rowcount > 0
|
|
1234
|
+
|
|
1235
|
+
def delete_model_version(self, name: str, version: str) -> bool:
|
|
1236
|
+
"""Delete a model version from the registry."""
|
|
1237
|
+
with self.engine.connect() as conn:
|
|
1238
|
+
stmt = delete(self.model_versions).where(
|
|
1239
|
+
(self.model_versions.c.name == name) & (self.model_versions.c.version == version),
|
|
1240
|
+
)
|
|
1241
|
+
result = conn.execute(stmt)
|
|
1242
|
+
conn.commit()
|
|
1243
|
+
return result.rowcount > 0
|
|
1244
|
+
|
|
1245
|
+
def get_latest_model_version(self, name: str, stage: str | None = None) -> dict | None:
|
|
1246
|
+
"""Get the latest version of a model, optionally filtered by stage."""
|
|
1247
|
+
with self.engine.connect() as conn:
|
|
1248
|
+
stmt = select(self.model_versions).where(self.model_versions.c.name == name)
|
|
1249
|
+
|
|
1250
|
+
if stage:
|
|
1251
|
+
stmt = stmt.where(self.model_versions.c.stage == stage)
|
|
1252
|
+
|
|
1253
|
+
stmt = stmt.order_by(self.model_versions.c.created_at.desc()).limit(1)
|
|
1254
|
+
row = conn.execute(stmt).fetchone()
|
|
1255
|
+
if row:
|
|
1256
|
+
return self._row_to_model_version(row)
|
|
1257
|
+
return None
|
|
1258
|
+
|
|
1259
|
+
# ========== Pipeline Template Methods ==========
|
|
1260
|
+
|
|
1261
|
+
def save_pipeline_template(
|
|
1262
|
+
self,
|
|
1263
|
+
template_id: str,
|
|
1264
|
+
name: str,
|
|
1265
|
+
definition: dict,
|
|
1266
|
+
description: str = "",
|
|
1267
|
+
tags: list[str] | None = None,
|
|
1268
|
+
author: str | None = None,
|
|
1269
|
+
category: str | None = None,
|
|
1270
|
+
is_public: bool = False,
|
|
1271
|
+
) -> None:
|
|
1272
|
+
"""Save a pipeline template."""
|
|
1273
|
+
now = datetime.now(UTC).isoformat()
|
|
1274
|
+
with self.engine.connect() as conn:
|
|
1275
|
+
stmt = select(self.pipeline_templates).where(
|
|
1276
|
+
self.pipeline_templates.c.template_id == template_id,
|
|
1277
|
+
)
|
|
1278
|
+
existing = conn.execute(stmt).fetchone()
|
|
1279
|
+
|
|
1280
|
+
values = {
|
|
1281
|
+
"template_id": template_id,
|
|
1282
|
+
"name": name,
|
|
1283
|
+
"description": description,
|
|
1284
|
+
"definition": json.dumps(definition),
|
|
1285
|
+
"tags": json.dumps(tags or []),
|
|
1286
|
+
"author": author,
|
|
1287
|
+
"category": category,
|
|
1288
|
+
"is_public": 1 if is_public else 0,
|
|
1289
|
+
"updated_at": now,
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if existing:
|
|
1293
|
+
conn.execute(
|
|
1294
|
+
update(self.pipeline_templates)
|
|
1295
|
+
.where(self.pipeline_templates.c.template_id == template_id)
|
|
1296
|
+
.values(**values),
|
|
1297
|
+
)
|
|
1298
|
+
else:
|
|
1299
|
+
conn.execute(insert(self.pipeline_templates).values(**values))
|
|
1300
|
+
|
|
1301
|
+
conn.commit()
|
|
1302
|
+
|
|
1303
|
+
def get_pipeline_template(self, template_id: str) -> dict | None:
|
|
1304
|
+
"""Get a pipeline template by ID."""
|
|
1305
|
+
with self.engine.connect() as conn:
|
|
1306
|
+
stmt = select(self.pipeline_templates).where(
|
|
1307
|
+
self.pipeline_templates.c.template_id == template_id,
|
|
1308
|
+
)
|
|
1309
|
+
row = conn.execute(stmt).fetchone()
|
|
1310
|
+
if row:
|
|
1311
|
+
return self._row_to_template(row)
|
|
1312
|
+
return None
|
|
1313
|
+
|
|
1314
|
+
def _row_to_template(self, row) -> dict:
|
|
1315
|
+
"""Convert a row to a template dict."""
|
|
1316
|
+
return {
|
|
1317
|
+
"template_id": row.template_id,
|
|
1318
|
+
"name": row.name,
|
|
1319
|
+
"description": row.description,
|
|
1320
|
+
"definition": json.loads(row.definition) if row.definition else {},
|
|
1321
|
+
"tags": json.loads(row.tags) if row.tags else [],
|
|
1322
|
+
"author": row.author,
|
|
1323
|
+
"category": row.category,
|
|
1324
|
+
"is_public": bool(row.is_public),
|
|
1325
|
+
"created_at": row.created_at,
|
|
1326
|
+
"updated_at": row.updated_at,
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
def list_pipeline_templates(
|
|
1330
|
+
self,
|
|
1331
|
+
category: str | None = None,
|
|
1332
|
+
include_public: bool = True,
|
|
1333
|
+
author: str | None = None,
|
|
1334
|
+
) -> list[dict]:
|
|
1335
|
+
"""List pipeline templates with optional filters."""
|
|
1336
|
+
with self.engine.connect() as conn:
|
|
1337
|
+
stmt = select(self.pipeline_templates)
|
|
1338
|
+
|
|
1339
|
+
if category:
|
|
1340
|
+
stmt = stmt.where(self.pipeline_templates.c.category == category)
|
|
1341
|
+
if author:
|
|
1342
|
+
stmt = stmt.where(self.pipeline_templates.c.author == author)
|
|
1343
|
+
if not include_public:
|
|
1344
|
+
stmt = stmt.where(self.pipeline_templates.c.is_public == 0)
|
|
1345
|
+
|
|
1346
|
+
stmt = stmt.order_by(self.pipeline_templates.c.created_at.desc())
|
|
1347
|
+
rows = conn.execute(stmt).fetchall()
|
|
1348
|
+
return [self._row_to_template(row) for row in rows]
|
|
1349
|
+
|
|
1350
|
+
def delete_pipeline_template(self, template_id: str) -> bool:
|
|
1351
|
+
"""Delete a pipeline template."""
|
|
1352
|
+
with self.engine.connect() as conn:
|
|
1353
|
+
stmt = delete(self.pipeline_templates).where(
|
|
1354
|
+
self.pipeline_templates.c.template_id == template_id,
|
|
1355
|
+
)
|
|
1356
|
+
result = conn.execute(stmt)
|
|
1357
|
+
conn.commit()
|
|
1358
|
+
return result.rowcount > 0
|
flowyml/tracking/experiment.py
CHANGED
|
@@ -42,8 +42,12 @@ class Experiment:
|
|
|
42
42
|
description: str = "",
|
|
43
43
|
tags: dict[str, str] | None = None,
|
|
44
44
|
parameters: dict[str, Any] | None = None,
|
|
45
|
-
experiment_dir: str =
|
|
45
|
+
experiment_dir: str | None = None,
|
|
46
|
+
metadata_store: Any = None,
|
|
46
47
|
):
|
|
48
|
+
from flowyml.utils.config import get_config
|
|
49
|
+
from flowyml.storage.sql import SQLMetadataStore
|
|
50
|
+
|
|
47
51
|
self.name = name
|
|
48
52
|
self.description = description
|
|
49
53
|
self.config = ExperimentConfig(
|
|
@@ -54,13 +58,12 @@ class Experiment:
|
|
|
54
58
|
)
|
|
55
59
|
|
|
56
60
|
# Storage
|
|
57
|
-
|
|
61
|
+
base_dir = Path(experiment_dir) if experiment_dir else get_config().experiments_dir
|
|
62
|
+
self.experiment_dir = base_dir / name
|
|
58
63
|
self.experiment_dir.mkdir(parents=True, exist_ok=True)
|
|
59
64
|
|
|
60
65
|
# Metadata store for UI
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
self.metadata_store = SQLiteMetadataStore()
|
|
66
|
+
self.metadata_store = metadata_store or SQLMetadataStore(db_path=str(get_config().metadata_db))
|
|
64
67
|
|
|
65
68
|
# Save experiment to DB
|
|
66
69
|
self.metadata_store.save_experiment(
|
flowyml/ui/backend/Dockerfile
CHANGED
|
@@ -1,31 +1,102 @@
|
|
|
1
|
-
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# FlowyML Backend - Multi-Stage Optimized Dockerfile
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# This Dockerfile uses multi-stage builds to create a minimal, secure, and
|
|
5
|
+
# optimized production image. Build artifacts and dev tools are excluded.
|
|
6
|
+
# =============================================================================
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
# -----------------------------------------------------------------------------
|
|
9
|
+
# Stage 1: Builder - Install dependencies and build the application
|
|
10
|
+
# -----------------------------------------------------------------------------
|
|
11
|
+
FROM python:3.11-slim AS builder
|
|
12
|
+
|
|
13
|
+
# Set environment variables for Python
|
|
14
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
15
|
+
PYTHONUNBUFFERED=1 \
|
|
16
|
+
PIP_NO_CACHE_DIR=1 \
|
|
17
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
18
|
+
POETRY_VERSION=1.8.2 \
|
|
19
|
+
POETRY_HOME="/opt/poetry" \
|
|
20
|
+
POETRY_VIRTUALENVS_CREATE=false \
|
|
21
|
+
POETRY_NO_INTERACTION=1
|
|
22
|
+
|
|
23
|
+
# Add Poetry to PATH
|
|
24
|
+
ENV PATH="$POETRY_HOME/bin:$PATH"
|
|
4
25
|
|
|
5
|
-
|
|
6
|
-
|
|
26
|
+
WORKDIR /build
|
|
27
|
+
|
|
28
|
+
# Install build dependencies
|
|
29
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
7
30
|
build-essential \
|
|
8
31
|
curl \
|
|
32
|
+
libpq-dev \
|
|
9
33
|
&& rm -rf /var/lib/apt/lists/*
|
|
10
34
|
|
|
11
|
-
# Install
|
|
12
|
-
RUN curl -sSL https://install.python-poetry.org | python3 -
|
|
13
|
-
|
|
35
|
+
# Install Poetry
|
|
36
|
+
RUN curl -sSL https://install.python-poetry.org | python3 - \
|
|
37
|
+
&& chmod +x $POETRY_HOME/bin/poetry
|
|
14
38
|
|
|
15
|
-
# Copy
|
|
39
|
+
# Copy only dependency files first (better layer caching)
|
|
16
40
|
COPY pyproject.toml poetry.lock ./
|
|
41
|
+
|
|
42
|
+
# Install dependencies only (no dev dependencies for production)
|
|
43
|
+
RUN poetry install --only main --no-root --no-directory
|
|
44
|
+
|
|
45
|
+
# Copy application source
|
|
17
46
|
COPY README.md ./
|
|
18
47
|
COPY flowyml ./flowyml
|
|
19
48
|
|
|
20
|
-
# Install
|
|
21
|
-
RUN poetry
|
|
22
|
-
|
|
49
|
+
# Install the application package
|
|
50
|
+
RUN poetry install --only main
|
|
51
|
+
|
|
52
|
+
# -----------------------------------------------------------------------------
|
|
53
|
+
# Stage 2: Runtime - Minimal production image
|
|
54
|
+
# -----------------------------------------------------------------------------
|
|
55
|
+
FROM python:3.11-slim AS runtime
|
|
23
56
|
|
|
24
|
-
#
|
|
25
|
-
RUN
|
|
57
|
+
# Security: Run as non-root user
|
|
58
|
+
RUN groupadd --gid 1000 flowyml \
|
|
59
|
+
&& useradd --uid 1000 --gid 1000 --shell /bin/bash --create-home flowyml
|
|
60
|
+
|
|
61
|
+
# Set environment variables
|
|
62
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
63
|
+
PYTHONUNBUFFERED=1 \
|
|
64
|
+
PYTHONPATH="/app" \
|
|
65
|
+
# Application defaults
|
|
66
|
+
FLOWYML_ENV=production \
|
|
67
|
+
FLOWYML_HOST=0.0.0.0 \
|
|
68
|
+
FLOWYML_PORT=8080
|
|
69
|
+
|
|
70
|
+
WORKDIR /app
|
|
71
|
+
|
|
72
|
+
# Install runtime dependencies only (no build tools)
|
|
73
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
74
|
+
libpq5 \
|
|
75
|
+
curl \
|
|
76
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
77
|
+
&& apt-get clean
|
|
78
|
+
|
|
79
|
+
# Copy installed packages from builder
|
|
80
|
+
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
|
81
|
+
COPY --from=builder /usr/local/bin /usr/local/bin
|
|
82
|
+
|
|
83
|
+
# Copy application code
|
|
84
|
+
COPY --from=builder /build/flowyml ./flowyml
|
|
85
|
+
COPY --from=builder /build/README.md ./
|
|
86
|
+
|
|
87
|
+
# Create data directories with correct permissions
|
|
88
|
+
RUN mkdir -p /app/data /app/artifacts \
|
|
89
|
+
&& chown -R flowyml:flowyml /app
|
|
90
|
+
|
|
91
|
+
# Switch to non-root user
|
|
92
|
+
USER flowyml
|
|
26
93
|
|
|
27
94
|
# Expose port
|
|
28
|
-
EXPOSE
|
|
95
|
+
EXPOSE 8080
|
|
96
|
+
|
|
97
|
+
# Health check
|
|
98
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
99
|
+
CMD curl -f http://localhost:8080/api/health || exit 1
|
|
29
100
|
|
|
30
|
-
# Run the application
|
|
31
|
-
CMD ["uvicorn", "flowyml.ui.backend.main:app", "--host", "0.0.0.0", "--port", "
|
|
101
|
+
# Run the application with uvicorn
|
|
102
|
+
CMD ["uvicorn", "flowyml.ui.backend.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
flowyml/ui/backend/auth.py
CHANGED
|
@@ -23,8 +23,18 @@ class TokenManager:
|
|
|
23
23
|
def _load_tokens(self) -> None:
|
|
24
24
|
"""Load tokens from file."""
|
|
25
25
|
if self.tokens_file.exists():
|
|
26
|
-
|
|
27
|
-
self.
|
|
26
|
+
try:
|
|
27
|
+
with open(self.tokens_file) as f:
|
|
28
|
+
content = f.read().strip()
|
|
29
|
+
if not content:
|
|
30
|
+
self.tokens = {}
|
|
31
|
+
self._save_tokens()
|
|
32
|
+
else:
|
|
33
|
+
self.tokens = json.loads(content)
|
|
34
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
35
|
+
print(f"Warning: Failed to load tokens from {self.tokens_file}: {e}")
|
|
36
|
+
self.tokens = {}
|
|
37
|
+
self._save_tokens()
|
|
28
38
|
else:
|
|
29
39
|
self.tokens = {}
|
|
30
40
|
self._save_tokens()
|