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.
@@ -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
+ )
@@ -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