flowyml 1.7.2__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.
Files changed (126) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/metrics.py +5 -0
  3. flowyml/cli/main.py +709 -0
  4. flowyml/cli/stack_cli.py +138 -25
  5. flowyml/core/__init__.py +17 -0
  6. flowyml/core/executor.py +161 -26
  7. flowyml/core/image_builder.py +129 -0
  8. flowyml/core/log_streamer.py +227 -0
  9. flowyml/core/orchestrator.py +22 -2
  10. flowyml/core/pipeline.py +34 -10
  11. flowyml/core/routing.py +558 -0
  12. flowyml/core/step.py +9 -1
  13. flowyml/core/step_grouping.py +49 -35
  14. flowyml/core/types.py +407 -0
  15. flowyml/monitoring/alerts.py +10 -0
  16. flowyml/monitoring/notifications.py +104 -25
  17. flowyml/monitoring/slack_blocks.py +323 -0
  18. flowyml/plugins/__init__.py +251 -0
  19. flowyml/plugins/alerters/__init__.py +1 -0
  20. flowyml/plugins/alerters/slack.py +168 -0
  21. flowyml/plugins/base.py +752 -0
  22. flowyml/plugins/config.py +478 -0
  23. flowyml/plugins/deployers/__init__.py +22 -0
  24. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  25. flowyml/plugins/deployers/sagemaker.py +306 -0
  26. flowyml/plugins/deployers/vertex.py +290 -0
  27. flowyml/plugins/integration.py +369 -0
  28. flowyml/plugins/manager.py +510 -0
  29. flowyml/plugins/model_registries/__init__.py +22 -0
  30. flowyml/plugins/model_registries/mlflow.py +159 -0
  31. flowyml/plugins/model_registries/sagemaker.py +489 -0
  32. flowyml/plugins/model_registries/vertex.py +386 -0
  33. flowyml/plugins/orchestrators/__init__.py +13 -0
  34. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  35. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  36. flowyml/plugins/registries/__init__.py +13 -0
  37. flowyml/plugins/registries/ecr.py +321 -0
  38. flowyml/plugins/registries/gcr.py +313 -0
  39. flowyml/plugins/registry.py +454 -0
  40. flowyml/plugins/stack.py +494 -0
  41. flowyml/plugins/stack_config.py +537 -0
  42. flowyml/plugins/stores/__init__.py +13 -0
  43. flowyml/plugins/stores/gcs.py +460 -0
  44. flowyml/plugins/stores/s3.py +453 -0
  45. flowyml/plugins/trackers/__init__.py +11 -0
  46. flowyml/plugins/trackers/mlflow.py +316 -0
  47. flowyml/plugins/validators/__init__.py +3 -0
  48. flowyml/plugins/validators/deepchecks.py +119 -0
  49. flowyml/registry/__init__.py +2 -1
  50. flowyml/registry/model_environment.py +109 -0
  51. flowyml/registry/model_registry.py +241 -96
  52. flowyml/serving/__init__.py +17 -0
  53. flowyml/serving/model_server.py +628 -0
  54. flowyml/stacks/__init__.py +60 -0
  55. flowyml/stacks/aws.py +93 -0
  56. flowyml/stacks/base.py +62 -0
  57. flowyml/stacks/components.py +12 -0
  58. flowyml/stacks/gcp.py +44 -9
  59. flowyml/stacks/plugins.py +115 -0
  60. flowyml/stacks/registry.py +2 -1
  61. flowyml/storage/sql.py +401 -12
  62. flowyml/tracking/experiment.py +8 -5
  63. flowyml/ui/backend/Dockerfile +87 -16
  64. flowyml/ui/backend/auth.py +12 -2
  65. flowyml/ui/backend/main.py +149 -5
  66. flowyml/ui/backend/routers/ai_context.py +226 -0
  67. flowyml/ui/backend/routers/assets.py +23 -4
  68. flowyml/ui/backend/routers/auth.py +96 -0
  69. flowyml/ui/backend/routers/deployments.py +660 -0
  70. flowyml/ui/backend/routers/model_explorer.py +597 -0
  71. flowyml/ui/backend/routers/plugins.py +103 -51
  72. flowyml/ui/backend/routers/projects.py +91 -8
  73. flowyml/ui/backend/routers/runs.py +20 -1
  74. flowyml/ui/backend/routers/schedules.py +22 -17
  75. flowyml/ui/backend/routers/templates.py +319 -0
  76. flowyml/ui/backend/routers/websocket.py +2 -2
  77. flowyml/ui/frontend/Dockerfile +55 -6
  78. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  79. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  80. flowyml/ui/frontend/dist/index.html +2 -2
  81. flowyml/ui/frontend/dist/logo.png +0 -0
  82. flowyml/ui/frontend/nginx.conf +65 -4
  83. flowyml/ui/frontend/package-lock.json +1404 -74
  84. flowyml/ui/frontend/package.json +3 -0
  85. flowyml/ui/frontend/public/logo.png +0 -0
  86. flowyml/ui/frontend/src/App.jsx +10 -7
  87. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  88. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  89. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  90. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  91. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  92. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  93. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
  94. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  95. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  96. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
  97. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  98. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  99. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  100. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  101. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  102. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  103. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  104. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  105. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  106. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  107. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  108. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  109. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  110. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  111. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  112. flowyml/ui/frontend/src/router/index.jsx +47 -20
  113. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  114. flowyml/ui/server_manager.py +5 -5
  115. flowyml/ui/utils.py +157 -39
  116. flowyml/utils/config.py +37 -15
  117. flowyml/utils/model_introspection.py +123 -0
  118. flowyml/utils/observability.py +30 -0
  119. flowyml-1.8.0.dist-info/METADATA +174 -0
  120. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
  121. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  122. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
  123. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
  124. flowyml-1.7.2.dist-info/METADATA +0 -477
  125. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  126. {flowyml-1.7.2.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
- UTC = UTC
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
- select,
25
- insert,
26
- update,
20
+ Integer,
21
+ MetaData,
22
+ String,
23
+ Table,
24
+ Text,
25
+ create_engine,
27
26
  delete,
28
27
  func,
29
- text,
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
@@ -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 = ".flowyml/experiments",
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
- self.experiment_dir = Path(experiment_dir) / name
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
- from flowyml.storage.metadata import SQLiteMetadataStore
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(
@@ -1,31 +1,102 @@
1
- FROM python:3.10-slim
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
- WORKDIR /app
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
- # Install system dependencies
6
- RUN apt-get update && apt-get install -y \
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 poetry
12
- RUN curl -sSL https://install.python-poetry.org | python3 -
13
- ENV PATH="/root/.local/bin:$PATH"
35
+ # Install Poetry
36
+ RUN curl -sSL https://install.python-poetry.org | python3 - \
37
+ && chmod +x $POETRY_HOME/bin/poetry
14
38
 
15
- # Copy project files
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 dependencies
21
- RUN poetry config virtualenvs.create false \
22
- && poetry install --no-interaction --no-ansi --no-root
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
- # Install the package itself
25
- RUN pip install .
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 8000
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", "8000"]
101
+ # Run the application with uvicorn
102
+ CMD ["uvicorn", "flowyml.ui.backend.main:app", "--host", "0.0.0.0", "--port", "8080"]
@@ -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
- with open(self.tokens_file) as f:
27
- self.tokens = json.load(f)
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()