econeval 0.3.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.
- econeval/__init__.py +85 -0
- econeval/__main__.py +10 -0
- econeval/cli.py +284 -0
- econeval/config.py +599 -0
- econeval/errors.py +12 -0
- econeval/interop.py +269 -0
- econeval/invariants.py +502 -0
- econeval/reporting.py +1083 -0
- econeval/scenarios.py +1515 -0
- econeval-0.3.1.dist-info/METADATA +373 -0
- econeval-0.3.1.dist-info/RECORD +15 -0
- econeval-0.3.1.dist-info/WHEEL +5 -0
- econeval-0.3.1.dist-info/entry_points.txt +2 -0
- econeval-0.3.1.dist-info/licenses/LICENSE +21 -0
- econeval-0.3.1.dist-info/top_level.txt +1 -0
econeval/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""EconEval: CI/CD checks for economic and policy models."""
|
|
2
|
+
|
|
3
|
+
from .cli import main, run_cli
|
|
4
|
+
from .config import (
|
|
5
|
+
DriftTest,
|
|
6
|
+
EconEvalConfig,
|
|
7
|
+
EconomicCheck,
|
|
8
|
+
EconomicDriftTest,
|
|
9
|
+
FairnessConfig,
|
|
10
|
+
InvariantRule,
|
|
11
|
+
StressTest,
|
|
12
|
+
load_config,
|
|
13
|
+
)
|
|
14
|
+
from .errors import ExecutionIssue
|
|
15
|
+
from .interop import ModelRuntime, describe_model_interface, resolve_model_runtime
|
|
16
|
+
from .invariants import (
|
|
17
|
+
InvariantResult,
|
|
18
|
+
evaluate_expression,
|
|
19
|
+
run_invariant,
|
|
20
|
+
run_invariant_suite,
|
|
21
|
+
suite_passed,
|
|
22
|
+
)
|
|
23
|
+
from .reporting import (
|
|
24
|
+
build_json_report,
|
|
25
|
+
write_dashboard_report,
|
|
26
|
+
write_html_report,
|
|
27
|
+
write_json_report,
|
|
28
|
+
write_markdown_report,
|
|
29
|
+
)
|
|
30
|
+
from .scenarios import (
|
|
31
|
+
DriftResult,
|
|
32
|
+
EconomicCheckResult,
|
|
33
|
+
EconomicDriftResult,
|
|
34
|
+
FairnessResult,
|
|
35
|
+
ScenarioResult,
|
|
36
|
+
economic_drift_suite_passed,
|
|
37
|
+
economic_suite_passed,
|
|
38
|
+
run_drift_suite,
|
|
39
|
+
run_economic_drift_suite,
|
|
40
|
+
run_economic_suite,
|
|
41
|
+
run_fairness_checks,
|
|
42
|
+
run_stress_suite,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"__version__",
|
|
47
|
+
"build_json_report",
|
|
48
|
+
"write_dashboard_report",
|
|
49
|
+
"EconEvalConfig",
|
|
50
|
+
"EconomicCheck",
|
|
51
|
+
"InvariantResult",
|
|
52
|
+
"InvariantRule",
|
|
53
|
+
"ExecutionIssue",
|
|
54
|
+
"ModelRuntime",
|
|
55
|
+
"DriftResult",
|
|
56
|
+
"DriftTest",
|
|
57
|
+
"EconomicCheckResult",
|
|
58
|
+
"EconomicDriftResult",
|
|
59
|
+
"EconomicDriftTest",
|
|
60
|
+
"FairnessConfig",
|
|
61
|
+
"FairnessResult",
|
|
62
|
+
"ScenarioResult",
|
|
63
|
+
"StressTest",
|
|
64
|
+
"main",
|
|
65
|
+
"load_config",
|
|
66
|
+
"evaluate_expression",
|
|
67
|
+
"describe_model_interface",
|
|
68
|
+
"run_invariant",
|
|
69
|
+
"run_invariant_suite",
|
|
70
|
+
"run_cli",
|
|
71
|
+
"resolve_model_runtime",
|
|
72
|
+
"run_drift_suite",
|
|
73
|
+
"run_economic_drift_suite",
|
|
74
|
+
"run_economic_suite",
|
|
75
|
+
"run_fairness_checks",
|
|
76
|
+
"run_stress_suite",
|
|
77
|
+
"economic_suite_passed",
|
|
78
|
+
"economic_drift_suite_passed",
|
|
79
|
+
"suite_passed",
|
|
80
|
+
"write_json_report",
|
|
81
|
+
"write_markdown_report",
|
|
82
|
+
"write_html_report",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
__version__ = "0.3.0"
|
econeval/__main__.py
ADDED
econeval/cli.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Command line interface for EconEval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from time import perf_counter
|
|
11
|
+
|
|
12
|
+
from .config import load_config
|
|
13
|
+
from .errors import ExecutionIssue
|
|
14
|
+
from .invariants import run_invariant_suite, suite_passed
|
|
15
|
+
from .reporting import (
|
|
16
|
+
build_json_report,
|
|
17
|
+
write_dashboard_report,
|
|
18
|
+
write_github_step_summary,
|
|
19
|
+
write_html_report,
|
|
20
|
+
write_json_report,
|
|
21
|
+
write_junit_report,
|
|
22
|
+
write_markdown_report,
|
|
23
|
+
)
|
|
24
|
+
from .scenarios import (
|
|
25
|
+
drift_suite_passed,
|
|
26
|
+
economic_drift_suite_passed,
|
|
27
|
+
economic_suite_passed,
|
|
28
|
+
fairness_suite_passed,
|
|
29
|
+
run_drift_suite,
|
|
30
|
+
run_economic_drift_suite,
|
|
31
|
+
run_economic_suite,
|
|
32
|
+
run_fairness_checks,
|
|
33
|
+
run_stress_suite,
|
|
34
|
+
)
|
|
35
|
+
from .scenarios import (
|
|
36
|
+
suite_passed as stress_suite_passed,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="econeval",
|
|
43
|
+
description="Run EconEval checks against a model.",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--config",
|
|
47
|
+
required=True,
|
|
48
|
+
help="Path to the EconEval YAML config.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--model",
|
|
52
|
+
required=True,
|
|
53
|
+
help="Path to the Python file that defines the model.",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--class",
|
|
57
|
+
dest="class_name",
|
|
58
|
+
default="DemoModel",
|
|
59
|
+
help="Model class name to instantiate from the model file.",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--report",
|
|
63
|
+
default="econeval-report.json",
|
|
64
|
+
help="Where to write the JSON report.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--format",
|
|
68
|
+
choices=("json", "junit", "markdown", "html", "dashboard"),
|
|
69
|
+
default="json",
|
|
70
|
+
help="Report format to write.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument("--verbose", action="store_true", help="Print detailed progress messages.")
|
|
73
|
+
parser.add_argument("--quiet", action="store_true", help="Suppress non-error output.")
|
|
74
|
+
return parser
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_model_class(model_path: str | Path, class_name: str):
|
|
78
|
+
"""Load a model class from a Python file path."""
|
|
79
|
+
|
|
80
|
+
module_path = Path(model_path).resolve()
|
|
81
|
+
if not module_path.exists():
|
|
82
|
+
raise FileNotFoundError(f"model file not found: {module_path}")
|
|
83
|
+
module_name = f"econeval_model_{module_path.stem}"
|
|
84
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
85
|
+
if spec is None or spec.loader is None:
|
|
86
|
+
raise ValueError(f"could not load model file: {module_path}")
|
|
87
|
+
|
|
88
|
+
module = importlib.util.module_from_spec(spec)
|
|
89
|
+
sys.modules[module_name] = module
|
|
90
|
+
spec.loader.exec_module(module)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
return getattr(module, class_name)
|
|
94
|
+
except AttributeError as exc:
|
|
95
|
+
available_classes = sorted(
|
|
96
|
+
name for name, value in vars(module).items() if isinstance(value, type)
|
|
97
|
+
)
|
|
98
|
+
suffix = f" available classes: {', '.join(available_classes)}" if available_classes else ""
|
|
99
|
+
raise ValueError(f"class {class_name!r} not found in {module_path}.{suffix}") from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_cli(argv: list[str] | None = None) -> int:
|
|
103
|
+
args = build_parser().parse_args(argv)
|
|
104
|
+
|
|
105
|
+
issues: list[ExecutionIssue] = []
|
|
106
|
+
log = _build_logger(args.verbose, args.quiet)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
log("loading config")
|
|
110
|
+
config = load_config(args.config)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
issues.append(ExecutionIssue(stage="config", message=str(exc), detail=str(args.config)))
|
|
113
|
+
report = build_json_report(
|
|
114
|
+
config=_placeholder_config(),
|
|
115
|
+
results=[],
|
|
116
|
+
scenario_results=[],
|
|
117
|
+
drift_results=[],
|
|
118
|
+
fairness_results=[],
|
|
119
|
+
issues=issues,
|
|
120
|
+
)
|
|
121
|
+
_write_report(args.format, args.report, report)
|
|
122
|
+
_write_step_summary(report)
|
|
123
|
+
_print_failure("config load", exc, args.quiet)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
log("loading model")
|
|
128
|
+
model_class = load_model_class(args.model, args.class_name)
|
|
129
|
+
model = model_class()
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
issues.append(ExecutionIssue(stage="model", message=str(exc), detail=str(args.model)))
|
|
132
|
+
report = build_json_report(
|
|
133
|
+
config=config,
|
|
134
|
+
results=[],
|
|
135
|
+
scenario_results=[],
|
|
136
|
+
drift_results=[],
|
|
137
|
+
fairness_results=[],
|
|
138
|
+
issues=issues,
|
|
139
|
+
)
|
|
140
|
+
_write_report(args.format, args.report, report)
|
|
141
|
+
_write_step_summary(report)
|
|
142
|
+
_print_failure("model load", exc, args.quiet)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
log("running invariants")
|
|
146
|
+
started_at = perf_counter()
|
|
147
|
+
results = run_invariant_suite(model, config.invariants)
|
|
148
|
+
_log_stage_summary(log, "invariants", results, perf_counter() - started_at)
|
|
149
|
+
log("running economic checks")
|
|
150
|
+
started_at = perf_counter()
|
|
151
|
+
economic_results = run_economic_suite(model, config.economic_checks)
|
|
152
|
+
_log_stage_summary(log, "economic checks", economic_results, perf_counter() - started_at)
|
|
153
|
+
log("running stress tests")
|
|
154
|
+
started_at = perf_counter()
|
|
155
|
+
scenario_results = run_stress_suite(
|
|
156
|
+
model,
|
|
157
|
+
config.stress_tests,
|
|
158
|
+
base_path=Path(args.config).parent,
|
|
159
|
+
)
|
|
160
|
+
_log_stage_summary(log, "stress tests", scenario_results, perf_counter() - started_at)
|
|
161
|
+
log("running drift checks")
|
|
162
|
+
started_at = perf_counter()
|
|
163
|
+
drift_results = run_drift_suite(config.drift_tests, base_path=Path(args.config).parent)
|
|
164
|
+
_log_stage_summary(log, "drift checks", drift_results, perf_counter() - started_at)
|
|
165
|
+
log("running economic drift checks")
|
|
166
|
+
started_at = perf_counter()
|
|
167
|
+
economic_drift_results = run_economic_drift_suite(
|
|
168
|
+
model,
|
|
169
|
+
config.economic_drift_tests,
|
|
170
|
+
base_path=Path(args.config).parent,
|
|
171
|
+
)
|
|
172
|
+
_log_stage_summary(
|
|
173
|
+
log,
|
|
174
|
+
"economic drift checks",
|
|
175
|
+
economic_drift_results,
|
|
176
|
+
perf_counter() - started_at,
|
|
177
|
+
)
|
|
178
|
+
fairness_results = []
|
|
179
|
+
if config.fairness.enabled and config.fairness.dataset:
|
|
180
|
+
log("running fairness checks")
|
|
181
|
+
started_at = perf_counter()
|
|
182
|
+
fairness_results = run_fairness_checks(
|
|
183
|
+
model,
|
|
184
|
+
config.fairness.dataset,
|
|
185
|
+
config.fairness.metrics,
|
|
186
|
+
group_column=config.fairness.group_column,
|
|
187
|
+
positive_threshold=config.fairness.positive_threshold,
|
|
188
|
+
actual_threshold=config.fairness.actual_threshold,
|
|
189
|
+
base_path=Path(args.config).parent,
|
|
190
|
+
)
|
|
191
|
+
_log_stage_summary(log, "fairness checks", fairness_results, perf_counter() - started_at)
|
|
192
|
+
|
|
193
|
+
report = build_json_report(
|
|
194
|
+
config,
|
|
195
|
+
results,
|
|
196
|
+
scenario_results,
|
|
197
|
+
drift_results,
|
|
198
|
+
fairness_results,
|
|
199
|
+
economic_results=economic_results,
|
|
200
|
+
economic_drift_results=economic_drift_results,
|
|
201
|
+
)
|
|
202
|
+
_write_report(args.format, args.report, report)
|
|
203
|
+
_write_step_summary(report)
|
|
204
|
+
log(f"wrote report to {args.report}")
|
|
205
|
+
|
|
206
|
+
status = report["summary"]["status"]
|
|
207
|
+
_print_summary(
|
|
208
|
+
status,
|
|
209
|
+
report["summary"]["passed"],
|
|
210
|
+
report["summary"]["total"],
|
|
211
|
+
args.quiet,
|
|
212
|
+
args.format,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
0
|
|
217
|
+
if (
|
|
218
|
+
suite_passed(results)
|
|
219
|
+
and economic_suite_passed(economic_results)
|
|
220
|
+
and stress_suite_passed(scenario_results)
|
|
221
|
+
and drift_suite_passed(drift_results)
|
|
222
|
+
and economic_drift_suite_passed(economic_drift_results)
|
|
223
|
+
and fairness_suite_passed(fairness_results)
|
|
224
|
+
)
|
|
225
|
+
else 1
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _placeholder_config():
|
|
230
|
+
from .config import EconEvalConfig
|
|
231
|
+
|
|
232
|
+
return EconEvalConfig(project="unavailable")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _write_report(report_format: str, path: str | Path, report: dict[str, object]) -> None:
|
|
236
|
+
writers = {
|
|
237
|
+
"json": write_json_report,
|
|
238
|
+
"junit": write_junit_report,
|
|
239
|
+
"markdown": write_markdown_report,
|
|
240
|
+
"html": write_html_report,
|
|
241
|
+
"dashboard": write_dashboard_report,
|
|
242
|
+
}
|
|
243
|
+
writers[report_format](path, report)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_step_summary(report: dict[str, object]) -> None:
|
|
247
|
+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
248
|
+
if not summary_path:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
write_github_step_summary(summary_path, report)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _build_logger(verbose: bool, quiet: bool):
|
|
255
|
+
if quiet:
|
|
256
|
+
return lambda message: None
|
|
257
|
+
if not verbose:
|
|
258
|
+
return lambda message: None
|
|
259
|
+
return lambda message: print(f"[econeval] {message}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _log_stage_summary(log, stage: str, results, elapsed: float) -> None:
|
|
263
|
+
if not results:
|
|
264
|
+
log(f"{stage}: no checks configured ({elapsed:.2f}s)")
|
|
265
|
+
return
|
|
266
|
+
passed = sum(1 for result in results if getattr(result, "passed", False))
|
|
267
|
+
total = len(results)
|
|
268
|
+
log(f"{stage}: {passed}/{total} passed ({elapsed:.2f}s)")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _print_summary(status: str, passed: int, total: int, quiet: bool, report_format: str) -> None:
|
|
272
|
+
if quiet:
|
|
273
|
+
return
|
|
274
|
+
print(f"EconEval: {status} ({passed}/{total} checks passed, format={report_format})")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _print_failure(stage: str, exc: Exception, quiet: bool) -> None:
|
|
278
|
+
if quiet:
|
|
279
|
+
return
|
|
280
|
+
print(f"EconEval: fail ({stage}: {exc})")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(argv: list[str] | None = None) -> int:
|
|
284
|
+
return run_cli(argv)
|