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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modelforge = modelforge.cli:app
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)