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,192 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import pandas as pd
7
+
8
+ from modelforge.reproducibility import ReproducibilityManager
9
+
10
+
11
+ class ReproducibilityIntegration:
12
+ """
13
+ Integrates reproducibility metadata with ModelForge runs,
14
+ experiments, and saved model artifacts.
15
+ """
16
+
17
+ def __init__(self, random_state: int = 42) -> None:
18
+ self.manager = ReproducibilityManager(
19
+ random_state=random_state
20
+ )
21
+
22
+ def create_run_snapshot(
23
+ self,
24
+ data: pd.DataFrame,
25
+ configuration: dict[str, Any],
26
+ target: str | None = None,
27
+ task_type: str | None = None,
28
+ run_id: str | None = None,
29
+ extra_metadata: dict[str, Any] | None = None,
30
+ ) -> dict[str, Any]:
31
+ """
32
+ Create reproducibility information for a ModelForge run.
33
+ """
34
+
35
+ metadata = dict(extra_metadata or {})
36
+
37
+ if run_id is not None:
38
+ metadata["run_id"] = run_id
39
+
40
+ return self.manager.create_snapshot(
41
+ data=data,
42
+ configuration=configuration,
43
+ target=target,
44
+ task_type=task_type,
45
+ extra_metadata=metadata,
46
+ )
47
+
48
+ def attach_to_result(
49
+ self,
50
+ result: dict[str, Any],
51
+ snapshot: dict[str, Any],
52
+ ) -> dict[str, Any]:
53
+ """
54
+ Attach reproducibility information to a ModelForge result.
55
+ """
56
+
57
+ if not isinstance(result, dict):
58
+ raise TypeError("result must be a dictionary.")
59
+
60
+ if not isinstance(snapshot, dict):
61
+ raise TypeError("snapshot must be a dictionary.")
62
+
63
+ result["reproducibility"] = snapshot
64
+
65
+ return result
66
+
67
+ def save_snapshot(
68
+ self,
69
+ snapshot: dict[str, Any],
70
+ directory: str | Path,
71
+ run_id: str,
72
+ overwrite: bool = False,
73
+ ) -> str:
74
+ """
75
+ Save a run reproducibility snapshot inside an experiment directory.
76
+ """
77
+
78
+ if not isinstance(run_id, str) or not run_id.strip():
79
+ raise ValueError(
80
+ "run_id must be a non-empty string."
81
+ )
82
+
83
+ directory = Path(directory)
84
+
85
+ path = (
86
+ directory
87
+ / run_id
88
+ / "reproducibility.json"
89
+ )
90
+
91
+ return self.manager.save_snapshot(
92
+ snapshot=snapshot,
93
+ path=path,
94
+ overwrite=overwrite,
95
+ )
96
+
97
+ def load_snapshot(
98
+ self,
99
+ directory: str | Path,
100
+ run_id: str,
101
+ ) -> dict[str, Any]:
102
+ """
103
+ Load a saved run reproducibility snapshot.
104
+ """
105
+
106
+ if not isinstance(run_id, str) or not run_id.strip():
107
+ raise ValueError(
108
+ "run_id must be a non-empty string."
109
+ )
110
+
111
+ path = (
112
+ Path(directory)
113
+ / run_id
114
+ / "reproducibility.json"
115
+ )
116
+
117
+ return self.manager.load_snapshot(path)
118
+
119
+ def attach_artifact_integrity(
120
+ self,
121
+ metadata: dict[str, Any],
122
+ artifact_path: str | Path,
123
+ ) -> dict[str, Any]:
124
+ """
125
+ Attach a SHA-256 fingerprint for a saved model artifact.
126
+ """
127
+
128
+ if not isinstance(metadata, dict):
129
+ raise TypeError(
130
+ "metadata must be a dictionary."
131
+ )
132
+
133
+ fingerprint = self.manager.artifact_fingerprint(
134
+ artifact_path
135
+ )
136
+
137
+ updated = dict(metadata)
138
+
139
+ updated["artifact"] = {
140
+ "path": str(artifact_path),
141
+ "sha256": fingerprint,
142
+ }
143
+
144
+ return updated
145
+
146
+ def verify_artifact(
147
+ self,
148
+ artifact_path: str | Path,
149
+ expected_fingerprint: str,
150
+ ) -> bool:
151
+ """
152
+ Verify the integrity of a saved artifact.
153
+ """
154
+
155
+ return self.manager.verify_artifact(
156
+ artifact_path,
157
+ expected_fingerprint,
158
+ )
159
+
160
+ def verify_dataset(
161
+ self,
162
+ data: pd.DataFrame,
163
+ snapshot: dict[str, Any],
164
+ ) -> bool:
165
+ """
166
+ Verify that a dataset matches the snapshot fingerprint.
167
+ """
168
+
169
+ if not isinstance(snapshot, dict):
170
+ raise TypeError(
171
+ "snapshot must be a dictionary."
172
+ )
173
+
174
+ dataset = snapshot.get("dataset")
175
+
176
+ if not isinstance(dataset, dict):
177
+ raise ValueError(
178
+ "Snapshot does not contain dataset metadata."
179
+ )
180
+
181
+ expected = dataset.get("fingerprint")
182
+
183
+ if not isinstance(expected, str):
184
+ raise ValueError(
185
+ "Snapshot does not contain a valid "
186
+ "dataset fingerprint."
187
+ )
188
+
189
+ return self.manager.verify_dataset(
190
+ data,
191
+ expected,
192
+ )
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ class RunManager:
10
+ """
11
+ Manage the lifecycle and metadata of a ModelForge run.
12
+
13
+ A run represents one execution of an AutoML workflow.
14
+ """
15
+
16
+ def __init__(self) -> None:
17
+ self.run_id: str | None = None
18
+ self.status: str = "not_started"
19
+ self.started_at: str | None = None
20
+ self.finished_at: str | None = None
21
+ self.duration_seconds: float | None = None
22
+ self.metadata: dict[str, Any] = {}
23
+ self._start_time: float | None = None
24
+
25
+ def start(
26
+ self,
27
+ metadata: dict[str, Any] | None = None,
28
+ ) -> str:
29
+ """
30
+ Start a new run.
31
+
32
+ Returns the generated run ID.
33
+ """
34
+
35
+ if self.status == "running":
36
+ raise RuntimeError(
37
+ "A run is already in progress."
38
+ )
39
+
40
+ self.run_id = self._create_run_id()
41
+ self.status = "running"
42
+ self.started_at = (
43
+ datetime.now(timezone.utc).isoformat()
44
+ )
45
+ self.finished_at = None
46
+ self.duration_seconds = None
47
+ self._start_time = time.perf_counter()
48
+
49
+ self.metadata = dict(
50
+ metadata or {}
51
+ )
52
+
53
+ return self.run_id
54
+
55
+ def complete(
56
+ self,
57
+ metadata: dict[str, Any] | None = None,
58
+ ) -> dict[str, Any]:
59
+ """
60
+ Mark the current run as completed.
61
+ """
62
+
63
+ self._require_running()
64
+
65
+ self._finish(
66
+ status="completed",
67
+ metadata=metadata,
68
+ )
69
+
70
+ return self.summary()
71
+
72
+ def fail(
73
+ self,
74
+ error: Exception | str,
75
+ metadata: dict[str, Any] | None = None,
76
+ ) -> dict[str, Any]:
77
+ """
78
+ Mark the current run as failed.
79
+ """
80
+
81
+ self._require_running()
82
+
83
+ error_message = str(error)
84
+
85
+ failure_metadata = dict(
86
+ metadata or {}
87
+ )
88
+
89
+ failure_metadata["error"] = (
90
+ error_message
91
+ )
92
+
93
+ self._finish(
94
+ status="failed",
95
+ metadata=failure_metadata,
96
+ )
97
+
98
+ return self.summary()
99
+
100
+ def update(
101
+ self,
102
+ metadata: dict[str, Any],
103
+ ) -> None:
104
+ """
105
+ Add or update run metadata.
106
+ """
107
+
108
+ if not isinstance(
109
+ metadata,
110
+ dict,
111
+ ):
112
+ raise TypeError(
113
+ "metadata must be a dictionary."
114
+ )
115
+
116
+ if self.status != "running":
117
+ raise RuntimeError(
118
+ "Run metadata can only be "
119
+ "updated while a run is running."
120
+ )
121
+
122
+ self.metadata.update(
123
+ metadata
124
+ )
125
+
126
+ def summary(self) -> dict[str, Any]:
127
+ """
128
+ Return the current run summary.
129
+ """
130
+
131
+ return {
132
+ "run_id": self.run_id,
133
+ "status": self.status,
134
+ "started_at": self.started_at,
135
+ "finished_at": self.finished_at,
136
+ "duration_seconds": (
137
+ self.duration_seconds
138
+ ),
139
+ "metadata": dict(
140
+ self.metadata
141
+ ),
142
+ }
143
+
144
+ def is_running(self) -> bool:
145
+ """
146
+ Return whether a run is currently active.
147
+ """
148
+
149
+ return self.status == "running"
150
+
151
+ def reset(self) -> None:
152
+ """
153
+ Reset the manager to its initial state.
154
+ """
155
+
156
+ self.run_id = None
157
+ self.status = "not_started"
158
+ self.started_at = None
159
+ self.finished_at = None
160
+ self.duration_seconds = None
161
+ self.metadata = {}
162
+ self._start_time = None
163
+
164
+ def _finish(
165
+ self,
166
+ status: str,
167
+ metadata: dict[str, Any] | None,
168
+ ) -> None:
169
+ self.status = status
170
+
171
+ self.finished_at = (
172
+ datetime.now(timezone.utc).isoformat()
173
+ )
174
+
175
+ if self._start_time is not None:
176
+ self.duration_seconds = (
177
+ time.perf_counter()
178
+ - self._start_time
179
+ )
180
+
181
+ if metadata:
182
+ self.metadata.update(
183
+ metadata
184
+ )
185
+
186
+ def _require_running(self) -> None:
187
+ if self.status != "running":
188
+ raise RuntimeError(
189
+ "No active run is currently running."
190
+ )
191
+
192
+ @staticmethod
193
+ def _create_run_id() -> str:
194
+ timestamp = datetime.now(
195
+ timezone.utc
196
+ ).strftime("%Y%m%d%H%M%S")
197
+
198
+ short_uuid = uuid.uuid4().hex[:8]
199
+
200
+ return f"run_{timestamp}_{short_uuid}"
@@ -0,0 +1,108 @@
1
+ import pandas as pd
2
+
3
+
4
+ class TargetSelector:
5
+ """Validate and analyze the target column selected by the user."""
6
+
7
+ def select(
8
+ self,
9
+ data: pd.DataFrame,
10
+ target: str,
11
+ task_type: str | None = None,
12
+ ) -> dict:
13
+ """
14
+ Validate the selected target and determine the ML task type.
15
+
16
+ Parameters
17
+ ----------
18
+ data : pd.DataFrame
19
+ Input dataset.
20
+
21
+ target : str
22
+ Name of the target column selected by the user.
23
+
24
+ task_type : str | None
25
+ Optional explicit task type:
26
+ 'regression' or 'classification'.
27
+
28
+ Returns
29
+ -------
30
+ dict
31
+ Target information and task type.
32
+ """
33
+
34
+ if not isinstance(data, pd.DataFrame):
35
+ raise TypeError("data must be a pandas DataFrame.")
36
+
37
+ if data.empty:
38
+ raise ValueError("Dataset is empty.")
39
+
40
+ if not target:
41
+ raise ValueError("Target column must be provided.")
42
+
43
+ if target not in data.columns:
44
+ raise ValueError(
45
+ f"Target column '{target}' does not exist in the dataset."
46
+ )
47
+
48
+ target_series = data[target]
49
+
50
+ if target_series.isna().all():
51
+ raise ValueError(
52
+ f"Target column '{target}' contains only missing values."
53
+ )
54
+
55
+ if task_type is not None:
56
+ task_type = task_type.lower()
57
+
58
+ if task_type not in {"regression", "classification"}:
59
+ raise ValueError(
60
+ "task_type must be 'regression' or 'classification'."
61
+ )
62
+ else:
63
+ task_type = self._detect_task_type(target_series)
64
+
65
+ return {
66
+ "target": target,
67
+ "task_type": task_type,
68
+ "dtype": str(target_series.dtype),
69
+ "unique_values": int(target_series.nunique(dropna=True)),
70
+ "missing_values": int(target_series.isna().sum()),
71
+ "rows": int(len(target_series)),
72
+ }
73
+
74
+ @staticmethod
75
+ def _detect_task_type(target: pd.Series) -> str:
76
+ """Detect the likely ML task from the target column."""
77
+
78
+ if pd.api.types.is_bool_dtype(target):
79
+ return "classification"
80
+
81
+ if pd.api.types.is_object_dtype(target):
82
+ return "classification"
83
+
84
+ if isinstance(target.dtype, pd.CategoricalDtype):
85
+ return "classification"
86
+
87
+ if pd.api.types.is_numeric_dtype(target):
88
+ unique_values = target.nunique(dropna=True)
89
+ total_values = target.notna().sum()
90
+
91
+ if total_values == 0:
92
+ raise ValueError("Target contains no valid values.")
93
+
94
+ unique_ratio = unique_values / total_values
95
+
96
+ # A small number of repeated integer values is likely
97
+ # classification. High-cardinality numeric targets are
98
+ # treated as regression.
99
+ if (
100
+ pd.api.types.is_integer_dtype(target)
101
+ and unique_values <= 10
102
+ and unique_ratio <= 0.20
103
+ ):
104
+ return "classification"
105
+
106
+ return "regression"
107
+
108
+ return "classification"