modeltest 0.2.0__tar.gz

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.
Files changed (43) hide show
  1. modeltest-0.2.0/LICENSE +21 -0
  2. modeltest-0.2.0/PKG-INFO +252 -0
  3. modeltest-0.2.0/README.md +209 -0
  4. modeltest-0.2.0/modeltest/__init__.py +28 -0
  5. modeltest-0.2.0/modeltest/cli.py +92 -0
  6. modeltest-0.2.0/modeltest/config.py +230 -0
  7. modeltest-0.2.0/modeltest/core/__init__.py +18 -0
  8. modeltest-0.2.0/modeltest/core/base.py +258 -0
  9. modeltest-0.2.0/modeltest/core/report.py +93 -0
  10. modeltest-0.2.0/modeltest/core/runner.py +14 -0
  11. modeltest-0.2.0/modeltest/integrations/__init__.py +7 -0
  12. modeltest-0.2.0/modeltest/integrations/mlflow.py +116 -0
  13. modeltest-0.2.0/modeltest/scenarios/__init__.py +27 -0
  14. modeltest-0.2.0/modeltest/scenarios/_utils.py +146 -0
  15. modeltest-0.2.0/modeltest/scenarios/data.py +43 -0
  16. modeltest-0.2.0/modeltest/scenarios/drift.py +84 -0
  17. modeltest-0.2.0/modeltest/scenarios/explainability.py +150 -0
  18. modeltest-0.2.0/modeltest/scenarios/fairness.py +81 -0
  19. modeltest-0.2.0/modeltest/scenarios/performance.py +147 -0
  20. modeltest-0.2.0/modeltest/scenarios/robustness.py +52 -0
  21. modeltest-0.2.0/modeltest/wrappers.py +185 -0
  22. modeltest-0.2.0/modeltest.egg-info/PKG-INFO +252 -0
  23. modeltest-0.2.0/modeltest.egg-info/SOURCES.txt +41 -0
  24. modeltest-0.2.0/modeltest.egg-info/dependency_links.txt +1 -0
  25. modeltest-0.2.0/modeltest.egg-info/entry_points.txt +2 -0
  26. modeltest-0.2.0/modeltest.egg-info/requires.txt +19 -0
  27. modeltest-0.2.0/modeltest.egg-info/top_level.txt +1 -0
  28. modeltest-0.2.0/pyproject.toml +68 -0
  29. modeltest-0.2.0/setup.cfg +4 -0
  30. modeltest-0.2.0/tests/test_bootstrap.py +135 -0
  31. modeltest-0.2.0/tests/test_cache.py +92 -0
  32. modeltest-0.2.0/tests/test_cli.py +167 -0
  33. modeltest-0.2.0/tests/test_config.py +143 -0
  34. modeltest-0.2.0/tests/test_core.py +263 -0
  35. modeltest-0.2.0/tests/test_custom.py +129 -0
  36. modeltest-0.2.0/tests/test_data_invariants.py +73 -0
  37. modeltest-0.2.0/tests/test_drift.py +131 -0
  38. modeltest-0.2.0/tests/test_explainability.py +165 -0
  39. modeltest-0.2.0/tests/test_fairness.py +98 -0
  40. modeltest-0.2.0/tests/test_mlflow.py +155 -0
  41. modeltest-0.2.0/tests/test_pipeline.py +122 -0
  42. modeltest-0.2.0/tests/test_report.py +94 -0
  43. modeltest-0.2.0/tests/test_wrappers.py +460 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 modeltest contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: modeltest
3
+ Version: 0.2.0
4
+ Summary: If you test your code, why not your model? Unit tests for machine learning models.
5
+ Author: modeltest contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Tzinny-dev/model-test
8
+ Project-URL: Repository, https://github.com/Tzinny-dev/model-test
9
+ Project-URL: Issues, https://github.com/Tzinny-dev/model-test/issues
10
+ Keywords: machine-learning,mlops,testing,validation,ci
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy
27
+ Requires-Dist: pandas
28
+ Requires-Dist: scikit-learn
29
+ Requires-Dist: scipy
30
+ Requires-Dist: PyYAML
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest; extra == "dev"
33
+ Requires-Dist: pytest-cov; extra == "dev"
34
+ Requires-Dist: ruff; extra == "dev"
35
+ Requires-Dist: pre-commit; extra == "dev"
36
+ Requires-Dist: shap; extra == "dev"
37
+ Requires-Dist: mlflow; extra == "dev"
38
+ Provides-Extra: explain
39
+ Requires-Dist: shap; extra == "explain"
40
+ Provides-Extra: mlflow
41
+ Requires-Dist: mlflow; extra == "mlflow"
42
+ Dynamic: license-file
43
+
44
+ # modeltest
45
+
46
+ > If you test your code, why not your model?
47
+
48
+ `modeltest` is a unit-testing framework for machine learning models. It lets you
49
+ define contracts for model quality, robustness, fairness, and data invariants,
50
+ and run them automatically in your CI/CD pipeline — just like `pytest` for code.
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ from modeltest import ModelSuite
56
+ from modeltest.scenarios import MinimumAccuracyTest, GroupPerformanceTest
57
+
58
+ suite = ModelSuite(name="Fraud Detection")
59
+ suite.add_test(MinimumAccuracyTest(threshold=0.85))
60
+ suite.add_test(GroupPerformanceTest(metric="accuracy", threshold=0.8, group_col="gender"))
61
+
62
+ result = suite.run(model, X_val, y_val, model_name="fraud_rf")
63
+ print(result.report(style="table"))
64
+ ```
65
+
66
+ ## CLI
67
+
68
+ After training, validate a model contract from the command line:
69
+
70
+ ```bash
71
+ # Declarative suite (recommended)
72
+ modeltest validate \
73
+ --suite suite.yaml \
74
+ --model model.pkl \
75
+ --data validation.csv \
76
+ --target target \
77
+ --train-data train.csv \
78
+ --output report.xml
79
+
80
+ # Python suite (suite.py exposing `suite`)
81
+ modeltest validate --suite suite.py --model model.pkl --data validation.csv --target target
82
+ ```
83
+
84
+ Extras:
85
+ - `--output report.xml` writes JUnit XML for CI reporters.
86
+ - `--train-data` enables the drift tests (they compare train vs. validation distributions).
87
+ - Exit code is `1` if any test fails, `0` otherwise.
88
+
89
+ ## Prediction caching
90
+
91
+ Within a single `suite.run(...)`, predictions are computed once and reused
92
+ across every test. `TestContext.predict()` caches by a content hash of the
93
+ input, so tests predicting on the *same* data (`MinimumAccuracyTest`,
94
+ `GroupPerformanceTest`, the fairness tests, ...) each reuse the result instead
95
+ of re-running the model. Perturbed inputs (e.g. the robustness test's noisy
96
+ copy) get their own cache entry, so caching never compromises correctness.
97
+
98
+ Disable it if you need a fresh prediction every call:
99
+
100
+ ```python
101
+ ctx = TestContext(model=model, X_val=X_val, y_val=y_val, cache_predictions=False)
102
+ ```
103
+
104
+ ## YAML suites
105
+
106
+ Define your contract declaratively — no code needed:
107
+
108
+ ```yaml
109
+ suite:
110
+ name: "Credit Scoring Model"
111
+ tests:
112
+ - type: minimum_accuracy
113
+ params: {threshold: 0.85}
114
+ - type: group_performance
115
+ params: {metric: accuracy, threshold: 0.8, group_col: "gender"}
116
+ - type: robustness
117
+ params: {noise_std: 0.01, max_drop: 0.03}
118
+ - type: data_drift
119
+ params: {features: [age, income], max_psi: 0.15}
120
+ - type: equal_opportunity
121
+ params: {protected: "gender", max_diff: 0.1}
122
+ - type: statistical_parity
123
+ params: {protected: "gender", max_diff: 0.1, min_ratio: 0.8}
124
+ - type: data_invariant
125
+ params: {expected_columns: [age, income], max_null_ratio: 0.02}
126
+ ```
127
+
128
+ ## Multi-framework support
129
+
130
+ `TestContext` talks to models through a small adapter interface
131
+ (`modeltest.wrappers`). Out of the box it normalizes:
132
+
133
+ - **scikit-learn** estimators (`predict`, and `predict_proba` when available)
134
+ - **PyTorch** `nn.Module` (`predict` = argmax over logits, `predict_proba` = softmax)
135
+ - **Keras / TensorFlow** models (binary threshold or multiclass argmax)
136
+
137
+ Pass any of these straight to `suite.run(model, ...)`; the right adapter is
138
+ picked automatically. Custom framework? Implement a
139
+ `ModelWrapper` subclass and pass an instance as the model.
140
+
141
+ **scikit-learn `Pipeline`s** (feature engineering + model) work out of the box:
142
+ the suite predicts through the whole pipeline, `model_features` filters
143
+ validation data to the pipeline's raw input columns, and the SHAP-based
144
+ explainability tests explain the final estimator against the engineered
145
+ features (e.g. `num__age`, `cat__cat_a`).
146
+
147
+ ## Built-in test types
148
+
149
+ | `type` (YAML) | Class | Checks |
150
+ |---------------|-------|--------|
151
+ | `minimum_accuracy` | `MinimumAccuracyTest` | Global metric above threshold |
152
+ | `group_performance` | `GroupPerformanceTest` | Metric above threshold per subgroup |
153
+ | `confidence_threshold` | `ConfidenceThresholdTest` | Metric floor via bootstrap CI (lower/upper bound) |
154
+ | `robustness` | `RobustnessTest` | Performance under feature noise |
155
+ | `data_invariant` | `DataInvariantTest` | Expected columns / null ratios |
156
+ | `no_null` | `NoNullTest` | No missing values |
157
+ | `data_drift` | `DataDriftTest` | PSI between train & validation |
158
+ | `ks` | `KSTest` | KS p-value per column |
159
+ | `equal_opportunity` | `EqualOpportunityTest` | Balanced TPR across protected groups |
160
+ | `statistical_parity` | `StatisticalParityTest` | Balanced selection rate (+ 4/5ths rule) |
161
+ | `feature_dominance` | `FeatureDominanceTest` | No single feature dominates attribution |
162
+ | `top_features` | `TopFeaturesTest` | Top-K attributed features are expected |
163
+
164
+ > Explainability tests use SHAP. Install with `pip install modeltest[explain]`
165
+ > (or `modeltest`'s `explain` extra). You can also pass your own explainer
166
+ > callable to any explainability test.
167
+
168
+ ## Custom tests
169
+
170
+ Any class subclassing `ModelTest` can be referenced from a YAML suite
171
+ directly, by dotted import path — no registration required:
172
+
173
+ ```yaml
174
+ suite:
175
+ name: "Income Model"
176
+ tests:
177
+ - type: minimum_accuracy
178
+ params: {threshold: 0.85}
179
+ - type: myproject.custom_tests:ZeroPredictionShareTest
180
+ params: {min_positive_share: 0.01}
181
+ ```
182
+
183
+ Both `module.path:Class` and `module.path.Class` work. The module is looked
184
+ up on the import path (your current working directory is added automatically).
185
+ Programmatic registration is also available for short, friendlier names:
186
+
187
+ ```python
188
+ from modeltest import register
189
+ register("zero_share", ZeroPredictionShareTest)
190
+ # ...now use `type: zero_share` in YAML
191
+ ```
192
+
193
+ ## MLflow integration
194
+
195
+ Log a finished validation run into [MLflow](https://mlflow.org/) as an
196
+ experiment run — one param per test, one metric per numeric value, and the
197
+ full JSON report saved as an artifact.
198
+
199
+ ```bash
200
+ pip install modeltest[mlflow]
201
+ ```
202
+
203
+ ```python
204
+ import mlflow
205
+ from modeltest import ModelSuite
206
+ from modeltest.integrations.mlflow import log_suite_result
207
+ from modeltest.scenarios import MinimumAccuracyTest
208
+
209
+ suite = ModelSuite(name="fraud-v2")
210
+ suite.add_test(MinimumAccuracyTest(threshold=0.85))
211
+ result = suite.run(model, X_val, y_val)
212
+
213
+ with mlflow.start_run():
214
+ log_suite_result(result)
215
+ ```
216
+
217
+ `log_suite_result` also supports a `run_id` parameter for logging into a
218
+ specific (possibly already-finished) run, plus optional `param_prefix` /
219
+ `metric_prefix` to namespace the logged names.
220
+
221
+ ## Development
222
+
223
+ Install dev tools and run the quality gates:
224
+
225
+ ```bash
226
+ make install # pip install -e ".[dev]"
227
+ make lint # ruff check
228
+ make format # ruff format
229
+ make test # pytest (with 80% coverage gate)
230
+ make precommit # install git pre-commit hooks (lint+format)
231
+ ```
232
+
233
+ Continuous integration mirrors these gates: `lint`, `test-library` (with
234
+ coverage) and `validate-model` all run on every push / PR
235
+ ([.github/workflows/validate.yml](.github/workflows/validate.yml)).
236
+
237
+ ## CI/CD
238
+
239
+ A ready-to-use [GitHub Actions workflow](.github/workflows/validate.yml) runs the
240
+ library's own tests and validates your model contract on every push / PR. It
241
+ publishes both reports (as JUnit) and fails the pipeline if the model doesn't
242
+ meet its contract.
243
+
244
+ The model job trains a sample model and validates it:
245
+
246
+ ```bash
247
+ modeltest validate --suite examples/suite.yaml --model examples/model.pkl \
248
+ --data examples/validation.csv --target target --train-data examples/train.csv
249
+ ```
250
+
251
+ To point it at your real artifacts, update the `validate-model` job's `run` step paths.
252
+
@@ -0,0 +1,209 @@
1
+ # modeltest
2
+
3
+ > If you test your code, why not your model?
4
+
5
+ `modeltest` is a unit-testing framework for machine learning models. It lets you
6
+ define contracts for model quality, robustness, fairness, and data invariants,
7
+ and run them automatically in your CI/CD pipeline — just like `pytest` for code.
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from modeltest import ModelSuite
13
+ from modeltest.scenarios import MinimumAccuracyTest, GroupPerformanceTest
14
+
15
+ suite = ModelSuite(name="Fraud Detection")
16
+ suite.add_test(MinimumAccuracyTest(threshold=0.85))
17
+ suite.add_test(GroupPerformanceTest(metric="accuracy", threshold=0.8, group_col="gender"))
18
+
19
+ result = suite.run(model, X_val, y_val, model_name="fraud_rf")
20
+ print(result.report(style="table"))
21
+ ```
22
+
23
+ ## CLI
24
+
25
+ After training, validate a model contract from the command line:
26
+
27
+ ```bash
28
+ # Declarative suite (recommended)
29
+ modeltest validate \
30
+ --suite suite.yaml \
31
+ --model model.pkl \
32
+ --data validation.csv \
33
+ --target target \
34
+ --train-data train.csv \
35
+ --output report.xml
36
+
37
+ # Python suite (suite.py exposing `suite`)
38
+ modeltest validate --suite suite.py --model model.pkl --data validation.csv --target target
39
+ ```
40
+
41
+ Extras:
42
+ - `--output report.xml` writes JUnit XML for CI reporters.
43
+ - `--train-data` enables the drift tests (they compare train vs. validation distributions).
44
+ - Exit code is `1` if any test fails, `0` otherwise.
45
+
46
+ ## Prediction caching
47
+
48
+ Within a single `suite.run(...)`, predictions are computed once and reused
49
+ across every test. `TestContext.predict()` caches by a content hash of the
50
+ input, so tests predicting on the *same* data (`MinimumAccuracyTest`,
51
+ `GroupPerformanceTest`, the fairness tests, ...) each reuse the result instead
52
+ of re-running the model. Perturbed inputs (e.g. the robustness test's noisy
53
+ copy) get their own cache entry, so caching never compromises correctness.
54
+
55
+ Disable it if you need a fresh prediction every call:
56
+
57
+ ```python
58
+ ctx = TestContext(model=model, X_val=X_val, y_val=y_val, cache_predictions=False)
59
+ ```
60
+
61
+ ## YAML suites
62
+
63
+ Define your contract declaratively — no code needed:
64
+
65
+ ```yaml
66
+ suite:
67
+ name: "Credit Scoring Model"
68
+ tests:
69
+ - type: minimum_accuracy
70
+ params: {threshold: 0.85}
71
+ - type: group_performance
72
+ params: {metric: accuracy, threshold: 0.8, group_col: "gender"}
73
+ - type: robustness
74
+ params: {noise_std: 0.01, max_drop: 0.03}
75
+ - type: data_drift
76
+ params: {features: [age, income], max_psi: 0.15}
77
+ - type: equal_opportunity
78
+ params: {protected: "gender", max_diff: 0.1}
79
+ - type: statistical_parity
80
+ params: {protected: "gender", max_diff: 0.1, min_ratio: 0.8}
81
+ - type: data_invariant
82
+ params: {expected_columns: [age, income], max_null_ratio: 0.02}
83
+ ```
84
+
85
+ ## Multi-framework support
86
+
87
+ `TestContext` talks to models through a small adapter interface
88
+ (`modeltest.wrappers`). Out of the box it normalizes:
89
+
90
+ - **scikit-learn** estimators (`predict`, and `predict_proba` when available)
91
+ - **PyTorch** `nn.Module` (`predict` = argmax over logits, `predict_proba` = softmax)
92
+ - **Keras / TensorFlow** models (binary threshold or multiclass argmax)
93
+
94
+ Pass any of these straight to `suite.run(model, ...)`; the right adapter is
95
+ picked automatically. Custom framework? Implement a
96
+ `ModelWrapper` subclass and pass an instance as the model.
97
+
98
+ **scikit-learn `Pipeline`s** (feature engineering + model) work out of the box:
99
+ the suite predicts through the whole pipeline, `model_features` filters
100
+ validation data to the pipeline's raw input columns, and the SHAP-based
101
+ explainability tests explain the final estimator against the engineered
102
+ features (e.g. `num__age`, `cat__cat_a`).
103
+
104
+ ## Built-in test types
105
+
106
+ | `type` (YAML) | Class | Checks |
107
+ |---------------|-------|--------|
108
+ | `minimum_accuracy` | `MinimumAccuracyTest` | Global metric above threshold |
109
+ | `group_performance` | `GroupPerformanceTest` | Metric above threshold per subgroup |
110
+ | `confidence_threshold` | `ConfidenceThresholdTest` | Metric floor via bootstrap CI (lower/upper bound) |
111
+ | `robustness` | `RobustnessTest` | Performance under feature noise |
112
+ | `data_invariant` | `DataInvariantTest` | Expected columns / null ratios |
113
+ | `no_null` | `NoNullTest` | No missing values |
114
+ | `data_drift` | `DataDriftTest` | PSI between train & validation |
115
+ | `ks` | `KSTest` | KS p-value per column |
116
+ | `equal_opportunity` | `EqualOpportunityTest` | Balanced TPR across protected groups |
117
+ | `statistical_parity` | `StatisticalParityTest` | Balanced selection rate (+ 4/5ths rule) |
118
+ | `feature_dominance` | `FeatureDominanceTest` | No single feature dominates attribution |
119
+ | `top_features` | `TopFeaturesTest` | Top-K attributed features are expected |
120
+
121
+ > Explainability tests use SHAP. Install with `pip install modeltest[explain]`
122
+ > (or `modeltest`'s `explain` extra). You can also pass your own explainer
123
+ > callable to any explainability test.
124
+
125
+ ## Custom tests
126
+
127
+ Any class subclassing `ModelTest` can be referenced from a YAML suite
128
+ directly, by dotted import path — no registration required:
129
+
130
+ ```yaml
131
+ suite:
132
+ name: "Income Model"
133
+ tests:
134
+ - type: minimum_accuracy
135
+ params: {threshold: 0.85}
136
+ - type: myproject.custom_tests:ZeroPredictionShareTest
137
+ params: {min_positive_share: 0.01}
138
+ ```
139
+
140
+ Both `module.path:Class` and `module.path.Class` work. The module is looked
141
+ up on the import path (your current working directory is added automatically).
142
+ Programmatic registration is also available for short, friendlier names:
143
+
144
+ ```python
145
+ from modeltest import register
146
+ register("zero_share", ZeroPredictionShareTest)
147
+ # ...now use `type: zero_share` in YAML
148
+ ```
149
+
150
+ ## MLflow integration
151
+
152
+ Log a finished validation run into [MLflow](https://mlflow.org/) as an
153
+ experiment run — one param per test, one metric per numeric value, and the
154
+ full JSON report saved as an artifact.
155
+
156
+ ```bash
157
+ pip install modeltest[mlflow]
158
+ ```
159
+
160
+ ```python
161
+ import mlflow
162
+ from modeltest import ModelSuite
163
+ from modeltest.integrations.mlflow import log_suite_result
164
+ from modeltest.scenarios import MinimumAccuracyTest
165
+
166
+ suite = ModelSuite(name="fraud-v2")
167
+ suite.add_test(MinimumAccuracyTest(threshold=0.85))
168
+ result = suite.run(model, X_val, y_val)
169
+
170
+ with mlflow.start_run():
171
+ log_suite_result(result)
172
+ ```
173
+
174
+ `log_suite_result` also supports a `run_id` parameter for logging into a
175
+ specific (possibly already-finished) run, plus optional `param_prefix` /
176
+ `metric_prefix` to namespace the logged names.
177
+
178
+ ## Development
179
+
180
+ Install dev tools and run the quality gates:
181
+
182
+ ```bash
183
+ make install # pip install -e ".[dev]"
184
+ make lint # ruff check
185
+ make format # ruff format
186
+ make test # pytest (with 80% coverage gate)
187
+ make precommit # install git pre-commit hooks (lint+format)
188
+ ```
189
+
190
+ Continuous integration mirrors these gates: `lint`, `test-library` (with
191
+ coverage) and `validate-model` all run on every push / PR
192
+ ([.github/workflows/validate.yml](.github/workflows/validate.yml)).
193
+
194
+ ## CI/CD
195
+
196
+ A ready-to-use [GitHub Actions workflow](.github/workflows/validate.yml) runs the
197
+ library's own tests and validates your model contract on every push / PR. It
198
+ publishes both reports (as JUnit) and fails the pipeline if the model doesn't
199
+ meet its contract.
200
+
201
+ The model job trains a sample model and validates it:
202
+
203
+ ```bash
204
+ modeltest validate --suite examples/suite.yaml --model examples/model.pkl \
205
+ --data examples/validation.csv --target target --train-data examples/train.csv
206
+ ```
207
+
208
+ To point it at your real artifacts, update the `validate-model` job's `run` step paths.
209
+
@@ -0,0 +1,28 @@
1
+ from modeltest.config import register, unregister
2
+ from modeltest.core.base import (
3
+ ModelSuite,
4
+ ModelTest,
5
+ SuiteResult,
6
+ TestContext,
7
+ TestResult,
8
+ )
9
+ from modeltest.core.runner import run_suite, run_test
10
+
11
+ try:
12
+ from importlib.metadata import version as _metadata_version
13
+
14
+ __version__ = _metadata_version("modeltest")
15
+ except Exception: # noqa: BLE001 - not installed via pip
16
+ __version__ = "unknown"
17
+
18
+ __all__ = [
19
+ "ModelTest",
20
+ "ModelSuite",
21
+ "SuiteResult",
22
+ "TestContext",
23
+ "TestResult",
24
+ "run_test",
25
+ "run_suite",
26
+ "register",
27
+ "unregister",
28
+ ]
@@ -0,0 +1,92 @@
1
+ """Command-line interface: `modeltest validate`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import Optional
8
+
9
+ from modeltest.core.base import ModelSuite
10
+
11
+
12
+ def main(argv: Optional[list] = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="modeltest", description="Unit tests for machine learning models."
15
+ )
16
+ sub = parser.add_subparsers(dest="command", required=True)
17
+
18
+ validate = sub.add_parser("validate", help="Run a test suite against a model.")
19
+ validate.add_argument(
20
+ "--suite",
21
+ required=True,
22
+ help="Path to suite.py (defining `suite`) or suite.yaml (declarative).",
23
+ )
24
+ validate.add_argument("--model", required=True, help="Path to model (pickle).")
25
+ validate.add_argument("--data", required=True, help="Path to validation CSV.")
26
+ validate.add_argument(
27
+ "--target", default="target", help="Name of the target column."
28
+ )
29
+ validate.add_argument(
30
+ "--output", default=None, help="Write JUnit XML to this path."
31
+ )
32
+ validate.add_argument(
33
+ "--train-data", default=None, help="Optional training CSV (for drift tests)."
34
+ )
35
+ validate.set_defaults(func=_run_validate)
36
+
37
+ args = parser.parse_args(argv)
38
+ return args.func(args)
39
+
40
+
41
+ def _load_suite(path: str) -> "ModelSuite":
42
+ import os
43
+
44
+ ext = os.path.splitext(path)[1].lower()
45
+ if ext in (".yaml", ".yml"):
46
+ from modeltest.config import load_suite_yaml
47
+
48
+ return load_suite_yaml(path)
49
+ # default: treat as a Python module exposing `suite`
50
+ import importlib.util
51
+
52
+ spec = importlib.util.spec_from_file_location("suite_module", os.path.abspath(path))
53
+ mod = importlib.util.module_from_spec(spec)
54
+ spec.loader.exec_module(mod)
55
+ return mod.suite
56
+
57
+
58
+ def _run_validate(args: argparse.Namespace) -> int:
59
+ import os
60
+
61
+ import joblib
62
+ import pandas as pd
63
+
64
+ suite: ModelSuite = _load_suite(args.suite)
65
+ model = joblib.load(os.path.abspath(args.model))
66
+ df = pd.read_csv(args.data)
67
+ y = df[args.target]
68
+ X = df.drop(columns=[args.target])
69
+
70
+ X_train = None
71
+ if args.train_data:
72
+ train_df = pd.read_csv(args.train_data)
73
+ X_train = train_df.drop(columns=[args.target])
74
+
75
+ result = suite.run(
76
+ model, X, y, X_train=X_train, model_name=os.path.basename(args.model)
77
+ )
78
+
79
+ if args.output:
80
+ from modeltest.core.report import to_junit_xml
81
+
82
+ out_path = os.path.abspath(args.output)
83
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
84
+ with open(out_path, "w") as fh:
85
+ fh.write(to_junit_xml(result))
86
+
87
+ print(result.report(style="table"))
88
+ return 0 if result.passed else 1
89
+
90
+
91
+ if __name__ == "__main__":
92
+ sys.exit(main())