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,26 @@
1
+ """Storage module for artifacts, metadata, and materializers."""
2
+
3
+ from flowyml.storage.artifacts import ArtifactStore, LocalArtifactStore
4
+ from flowyml.storage.metadata import MetadataStore, SQLiteMetadataStore
5
+ from flowyml.storage.materializers.base import BaseMaterializer, materializer_registry
6
+ from flowyml.storage.materializers.pytorch import PyTorchMaterializer
7
+ from flowyml.storage.materializers.tensorflow import TensorFlowMaterializer
8
+ from flowyml.storage.materializers.sklearn import SklearnMaterializer
9
+ from flowyml.storage.materializers.pandas import PandasMaterializer
10
+ from flowyml.storage.materializers.numpy import NumPyMaterializer
11
+
12
+ __all__ = [
13
+ # Core Storage
14
+ "ArtifactStore",
15
+ "LocalArtifactStore",
16
+ "MetadataStore",
17
+ "SQLiteMetadataStore",
18
+ # Materializers
19
+ "BaseMaterializer",
20
+ "materializer_registry",
21
+ "PyTorchMaterializer",
22
+ "TensorFlowMaterializer",
23
+ "SklearnMaterializer",
24
+ "PandasMaterializer",
25
+ "NumPyMaterializer",
26
+ ]
@@ -0,0 +1,246 @@
1
+ """Artifact storage backends for flowyml."""
2
+
3
+ import shutil
4
+ from abc import ABC, abstractmethod
5
+ from pathlib import Path
6
+ from typing import Any
7
+ import pickle
8
+
9
+
10
+ class ArtifactStore(ABC):
11
+ """Base class for artifact storage backends."""
12
+
13
+ @abstractmethod
14
+ def save(self, artifact: Any, path: str, metadata: dict | None = None) -> str:
15
+ """Save an artifact to storage.
16
+
17
+ Args:
18
+ artifact: The artifact to save
19
+ path: Storage path for the artifact
20
+ metadata: Optional metadata dictionary
21
+
22
+ Returns:
23
+ Full path where artifact was saved
24
+ """
25
+ pass
26
+
27
+ @abstractmethod
28
+ def load(self, path: str) -> Any:
29
+ """Load an artifact from storage.
30
+
31
+ Args:
32
+ path: Storage path of the artifact
33
+
34
+ Returns:
35
+ The loaded artifact
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def exists(self, path: str) -> bool:
41
+ """Check if artifact exists at path."""
42
+ pass
43
+
44
+ @abstractmethod
45
+ def delete(self, path: str) -> None:
46
+ """Delete artifact at path."""
47
+ pass
48
+
49
+ @abstractmethod
50
+ def list_artifacts(self, prefix: str = "") -> list[str]:
51
+ """List all artifacts with optional prefix filter."""
52
+ pass
53
+
54
+ def materialize(self, obj: Any, name: str, run_id: str, step_name: str, project_name: str = "default") -> str:
55
+ """Materialize artifact to structured storage.
56
+
57
+ Args:
58
+ obj: Object to materialize
59
+ name: Name of the artifact
60
+ run_id: ID of the current run
61
+ step_name: Name of the step producing the artifact
62
+ project_name: Name of the project
63
+
64
+ Returns:
65
+ Path where artifact was saved
66
+ """
67
+ pass
68
+
69
+
70
+ class LocalArtifactStore(ArtifactStore):
71
+ """Local filesystem artifact storage."""
72
+
73
+ def __init__(self, base_path: str = ".flowyml/artifacts"):
74
+ """Initialize local artifact store.
75
+
76
+ Args:
77
+ base_path: Base directory for storing artifacts
78
+ """
79
+ self.base_path = Path(base_path)
80
+ self.base_path.mkdir(parents=True, exist_ok=True)
81
+
82
+ def save(self, artifact: Any, path: str, metadata: dict | None = None) -> str:
83
+ """Save artifact to local filesystem.
84
+
85
+ Args:
86
+ artifact: The artifact to save
87
+ path: Relative path for the artifact
88
+ metadata: Optional metadata dictionary
89
+
90
+ Returns:
91
+ Full path where artifact was saved
92
+ """
93
+ full_path = self.base_path / path
94
+ full_path.parent.mkdir(parents=True, exist_ok=True)
95
+
96
+ # Save artifact using pickle by default
97
+ with open(full_path, "wb") as f:
98
+ pickle.dump(artifact, f)
99
+
100
+ # Save metadata if provided
101
+ if metadata:
102
+ metadata_path = full_path.with_suffix(".meta.json")
103
+ import json
104
+
105
+ with open(metadata_path, "w") as f:
106
+ json.dump(metadata, f, indent=2)
107
+
108
+ return str(full_path)
109
+
110
+ def load(self, path: str) -> Any:
111
+ """Load artifact from local filesystem.
112
+
113
+ Args:
114
+ path: Relative or absolute path to the artifact
115
+
116
+ Returns:
117
+ The loaded artifact
118
+ """
119
+ full_path = Path(path) if Path(path).is_absolute() else self.base_path / path
120
+
121
+ if not full_path.exists():
122
+ raise FileNotFoundError(f"Artifact not found at {full_path}")
123
+
124
+ with open(full_path, "rb") as f:
125
+ return pickle.load(f)
126
+
127
+ def exists(self, path: str) -> bool:
128
+ """Check if artifact exists.
129
+
130
+ Args:
131
+ path: Relative path to check
132
+
133
+ Returns:
134
+ True if artifact exists, False otherwise
135
+ """
136
+ full_path = self.base_path / path
137
+ return full_path.exists()
138
+
139
+ def delete(self, path: str) -> None:
140
+ """Delete artifact from filesystem.
141
+
142
+ Args:
143
+ path: Relative path to delete
144
+ """
145
+ full_path = self.base_path / path
146
+ if full_path.exists():
147
+ if full_path.is_dir():
148
+ shutil.rmtree(full_path)
149
+ else:
150
+ full_path.unlink()
151
+
152
+ # Also delete metadata if exists
153
+ metadata_path = full_path.with_suffix(".meta.json")
154
+ if metadata_path.exists():
155
+ metadata_path.unlink()
156
+
157
+ def list_artifacts(self, prefix: str = "") -> list[str]:
158
+ """List all artifacts with optional prefix.
159
+
160
+ Args:
161
+ prefix: Optional prefix filter
162
+
163
+ Returns:
164
+ List of artifact paths
165
+ """
166
+ search_path = self.base_path / prefix if prefix else self.base_path
167
+
168
+ if not search_path.exists():
169
+ return []
170
+
171
+ artifacts = []
172
+ for item in search_path.rglob("*"):
173
+ if item.is_file() and not item.name.endswith(".meta.json"):
174
+ rel_path = item.relative_to(self.base_path)
175
+ artifacts.append(str(rel_path))
176
+
177
+ return sorted(artifacts)
178
+
179
+ def get_metadata(self, path: str) -> dict | None:
180
+ """Get metadata for an artifact.
181
+
182
+ Args:
183
+ path: Relative path to the artifact
184
+
185
+ Returns:
186
+ Metadata dictionary or None if no metadata exists
187
+ """
188
+ full_path = self.base_path / path
189
+ metadata_path = full_path.with_suffix(".meta.json")
190
+
191
+ if not metadata_path.exists():
192
+ return None
193
+
194
+ import json
195
+
196
+ with open(metadata_path) as f:
197
+ return json.load(f)
198
+
199
+ def size(self, path: str) -> int:
200
+ """Get size of artifact in bytes.
201
+
202
+ Args:
203
+ path: Relative path to the artifact
204
+
205
+ Returns:
206
+ Size in bytes
207
+ """
208
+ full_path = self.base_path / path
209
+ if full_path.exists():
210
+ return full_path.stat().st_size
211
+ return 0
212
+
213
+ def materialize(self, obj: Any, name: str, run_id: str, step_name: str, project_name: str = "default") -> str:
214
+ """Materialize artifact to structured storage."""
215
+ from datetime import datetime
216
+ from flowyml.storage.materializers.base import get_materializer
217
+ import shutil
218
+ import pickle
219
+ import json
220
+
221
+ date_str = datetime.now().strftime("%Y-%m-%d")
222
+ # Structure: project / date / run_id / data / step / name
223
+ rel_path = Path(project_name) / date_str / run_id / "data" / step_name / name
224
+ full_path = self.base_path / rel_path
225
+
226
+ # Clean up if exists
227
+ if full_path.exists():
228
+ if full_path.is_dir():
229
+ shutil.rmtree(full_path)
230
+ else:
231
+ full_path.unlink()
232
+
233
+ full_path.mkdir(parents=True, exist_ok=True)
234
+
235
+ materializer = get_materializer(obj)
236
+ if materializer:
237
+ materializer.save(obj, full_path)
238
+ else:
239
+ # Fallback to pickle
240
+ with open(full_path / "data.pkl", "wb") as f:
241
+ pickle.dump(obj, f)
242
+ # Save metadata
243
+ with open(full_path / "metadata.json", "w") as f:
244
+ json.dump({"type": "pickle", "format": "pickle"}, f, indent=2)
245
+
246
+ return str(full_path)
@@ -0,0 +1,20 @@
1
+ """Materializers for framework-specific serialization."""
2
+
3
+ from flowyml.storage.materializers.base import BaseMaterializer, materializer_registry
4
+ from flowyml.storage.materializers.pytorch import PyTorchMaterializer
5
+ from flowyml.storage.materializers.tensorflow import TensorFlowMaterializer
6
+ from flowyml.storage.materializers.sklearn import SklearnMaterializer
7
+ from flowyml.storage.materializers.pandas import PandasMaterializer
8
+ from flowyml.storage.materializers.numpy import NumPyMaterializer
9
+ from flowyml.storage.materializers.keras import KerasMaterializer
10
+
11
+ __all__ = [
12
+ "BaseMaterializer",
13
+ "materializer_registry",
14
+ "PyTorchMaterializer",
15
+ "TensorFlowMaterializer",
16
+ "SklearnMaterializer",
17
+ "PandasMaterializer",
18
+ "NumPyMaterializer",
19
+ "KerasMaterializer",
20
+ ]
@@ -0,0 +1,133 @@
1
+ """Base materializer class and registry."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ class BaseMaterializer(ABC):
9
+ """Base class for all materializers.
10
+
11
+ Materializers handle serialization/deserialization of specific types.
12
+ """
13
+
14
+ @abstractmethod
15
+ def save(self, obj: Any, path: Path) -> None:
16
+ """Save object to path.
17
+
18
+ Args:
19
+ obj: Object to save
20
+ path: Directory path where object should be saved
21
+ """
22
+ pass
23
+
24
+ @abstractmethod
25
+ def load(self, path: Path) -> Any:
26
+ """Load object from path.
27
+
28
+ Args:
29
+ path: Directory path from which to load object
30
+
31
+ Returns:
32
+ Loaded object
33
+ """
34
+ pass
35
+
36
+ @classmethod
37
+ @abstractmethod
38
+ def supported_types(cls) -> list[type]:
39
+ """Return list of types this materializer supports."""
40
+ pass
41
+
42
+ @classmethod
43
+ def can_handle(cls, obj: Any) -> bool:
44
+ """Check if this materializer can handle the given object.
45
+
46
+ Args:
47
+ obj: Object to check
48
+
49
+ Returns:
50
+ True if this materializer can handle the object
51
+ """
52
+ obj_type = type(obj)
53
+ for supported_type in cls.supported_types():
54
+ if isinstance(obj, supported_type):
55
+ return True
56
+ # Check module name for cross-import compatibility
57
+ if hasattr(supported_type, "__module__") and hasattr(obj_type, "__module__"):
58
+ if obj_type.__name__ == supported_type.__name__ and obj_type.__module__ == supported_type.__module__:
59
+ return True
60
+ return False
61
+
62
+
63
+ class MaterializerRegistry:
64
+ """Registry for materializers."""
65
+
66
+ def __init__(self):
67
+ self._materializers: dict[str, type[BaseMaterializer]] = {}
68
+
69
+ def register(self, materializer: type[BaseMaterializer]) -> None:
70
+ """Register a materializer.
71
+
72
+ Args:
73
+ materializer: Materializer class to register
74
+ """
75
+ for supported_type in materializer.supported_types():
76
+ key = f"{supported_type.__module__}.{supported_type.__name__}"
77
+ self._materializers[key] = materializer
78
+
79
+ def get_materializer(self, obj: Any) -> BaseMaterializer | None:
80
+ """Get materializer for object.
81
+
82
+ Args:
83
+ obj: Object to find materializer for
84
+
85
+ Returns:
86
+ Materializer instance or None if no suitable materializer found
87
+ """
88
+ # First try exact match
89
+ obj_type = type(obj)
90
+ key = f"{obj_type.__module__}.{obj_type.__name__}"
91
+
92
+ if key in self._materializers:
93
+ return self._materializers[key]()
94
+
95
+ # Then try checking all materializers
96
+ for materializer_cls in set(self._materializers.values()):
97
+ if materializer_cls.can_handle(obj):
98
+ return materializer_cls()
99
+
100
+ return None
101
+
102
+ def list_materializers(self) -> dict[str, type[BaseMaterializer]]:
103
+ """List all registered materializers.
104
+
105
+ Returns:
106
+ Dictionary mapping type names to materializer classes
107
+ """
108
+ return self._materializers.copy()
109
+
110
+
111
+ # Global registry instance
112
+ materializer_registry = MaterializerRegistry()
113
+
114
+
115
+ def register_materializer(materializer: type[BaseMaterializer]) -> None:
116
+ """Register a materializer with the global registry.
117
+
118
+ Args:
119
+ materializer: Materializer class to register
120
+ """
121
+ materializer_registry.register(materializer)
122
+
123
+
124
+ def get_materializer(obj: Any) -> BaseMaterializer | None:
125
+ """Get materializer for object from global registry.
126
+
127
+ Args:
128
+ obj: Object to find materializer for
129
+
130
+ Returns:
131
+ Materializer instance or None
132
+ """
133
+ return materializer_registry.get_materializer(obj)
@@ -0,0 +1,185 @@
1
+ """Keras materializer for model serialization."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
8
+
9
+ try:
10
+ import keras
11
+
12
+ # Verify Keras has expected attributes
13
+ _ = keras.Model
14
+ _ = keras.Sequential
15
+ KERAS_AVAILABLE = True
16
+ except (ImportError, AttributeError):
17
+ KERAS_AVAILABLE = False
18
+ keras = None
19
+
20
+
21
+ if KERAS_AVAILABLE:
22
+
23
+ class KerasMaterializer(BaseMaterializer):
24
+ """Materializer for Keras models with full support for Sequential and Functional API."""
25
+
26
+ def save(self, obj: Any, path: Path) -> None:
27
+ """Save Keras model to path.
28
+
29
+ Args:
30
+ obj: Keras model (Sequential or Functional)
31
+ path: Directory path where model should be saved
32
+ """
33
+ path.mkdir(parents=True, exist_ok=True)
34
+
35
+ if isinstance(obj, keras.Model):
36
+ # Save model in Keras native format (.keras)
37
+ model_path = path / "model.keras"
38
+ try:
39
+ obj.save(model_path)
40
+ except ValueError as e:
41
+ if "save_format" in str(e):
42
+ # Fallback for Keras 3 compatibility - try H5
43
+ h5_path = path / "model.h5"
44
+ obj.save(h5_path)
45
+ else:
46
+ raise e
47
+
48
+ # Save comprehensive metadata
49
+ metadata = {
50
+ "type": "keras_model",
51
+ "class_name": obj.__class__.__name__,
52
+ "model_type": "Sequential" if isinstance(obj, keras.Sequential) else "Functional",
53
+ }
54
+
55
+ # Get input/output shapes
56
+ try:
57
+ if hasattr(obj, "input_shape"):
58
+ metadata["input_shape"] = str(obj.input_shape)
59
+ if hasattr(obj, "output_shape"):
60
+ metadata["output_shape"] = str(obj.output_shape)
61
+ except Exception:
62
+ pass
63
+
64
+ # Get model architecture summary
65
+ try:
66
+ import io
67
+
68
+ string_buffer = io.StringIO()
69
+ obj.summary(print_fn=lambda x: string_buffer.write(x + "\n"))
70
+ metadata["summary"] = string_buffer.getvalue()
71
+ except Exception:
72
+ pass
73
+
74
+ # Get layer information
75
+ try:
76
+ metadata["num_layers"] = len(obj.layers)
77
+ metadata["layer_names"] = [layer.name for layer in obj.layers]
78
+ metadata["trainable_params"] = obj.count_params()
79
+ except Exception:
80
+ pass
81
+
82
+ # Get optimizer info if compiled
83
+ try:
84
+ if obj.optimizer:
85
+ metadata["optimizer"] = obj.optimizer.__class__.__name__
86
+ metadata["learning_rate"] = float(keras.backend.get_value(obj.optimizer.learning_rate))
87
+ except Exception:
88
+ pass
89
+
90
+ # Get loss function if compiled
91
+ try:
92
+ if obj.loss:
93
+ if hasattr(obj.loss, "__name__"):
94
+ metadata["loss"] = obj.loss.__name__
95
+ else:
96
+ metadata["loss"] = str(obj.loss)
97
+ except Exception:
98
+ pass
99
+
100
+ # Get metrics if compiled
101
+ try:
102
+ if obj.metrics:
103
+ metadata["metrics"] = [m.name for m in obj.metrics]
104
+ except Exception:
105
+ pass
106
+
107
+ # Save model config
108
+ try:
109
+ config = obj.get_config()
110
+ config_path = path / "config.json"
111
+ with open(config_path, "w") as f:
112
+ json.dump(config, f, indent=2, default=str)
113
+ metadata["has_config"] = True
114
+ except Exception:
115
+ metadata["has_config"] = False
116
+
117
+ # Save weights separately for backup - REMOVED for Keras 3 compatibility
118
+ # try:
119
+ # weights_path = path / "weights.h5"
120
+ # obj.save_weights(weights_path)
121
+ # metadata["has_weights_backup"] = True
122
+ # except:
123
+ # metadata["has_weights_backup"] = False
124
+
125
+ # Save metadata
126
+ metadata_path = path / "metadata.json"
127
+ with open(metadata_path, "w") as f:
128
+ json.dump(metadata, f, indent=2, default=str)
129
+
130
+ else:
131
+ raise TypeError(f"Expected keras.Model, got {type(obj)}")
132
+
133
+ def load(self, path: Path) -> Any:
134
+ """Load Keras model from path.
135
+
136
+ Args:
137
+ path: Directory path from which to load model
138
+
139
+ Returns:
140
+ Loaded Keras model
141
+ """
142
+ # Load metadata
143
+ metadata_path = path / "metadata.json"
144
+ if metadata_path.exists():
145
+ with open(metadata_path) as f:
146
+ json.load(f)
147
+ else:
148
+ pass
149
+
150
+ # Load model from .keras file
151
+ model_path = path / "model.keras"
152
+ if model_path.exists():
153
+ model = keras.models.load_model(model_path)
154
+ return model
155
+
156
+ # Fallback: try to load from SavedModel format
157
+ saved_model_path = path / "saved_model"
158
+ if saved_model_path.exists():
159
+ model = keras.models.load_model(saved_model_path)
160
+ return model
161
+
162
+ raise FileNotFoundError(f"No Keras model found at {path}")
163
+
164
+ @classmethod
165
+ def supported_types(cls) -> list[type]:
166
+ """Return Keras types supported by this materializer."""
167
+ return [keras.Model, keras.Sequential]
168
+
169
+ # Auto-register
170
+ register_materializer(KerasMaterializer)
171
+
172
+ else:
173
+ # Placeholder when Keras not available
174
+ class KerasMaterializer(BaseMaterializer):
175
+ """Placeholder materializer when Keras is not installed."""
176
+
177
+ def save(self, obj: Any, path: Path) -> None:
178
+ raise ImportError("Keras/TensorFlow is not installed. Install with: pip install tensorflow")
179
+
180
+ def load(self, path: Path) -> Any:
181
+ raise ImportError("Keras/TensorFlow is not installed. Install with: pip install tensorflow")
182
+
183
+ @classmethod
184
+ def supported_types(cls) -> list[type]:
185
+ return []
@@ -0,0 +1,94 @@
1
+ """NumPy materializer for array serialization."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from flowyml.storage.materializers.base import BaseMaterializer, register_materializer
8
+
9
+ try:
10
+ import numpy as np
11
+
12
+ NUMPY_AVAILABLE = True
13
+ except ImportError:
14
+ NUMPY_AVAILABLE = False
15
+
16
+
17
+ if NUMPY_AVAILABLE:
18
+
19
+ class NumPyMaterializer(BaseMaterializer):
20
+ """Materializer for NumPy arrays."""
21
+
22
+ def save(self, obj: Any, path: Path) -> None:
23
+ """Save NumPy array to path.
24
+
25
+ Args:
26
+ obj: NumPy array
27
+ path: Directory path where object should be saved
28
+ """
29
+ path.mkdir(parents=True, exist_ok=True)
30
+
31
+ # Save array
32
+ array_path = path / "array.npy"
33
+ np.save(array_path, obj)
34
+
35
+ # Save metadata
36
+ metadata = {
37
+ "type": "numpy_array",
38
+ "shape": list(obj.shape),
39
+ "dtype": str(obj.dtype),
40
+ "size": int(obj.size),
41
+ "ndim": int(obj.ndim),
42
+ }
43
+
44
+ # Add statistics for numerical arrays
45
+ if np.issubdtype(obj.dtype, np.number):
46
+ try:
47
+ metadata["min"] = float(np.min(obj))
48
+ metadata["max"] = float(np.max(obj))
49
+ metadata["mean"] = float(np.mean(obj))
50
+ metadata["std"] = float(np.std(obj))
51
+ except Exception:
52
+ pass
53
+
54
+ with open(path / "metadata.json", "w") as f:
55
+ json.dump(metadata, f, indent=2)
56
+
57
+ def load(self, path: Path) -> Any:
58
+ """Load NumPy array from path.
59
+
60
+ Args:
61
+ path: Directory path from which to load object
62
+
63
+ Returns:
64
+ Loaded NumPy array
65
+ """
66
+ array_path = path / "array.npy"
67
+
68
+ if not array_path.exists():
69
+ raise FileNotFoundError(f"Array file not found at {array_path}")
70
+
71
+ return np.load(array_path)
72
+
73
+ @classmethod
74
+ def supported_types(cls) -> list[type]:
75
+ """Return NumPy types supported by this materializer."""
76
+ return [np.ndarray]
77
+
78
+ # Auto-register
79
+ register_materializer(NumPyMaterializer)
80
+
81
+ else:
82
+ # Placeholder when NumPy not available
83
+ class NumPyMaterializer(BaseMaterializer):
84
+ """Placeholder materializer when NumPy is not installed."""
85
+
86
+ def save(self, obj: Any, path: Path) -> None:
87
+ raise ImportError("NumPy is not installed. Install with: pip install numpy")
88
+
89
+ def load(self, path: Path) -> Any:
90
+ raise ImportError("NumPy is not installed. Install with: pip install numpy")
91
+
92
+ @classmethod
93
+ def supported_types(cls) -> list[type]:
94
+ return []