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
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
"""Model registry for version management and deployment.
|
|
1
|
+
"""Model registry for version management and deployment.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This module provides SQL-backed model registry capabilities for managing
|
|
4
|
+
model versions, stages, and metadata in a production-safe manner.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
5
10
|
from datetime import datetime
|
|
6
11
|
from enum import Enum
|
|
7
12
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from flowyml.assets.base import Asset
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
class ModelStage(str, Enum):
|
|
@@ -31,6 +38,7 @@ class ModelVersion:
|
|
|
31
38
|
framework: str
|
|
32
39
|
metrics: dict[str, float] = field(default_factory=dict)
|
|
33
40
|
tags: dict[str, str] = field(default_factory=dict)
|
|
41
|
+
schema: dict[str, Any] = field(default_factory=dict)
|
|
34
42
|
description: str = ""
|
|
35
43
|
author: str | None = None
|
|
36
44
|
parent_version: str | None = None
|
|
@@ -44,13 +52,21 @@ class ModelVersion:
|
|
|
44
52
|
@classmethod
|
|
45
53
|
def from_dict(cls, data: dict[str, Any]) -> "ModelVersion":
|
|
46
54
|
"""Create from dictionary."""
|
|
47
|
-
data
|
|
55
|
+
data = data.copy()
|
|
56
|
+
# Handle stage conversion
|
|
57
|
+
if isinstance(data.get("stage"), str):
|
|
58
|
+
data["stage"] = ModelStage(data["stage"])
|
|
59
|
+
# Remove SQL-specific fields
|
|
60
|
+
data.pop("id", None)
|
|
48
61
|
return cls(**data)
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
class ModelRegistry:
|
|
52
65
|
"""Registry for managing model versions and deployments.
|
|
53
66
|
|
|
67
|
+
This registry uses SQL storage for production-safe concurrent access.
|
|
68
|
+
Model files are still stored on the filesystem, but metadata is in the database.
|
|
69
|
+
|
|
54
70
|
Example:
|
|
55
71
|
```python
|
|
56
72
|
from flowyml import ModelRegistry
|
|
@@ -78,30 +94,32 @@ class ModelRegistry:
|
|
|
78
94
|
```
|
|
79
95
|
"""
|
|
80
96
|
|
|
81
|
-
def __init__(
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
registry_path: str = ".flowyml/model_registry",
|
|
100
|
+
db_url: str | None = None,
|
|
101
|
+
):
|
|
82
102
|
"""Initialize model registry.
|
|
83
103
|
|
|
84
104
|
Args:
|
|
85
|
-
registry_path: Path to
|
|
105
|
+
registry_path: Path to model file storage
|
|
106
|
+
db_url: Database URL for metadata storage (uses env var if not provided)
|
|
86
107
|
"""
|
|
87
108
|
self.registry_path = Path(registry_path)
|
|
88
109
|
self.registry_path.mkdir(parents=True, exist_ok=True)
|
|
89
|
-
self.metadata_file = self.registry_path / "registry.json"
|
|
90
|
-
self._metadata: dict[str, list[dict]] = {}
|
|
91
|
-
self._load_metadata()
|
|
92
|
-
|
|
93
|
-
def _load_metadata(self) -> None:
|
|
94
|
-
"""Load registry metadata from disk."""
|
|
95
|
-
if self.metadata_file.exists():
|
|
96
|
-
with open(self.metadata_file) as f:
|
|
97
|
-
self._metadata = json.load(f)
|
|
98
|
-
else:
|
|
99
|
-
self._metadata = {}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
# Initialize SQL storage for metadata
|
|
112
|
+
self._db_url = db_url or os.getenv("FLOWYML_DATABASE_URL")
|
|
113
|
+
self._store = None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def _metadata_store(self):
|
|
117
|
+
"""Lazy-load the metadata store."""
|
|
118
|
+
if self._store is None:
|
|
119
|
+
from flowyml.storage.sql import SQLMetadataStore
|
|
120
|
+
|
|
121
|
+
self._store = SQLMetadataStore(db_url=self._db_url)
|
|
122
|
+
return self._store
|
|
105
123
|
|
|
106
124
|
def register(
|
|
107
125
|
self,
|
|
@@ -112,6 +130,7 @@ class ModelRegistry:
|
|
|
112
130
|
stage: ModelStage = ModelStage.DEVELOPMENT,
|
|
113
131
|
metrics: dict[str, float] | None = None,
|
|
114
132
|
tags: dict[str, str] | None = None,
|
|
133
|
+
schema: dict[str, Any] | None = None,
|
|
115
134
|
description: str = "",
|
|
116
135
|
author: str | None = None,
|
|
117
136
|
parent_version: str | None = None,
|
|
@@ -126,6 +145,7 @@ class ModelRegistry:
|
|
|
126
145
|
stage: Deployment stage
|
|
127
146
|
metrics: Model metrics
|
|
128
147
|
tags: Model tags
|
|
148
|
+
schema: Optional explicit schema (overrides introspection)
|
|
129
149
|
description: Model description
|
|
130
150
|
author: Model author
|
|
131
151
|
parent_version: Parent version if this is an update
|
|
@@ -137,10 +157,18 @@ class ModelRegistry:
|
|
|
137
157
|
ValueError: If version already exists
|
|
138
158
|
"""
|
|
139
159
|
# Check if version already exists
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
existing = self._metadata_store.get_model_version(name, version)
|
|
161
|
+
if existing:
|
|
162
|
+
raise ValueError(f"Version {version} already exists for model {name}")
|
|
163
|
+
|
|
164
|
+
# Introspect model schema if not provided
|
|
165
|
+
from flowyml.utils.model_introspection import introspect_model
|
|
166
|
+
|
|
167
|
+
inferred_schema = introspect_model(model, framework)
|
|
168
|
+
# Merge inferred schema with provided schema (provided takes precedence)
|
|
169
|
+
final_schema = inferred_schema
|
|
170
|
+
if schema:
|
|
171
|
+
final_schema.update(schema)
|
|
144
172
|
|
|
145
173
|
# Create model directory
|
|
146
174
|
model_dir = self.registry_path / name / version
|
|
@@ -152,7 +180,23 @@ class ModelRegistry:
|
|
|
152
180
|
|
|
153
181
|
# Create version metadata
|
|
154
182
|
now = datetime.now().isoformat()
|
|
155
|
-
|
|
183
|
+
|
|
184
|
+
# Save to SQL database
|
|
185
|
+
self._metadata_store.save_model_version(
|
|
186
|
+
name=name,
|
|
187
|
+
version=version,
|
|
188
|
+
stage=stage.value,
|
|
189
|
+
framework=framework,
|
|
190
|
+
model_path=str(model_path),
|
|
191
|
+
metrics=metrics,
|
|
192
|
+
tags=tags,
|
|
193
|
+
schema=final_schema,
|
|
194
|
+
description=description,
|
|
195
|
+
author=author,
|
|
196
|
+
parent_version=parent_version,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return ModelVersion(
|
|
156
200
|
name=name,
|
|
157
201
|
version=version,
|
|
158
202
|
stage=stage,
|
|
@@ -162,20 +206,12 @@ class ModelRegistry:
|
|
|
162
206
|
framework=framework,
|
|
163
207
|
metrics=metrics or {},
|
|
164
208
|
tags=tags or {},
|
|
209
|
+
schema=final_schema,
|
|
165
210
|
description=description,
|
|
166
211
|
author=author,
|
|
167
212
|
parent_version=parent_version,
|
|
168
213
|
)
|
|
169
214
|
|
|
170
|
-
# Add to metadata
|
|
171
|
-
if name not in self._metadata:
|
|
172
|
-
self._metadata[name] = []
|
|
173
|
-
|
|
174
|
-
self._metadata[name].append(model_version.to_dict())
|
|
175
|
-
self._save_metadata()
|
|
176
|
-
|
|
177
|
-
return model_version
|
|
178
|
-
|
|
179
215
|
def _save_model(self, model: Any, path: Path, framework: str) -> None:
|
|
180
216
|
"""Save model using appropriate method.
|
|
181
217
|
|
|
@@ -226,7 +262,7 @@ class ModelRegistry:
|
|
|
226
262
|
import pickle
|
|
227
263
|
|
|
228
264
|
with open(path, "rb") as f:
|
|
229
|
-
return pickle.load(f)
|
|
265
|
+
return pickle.load(f) # noqa: S301
|
|
230
266
|
|
|
231
267
|
def get_version(self, name: str, version: str) -> ModelVersion | None:
|
|
232
268
|
"""Get specific model version.
|
|
@@ -238,13 +274,9 @@ class ModelRegistry:
|
|
|
238
274
|
Returns:
|
|
239
275
|
ModelVersion or None if not found
|
|
240
276
|
"""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for v in self._metadata[name]:
|
|
245
|
-
if v["version"] == version:
|
|
246
|
-
return ModelVersion.from_dict(v)
|
|
247
|
-
|
|
277
|
+
data = self._metadata_store.get_model_version(name, version)
|
|
278
|
+
if data:
|
|
279
|
+
return ModelVersion.from_dict(data)
|
|
248
280
|
return None
|
|
249
281
|
|
|
250
282
|
def list_versions(self, name: str) -> list[ModelVersion]:
|
|
@@ -256,10 +288,8 @@ class ModelRegistry:
|
|
|
256
288
|
Returns:
|
|
257
289
|
List of ModelVersion instances
|
|
258
290
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return [ModelVersion.from_dict(v) for v in self._metadata[name]]
|
|
291
|
+
versions = self._metadata_store.list_model_versions(name=name)
|
|
292
|
+
return [ModelVersion.from_dict(v) for v in versions]
|
|
263
293
|
|
|
264
294
|
def list_models(self) -> list[str]:
|
|
265
295
|
"""List all registered models.
|
|
@@ -267,7 +297,7 @@ class ModelRegistry:
|
|
|
267
297
|
Returns:
|
|
268
298
|
List of model names
|
|
269
299
|
"""
|
|
270
|
-
return
|
|
300
|
+
return self._metadata_store.list_registered_models()
|
|
271
301
|
|
|
272
302
|
def get_latest_version(self, name: str, stage: ModelStage | None = None) -> ModelVersion | None:
|
|
273
303
|
"""Get latest version of a model.
|
|
@@ -279,17 +309,11 @@ class ModelRegistry:
|
|
|
279
309
|
Returns:
|
|
280
310
|
Latest ModelVersion or None
|
|
281
311
|
"""
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if not versions:
|
|
288
|
-
return None
|
|
289
|
-
|
|
290
|
-
# Sort by created_at
|
|
291
|
-
versions.sort(key=lambda v: v.created_at, reverse=True)
|
|
292
|
-
return versions[0]
|
|
312
|
+
stage_str = stage.value if stage else None
|
|
313
|
+
data = self._metadata_store.get_latest_model_version(name, stage=stage_str)
|
|
314
|
+
if data:
|
|
315
|
+
return ModelVersion.from_dict(data)
|
|
316
|
+
return None
|
|
293
317
|
|
|
294
318
|
def load(
|
|
295
319
|
self,
|
|
@@ -341,14 +365,10 @@ class ModelRegistry:
|
|
|
341
365
|
if not model_version:
|
|
342
366
|
raise ValueError(f"Model {name} version {version} not found")
|
|
343
367
|
|
|
344
|
-
# Update stage in
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
v["updated_at"] = datetime.now().isoformat()
|
|
349
|
-
break
|
|
350
|
-
|
|
351
|
-
self._save_metadata()
|
|
368
|
+
# Update stage in database
|
|
369
|
+
success = self._metadata_store.promote_model(name, version, to_stage.value)
|
|
370
|
+
if not success:
|
|
371
|
+
raise ValueError(f"Failed to promote model {name} version {version}")
|
|
352
372
|
|
|
353
373
|
return self.get_version(name, version)
|
|
354
374
|
|
|
@@ -392,16 +412,14 @@ class ModelRegistry:
|
|
|
392
412
|
if model_version.stage == ModelStage.PRODUCTION:
|
|
393
413
|
raise ValueError("Cannot delete production model. Demote first.")
|
|
394
414
|
|
|
395
|
-
#
|
|
396
|
-
self.
|
|
415
|
+
# Delete from database
|
|
416
|
+
self._metadata_store.delete_model_version(name, version)
|
|
397
417
|
|
|
398
418
|
# Delete model files
|
|
399
419
|
model_dir = Path(model_version.model_path).parent
|
|
400
420
|
if model_dir.exists():
|
|
401
421
|
shutil.rmtree(model_dir)
|
|
402
422
|
|
|
403
|
-
self._save_metadata()
|
|
404
|
-
|
|
405
423
|
def compare_versions(
|
|
406
424
|
self,
|
|
407
425
|
name: str,
|
|
@@ -447,26 +465,23 @@ class ModelRegistry:
|
|
|
447
465
|
Returns:
|
|
448
466
|
List of matching ModelVersion instances
|
|
449
467
|
"""
|
|
450
|
-
|
|
468
|
+
stage_str = stage.value if stage else None
|
|
469
|
+
all_versions = self._metadata_store.list_model_versions(stage=stage_str)
|
|
451
470
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
471
|
+
results = []
|
|
472
|
+
for version_dict in all_versions:
|
|
473
|
+
version = ModelVersion.from_dict(version_dict)
|
|
455
474
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
475
|
+
# Check tags
|
|
476
|
+
if tags and not all(version.tags.get(k) == v for k, v in tags.items()):
|
|
477
|
+
continue
|
|
459
478
|
|
|
460
|
-
|
|
461
|
-
|
|
479
|
+
# Check metrics
|
|
480
|
+
if min_metrics:
|
|
481
|
+
if not all(version.metrics.get(k, float("-inf")) >= v for k, v in min_metrics.items()):
|
|
462
482
|
continue
|
|
463
483
|
|
|
464
|
-
|
|
465
|
-
if min_metrics:
|
|
466
|
-
if not all(version.metrics.get(k, float("-inf")) >= v for k, v in min_metrics.items()):
|
|
467
|
-
continue
|
|
468
|
-
|
|
469
|
-
results.append(version)
|
|
484
|
+
results.append(version)
|
|
470
485
|
|
|
471
486
|
return results
|
|
472
487
|
|
|
@@ -476,16 +491,146 @@ class ModelRegistry:
|
|
|
476
491
|
Returns:
|
|
477
492
|
Dictionary with statistics
|
|
478
493
|
"""
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
494
|
+
all_versions = self._metadata_store.list_model_versions()
|
|
495
|
+
models = set()
|
|
482
496
|
stage_counts = {stage.value: 0 for stage in ModelStage}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
497
|
+
|
|
498
|
+
for v in all_versions:
|
|
499
|
+
models.add(v["name"])
|
|
500
|
+
stage_counts[v["stage"]] += 1
|
|
486
501
|
|
|
487
502
|
return {
|
|
488
|
-
"total_models":
|
|
489
|
-
"total_versions":
|
|
503
|
+
"total_models": len(models),
|
|
504
|
+
"total_versions": len(all_versions),
|
|
490
505
|
"by_stage": stage_counts,
|
|
491
506
|
}
|
|
507
|
+
|
|
508
|
+
# ========== Asset Integration Methods ==========
|
|
509
|
+
|
|
510
|
+
def register_asset(
|
|
511
|
+
self,
|
|
512
|
+
model_asset: "Asset",
|
|
513
|
+
version: str | None = None,
|
|
514
|
+
stage: ModelStage = ModelStage.DEVELOPMENT,
|
|
515
|
+
description: str = "",
|
|
516
|
+
author: str | None = None,
|
|
517
|
+
capture_environment: bool = False,
|
|
518
|
+
) -> ModelVersion:
|
|
519
|
+
"""Register a Model asset directly to the registry.
|
|
520
|
+
|
|
521
|
+
This method leverages the auto-extracted metadata from Model assets,
|
|
522
|
+
making registration simpler and more consistent.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
model_asset: A Model asset (from flowyml.assets.model)
|
|
526
|
+
version: Version string (defaults to asset's version)
|
|
527
|
+
stage: Deployment stage
|
|
528
|
+
description: Model description
|
|
529
|
+
author: Model author
|
|
530
|
+
capture_environment: Whether to capture Python environment
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
ModelVersion instance
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
>>> from flowyml import Model, ModelRegistry
|
|
537
|
+
>>> model_asset = Model.create(data=trained_model, name="classifier")
|
|
538
|
+
>>> registry = ModelRegistry()
|
|
539
|
+
>>> registry.register_asset(model_asset, version="v1.0.0")
|
|
540
|
+
"""
|
|
541
|
+
# Get version from asset if not provided
|
|
542
|
+
version = version or model_asset.version
|
|
543
|
+
|
|
544
|
+
# Extract metadata from asset
|
|
545
|
+
properties = model_asset.properties if hasattr(model_asset, "properties") else {}
|
|
546
|
+
tags = model_asset.tags if hasattr(model_asset, "tags") else {}
|
|
547
|
+
|
|
548
|
+
# Get framework from auto-extracted properties
|
|
549
|
+
framework = properties.get("framework", "unknown")
|
|
550
|
+
|
|
551
|
+
# Extract metrics from properties (common keys)
|
|
552
|
+
metrics = {}
|
|
553
|
+
for key in ["accuracy", "f1", "precision", "recall", "loss", "auc", "mse", "mae"]:
|
|
554
|
+
if key in properties:
|
|
555
|
+
metrics[key] = properties[key]
|
|
556
|
+
|
|
557
|
+
# Capture environment if requested
|
|
558
|
+
if capture_environment:
|
|
559
|
+
from flowyml.registry.model_environment import ModelEnvironment
|
|
560
|
+
|
|
561
|
+
env = ModelEnvironment.from_current()
|
|
562
|
+
tags["python_version"] = env.python_version
|
|
563
|
+
tags["platform"] = env.platform
|
|
564
|
+
properties["environment"] = env.to_dict()
|
|
565
|
+
|
|
566
|
+
return self.register(
|
|
567
|
+
model=model_asset.data,
|
|
568
|
+
name=model_asset.name,
|
|
569
|
+
version=version,
|
|
570
|
+
framework=framework,
|
|
571
|
+
stage=stage,
|
|
572
|
+
metrics=metrics,
|
|
573
|
+
tags=tags,
|
|
574
|
+
description=description or properties.get("description", ""),
|
|
575
|
+
author=author,
|
|
576
|
+
parent_version=None,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def to_asset(
|
|
580
|
+
self,
|
|
581
|
+
name: str,
|
|
582
|
+
version: str | None = None,
|
|
583
|
+
stage: ModelStage | None = None,
|
|
584
|
+
) -> "Asset":
|
|
585
|
+
"""Load a model version as a Model asset.
|
|
586
|
+
|
|
587
|
+
This creates a Model asset with all the stored metadata,
|
|
588
|
+
enabling seamless integration with FlowyML pipelines.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
name: Model name
|
|
592
|
+
version: Specific version (if None, loads latest)
|
|
593
|
+
stage: Stage filter (if version is None)
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Model asset with loaded model and metadata
|
|
597
|
+
|
|
598
|
+
Example:
|
|
599
|
+
>>> registry = ModelRegistry()
|
|
600
|
+
>>> model_asset = registry.to_asset("classifier", version="v1.0.0")
|
|
601
|
+
>>> print(model_asset.properties)
|
|
602
|
+
"""
|
|
603
|
+
from flowyml.assets.model import Model
|
|
604
|
+
|
|
605
|
+
model_version = self.get_version(name, version) if version else self.get_latest_version(name, stage)
|
|
606
|
+
|
|
607
|
+
if not model_version:
|
|
608
|
+
raise ValueError(f"Model {name} not found")
|
|
609
|
+
|
|
610
|
+
model_data = self.load(name, version or model_version.version)
|
|
611
|
+
|
|
612
|
+
properties = {
|
|
613
|
+
"framework": model_version.framework,
|
|
614
|
+
"stage": model_version.stage.value,
|
|
615
|
+
"created_at": model_version.created_at,
|
|
616
|
+
"updated_at": model_version.updated_at,
|
|
617
|
+
**model_version.metrics,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return Model(
|
|
621
|
+
name=name,
|
|
622
|
+
version=model_version.version,
|
|
623
|
+
data=model_data,
|
|
624
|
+
tags=model_version.tags,
|
|
625
|
+
properties=properties,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def capture_environment(self) -> dict[str, Any]:
|
|
629
|
+
"""Capture current Python environment.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Dictionary with environment info
|
|
633
|
+
"""
|
|
634
|
+
from flowyml.registry.model_environment import ModelEnvironment
|
|
635
|
+
|
|
636
|
+
return ModelEnvironment.from_current().to_dict()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Model serving utilities for FlowyML deployments."""
|
|
2
|
+
|
|
3
|
+
from .model_server import (
|
|
4
|
+
ModelServer,
|
|
5
|
+
ServerConfig,
|
|
6
|
+
start_model_server,
|
|
7
|
+
stop_model_server,
|
|
8
|
+
load_and_predict,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ModelServer",
|
|
13
|
+
"ServerConfig",
|
|
14
|
+
"start_model_server",
|
|
15
|
+
"stop_model_server",
|
|
16
|
+
"load_and_predict",
|
|
17
|
+
]
|