flowyml 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Model registry for version management and deployment."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field, asdict
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelStage(str, Enum):
|
|
13
|
+
"""Model deployment stages."""
|
|
14
|
+
|
|
15
|
+
DEVELOPMENT = "development"
|
|
16
|
+
STAGING = "staging"
|
|
17
|
+
PRODUCTION = "production"
|
|
18
|
+
ARCHIVED = "archived"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ModelVersion:
|
|
23
|
+
"""Model version metadata."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
version: str
|
|
27
|
+
stage: ModelStage
|
|
28
|
+
created_at: str
|
|
29
|
+
updated_at: str
|
|
30
|
+
model_path: str
|
|
31
|
+
framework: str
|
|
32
|
+
metrics: dict[str, float] = field(default_factory=dict)
|
|
33
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
description: str = ""
|
|
35
|
+
author: str | None = None
|
|
36
|
+
parent_version: str | None = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""Convert to dictionary."""
|
|
40
|
+
data = asdict(self)
|
|
41
|
+
data["stage"] = self.stage.value
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, data: dict[str, Any]) -> "ModelVersion":
|
|
46
|
+
"""Create from dictionary."""
|
|
47
|
+
data["stage"] = ModelStage(data["stage"])
|
|
48
|
+
return cls(**data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModelRegistry:
|
|
52
|
+
"""Registry for managing model versions and deployments.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
```python
|
|
56
|
+
from flowyml import ModelRegistry
|
|
57
|
+
|
|
58
|
+
registry = ModelRegistry()
|
|
59
|
+
|
|
60
|
+
# Register a new model
|
|
61
|
+
registry.register(
|
|
62
|
+
model=trained_model,
|
|
63
|
+
name="sentiment_classifier",
|
|
64
|
+
version="v1.0.0",
|
|
65
|
+
framework="pytorch",
|
|
66
|
+
metrics={"accuracy": 0.95, "f1": 0.94},
|
|
67
|
+
tags={"task": "classification", "lang": "en"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Promote to production
|
|
71
|
+
registry.promote("sentiment_classifier", "v1.0.0", ModelStage.PRODUCTION)
|
|
72
|
+
|
|
73
|
+
# Load production model
|
|
74
|
+
model = registry.load("sentiment_classifier", stage=ModelStage.PRODUCTION)
|
|
75
|
+
|
|
76
|
+
# Compare versions
|
|
77
|
+
comparison = registry.compare_versions("sentiment_classifier", ["v1.0.0", "v1.1.0"])
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, registry_path: str = ".flowyml/model_registry"):
|
|
82
|
+
"""Initialize model registry.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
registry_path: Path to registry storage
|
|
86
|
+
"""
|
|
87
|
+
self.registry_path = Path(registry_path)
|
|
88
|
+
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
|
+
|
|
101
|
+
def _save_metadata(self) -> None:
|
|
102
|
+
"""Save registry metadata to disk."""
|
|
103
|
+
with open(self.metadata_file, "w") as f:
|
|
104
|
+
json.dump(self._metadata, f, indent=2)
|
|
105
|
+
|
|
106
|
+
def register(
|
|
107
|
+
self,
|
|
108
|
+
model: Any,
|
|
109
|
+
name: str,
|
|
110
|
+
version: str,
|
|
111
|
+
framework: str,
|
|
112
|
+
stage: ModelStage = ModelStage.DEVELOPMENT,
|
|
113
|
+
metrics: dict[str, float] | None = None,
|
|
114
|
+
tags: dict[str, str] | None = None,
|
|
115
|
+
description: str = "",
|
|
116
|
+
author: str | None = None,
|
|
117
|
+
parent_version: str | None = None,
|
|
118
|
+
) -> ModelVersion:
|
|
119
|
+
"""Register a new model version.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
model: Model object to register
|
|
123
|
+
name: Model name
|
|
124
|
+
version: Version string (e.g., "v1.0.0")
|
|
125
|
+
framework: Framework name (pytorch, tensorflow, sklearn)
|
|
126
|
+
stage: Deployment stage
|
|
127
|
+
metrics: Model metrics
|
|
128
|
+
tags: Model tags
|
|
129
|
+
description: Model description
|
|
130
|
+
author: Model author
|
|
131
|
+
parent_version: Parent version if this is an update
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
ModelVersion instance
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If version already exists
|
|
138
|
+
"""
|
|
139
|
+
# Check if version already exists
|
|
140
|
+
if name in self._metadata:
|
|
141
|
+
existing_versions = [v["version"] for v in self._metadata[name]]
|
|
142
|
+
if version in existing_versions:
|
|
143
|
+
raise ValueError(f"Version {version} already exists for model {name}")
|
|
144
|
+
|
|
145
|
+
# Create model directory
|
|
146
|
+
model_dir = self.registry_path / name / version
|
|
147
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
# Save model using appropriate materializer
|
|
150
|
+
model_path = model_dir / "model"
|
|
151
|
+
self._save_model(model, model_path, framework)
|
|
152
|
+
|
|
153
|
+
# Create version metadata
|
|
154
|
+
now = datetime.now().isoformat()
|
|
155
|
+
model_version = ModelVersion(
|
|
156
|
+
name=name,
|
|
157
|
+
version=version,
|
|
158
|
+
stage=stage,
|
|
159
|
+
created_at=now,
|
|
160
|
+
updated_at=now,
|
|
161
|
+
model_path=str(model_path),
|
|
162
|
+
framework=framework,
|
|
163
|
+
metrics=metrics or {},
|
|
164
|
+
tags=tags or {},
|
|
165
|
+
description=description,
|
|
166
|
+
author=author,
|
|
167
|
+
parent_version=parent_version,
|
|
168
|
+
)
|
|
169
|
+
|
|
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
|
+
def _save_model(self, model: Any, path: Path, framework: str) -> None:
|
|
180
|
+
"""Save model using appropriate method.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
model: Model to save
|
|
184
|
+
path: Path to save to
|
|
185
|
+
framework: Framework name
|
|
186
|
+
"""
|
|
187
|
+
from flowyml.storage.materializers import get_materializer
|
|
188
|
+
|
|
189
|
+
# Try to get appropriate materializer
|
|
190
|
+
materializer = get_materializer(model)
|
|
191
|
+
|
|
192
|
+
if materializer:
|
|
193
|
+
materializer.save(model, path)
|
|
194
|
+
else:
|
|
195
|
+
# Fallback to pickle
|
|
196
|
+
import pickle
|
|
197
|
+
|
|
198
|
+
with open(path, "wb") as f:
|
|
199
|
+
pickle.dump(model, f)
|
|
200
|
+
|
|
201
|
+
def _load_model(self, path: Path, framework: str) -> Any:
|
|
202
|
+
"""Load model from path.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: Path to load from
|
|
206
|
+
framework: Framework name
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Loaded model
|
|
210
|
+
"""
|
|
211
|
+
# Try framework-specific loading
|
|
212
|
+
if framework == "pytorch":
|
|
213
|
+
from flowyml.storage.materializers.pytorch import PyTorchMaterializer
|
|
214
|
+
|
|
215
|
+
return PyTorchMaterializer().load(path)
|
|
216
|
+
elif framework == "tensorflow":
|
|
217
|
+
from flowyml.storage.materializers.tensorflow import TensorFlowMaterializer
|
|
218
|
+
|
|
219
|
+
return TensorFlowMaterializer().load(path)
|
|
220
|
+
elif framework == "sklearn":
|
|
221
|
+
from flowyml.storage.materializers.sklearn import SklearnMaterializer
|
|
222
|
+
|
|
223
|
+
return SklearnMaterializer().load(path)
|
|
224
|
+
else:
|
|
225
|
+
# Fallback to pickle
|
|
226
|
+
import pickle
|
|
227
|
+
|
|
228
|
+
with open(path, "rb") as f:
|
|
229
|
+
return pickle.load(f)
|
|
230
|
+
|
|
231
|
+
def get_version(self, name: str, version: str) -> ModelVersion | None:
|
|
232
|
+
"""Get specific model version.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Model name
|
|
236
|
+
version: Version string
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
ModelVersion or None if not found
|
|
240
|
+
"""
|
|
241
|
+
if name not in self._metadata:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
for v in self._metadata[name]:
|
|
245
|
+
if v["version"] == version:
|
|
246
|
+
return ModelVersion.from_dict(v)
|
|
247
|
+
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def list_versions(self, name: str) -> list[ModelVersion]:
|
|
251
|
+
"""List all versions of a model.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: Model name
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of ModelVersion instances
|
|
258
|
+
"""
|
|
259
|
+
if name not in self._metadata:
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
return [ModelVersion.from_dict(v) for v in self._metadata[name]]
|
|
263
|
+
|
|
264
|
+
def list_models(self) -> list[str]:
|
|
265
|
+
"""List all registered models.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of model names
|
|
269
|
+
"""
|
|
270
|
+
return list(self._metadata.keys())
|
|
271
|
+
|
|
272
|
+
def get_latest_version(self, name: str, stage: ModelStage | None = None) -> ModelVersion | None:
|
|
273
|
+
"""Get latest version of a model.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
name: Model name
|
|
277
|
+
stage: Optional stage filter
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Latest ModelVersion or None
|
|
281
|
+
"""
|
|
282
|
+
versions = self.list_versions(name)
|
|
283
|
+
|
|
284
|
+
if stage:
|
|
285
|
+
versions = [v for v in versions if v.stage == stage]
|
|
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]
|
|
293
|
+
|
|
294
|
+
def load(
|
|
295
|
+
self,
|
|
296
|
+
name: str,
|
|
297
|
+
version: str | None = None,
|
|
298
|
+
stage: ModelStage | None = None,
|
|
299
|
+
) -> Any:
|
|
300
|
+
"""Load a model from registry.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
name: Model name
|
|
304
|
+
version: Specific version (if None, loads latest)
|
|
305
|
+
stage: Stage filter (if version is None)
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Loaded model
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
ValueError: If model not found
|
|
312
|
+
"""
|
|
313
|
+
model_version = self.get_version(name, version) if version else self.get_latest_version(name, stage)
|
|
314
|
+
|
|
315
|
+
if not model_version:
|
|
316
|
+
raise ValueError(f"Model {name} not found")
|
|
317
|
+
|
|
318
|
+
return self._load_model(Path(model_version.model_path), model_version.framework)
|
|
319
|
+
|
|
320
|
+
def promote(
|
|
321
|
+
self,
|
|
322
|
+
name: str,
|
|
323
|
+
version: str,
|
|
324
|
+
to_stage: ModelStage,
|
|
325
|
+
) -> ModelVersion:
|
|
326
|
+
"""Promote model to a different stage.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
name: Model name
|
|
330
|
+
version: Version to promote
|
|
331
|
+
to_stage: Target stage
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Updated ModelVersion
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValueError: If model not found
|
|
338
|
+
"""
|
|
339
|
+
model_version = self.get_version(name, version)
|
|
340
|
+
|
|
341
|
+
if not model_version:
|
|
342
|
+
raise ValueError(f"Model {name} version {version} not found")
|
|
343
|
+
|
|
344
|
+
# Update stage in metadata
|
|
345
|
+
for v in self._metadata[name]:
|
|
346
|
+
if v["version"] == version:
|
|
347
|
+
v["stage"] = to_stage.value
|
|
348
|
+
v["updated_at"] = datetime.now().isoformat()
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
self._save_metadata()
|
|
352
|
+
|
|
353
|
+
return self.get_version(name, version)
|
|
354
|
+
|
|
355
|
+
def rollback(
|
|
356
|
+
self,
|
|
357
|
+
name: str,
|
|
358
|
+
to_version: str,
|
|
359
|
+
stage: ModelStage = ModelStage.PRODUCTION,
|
|
360
|
+
) -> ModelVersion:
|
|
361
|
+
"""Rollback to a previous version.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
name: Model name
|
|
365
|
+
to_version: Version to rollback to
|
|
366
|
+
stage: Stage to set (default: production)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Rolled back ModelVersion
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
ValueError: If version not found
|
|
373
|
+
"""
|
|
374
|
+
return self.promote(name, to_version, stage)
|
|
375
|
+
|
|
376
|
+
def delete_version(self, name: str, version: str) -> None:
|
|
377
|
+
"""Delete a model version.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
name: Model name
|
|
381
|
+
version: Version to delete
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
ValueError: If model not found or in production
|
|
385
|
+
"""
|
|
386
|
+
model_version = self.get_version(name, version)
|
|
387
|
+
|
|
388
|
+
if not model_version:
|
|
389
|
+
raise ValueError(f"Model {name} version {version} not found")
|
|
390
|
+
|
|
391
|
+
# Don't allow deleting production models
|
|
392
|
+
if model_version.stage == ModelStage.PRODUCTION:
|
|
393
|
+
raise ValueError("Cannot delete production model. Demote first.")
|
|
394
|
+
|
|
395
|
+
# Remove from metadata
|
|
396
|
+
self._metadata[name] = [v for v in self._metadata[name] if v["version"] != version]
|
|
397
|
+
|
|
398
|
+
# Delete model files
|
|
399
|
+
model_dir = Path(model_version.model_path).parent
|
|
400
|
+
if model_dir.exists():
|
|
401
|
+
shutil.rmtree(model_dir)
|
|
402
|
+
|
|
403
|
+
self._save_metadata()
|
|
404
|
+
|
|
405
|
+
def compare_versions(
|
|
406
|
+
self,
|
|
407
|
+
name: str,
|
|
408
|
+
versions: list[str],
|
|
409
|
+
) -> dict[str, dict[str, Any]]:
|
|
410
|
+
"""Compare multiple versions of a model.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
name: Model name
|
|
414
|
+
versions: List of versions to compare
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Dictionary with comparison data
|
|
418
|
+
"""
|
|
419
|
+
comparison = {}
|
|
420
|
+
|
|
421
|
+
for version in versions:
|
|
422
|
+
model_version = self.get_version(name, version)
|
|
423
|
+
if model_version:
|
|
424
|
+
comparison[version] = {
|
|
425
|
+
"stage": model_version.stage.value,
|
|
426
|
+
"metrics": model_version.metrics,
|
|
427
|
+
"tags": model_version.tags,
|
|
428
|
+
"created_at": model_version.created_at,
|
|
429
|
+
"framework": model_version.framework,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return comparison
|
|
433
|
+
|
|
434
|
+
def search(
|
|
435
|
+
self,
|
|
436
|
+
tags: dict[str, str] | None = None,
|
|
437
|
+
stage: ModelStage | None = None,
|
|
438
|
+
min_metrics: dict[str, float] | None = None,
|
|
439
|
+
) -> list[ModelVersion]:
|
|
440
|
+
"""Search for models by criteria.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
tags: Tags to match
|
|
444
|
+
stage: Stage to filter by
|
|
445
|
+
min_metrics: Minimum metric values
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
List of matching ModelVersion instances
|
|
449
|
+
"""
|
|
450
|
+
results = []
|
|
451
|
+
|
|
452
|
+
for name in self._metadata:
|
|
453
|
+
for version_dict in self._metadata[name]:
|
|
454
|
+
version = ModelVersion.from_dict(version_dict)
|
|
455
|
+
|
|
456
|
+
# Check stage
|
|
457
|
+
if stage and version.stage != stage:
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
# Check tags
|
|
461
|
+
if tags and not all(version.tags.get(k) == v for k, v in tags.items()):
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
# Check metrics
|
|
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)
|
|
470
|
+
|
|
471
|
+
return results
|
|
472
|
+
|
|
473
|
+
def get_stats(self) -> dict[str, Any]:
|
|
474
|
+
"""Get registry statistics.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Dictionary with statistics
|
|
478
|
+
"""
|
|
479
|
+
total_models = len(self._metadata)
|
|
480
|
+
total_versions = sum(len(versions) for versions in self._metadata.values())
|
|
481
|
+
|
|
482
|
+
stage_counts = {stage.value: 0 for stage in ModelStage}
|
|
483
|
+
for versions in self._metadata.values():
|
|
484
|
+
for v in versions:
|
|
485
|
+
stage_counts[v["stage"]] += 1
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
"total_models": total_models,
|
|
489
|
+
"total_versions": total_versions,
|
|
490
|
+
"by_stage": stage_counts,
|
|
491
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Pipeline registry for managing available pipelines."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from flowyml.core.pipeline import Pipeline
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PipelineRegistry:
|
|
8
|
+
"""Registry for pipelines to enable lookup by name.
|
|
9
|
+
|
|
10
|
+
Crucial for scheduling and remote execution where we need to
|
|
11
|
+
find a pipeline definition by its string name.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
def __new__(cls):
|
|
17
|
+
if cls._instance is None:
|
|
18
|
+
cls._instance = super().__new__(cls)
|
|
19
|
+
cls._instance.pipelines = {}
|
|
20
|
+
return cls._instance
|
|
21
|
+
|
|
22
|
+
def register(self, name: str, pipeline_factory: Callable[..., Pipeline]) -> None:
|
|
23
|
+
"""Register a pipeline factory function.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name: Unique name for the pipeline
|
|
27
|
+
pipeline_factory: Function that returns a Pipeline instance
|
|
28
|
+
"""
|
|
29
|
+
self.pipelines[name] = pipeline_factory
|
|
30
|
+
|
|
31
|
+
def get(self, name: str) -> Callable[..., Pipeline] | None:
|
|
32
|
+
"""Get a pipeline factory by name."""
|
|
33
|
+
return self.pipelines.get(name)
|
|
34
|
+
|
|
35
|
+
def list_pipelines(self) -> dict[str, str]:
|
|
36
|
+
"""List all registered pipelines."""
|
|
37
|
+
return {name: str(func) for name, func in self.pipelines.items()}
|
|
38
|
+
|
|
39
|
+
def clear(self) -> None:
|
|
40
|
+
"""Clear all registrations."""
|
|
41
|
+
self.pipelines = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Global instance
|
|
45
|
+
pipeline_registry = PipelineRegistry()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def register_pipeline(name: str):
|
|
49
|
+
"""Decorator to register a pipeline factory."""
|
|
50
|
+
|
|
51
|
+
def decorator(func):
|
|
52
|
+
pipeline_registry.register(name, func)
|
|
53
|
+
return func
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Stack management for flowyml."""
|
|
2
|
+
|
|
3
|
+
from flowyml.stacks.base import Stack, StackConfig
|
|
4
|
+
from flowyml.stacks.local import LocalStack
|
|
5
|
+
from flowyml.stacks.components import (
|
|
6
|
+
ResourceConfig,
|
|
7
|
+
DockerConfig,
|
|
8
|
+
Orchestrator,
|
|
9
|
+
ArtifactStore,
|
|
10
|
+
ContainerRegistry,
|
|
11
|
+
)
|
|
12
|
+
from flowyml.stacks.registry import StackRegistry, get_registry, get_active_stack, set_active_stack
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Stack",
|
|
16
|
+
"StackConfig",
|
|
17
|
+
"LocalStack",
|
|
18
|
+
"ResourceConfig",
|
|
19
|
+
"DockerConfig",
|
|
20
|
+
"Orchestrator",
|
|
21
|
+
"ArtifactStore",
|
|
22
|
+
"ContainerRegistry",
|
|
23
|
+
"StackRegistry",
|
|
24
|
+
"get_registry",
|
|
25
|
+
"get_active_stack",
|
|
26
|
+
"set_active_stack",
|
|
27
|
+
]
|
flowyml/stacks/base.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base Stack - Defines execution environment for pipelines."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class StackConfig:
|
|
9
|
+
"""Configuration for a stack."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
executor_type: str
|
|
13
|
+
artifact_store: str
|
|
14
|
+
metadata_store: str
|
|
15
|
+
container_registry: str | None = None
|
|
16
|
+
orchestrator: str | None = None
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict[str, Any]:
|
|
19
|
+
"""Convert to dictionary."""
|
|
20
|
+
return {
|
|
21
|
+
"name": self.name,
|
|
22
|
+
"executor_type": self.executor_type,
|
|
23
|
+
"artifact_store": self.artifact_store,
|
|
24
|
+
"metadata_store": self.metadata_store,
|
|
25
|
+
"container_registry": self.container_registry,
|
|
26
|
+
"orchestrator": self.orchestrator,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Stack:
|
|
31
|
+
"""Stack defines the execution environment for pipelines.
|
|
32
|
+
|
|
33
|
+
A stack includes:
|
|
34
|
+
- Executor: Where steps run (local, cloud, kubernetes)
|
|
35
|
+
- Artifact Store: Where outputs are stored (local, S3, GCS)
|
|
36
|
+
- Metadata Store: Where run metadata is stored (SQLite, Postgres)
|
|
37
|
+
- Container Registry: For containerized execution (optional)
|
|
38
|
+
- Orchestrator: For workflow orchestration (optional)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
name: str,
|
|
44
|
+
executor: Any,
|
|
45
|
+
artifact_store: Any,
|
|
46
|
+
metadata_store: Any,
|
|
47
|
+
container_registry: Any | None = None,
|
|
48
|
+
orchestrator: Any | None = None,
|
|
49
|
+
):
|
|
50
|
+
self.name = name
|
|
51
|
+
self.executor = executor
|
|
52
|
+
self.artifact_store = artifact_store
|
|
53
|
+
self.metadata_store = metadata_store
|
|
54
|
+
self.container_registry = container_registry
|
|
55
|
+
self.orchestrator = orchestrator
|
|
56
|
+
|
|
57
|
+
self.config = StackConfig(
|
|
58
|
+
name=name,
|
|
59
|
+
executor_type=type(executor).__name__,
|
|
60
|
+
artifact_store=type(artifact_store).__name__,
|
|
61
|
+
metadata_store=type(metadata_store).__name__,
|
|
62
|
+
container_registry=type(container_registry).__name__ if container_registry else None,
|
|
63
|
+
orchestrator=type(orchestrator).__name__ if orchestrator else None,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def activate(self) -> None:
|
|
67
|
+
"""Activate this stack as the active stack."""
|
|
68
|
+
# In a real implementation, this would set the global active stack
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def validate(self) -> bool:
|
|
72
|
+
"""Validate that all stack components are properly configured."""
|
|
73
|
+
# Check that all components are properly configured
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def __repr__(self) -> str:
|
|
77
|
+
return f"Stack(name='{self.name}', executor={type(self.executor).__name__})"
|