autoforge-engine 0.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.
- autoforge_engine-0.1.0.dist-info/METADATA +105 -0
- autoforge_engine-0.1.0.dist-info/RECORD +32 -0
- autoforge_engine-0.1.0.dist-info/WHEEL +5 -0
- autoforge_engine-0.1.0.dist-info/entry_points.txt +2 -0
- autoforge_engine-0.1.0.dist-info/licenses/LICENSE +0 -0
- autoforge_engine-0.1.0.dist-info/top_level.txt +1 -0
- modelforge/artifact_manager.py +485 -0
- modelforge/automl.py +1472 -0
- modelforge/cli.py +1258 -0
- modelforge/column_intelligence.py +404 -0
- modelforge/config.py +580 -0
- modelforge/cross_validation.py +749 -0
- modelforge/data_audit.py +392 -0
- modelforge/data_loader.py +76 -0
- modelforge/evaluation.py +397 -0
- modelforge/experiment_tracker.py +490 -0
- modelforge/explainability.py +346 -0
- modelforge/feature_engineering.py +393 -0
- modelforge/feature_selection.py +528 -0
- modelforge/hyperparameter_optimization.py +593 -0
- modelforge/model_registry.py +684 -0
- modelforge/model_screening.py +531 -0
- modelforge/persistence.py +456 -0
- modelforge/pipeline_generator.py +278 -0
- modelforge/prediction_validator.py +316 -0
- modelforge/preprocessing.py +179 -0
- modelforge/profiler.py +85 -0
- modelforge/ranking.py +351 -0
- modelforge/reproducibility.py +295 -0
- modelforge/reproducibility_integration.py +192 -0
- modelforge/run_manager.py +200 -0
- modelforge/target_selector.py +108 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autoforge-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A transparent, local-first AutoML framework for automated ML pipeline discovery.
|
|
5
|
+
Author: Aditya Kumar Singh
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Requires-Dist: pandas>=2.1
|
|
11
|
+
Requires-Dist: scikit-learn>=1.4
|
|
12
|
+
Requires-Dist: rich>=13.7
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Provides-Extra: boosting
|
|
16
|
+
Requires-Dist: xgboost>=2.0; extra == "boosting"
|
|
17
|
+
Requires-Dist: lightgbm>=4.0; extra == "boosting"
|
|
18
|
+
Requires-Dist: catboost>=1.2; extra == "boosting"
|
|
19
|
+
Provides-Extra: optimization
|
|
20
|
+
Requires-Dist: optuna>=3.6; extra == "optimization"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# ModelForge
|
|
29
|
+
|
|
30
|
+
ModelForge is a transparent, local-first AutoML framework for discovering,
|
|
31
|
+
evaluating, explaining, and persisting scikit-learn pipelines. It keeps the
|
|
32
|
+
workflow inspectable: data audits, preprocessing, model screening,
|
|
33
|
+
cross-validation, ranking, reproducibility metadata, and artifacts are all
|
|
34
|
+
available as Python objects and local files.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install -e ".[dev]"
|
|
40
|
+
modelforge train --data examples/house_prices.csv --target price \
|
|
41
|
+
--task-type regression --output model.joblib
|
|
42
|
+
modelforge predict --model model.joblib --data examples/house_prices_new.csv
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The same workflow is available from Python:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from modelforge import AutoML
|
|
49
|
+
|
|
50
|
+
automl = AutoML(cv=5, random_state=42)
|
|
51
|
+
result = automl.fit(data, target="price", task_type="regression")
|
|
52
|
+
automl.save("artifacts/house-prices.joblib", overwrite=True)
|
|
53
|
+
print(result["best_model"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What is included
|
|
57
|
+
|
|
58
|
+
ModelForge currently provides ingestion, target detection, profiling, column
|
|
59
|
+
intelligence, data-quality and leakage auditing, preprocessing, feature
|
|
60
|
+
engineering and selection, a model registry, screening, cross-validation,
|
|
61
|
+
ranking, explainability, persistence, prediction validation, optimization,
|
|
62
|
+
experiment tracking, run management, configuration, CLI workflows, failure
|
|
63
|
+
isolation, robustness checks, and reproducibility snapshots.
|
|
64
|
+
|
|
65
|
+
Optional model and optimization integrations are declared in `pyproject.toml`:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install -e ".[boosting,optimization]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## CLI
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
modelforge --help
|
|
75
|
+
modelforge models --task-type classification
|
|
76
|
+
modelforge experiments list --directory .modelforge/experiments
|
|
77
|
+
modelforge experiments get EXPERIMENT_ID
|
|
78
|
+
modelforge experiments compare ID_ONE ID_TWO
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The CLI uses Rich tables and keeps generated models, experiment JSON, and
|
|
82
|
+
metadata local by default.
|
|
83
|
+
|
|
84
|
+
## Examples and benchmarks
|
|
85
|
+
|
|
86
|
+
- `examples/end_to_end_regression.py` demonstrates a complete Python workflow.
|
|
87
|
+
- `examples/end_to_end_classification.py` demonstrates classification and
|
|
88
|
+
probability prediction.
|
|
89
|
+
- `benchmarks/benchmark_baselines.py` compares ModelForge with simple baseline
|
|
90
|
+
estimators and records runtime and quality metrics.
|
|
91
|
+
|
|
92
|
+
Run an example with `python examples/end_to_end_regression.py` or run the
|
|
93
|
+
benchmark with `python benchmarks/benchmark_baselines.py`.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
.venv\\Scripts\\python.exe -m pytest -q
|
|
99
|
+
ruff check .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
GitHub Actions runs the test suite on supported Python versions. The roadmap
|
|
103
|
+
for the remaining product work lives in `docs/ROADMAP.md`; it separates
|
|
104
|
+
implemented foundations from integrations that require optional dependencies
|
|
105
|
+
or additional design work.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
autoforge_engine-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
modelforge/artifact_manager.py,sha256=ZzINwGTiyy6aNf1hVW0lxodcseTbDLaRx8CDhmQPfsc,10310
|
|
3
|
+
modelforge/automl.py,sha256=TfAkoeK0VT8aHjxADDzx9D1n_G2IPkRP2QrfBvaYs7E,39010
|
|
4
|
+
modelforge/cli.py,sha256=gDUbnBYZe5MAjVR4dYUyF7Fn4lCshNLt7me2yJIKBHs,28438
|
|
5
|
+
modelforge/column_intelligence.py,sha256=6GbmXcPJtAGnM8Glg7i0CIOq6W0GXr6K3q8rSe8L68M,11648
|
|
6
|
+
modelforge/config.py,sha256=aJvEAFQ-gWkXb0EO7Z1sStmjQCcMlzrxOxL5CX15YGI,13415
|
|
7
|
+
modelforge/cross_validation.py,sha256=8EJzWTWKIH2pvZqzm3Hj2hdvX-5Mf7jCcWa3j02lxYs,18983
|
|
8
|
+
modelforge/data_audit.py,sha256=1_ea_72B9_NtFY1kcogLPIpQdR3RSrCuZI69yiLmEFk,10681
|
|
9
|
+
modelforge/data_loader.py,sha256=sTvkBKiCAc7GE-5z_h-TmpC30ggGa8GTdfIlmRsXkbI,1705
|
|
10
|
+
modelforge/evaluation.py,sha256=1GTUE07KHn9xift1vR0AKFkX8AF-7z8j68ZcC-JDcWw,9516
|
|
11
|
+
modelforge/experiment_tracker.py,sha256=g21CueH21G0ZOJ8oYuq9rCspZf63WUl5y_0Kyf6K-L4,11861
|
|
12
|
+
modelforge/explainability.py,sha256=zKldBF-WTqjbTU7M_8l1EHqUfyipYoXkGHQm5_eGPGc,8299
|
|
13
|
+
modelforge/feature_engineering.py,sha256=u91tSOcnZY1_dE5syn0Nfo8BacrgC4ES1GD7gY_u8zI,10056
|
|
14
|
+
modelforge/feature_selection.py,sha256=P5LsQutsKcdwee8g_4CXUx4UoegvjoEPO8TNiR5KMW8,13667
|
|
15
|
+
modelforge/hyperparameter_optimization.py,sha256=ZTV_4PI9pKHSOOmgH5QUdHzw6O64yddQntcu7p_55aE,14403
|
|
16
|
+
modelforge/model_registry.py,sha256=8wuqNR2DegYsWwXG_QsO9Fv6_CwfvqhFu1tH1gkZv_c,19532
|
|
17
|
+
modelforge/model_screening.py,sha256=On3GmIZrjcOW747V74S-RNtolX6djWaj-olcxvHYGQ4,13020
|
|
18
|
+
modelforge/persistence.py,sha256=yOeShK8kqrS-cLa4NHnYEQaP7baTzagYhI6tF1eMCjw,10221
|
|
19
|
+
modelforge/pipeline_generator.py,sha256=esWVlN4ZSOp9Vgg7tkvefdIdMidyh9tEDFBfHQfFKrs,6910
|
|
20
|
+
modelforge/prediction_validator.py,sha256=xMOH-oU4HpJ_P1f0yz_ONxsxL_azr0vW0fMHEnfwAUM,7539
|
|
21
|
+
modelforge/preprocessing.py,sha256=fpOYsBNgPgJiZ_O2WvICyvS9b3OIEq2lIRhNOTC3uUI,4490
|
|
22
|
+
modelforge/profiler.py,sha256=DOIHWcrDKvIoA8roS1qJO3U1sjWtjydbx8SuZHsiAhY,2486
|
|
23
|
+
modelforge/ranking.py,sha256=pqcKv4rhyiTcsKp8wQ65nqDAZbXq78bIsTS1B0yg2po,7991
|
|
24
|
+
modelforge/reproducibility.py,sha256=yz_ammbZb57ASE3zAmpxgwJ9ZHnyok2mUBR9L9jFgYU,8020
|
|
25
|
+
modelforge/reproducibility_integration.py,sha256=Lm5Zck6OphqTQQA3n1nvW_2xk6AkQMRnfAnEPnoDklc,4738
|
|
26
|
+
modelforge/run_manager.py,sha256=SkvPzUmJIEuABSNAz8ddlGPNRwQ4GIMrPAg7E9mOc9o,4568
|
|
27
|
+
modelforge/target_selector.py,sha256=eq9zeNL7A5iq0uRdI5GNKtqB1wdH1s0lAZdDZzKpXlc,3182
|
|
28
|
+
autoforge_engine-0.1.0.dist-info/METADATA,sha256=lRtEINfeV7EQzee93g4Gc26D4oCxjDaySvytqE-807g,3534
|
|
29
|
+
autoforge_engine-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
30
|
+
autoforge_engine-0.1.0.dist-info/entry_points.txt,sha256=X6ugP0SKpNhhpiWDbNpY_XYrAsOjR1yPl-JUQEepct4,50
|
|
31
|
+
autoforge_engine-0.1.0.dist-info/top_level.txt,sha256=ZyKkWfjMHbQ1mlQyLT2xZlbeT0M1jOjB4nRWVR_YiI8,11
|
|
32
|
+
autoforge_engine-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
modelforge
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import joblib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArtifactManager:
|
|
13
|
+
"""
|
|
14
|
+
Manage persisted ModelForge model artifacts and metadata.
|
|
15
|
+
|
|
16
|
+
Each artifact consists of:
|
|
17
|
+
<artifact_id>.pkl
|
|
18
|
+
<artifact_id>.json
|
|
19
|
+
|
|
20
|
+
The JSON file stores metadata describing the trained model.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
directory: str | Path = ".modelforge/artifacts",
|
|
26
|
+
):
|
|
27
|
+
self.directory = Path(directory)
|
|
28
|
+
|
|
29
|
+
self.directory.mkdir(
|
|
30
|
+
parents=True,
|
|
31
|
+
exist_ok=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def create_artifact_id(
|
|
35
|
+
self,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Generate a unique artifact identifier.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
timestamp = datetime.now(
|
|
42
|
+
timezone.utc
|
|
43
|
+
).strftime("%Y%m%d%H%M%S")
|
|
44
|
+
|
|
45
|
+
short_uuid = uuid.uuid4().hex[:8]
|
|
46
|
+
|
|
47
|
+
return f"artifact_{timestamp}_{short_uuid}"
|
|
48
|
+
|
|
49
|
+
def save(
|
|
50
|
+
self,
|
|
51
|
+
pipeline: Any,
|
|
52
|
+
metadata: dict[str, Any] | None = None,
|
|
53
|
+
artifact_id: str | None = None,
|
|
54
|
+
overwrite: bool = False,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Save a trained pipeline and its metadata.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Artifact ID.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if pipeline is None:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"pipeline cannot be None."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if metadata is not None:
|
|
69
|
+
if not isinstance(
|
|
70
|
+
metadata,
|
|
71
|
+
dict,
|
|
72
|
+
):
|
|
73
|
+
raise TypeError(
|
|
74
|
+
"metadata must be a dictionary."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
artifact_id = (
|
|
78
|
+
artifact_id
|
|
79
|
+
or self.create_artifact_id()
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self._validate_artifact_id(
|
|
83
|
+
artifact_id
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
model_path = self.model_path(
|
|
87
|
+
artifact_id
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
metadata_path = self.metadata_path(
|
|
91
|
+
artifact_id
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not overwrite:
|
|
95
|
+
if model_path.exists():
|
|
96
|
+
raise FileExistsError(
|
|
97
|
+
f"Artifact '{artifact_id}' "
|
|
98
|
+
"already exists."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if metadata_path.exists():
|
|
102
|
+
raise FileExistsError(
|
|
103
|
+
f"Metadata for artifact "
|
|
104
|
+
f"'{artifact_id}' already exists."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
artifact_metadata = {
|
|
108
|
+
"artifact_id": artifact_id,
|
|
109
|
+
"created_at": datetime.now(
|
|
110
|
+
timezone.utc
|
|
111
|
+
).isoformat(),
|
|
112
|
+
"artifact_type": "model_pipeline",
|
|
113
|
+
"metadata": self._safe_value(
|
|
114
|
+
metadata or {}
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
joblib.dump(
|
|
119
|
+
pipeline,
|
|
120
|
+
model_path,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self._write_json(
|
|
124
|
+
metadata_path,
|
|
125
|
+
artifact_metadata,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return artifact_id
|
|
129
|
+
|
|
130
|
+
def load(
|
|
131
|
+
self,
|
|
132
|
+
artifact_id: str,
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""
|
|
135
|
+
Load a saved model pipeline.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
self._validate_artifact_id(
|
|
139
|
+
artifact_id
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
path = self.model_path(
|
|
143
|
+
artifact_id
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if not path.exists():
|
|
147
|
+
raise FileNotFoundError(
|
|
148
|
+
f"Artifact '{artifact_id}' "
|
|
149
|
+
"does not exist."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return joblib.load(path)
|
|
153
|
+
|
|
154
|
+
def get_metadata(
|
|
155
|
+
self,
|
|
156
|
+
artifact_id: str,
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Load artifact metadata.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
self._validate_artifact_id(
|
|
163
|
+
artifact_id
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
path = self.metadata_path(
|
|
167
|
+
artifact_id
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if not path.exists():
|
|
171
|
+
raise FileNotFoundError(
|
|
172
|
+
f"Metadata for artifact "
|
|
173
|
+
f"'{artifact_id}' does not exist."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
with path.open(
|
|
177
|
+
"r",
|
|
178
|
+
encoding="utf-8",
|
|
179
|
+
) as file:
|
|
180
|
+
return json.load(file)
|
|
181
|
+
|
|
182
|
+
def exists(
|
|
183
|
+
self,
|
|
184
|
+
artifact_id: str,
|
|
185
|
+
) -> bool:
|
|
186
|
+
"""
|
|
187
|
+
Check whether an artifact exists.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
self._validate_artifact_id(
|
|
191
|
+
artifact_id
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return self.model_path(
|
|
195
|
+
artifact_id
|
|
196
|
+
).exists()
|
|
197
|
+
|
|
198
|
+
def delete(
|
|
199
|
+
self,
|
|
200
|
+
artifact_id: str,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Delete an artifact and its metadata.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
self._validate_artifact_id(
|
|
207
|
+
artifact_id
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
model_path = self.model_path(
|
|
211
|
+
artifact_id
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
metadata_path = self.metadata_path(
|
|
215
|
+
artifact_id
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if not model_path.exists():
|
|
219
|
+
raise FileNotFoundError(
|
|
220
|
+
f"Artifact '{artifact_id}' "
|
|
221
|
+
"does not exist."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
model_path.unlink()
|
|
225
|
+
|
|
226
|
+
if metadata_path.exists():
|
|
227
|
+
metadata_path.unlink()
|
|
228
|
+
|
|
229
|
+
def list_artifacts(
|
|
230
|
+
self,
|
|
231
|
+
) -> list[dict[str, Any]]:
|
|
232
|
+
"""
|
|
233
|
+
Return metadata for all saved artifacts.
|
|
234
|
+
|
|
235
|
+
Results are ordered newest first.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
artifacts = []
|
|
239
|
+
|
|
240
|
+
for path in self.directory.glob(
|
|
241
|
+
"artifact_*.json"
|
|
242
|
+
):
|
|
243
|
+
try:
|
|
244
|
+
with path.open(
|
|
245
|
+
"r",
|
|
246
|
+
encoding="utf-8",
|
|
247
|
+
) as file:
|
|
248
|
+
metadata = json.load(file)
|
|
249
|
+
|
|
250
|
+
artifact_id = metadata.get(
|
|
251
|
+
"artifact_id"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if not artifact_id:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
artifacts.append(
|
|
258
|
+
metadata
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
except (
|
|
262
|
+
OSError,
|
|
263
|
+
json.JSONDecodeError,
|
|
264
|
+
):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
artifacts.sort(
|
|
268
|
+
key=lambda item: item.get(
|
|
269
|
+
"created_at",
|
|
270
|
+
"",
|
|
271
|
+
),
|
|
272
|
+
reverse=True,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return artifacts
|
|
276
|
+
|
|
277
|
+
def count(
|
|
278
|
+
self,
|
|
279
|
+
) -> int:
|
|
280
|
+
"""
|
|
281
|
+
Return the number of saved artifacts.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
return len(
|
|
285
|
+
list(
|
|
286
|
+
self.directory.glob(
|
|
287
|
+
"artifact_*.pkl"
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def model_path(
|
|
293
|
+
self,
|
|
294
|
+
artifact_id: str,
|
|
295
|
+
) -> Path:
|
|
296
|
+
"""
|
|
297
|
+
Return the model path for an artifact.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
self._validate_artifact_id(
|
|
301
|
+
artifact_id
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
self.directory
|
|
306
|
+
/ f"{artifact_id}.pkl"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def metadata_path(
|
|
310
|
+
self,
|
|
311
|
+
artifact_id: str,
|
|
312
|
+
) -> Path:
|
|
313
|
+
"""
|
|
314
|
+
Return the metadata path for an artifact.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
self._validate_artifact_id(
|
|
318
|
+
artifact_id
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
self.directory
|
|
323
|
+
/ f"{artifact_id}.json"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def validate(
|
|
327
|
+
self,
|
|
328
|
+
artifact_id: str,
|
|
329
|
+
) -> dict[str, Any]:
|
|
330
|
+
"""
|
|
331
|
+
Validate that an artifact has both model
|
|
332
|
+
and metadata files.
|
|
333
|
+
|
|
334
|
+
Returns a validation report.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
self._validate_artifact_id(
|
|
338
|
+
artifact_id
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
model_path = self.model_path(
|
|
342
|
+
artifact_id
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
metadata_path = self.metadata_path(
|
|
346
|
+
artifact_id
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
model_exists = model_path.exists()
|
|
350
|
+
metadata_exists = metadata_path.exists()
|
|
351
|
+
|
|
352
|
+
metadata_valid = False
|
|
353
|
+
metadata = None
|
|
354
|
+
|
|
355
|
+
if metadata_exists:
|
|
356
|
+
try:
|
|
357
|
+
metadata = self.get_metadata(
|
|
358
|
+
artifact_id
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
metadata_valid = (
|
|
362
|
+
isinstance(
|
|
363
|
+
metadata,
|
|
364
|
+
dict,
|
|
365
|
+
)
|
|
366
|
+
and metadata.get(
|
|
367
|
+
"artifact_id"
|
|
368
|
+
)
|
|
369
|
+
== artifact_id
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
except (
|
|
373
|
+
OSError,
|
|
374
|
+
json.JSONDecodeError,
|
|
375
|
+
):
|
|
376
|
+
metadata_valid = False
|
|
377
|
+
|
|
378
|
+
valid = (
|
|
379
|
+
model_exists
|
|
380
|
+
and metadata_exists
|
|
381
|
+
and metadata_valid
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
"artifact_id": artifact_id,
|
|
386
|
+
"valid": valid,
|
|
387
|
+
"model_exists": model_exists,
|
|
388
|
+
"metadata_exists": metadata_exists,
|
|
389
|
+
"metadata_valid": metadata_valid,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def _write_json(
|
|
394
|
+
path: Path,
|
|
395
|
+
data: dict[str, Any],
|
|
396
|
+
) -> None:
|
|
397
|
+
with path.open(
|
|
398
|
+
"w",
|
|
399
|
+
encoding="utf-8",
|
|
400
|
+
) as file:
|
|
401
|
+
json.dump(
|
|
402
|
+
data,
|
|
403
|
+
file,
|
|
404
|
+
indent=2,
|
|
405
|
+
ensure_ascii=False,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def _validate_artifact_id(
|
|
410
|
+
artifact_id: str,
|
|
411
|
+
) -> None:
|
|
412
|
+
if not isinstance(
|
|
413
|
+
artifact_id,
|
|
414
|
+
str,
|
|
415
|
+
):
|
|
416
|
+
raise TypeError(
|
|
417
|
+
"artifact_id must be a string."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if not artifact_id:
|
|
421
|
+
raise ValueError(
|
|
422
|
+
"artifact_id cannot be empty."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if Path(artifact_id).name != artifact_id:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
"Invalid artifact_id."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if not artifact_id.startswith(
|
|
431
|
+
"artifact_"
|
|
432
|
+
):
|
|
433
|
+
raise ValueError(
|
|
434
|
+
"Invalid artifact_id."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _safe_value(
|
|
439
|
+
value: Any,
|
|
440
|
+
) -> Any:
|
|
441
|
+
if value is None:
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
if isinstance(
|
|
445
|
+
value,
|
|
446
|
+
(str, int, float, bool),
|
|
447
|
+
):
|
|
448
|
+
return value
|
|
449
|
+
|
|
450
|
+
if isinstance(
|
|
451
|
+
value,
|
|
452
|
+
Path,
|
|
453
|
+
):
|
|
454
|
+
return str(value)
|
|
455
|
+
|
|
456
|
+
if isinstance(
|
|
457
|
+
value,
|
|
458
|
+
dict,
|
|
459
|
+
):
|
|
460
|
+
return {
|
|
461
|
+
str(key): ArtifactManager._safe_value(
|
|
462
|
+
item
|
|
463
|
+
)
|
|
464
|
+
for key, item in value.items()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if isinstance(
|
|
468
|
+
value,
|
|
469
|
+
(list, tuple, set),
|
|
470
|
+
):
|
|
471
|
+
return [
|
|
472
|
+
ArtifactManager._safe_value(
|
|
473
|
+
item
|
|
474
|
+
)
|
|
475
|
+
for item in value
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
json.dumps(value)
|
|
480
|
+
return value
|
|
481
|
+
except (
|
|
482
|
+
TypeError,
|
|
483
|
+
ValueError,
|
|
484
|
+
):
|
|
485
|
+
return str(value)
|