eval-toolkit 0.27.1__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,98 @@
1
+ """Lightweight public Protocols with minimal dependency surface.
2
+
3
+ These contracts are intentionally kept out of the heavier implementation
4
+ modules so consumers can type adapters without importing pandas, matplotlib,
5
+ or filesystem-oriented helpers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping, Sequence
11
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
12
+
13
+ import numpy as np
14
+
15
+ if TYPE_CHECKING:
16
+ import pandas as pd
17
+
18
+ __all__ = [
19
+ "EvalSliceLike",
20
+ "PredictionReader",
21
+ "Scorer",
22
+ "SliceAwareScorer",
23
+ "Versioned",
24
+ ]
25
+
26
+
27
+ @runtime_checkable
28
+ class Scorer(Protocol):
29
+ """Anything exposing ``predict_proba(X) -> np.ndarray of P(positive)``.
30
+
31
+ Accepts ``list[str]``, ``np.ndarray``, or ``pd.Series`` of features.
32
+ Pandas is imported under ``TYPE_CHECKING`` only, so this Protocol
33
+ has no runtime pandas dependency.
34
+ """
35
+
36
+ def predict_proba( # pragma: no cover
37
+ self, X: Sequence[str] | np.ndarray | pd.Series
38
+ ) -> np.ndarray:
39
+ """Return one P(positive) score per input row."""
40
+ ...
41
+
42
+
43
+ @runtime_checkable
44
+ class SliceAwareScorer(Scorer, Protocol):
45
+ """Optional scorer contract for cost-controlled slice skipping."""
46
+
47
+ def should_score_slice(self, slice_name: str) -> bool: # pragma: no cover
48
+ """Return whether this scorer should run on the named slice."""
49
+ ...
50
+
51
+
52
+ @runtime_checkable
53
+ class Versioned(Protocol):
54
+ """Anything exposing a stable version string."""
55
+
56
+ @property
57
+ def version(self) -> str: # pragma: no cover
58
+ """Stable version string for this implementation."""
59
+ ...
60
+
61
+
62
+ @runtime_checkable
63
+ class EvalSliceLike(Protocol):
64
+ """Pandas-free slice surface needed by evaluation contracts."""
65
+
66
+ @property
67
+ def name(self) -> str: # pragma: no cover
68
+ """Stable slice identifier."""
69
+ ...
70
+
71
+ @property
72
+ def y_true(self) -> np.ndarray: # pragma: no cover
73
+ """Binary labels as a 1-D array."""
74
+ ...
75
+
76
+ @property
77
+ def features(self) -> Sequence[str]: # pragma: no cover
78
+ """Feature values passed to a scorer."""
79
+ ...
80
+
81
+ @property
82
+ def strata(self) -> np.ndarray | None: # pragma: no cover
83
+ """Optional stratum labels for slice-aware reporting."""
84
+ ...
85
+
86
+
87
+ @runtime_checkable
88
+ class PredictionReader(Protocol):
89
+ """Reads manifest-referenced prediction artifacts into column arrays."""
90
+
91
+ def read_predictions( # pragma: no cover
92
+ self,
93
+ uri: str,
94
+ *,
95
+ columns: Mapping[str, str],
96
+ ) -> Mapping[str, Sequence[object]]:
97
+ """Return a column-oriented table for the requested artifact URI."""
98
+ ...
@@ -0,0 +1,255 @@
1
+ """Provenance helpers: file hashing, run-directory layout, optional git SHA, figure metadata.
2
+
3
+ All filesystem-touching functions are isolated here so library callers can
4
+ import :mod:`eval_toolkit.metrics` and :mod:`eval_toolkit.bootstrap` without
5
+ pulling in any IO surface.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import subprocess
12
+ import time
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+
17
+ __all__ = [
18
+ "FileHash",
19
+ "FileHashMissing",
20
+ "capture_git_sha",
21
+ "compute_file_hash",
22
+ "figure_metadata",
23
+ "file_sha256",
24
+ "make_run_dir",
25
+ ]
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class FileHash:
30
+ """Sentinel-style hit for a successful file hash.
31
+
32
+ Returned (alongside :class:`FileHashMissing`) from
33
+ :func:`compute_file_hash` so callers can pattern-match on the outcome
34
+ rather than testing for a magic ``None`` (F5.1).
35
+ """
36
+
37
+ sha256: str
38
+
39
+ def __post_init__(self) -> None:
40
+ """Validate the hex-digest shape."""
41
+ if not isinstance(self.sha256, str) or len(self.sha256) != 64:
42
+ raise ValueError(
43
+ f"FileHash.sha256 must be a 64-character hex digest, got {self.sha256!r}"
44
+ )
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class FileHashMissing:
49
+ """Sentinel-style miss for a file hash that could not be computed.
50
+
51
+ Carries the reason ``"missing"`` (path does not exist) or
52
+ ``"not_a_file"`` (path exists but is not a regular file). Pair with
53
+ :class:`FileHash` for exhaustive pattern matching.
54
+ """
55
+
56
+ reason: str
57
+ path: str
58
+
59
+
60
+ def compute_file_hash(path: Path | str) -> FileHash | FileHashMissing:
61
+ """SHA-256 hex digest of an existing file (sentinel-typed).
62
+
63
+ Replaces the v1 :func:`file_sha256` ``str | None`` shape with an
64
+ explicit union (F5.1). Callers can match on the union members
65
+ instead of testing the return for ``None``:
66
+
67
+ .. code-block:: python
68
+
69
+ match compute_file_hash(path):
70
+ case FileHash(sha256=h):
71
+ record["digest"] = f"sha256:{h}"
72
+ case FileHashMissing(reason="missing"):
73
+ ...
74
+
75
+ :func:`file_sha256` remains available as a backward-compatible thin
76
+ wrapper that flattens to ``str | None``.
77
+
78
+ Examples
79
+ --------
80
+ >>> import tempfile
81
+ >>> from pathlib import Path
82
+ >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
83
+ ... _ = f.write("hello\\n")
84
+ ... p = Path(f.name)
85
+ >>> result = compute_file_hash(p)
86
+ >>> isinstance(result, FileHash) and len(result.sha256) == 64
87
+ True
88
+ >>> compute_file_hash("/no/such/file")
89
+ FileHashMissing(reason='missing', path='/no/such/file')
90
+ """
91
+ p = Path(path)
92
+ if not p.exists():
93
+ return FileHashMissing(reason="missing", path=str(path))
94
+ if not p.is_file():
95
+ return FileHashMissing(reason="not_a_file", path=str(path))
96
+ h = hashlib.sha256()
97
+ with p.open("rb") as fh:
98
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
99
+ h.update(chunk)
100
+ return FileHash(sha256=h.hexdigest())
101
+
102
+
103
+ def file_sha256(path: Path | str, *, strict: bool = False) -> str | None:
104
+ """SHA-256 hex digest of an existing file.
105
+
106
+ Default behavior is permissive: returns ``None`` if the path does not
107
+ exist or is not a regular file. Pass ``strict=True`` to raise
108
+ :class:`FileNotFoundError` instead — useful when the caller's invariants
109
+ require the digest to exist.
110
+
111
+ .. note::
112
+ New code should prefer :func:`compute_file_hash`, which returns a
113
+ :class:`FileHash` / :class:`FileHashMissing` union and avoids the
114
+ ``None`` ambiguity (F5.1). This helper is retained for backward
115
+ compatibility.
116
+
117
+ Parameters
118
+ ----------
119
+ path : pathlib.Path or str
120
+ strict : bool, optional
121
+ If ``True``, raise :class:`FileNotFoundError` when ``path`` is missing
122
+ or not a regular file. Default ``False`` (return ``None``).
123
+
124
+ Returns
125
+ -------
126
+ str or None
127
+ 64-character hex digest. ``None`` only when ``strict=False`` and the
128
+ path is absent / not a regular file.
129
+
130
+ Raises
131
+ ------
132
+ FileNotFoundError
133
+ Only when ``strict=True`` and ``path`` is missing or not a regular file.
134
+
135
+ Examples
136
+ --------
137
+ >>> import tempfile
138
+ >>> from pathlib import Path
139
+ >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
140
+ ... _ = f.write("hello\\n")
141
+ ... p = Path(f.name)
142
+ >>> digest = file_sha256(p)
143
+ >>> len(digest)
144
+ 64
145
+ >>> file_sha256("/no/such/file") is None
146
+ True
147
+ >>> try:
148
+ ... file_sha256("/no/such/file", strict=True)
149
+ ... except FileNotFoundError:
150
+ ... print("raised")
151
+ raised
152
+ """
153
+ result = compute_file_hash(path)
154
+ if isinstance(result, FileHash):
155
+ return result.sha256
156
+ if strict:
157
+ raise FileNotFoundError(f"file_sha256: path missing or not a regular file: {result.path}")
158
+ return None
159
+
160
+
161
+ def make_run_dir(base: Path | str, prefix: str = "run") -> Path:
162
+ """Create and return a timestamped run directory.
163
+
164
+ Format: ``{base}/{prefix}_{YYYY-MM-DD_HHMMSS}``.
165
+
166
+ Parameters
167
+ ----------
168
+ base : pathlib.Path or str
169
+ Parent directory for run dirs (e.g., ``evals/``).
170
+ prefix : str, optional
171
+ Prefix before the timestamp. Default ``"run"``.
172
+
173
+ Returns
174
+ -------
175
+ pathlib.Path
176
+ The newly-created (or pre-existing) run directory.
177
+ """
178
+ ts = time.strftime("%Y-%m-%d_%H%M%S")
179
+ run_dir = Path(base) / f"{prefix}_{ts}"
180
+ run_dir.mkdir(parents=True, exist_ok=True)
181
+ return run_dir
182
+
183
+
184
+ def capture_git_sha(repo_root: Path | str | None = None) -> str | None:
185
+ """Current HEAD full SHA, or ``None`` if not in a git repo.
186
+
187
+ Optional and injectable: callers that want a specific SHA (e.g., from CI
188
+ metadata) should pass it directly to whichever function consumes it
189
+ rather than calling this helper.
190
+
191
+ Parameters
192
+ ----------
193
+ repo_root : pathlib.Path or str or None, optional
194
+ Working directory for ``git``. ``None`` uses the current process cwd.
195
+
196
+ Returns
197
+ -------
198
+ str or None
199
+ Full SHA hex string, or ``None`` if ``git`` is unavailable or the
200
+ directory is not inside a repository.
201
+ """
202
+ cwd = Path(repo_root) if repo_root is not None else None
203
+ try:
204
+ result = subprocess.run(
205
+ ["git", "rev-parse", "HEAD"],
206
+ capture_output=True,
207
+ text=True,
208
+ check=True,
209
+ cwd=cwd,
210
+ )
211
+ return result.stdout.strip()
212
+ except (subprocess.CalledProcessError, FileNotFoundError):
213
+ return None
214
+
215
+
216
+ def figure_metadata(provenance: dict[str, str], dpi: int = 300) -> dict[str, str]:
217
+ """Build a JSON-serializable provenance dict for ``save_figure``.
218
+
219
+ Combines caller-supplied keys with auto-fields ``timestamp_utc``
220
+ (ISO-8601), ``matplotlib_version``, and ``figure_dpi``. Matches the
221
+ schema produced inside :func:`eval_toolkit.plotting.save_figure`.
222
+
223
+ Parameters
224
+ ----------
225
+ provenance : dict[str, str]
226
+ Caller-supplied provenance keys (e.g., ``git_sha``, ``run_id``).
227
+ dpi : int, optional
228
+ DPI to record. Default 300.
229
+
230
+ Returns
231
+ -------
232
+ dict[str, str]
233
+ Combined provenance dict.
234
+
235
+ Examples
236
+ --------
237
+ >>> meta = figure_metadata({"run_id": "test-001"}, dpi=150)
238
+ >>> sorted(meta.keys())
239
+ ['figure_dpi', 'matplotlib_version', 'run_id', 'timestamp_utc']
240
+ >>> meta["figure_dpi"]
241
+ '150'
242
+ """
243
+ try:
244
+ import matplotlib as mpl # noqa: PLC0415
245
+
246
+ matplotlib_version = str(mpl.__version__)
247
+ except ImportError:
248
+ matplotlib_version = "unavailable"
249
+
250
+ return {
251
+ **provenance,
252
+ "timestamp_utc": datetime.now(UTC).isoformat(timespec="seconds"),
253
+ "matplotlib_version": matplotlib_version,
254
+ "figure_dpi": str(dpi),
255
+ }
eval_toolkit/py.typed ADDED
File without changes
@@ -0,0 +1,155 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://eval-toolkit/schemas/manifest.v1.json",
4
+ "version": "1",
5
+ "title": "eval-toolkit RunManifest",
6
+ "description": "Schema for run_dir/manifest.json. Produced by eval_toolkit.manifest.write_manifest. NeurIPS Reproducibility Checklist-aligned.",
7
+ "type": "object",
8
+ "required": ["schema_version", "run_id", "code_versions", "env"],
9
+ "properties": {
10
+ "schema_version": {
11
+ "const": "v1"
12
+ },
13
+ "run_id": {
14
+ "type": "string",
15
+ "minLength": 1
16
+ },
17
+ "git_sha": {
18
+ "type": ["string", "null"]
19
+ },
20
+ "dirty_flag": {
21
+ "type": "boolean",
22
+ "description": "True iff the working tree had uncommitted changes at run time (NeurIPS clean-replay concern)."
23
+ },
24
+ "code_versions": {
25
+ "type": "object",
26
+ "description": "{package_name: version_string} — at minimum eval_toolkit.",
27
+ "additionalProperties": {
28
+ "type": "string"
29
+ }
30
+ },
31
+ "seeds": {
32
+ "type": "object",
33
+ "description": "{source: seed} for global / bootstrap / torch / dataloader RNGs.",
34
+ "additionalProperties": {
35
+ "type": "integer"
36
+ }
37
+ },
38
+ "data_hashes": {
39
+ "type": "object",
40
+ "description": "{logical_name: 'sha256:...'} for input artifacts.",
41
+ "additionalProperties": {
42
+ "type": "string"
43
+ }
44
+ },
45
+ "config_hash": {
46
+ "type": "string",
47
+ "description": "SHA-256 over the canonical-JSON encoding of the eval config."
48
+ },
49
+ "env": {
50
+ "type": "object",
51
+ "description": "Environment fingerprint (python, platform, key dep versions).",
52
+ "additionalProperties": {
53
+ "type": "string"
54
+ }
55
+ },
56
+ "gpu_info": {
57
+ "type": "object",
58
+ "description": "{name, count, memory_gb} from nvidia-smi (empty dict if unavailable).",
59
+ "additionalProperties": {
60
+ "type": "string"
61
+ }
62
+ },
63
+ "cuda_version": {
64
+ "type": ["string", "null"]
65
+ },
66
+ "wall_clock_seconds": {
67
+ "type": ["number", "null"]
68
+ },
69
+ "versioned_objects": {
70
+ "type": "object",
71
+ "description": "{name: version} for any Tier-2 implementation exposing a `version` attribute.",
72
+ "additionalProperties": {
73
+ "type": "string"
74
+ }
75
+ },
76
+ "leakage_report": {
77
+ "type": ["object", "null"],
78
+ "description": "Serialized LeakageReport (or null if no leakage checks ran).",
79
+ "properties": {
80
+ "findings": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "object",
84
+ "required": ["check_name", "severity", "drop_indices", "evidence", "message", "n_affected"],
85
+ "properties": {
86
+ "check_name": {"type": "string"},
87
+ "severity": {"enum": ["error", "warning", "info"]},
88
+ "drop_indices": {"type": "object"},
89
+ "evidence": {"type": "object"},
90
+ "message": {"type": "string"},
91
+ "n_affected": {"type": "integer", "minimum": 0}
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
97
+ "source_roles": {
98
+ "type": "array",
99
+ "description": "Optional generic source-role metadata for data-quality and evidence audits. Recommended roles: train, calibration, locked_eval, external_diagnostic, excluded; custom roles are allowed.",
100
+ "items": {
101
+ "type": "object",
102
+ "required": ["source", "role"],
103
+ "properties": {
104
+ "source": {"type": "string", "minLength": 1},
105
+ "role": {"type": "string", "minLength": 1},
106
+ "n_rows": {"type": "integer", "minimum": 0},
107
+ "notes": {"type": "string"},
108
+ "metadata": {"type": "object"}
109
+ },
110
+ "additionalProperties": true
111
+ }
112
+ },
113
+ "guardrails": {
114
+ "type": "array",
115
+ "description": "Optional predeclared guardrails for claim/evidence discipline.",
116
+ "items": {"type": "string", "minLength": 1}
117
+ },
118
+ "prediction_artifacts": {
119
+ "type": "array",
120
+ "description": "Optional references to retained prediction artifacts. The toolkit records artifact metadata and column mappings without mandating a storage format.",
121
+ "items": {
122
+ "type": "object",
123
+ "required": ["uri", "media_type", "columns"],
124
+ "properties": {
125
+ "uri": {"type": "string", "minLength": 1},
126
+ "media_type": {"type": "string", "minLength": 1},
127
+ "sha256": {"type": "string"},
128
+ "n_rows": {"type": "integer", "minimum": 0},
129
+ "role": {"type": "string", "minLength": 1},
130
+ "metadata": {"type": "object"},
131
+ "columns": {
132
+ "type": "object",
133
+ "required": ["label", "score"],
134
+ "properties": {
135
+ "row_id": {"type": "string", "minLength": 1},
136
+ "content_hash": {"type": "string", "minLength": 1},
137
+ "label": {"type": "string", "minLength": 1},
138
+ "score": {"type": "string", "minLength": 1},
139
+ "scorer": {"type": "string", "minLength": 1},
140
+ "slice": {"type": "string", "minLength": 1},
141
+ "text": {"type": "string", "minLength": 1},
142
+ "provenance": {
143
+ "type": "object",
144
+ "additionalProperties": {"type": "string"}
145
+ }
146
+ },
147
+ "additionalProperties": true
148
+ }
149
+ },
150
+ "additionalProperties": true
151
+ }
152
+ }
153
+ },
154
+ "additionalProperties": true
155
+ }