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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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__})"