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,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 []
|