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.
Files changed (137) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/dataset.py +570 -17
  3. flowyml/assets/metrics.py +5 -0
  4. flowyml/assets/model.py +1052 -15
  5. flowyml/cli/main.py +709 -0
  6. flowyml/cli/stack_cli.py +138 -25
  7. flowyml/core/__init__.py +17 -0
  8. flowyml/core/executor.py +231 -37
  9. flowyml/core/image_builder.py +129 -0
  10. flowyml/core/log_streamer.py +227 -0
  11. flowyml/core/orchestrator.py +59 -4
  12. flowyml/core/pipeline.py +65 -13
  13. flowyml/core/routing.py +558 -0
  14. flowyml/core/scheduler.py +88 -5
  15. flowyml/core/step.py +9 -1
  16. flowyml/core/step_grouping.py +49 -35
  17. flowyml/core/types.py +407 -0
  18. flowyml/integrations/keras.py +247 -82
  19. flowyml/monitoring/alerts.py +10 -0
  20. flowyml/monitoring/notifications.py +104 -25
  21. flowyml/monitoring/slack_blocks.py +323 -0
  22. flowyml/plugins/__init__.py +251 -0
  23. flowyml/plugins/alerters/__init__.py +1 -0
  24. flowyml/plugins/alerters/slack.py +168 -0
  25. flowyml/plugins/base.py +752 -0
  26. flowyml/plugins/config.py +478 -0
  27. flowyml/plugins/deployers/__init__.py +22 -0
  28. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  29. flowyml/plugins/deployers/sagemaker.py +306 -0
  30. flowyml/plugins/deployers/vertex.py +290 -0
  31. flowyml/plugins/integration.py +369 -0
  32. flowyml/plugins/manager.py +510 -0
  33. flowyml/plugins/model_registries/__init__.py +22 -0
  34. flowyml/plugins/model_registries/mlflow.py +159 -0
  35. flowyml/plugins/model_registries/sagemaker.py +489 -0
  36. flowyml/plugins/model_registries/vertex.py +386 -0
  37. flowyml/plugins/orchestrators/__init__.py +13 -0
  38. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  39. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  40. flowyml/plugins/registries/__init__.py +13 -0
  41. flowyml/plugins/registries/ecr.py +321 -0
  42. flowyml/plugins/registries/gcr.py +313 -0
  43. flowyml/plugins/registry.py +454 -0
  44. flowyml/plugins/stack.py +494 -0
  45. flowyml/plugins/stack_config.py +537 -0
  46. flowyml/plugins/stores/__init__.py +13 -0
  47. flowyml/plugins/stores/gcs.py +460 -0
  48. flowyml/plugins/stores/s3.py +453 -0
  49. flowyml/plugins/trackers/__init__.py +11 -0
  50. flowyml/plugins/trackers/mlflow.py +316 -0
  51. flowyml/plugins/validators/__init__.py +3 -0
  52. flowyml/plugins/validators/deepchecks.py +119 -0
  53. flowyml/registry/__init__.py +2 -1
  54. flowyml/registry/model_environment.py +109 -0
  55. flowyml/registry/model_registry.py +241 -96
  56. flowyml/serving/__init__.py +17 -0
  57. flowyml/serving/model_server.py +628 -0
  58. flowyml/stacks/__init__.py +60 -0
  59. flowyml/stacks/aws.py +93 -0
  60. flowyml/stacks/base.py +62 -0
  61. flowyml/stacks/components.py +12 -0
  62. flowyml/stacks/gcp.py +44 -9
  63. flowyml/stacks/plugins.py +115 -0
  64. flowyml/stacks/registry.py +2 -1
  65. flowyml/storage/sql.py +401 -12
  66. flowyml/tracking/experiment.py +8 -5
  67. flowyml/ui/backend/Dockerfile +87 -16
  68. flowyml/ui/backend/auth.py +12 -2
  69. flowyml/ui/backend/main.py +149 -5
  70. flowyml/ui/backend/routers/ai_context.py +226 -0
  71. flowyml/ui/backend/routers/assets.py +23 -4
  72. flowyml/ui/backend/routers/auth.py +96 -0
  73. flowyml/ui/backend/routers/deployments.py +660 -0
  74. flowyml/ui/backend/routers/model_explorer.py +597 -0
  75. flowyml/ui/backend/routers/plugins.py +103 -51
  76. flowyml/ui/backend/routers/projects.py +91 -8
  77. flowyml/ui/backend/routers/runs.py +132 -1
  78. flowyml/ui/backend/routers/schedules.py +54 -29
  79. flowyml/ui/backend/routers/templates.py +319 -0
  80. flowyml/ui/backend/routers/websocket.py +2 -2
  81. flowyml/ui/frontend/Dockerfile +55 -6
  82. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  83. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  84. flowyml/ui/frontend/dist/index.html +2 -2
  85. flowyml/ui/frontend/dist/logo.png +0 -0
  86. flowyml/ui/frontend/nginx.conf +65 -4
  87. flowyml/ui/frontend/package-lock.json +1415 -74
  88. flowyml/ui/frontend/package.json +4 -0
  89. flowyml/ui/frontend/public/logo.png +0 -0
  90. flowyml/ui/frontend/src/App.jsx +10 -7
  91. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  92. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  93. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  94. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  95. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  96. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  97. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  98. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  99. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
  100. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  101. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  102. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  103. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
  104. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  105. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  106. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  107. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  108. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  109. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  110. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  111. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  112. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  113. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  114. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  115. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  116. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  117. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  118. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  119. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  120. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  121. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  122. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  123. flowyml/ui/frontend/src/router/index.jsx +47 -20
  124. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  125. flowyml/ui/server_manager.py +5 -5
  126. flowyml/ui/utils.py +157 -39
  127. flowyml/utils/config.py +37 -15
  128. flowyml/utils/model_introspection.py +123 -0
  129. flowyml/utils/observability.py +30 -0
  130. flowyml-1.8.0.dist-info/METADATA +174 -0
  131. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
  132. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  133. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  134. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  135. flowyml-1.7.1.dist-info/METADATA +0 -477
  136. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  137. {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
- import json
4
- from dataclasses import dataclass, field, asdict
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
- import shutil
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["stage"] = ModelStage(data["stage"])
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__(self, registry_path: str = ".flowyml/model_registry"):
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 registry storage
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
- 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)
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
- 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}")
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
- model_version = ModelVersion(
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
- 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
-
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
- if name not in self._metadata:
260
- return []
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 list(self._metadata.keys())
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
- 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]
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 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()
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
- # Remove from metadata
396
- self._metadata[name] = [v for v in self._metadata[name] if v["version"] != version]
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
- results = []
468
+ stage_str = stage.value if stage else None
469
+ all_versions = self._metadata_store.list_model_versions(stage=stage_str)
451
470
 
452
- for name in self._metadata:
453
- for version_dict in self._metadata[name]:
454
- version = ModelVersion.from_dict(version_dict)
471
+ results = []
472
+ for version_dict in all_versions:
473
+ version = ModelVersion.from_dict(version_dict)
455
474
 
456
- # Check stage
457
- if stage and version.stage != stage:
458
- continue
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
- # Check tags
461
- if tags and not all(version.tags.get(k) == v for k, v in tags.items()):
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
- # 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)
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
- total_models = len(self._metadata)
480
- total_versions = sum(len(versions) for versions in self._metadata.values())
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
- for versions in self._metadata.values():
484
- for v in versions:
485
- stage_counts[v["stage"]] += 1
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": total_models,
489
- "total_versions": 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
+ ]