modelwright 0.1.0a1__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.
- modelwright/__init__.py +148 -0
- modelwright/cli.py +466 -0
- modelwright/conversion.py +931 -0
- modelwright/evaluation.py +173 -0
- modelwright/execution.py +239 -0
- modelwright/extraction.py +662 -0
- modelwright/formulas.py +571 -0
- modelwright/formulas_oracle.py +153 -0
- modelwright/generation.py +726 -0
- modelwright/graph.py +591 -0
- modelwright/oracle_validation.py +59 -0
- modelwright/oracles.py +132 -0
- modelwright/references.py +209 -0
- modelwright/validation.py +475 -0
- modelwright-0.1.0a1.dist-info/METADATA +160 -0
- modelwright-0.1.0a1.dist-info/RECORD +20 -0
- modelwright-0.1.0a1.dist-info/WHEEL +5 -0
- modelwright-0.1.0a1.dist-info/entry_points.txt +2 -0
- modelwright-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- modelwright-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Validation orchestration over generated outputs, cached values, and oracles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from modelwright.execution import GeneratedExecutionResult, execute_generated_model
|
|
10
|
+
from modelwright.extraction import WorkbookRecord
|
|
11
|
+
from modelwright.generation import GeneratedModuleContract
|
|
12
|
+
from modelwright.oracle_validation import build_oracle_validation_report
|
|
13
|
+
from modelwright.oracles import OracleResult
|
|
14
|
+
from modelwright.validation import (
|
|
15
|
+
Diagnostic,
|
|
16
|
+
JsonValue,
|
|
17
|
+
OracleConfig,
|
|
18
|
+
ValidationReport,
|
|
19
|
+
ValidationScenario,
|
|
20
|
+
build_validation_report,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ValidationEvaluationResult:
|
|
26
|
+
"""Generated-model execution plus validation reports for one scenario."""
|
|
27
|
+
|
|
28
|
+
scenario_id: str
|
|
29
|
+
generated_execution: GeneratedExecutionResult
|
|
30
|
+
cached_validation_report: ValidationReport | None = None
|
|
31
|
+
oracle_validation_report: ValidationReport | None = None
|
|
32
|
+
diagnostics: tuple[Diagnostic, ...] = field(default_factory=tuple)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: dict[str, Any]) -> "ValidationEvaluationResult":
|
|
36
|
+
cached_report_data = data.get("cached_validation_report")
|
|
37
|
+
oracle_report_data = data.get("oracle_validation_report")
|
|
38
|
+
return cls(
|
|
39
|
+
scenario_id=data["scenario_id"],
|
|
40
|
+
generated_execution=GeneratedExecutionResult.from_dict(data["generated_execution"]),
|
|
41
|
+
cached_validation_report=ValidationReport.from_dict(cached_report_data)
|
|
42
|
+
if cached_report_data is not None
|
|
43
|
+
else None,
|
|
44
|
+
oracle_validation_report=ValidationReport.from_dict(oracle_report_data)
|
|
45
|
+
if oracle_report_data is not None
|
|
46
|
+
else None,
|
|
47
|
+
diagnostics=tuple(Diagnostic.from_dict(item) for item in data.get("diagnostics", [])),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, JsonValue]:
|
|
51
|
+
return {
|
|
52
|
+
"scenario_id": self.scenario_id,
|
|
53
|
+
"generated_execution": self.generated_execution.to_dict(),
|
|
54
|
+
"cached_validation_report": self.cached_validation_report.to_dict()
|
|
55
|
+
if self.cached_validation_report is not None
|
|
56
|
+
else None,
|
|
57
|
+
"oracle_validation_report": self.oracle_validation_report.to_dict()
|
|
58
|
+
if self.oracle_validation_report is not None
|
|
59
|
+
else None,
|
|
60
|
+
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def evaluate_generated_model(
|
|
65
|
+
*,
|
|
66
|
+
contract: GeneratedModuleContract,
|
|
67
|
+
module_path: str | Path,
|
|
68
|
+
scenario: ValidationScenario,
|
|
69
|
+
workbook: WorkbookRecord | None = None,
|
|
70
|
+
oracle_result: OracleResult | None = None,
|
|
71
|
+
) -> ValidationEvaluationResult:
|
|
72
|
+
"""Execute a generated model and build available validation reports."""
|
|
73
|
+
|
|
74
|
+
generated_execution = execute_generated_model(
|
|
75
|
+
contract=contract,
|
|
76
|
+
module_path=module_path,
|
|
77
|
+
inputs={scenario_input.cell_ref: scenario_input.value for scenario_input in scenario.inputs},
|
|
78
|
+
)
|
|
79
|
+
diagnostics = _execution_diagnostics(generated_execution)
|
|
80
|
+
|
|
81
|
+
cached_validation_report = None
|
|
82
|
+
if workbook is not None:
|
|
83
|
+
cached_values, cached_diagnostics = _cached_output_values(workbook=workbook, scenario=scenario)
|
|
84
|
+
diagnostics.extend(cached_diagnostics)
|
|
85
|
+
cached_report = build_validation_report(
|
|
86
|
+
scenario=_scenario_with_oracle_backend(scenario, "cached_workbook"),
|
|
87
|
+
generated_values=generated_execution.output_values,
|
|
88
|
+
oracle_values=cached_values,
|
|
89
|
+
)
|
|
90
|
+
cached_validation_report = _report_with_diagnostics(
|
|
91
|
+
cached_report,
|
|
92
|
+
tuple(diagnostics),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
oracle_validation_report = None
|
|
96
|
+
if oracle_result is not None:
|
|
97
|
+
oracle_report = build_oracle_validation_report(
|
|
98
|
+
scenario=scenario,
|
|
99
|
+
generated_values=generated_execution.output_values,
|
|
100
|
+
oracle_result=oracle_result,
|
|
101
|
+
)
|
|
102
|
+
oracle_validation_report = _report_with_diagnostics(
|
|
103
|
+
oracle_report,
|
|
104
|
+
tuple(_execution_diagnostics(generated_execution)),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return ValidationEvaluationResult(
|
|
108
|
+
scenario_id=scenario.scenario_id,
|
|
109
|
+
generated_execution=generated_execution,
|
|
110
|
+
cached_validation_report=cached_validation_report,
|
|
111
|
+
oracle_validation_report=oracle_validation_report,
|
|
112
|
+
diagnostics=tuple(diagnostics),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cached_output_values(
|
|
117
|
+
*,
|
|
118
|
+
workbook: WorkbookRecord,
|
|
119
|
+
scenario: ValidationScenario,
|
|
120
|
+
) -> tuple[dict[str, JsonValue], tuple[Diagnostic, ...]]:
|
|
121
|
+
cells_by_ref = {cell.cell_ref: cell for cell in workbook.cells}
|
|
122
|
+
values: dict[str, JsonValue] = {}
|
|
123
|
+
diagnostics: list[Diagnostic] = []
|
|
124
|
+
for output in scenario.outputs:
|
|
125
|
+
cell = cells_by_ref.get(output.cell_ref)
|
|
126
|
+
if cell is None or cell.cached_value is None:
|
|
127
|
+
diagnostics.append(
|
|
128
|
+
Diagnostic(
|
|
129
|
+
diagnostic_code="missing_cached_formula_value",
|
|
130
|
+
message="cached workbook value is unavailable for validation output",
|
|
131
|
+
location=output.cell_ref,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
continue
|
|
135
|
+
values[output.cell_ref] = cell.cached_value
|
|
136
|
+
return values, tuple(diagnostics)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _execution_diagnostics(generated_execution: GeneratedExecutionResult) -> list[Diagnostic]:
|
|
140
|
+
return [
|
|
141
|
+
Diagnostic(
|
|
142
|
+
diagnostic_code=diagnostic.code,
|
|
143
|
+
message=diagnostic.message,
|
|
144
|
+
severity=diagnostic.severity,
|
|
145
|
+
location=diagnostic.location,
|
|
146
|
+
)
|
|
147
|
+
for diagnostic in generated_execution.diagnostics
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _scenario_with_oracle_backend(scenario: ValidationScenario, backend: str) -> ValidationScenario:
|
|
152
|
+
return ValidationScenario(
|
|
153
|
+
scenario_id=scenario.scenario_id,
|
|
154
|
+
description=scenario.description,
|
|
155
|
+
source_workbook=scenario.source_workbook,
|
|
156
|
+
generated_model=scenario.generated_model,
|
|
157
|
+
oracle=OracleConfig(backend=backend),
|
|
158
|
+
inputs=scenario.inputs,
|
|
159
|
+
outputs=scenario.outputs,
|
|
160
|
+
comparison=scenario.comparison,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _report_with_diagnostics(
|
|
165
|
+
report: ValidationReport,
|
|
166
|
+
diagnostics: tuple[Diagnostic, ...],
|
|
167
|
+
) -> ValidationReport:
|
|
168
|
+
return ValidationReport(
|
|
169
|
+
scenario_id=report.scenario_id,
|
|
170
|
+
oracle_backend=report.oracle_backend,
|
|
171
|
+
comparisons=report.comparisons,
|
|
172
|
+
diagnostics=(*report.diagnostics, *diagnostics),
|
|
173
|
+
)
|
modelwright/execution.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Execution helpers for generated Modelwright Python models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from modelwright.generation import GeneratedModuleContract
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
JsonValue = str | int | float | bool | None | list[Any] | dict[str, Any]
|
|
19
|
+
DiagnosticSeverity = Literal["info", "warning", "error"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ExecutionDiagnostic:
|
|
24
|
+
"""Execution concern tied to a generated model run."""
|
|
25
|
+
|
|
26
|
+
code: str
|
|
27
|
+
message: str
|
|
28
|
+
severity: DiagnosticSeverity = "warning"
|
|
29
|
+
location: str | None = None
|
|
30
|
+
raw_value: JsonValue = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, data: dict[str, Any]) -> "ExecutionDiagnostic":
|
|
34
|
+
return cls(
|
|
35
|
+
code=data["code"],
|
|
36
|
+
message=data["message"],
|
|
37
|
+
severity=data.get("severity", "warning"),
|
|
38
|
+
location=data.get("location"),
|
|
39
|
+
raw_value=data.get("raw_value"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, JsonValue]:
|
|
43
|
+
return {
|
|
44
|
+
"code": self.code,
|
|
45
|
+
"message": self.message,
|
|
46
|
+
"severity": self.severity,
|
|
47
|
+
"location": self.location,
|
|
48
|
+
"raw_value": self.raw_value,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class GeneratedExecutionResult:
|
|
54
|
+
"""Observed outputs from one generated Python model execution."""
|
|
55
|
+
|
|
56
|
+
contract: GeneratedModuleContract
|
|
57
|
+
module_path: str
|
|
58
|
+
entrypoint: str
|
|
59
|
+
output_values: dict[str, JsonValue] = field(default_factory=dict)
|
|
60
|
+
diagnostics: tuple[ExecutionDiagnostic, ...] = field(default_factory=tuple)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def executed(self) -> bool:
|
|
64
|
+
return not any(diagnostic.severity == "error" for diagnostic in self.diagnostics)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_dict(cls, data: dict[str, Any]) -> "GeneratedExecutionResult":
|
|
68
|
+
return cls(
|
|
69
|
+
contract=GeneratedModuleContract.from_dict(data["contract"]),
|
|
70
|
+
module_path=data["module_path"],
|
|
71
|
+
entrypoint=data["entrypoint"],
|
|
72
|
+
output_values=dict(data.get("output_values", {})),
|
|
73
|
+
diagnostics=tuple(ExecutionDiagnostic.from_dict(item) for item in data.get("diagnostics", [])),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, JsonValue]:
|
|
77
|
+
return {
|
|
78
|
+
"contract": self.contract.to_dict(),
|
|
79
|
+
"module_path": self.module_path,
|
|
80
|
+
"entrypoint": self.entrypoint,
|
|
81
|
+
"executed": self.executed,
|
|
82
|
+
"output_values": self.output_values,
|
|
83
|
+
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def execute_generated_model(
|
|
88
|
+
*,
|
|
89
|
+
contract: GeneratedModuleContract,
|
|
90
|
+
module_path: str | Path,
|
|
91
|
+
inputs: Mapping[str, JsonValue] | None = None,
|
|
92
|
+
) -> GeneratedExecutionResult:
|
|
93
|
+
"""Execute a generated Python module and return observed declared outputs."""
|
|
94
|
+
|
|
95
|
+
path = Path(module_path)
|
|
96
|
+
diagnostics: list[ExecutionDiagnostic] = []
|
|
97
|
+
if not path.exists():
|
|
98
|
+
return _result(
|
|
99
|
+
contract=contract,
|
|
100
|
+
module_path=path,
|
|
101
|
+
diagnostics=(
|
|
102
|
+
ExecutionDiagnostic(
|
|
103
|
+
code="generated_model_not_found",
|
|
104
|
+
message="generated Python model file does not exist",
|
|
105
|
+
severity="error",
|
|
106
|
+
location=str(path),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
module = _load_generated_module(path)
|
|
112
|
+
if isinstance(module, ExecutionDiagnostic):
|
|
113
|
+
return _result(contract=contract, module_path=path, diagnostics=(module,))
|
|
114
|
+
|
|
115
|
+
entrypoint = getattr(module, contract.entrypoint, None)
|
|
116
|
+
if not callable(entrypoint):
|
|
117
|
+
return _result(
|
|
118
|
+
contract=contract,
|
|
119
|
+
module_path=path,
|
|
120
|
+
diagnostics=(
|
|
121
|
+
ExecutionDiagnostic(
|
|
122
|
+
code="generated_model_entrypoint_missing",
|
|
123
|
+
message="generated Python model entrypoint is missing or not callable",
|
|
124
|
+
severity="error",
|
|
125
|
+
location=contract.entrypoint,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
raw_outputs = entrypoint(dict(inputs or {}))
|
|
132
|
+
except Exception as error: # noqa: BLE001
|
|
133
|
+
return _result(
|
|
134
|
+
contract=contract,
|
|
135
|
+
module_path=path,
|
|
136
|
+
diagnostics=(
|
|
137
|
+
ExecutionDiagnostic(
|
|
138
|
+
code="generated_model_execution_failed",
|
|
139
|
+
message="generated Python model raised during execution",
|
|
140
|
+
severity="error",
|
|
141
|
+
location=contract.entrypoint,
|
|
142
|
+
raw_value=repr(error),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if not isinstance(raw_outputs, Mapping):
|
|
148
|
+
return _result(
|
|
149
|
+
contract=contract,
|
|
150
|
+
module_path=path,
|
|
151
|
+
diagnostics=(
|
|
152
|
+
ExecutionDiagnostic(
|
|
153
|
+
code="generated_model_outputs_not_mapping",
|
|
154
|
+
message="generated Python model entrypoint must return a mapping of cell refs to values",
|
|
155
|
+
severity="error",
|
|
156
|
+
location=contract.entrypoint,
|
|
157
|
+
raw_value=repr(raw_outputs),
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
output_values: dict[str, JsonValue] = {}
|
|
163
|
+
output_refs = contract.output_refs or tuple(str(key) for key in raw_outputs)
|
|
164
|
+
for output_ref in output_refs:
|
|
165
|
+
if output_ref not in raw_outputs:
|
|
166
|
+
diagnostics.append(
|
|
167
|
+
ExecutionDiagnostic(
|
|
168
|
+
code="missing_generated_output",
|
|
169
|
+
message="generated Python model did not return a declared output",
|
|
170
|
+
location=output_ref,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
value = raw_outputs[output_ref]
|
|
175
|
+
if not _is_json_value(value):
|
|
176
|
+
diagnostics.append(
|
|
177
|
+
ExecutionDiagnostic(
|
|
178
|
+
code="non_json_generated_output",
|
|
179
|
+
message="generated output value is not JSON-serializable",
|
|
180
|
+
severity="error",
|
|
181
|
+
location=output_ref,
|
|
182
|
+
raw_value=repr(value),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
output_values[output_ref] = value
|
|
187
|
+
|
|
188
|
+
return _result(contract=contract, module_path=path, output_values=output_values, diagnostics=tuple(diagnostics))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _load_generated_module(path: Path) -> ModuleType | ExecutionDiagnostic:
|
|
192
|
+
module_name = f"_modelwright_generated_{path.stem}_{uuid.uuid4().hex}"
|
|
193
|
+
try:
|
|
194
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
195
|
+
if spec is None or spec.loader is None:
|
|
196
|
+
return ExecutionDiagnostic(
|
|
197
|
+
code="generated_model_import_failed",
|
|
198
|
+
message="could not build an import specification for generated Python model",
|
|
199
|
+
severity="error",
|
|
200
|
+
location=str(path),
|
|
201
|
+
)
|
|
202
|
+
module = importlib.util.module_from_spec(spec)
|
|
203
|
+
sys.modules[module_name] = module
|
|
204
|
+
spec.loader.exec_module(module)
|
|
205
|
+
except Exception as error: # noqa: BLE001
|
|
206
|
+
return ExecutionDiagnostic(
|
|
207
|
+
code="generated_model_import_failed",
|
|
208
|
+
message="generated Python model could not be imported",
|
|
209
|
+
severity="error",
|
|
210
|
+
location=str(path),
|
|
211
|
+
raw_value=repr(error),
|
|
212
|
+
)
|
|
213
|
+
finally:
|
|
214
|
+
sys.modules.pop(module_name, None)
|
|
215
|
+
return module
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _result(
|
|
219
|
+
*,
|
|
220
|
+
contract: GeneratedModuleContract,
|
|
221
|
+
module_path: Path,
|
|
222
|
+
output_values: dict[str, JsonValue] | None = None,
|
|
223
|
+
diagnostics: tuple[ExecutionDiagnostic, ...] = (),
|
|
224
|
+
) -> GeneratedExecutionResult:
|
|
225
|
+
return GeneratedExecutionResult(
|
|
226
|
+
contract=contract,
|
|
227
|
+
module_path=str(module_path),
|
|
228
|
+
entrypoint=contract.entrypoint,
|
|
229
|
+
output_values=output_values or {},
|
|
230
|
+
diagnostics=diagnostics,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _is_json_value(value: Any) -> bool:
|
|
235
|
+
try:
|
|
236
|
+
json.dumps(value)
|
|
237
|
+
except (TypeError, ValueError):
|
|
238
|
+
return False
|
|
239
|
+
return True
|