modelwright 0.1.0a2__tar.gz → 0.1.0a4__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.
- {modelwright-0.1.0a2/src/modelwright.egg-info → modelwright-0.1.0a4}/PKG-INFO +2 -2
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/README.md +1 -1
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/pyproject.toml +4 -1
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/__init__.py +27 -1
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/generation.py +141 -32
- modelwright-0.1.0a4/src/modelwright/wrappers.py +426 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4/src/modelwright.egg-info}/PKG-INFO +2 -2
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright.egg-info/SOURCES.txt +4 -1
- modelwright-0.1.0a4/tests/test_fable_wrapper_benchmark.py +89 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_generation_contract.py +1 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_import.py +1 -1
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_public_api.py +5 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_python_generation.py +179 -0
- modelwright-0.1.0a4/tests/test_wrappers.py +212 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/LICENSE +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/setup.cfg +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/cli.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/conversion.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/evaluation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/execution.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/extraction.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/formulas.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/formulas_oracle.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/graph.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/oracle_validation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/oracles.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/references.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright/validation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright.egg-info/dependency_links.txt +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright.egg-info/entry_points.txt +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright.egg-info/requires.txt +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/src/modelwright.egg-info/top_level.txt +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_cli.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_conversion_plan.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_dependency_graph.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_evaluation_orchestration.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_extraction_records.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_formula_expressions.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_formula_translation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_formulas_oracle.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_generated_execution.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_materialize_fable_benchmarks.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_openpyxl_extraction.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_oracle_backed_validation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_oracle_interface.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_references.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_scalar_comparison.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_supported_semantics_fixture.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_synthetic_fixture.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_validation.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_validation_regression.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_validation_report_builder.py +0 -0
- {modelwright-0.1.0a2 → modelwright-0.1.0a4}/tests/test_validation_scenario.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modelwright
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a4
|
|
4
4
|
Summary: Tools for converting spreadsheet workbooks into transparent Python models.
|
|
5
5
|
Author: UBC FRESH Lab
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,7 +135,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
135
135
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
138
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a4`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
139
139
|
|
|
140
140
|
Check release artifacts locally:
|
|
141
141
|
|
|
@@ -84,7 +84,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
84
84
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
87
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a4`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
88
88
|
|
|
89
89
|
Check release artifacts locally:
|
|
90
90
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modelwright"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.0a4"
|
|
8
8
|
description = "Tools for converting spreadsheet workbooks into transparent Python models."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -82,6 +82,9 @@ where = ["src"]
|
|
|
82
82
|
|
|
83
83
|
[tool.pytest.ini_options]
|
|
84
84
|
testpaths = ["tests"]
|
|
85
|
+
markers = [
|
|
86
|
+
"benchmark: tests that require local benchmark artifacts and are skipped unless explicitly enabled"
|
|
87
|
+
]
|
|
85
88
|
|
|
86
89
|
[tool.ruff]
|
|
87
90
|
line-length = 120
|
|
@@ -80,11 +80,27 @@ from modelwright.validation import (
|
|
|
80
80
|
compare_scalar_output,
|
|
81
81
|
load_validation_scenario,
|
|
82
82
|
)
|
|
83
|
+
from modelwright.wrappers import (
|
|
84
|
+
CellRef,
|
|
85
|
+
CellView,
|
|
86
|
+
ModelFacade,
|
|
87
|
+
ReportRef,
|
|
88
|
+
Scenario,
|
|
89
|
+
TableRef,
|
|
90
|
+
TableView,
|
|
91
|
+
WrapperDeclarationError,
|
|
92
|
+
WrapperExecutionError,
|
|
93
|
+
cell,
|
|
94
|
+
report,
|
|
95
|
+
table,
|
|
96
|
+
)
|
|
83
97
|
|
|
84
|
-
__version__ = "0.1.
|
|
98
|
+
__version__ = "0.1.0a4"
|
|
85
99
|
|
|
86
100
|
__all__ = [
|
|
87
101
|
"CellRecord",
|
|
102
|
+
"CellRef",
|
|
103
|
+
"CellView",
|
|
88
104
|
"ComparisonResult",
|
|
89
105
|
"ComparisonRules",
|
|
90
106
|
"ConversionPlan",
|
|
@@ -109,6 +125,7 @@ __all__ = [
|
|
|
109
125
|
"GenerationDiagnostic",
|
|
110
126
|
"GenerationResult",
|
|
111
127
|
"MISSING_VALUE",
|
|
128
|
+
"ModelFacade",
|
|
112
129
|
"NamedRangeRecord",
|
|
113
130
|
"OracleConfig",
|
|
114
131
|
"OracleDiagnostic",
|
|
@@ -117,10 +134,14 @@ __all__ = [
|
|
|
117
134
|
"PlanRecommendation",
|
|
118
135
|
"PrivacyReview",
|
|
119
136
|
"ResidualBlocker",
|
|
137
|
+
"ReportRef",
|
|
138
|
+
"Scenario",
|
|
120
139
|
"ScenarioInput",
|
|
121
140
|
"ScenarioOutput",
|
|
122
141
|
"SheetRecord",
|
|
142
|
+
"TableRef",
|
|
123
143
|
"TableRecord",
|
|
144
|
+
"TableView",
|
|
124
145
|
"ValidationReport",
|
|
125
146
|
"ValidationEvaluationResult",
|
|
126
147
|
"ValidationScenario",
|
|
@@ -129,12 +150,15 @@ __all__ = [
|
|
|
129
150
|
"WorkbookReference",
|
|
130
151
|
"WorkbookRecord",
|
|
131
152
|
"WorkflowStatus",
|
|
153
|
+
"WrapperDeclarationError",
|
|
154
|
+
"WrapperExecutionError",
|
|
132
155
|
"__version__",
|
|
133
156
|
"build_dependency_graph",
|
|
134
157
|
"build_conversion_plan",
|
|
135
158
|
"build_formula_reference_index",
|
|
136
159
|
"build_oracle_validation_report",
|
|
137
160
|
"build_validation_report",
|
|
161
|
+
"cell",
|
|
138
162
|
"compare_scalar_output",
|
|
139
163
|
"execute_generated_model",
|
|
140
164
|
"evaluate_generated_model",
|
|
@@ -144,5 +168,7 @@ __all__ = [
|
|
|
144
168
|
"load_validation_scenario",
|
|
145
169
|
"normalize_cell_reference",
|
|
146
170
|
"normalize_reference",
|
|
171
|
+
"report",
|
|
172
|
+
"table",
|
|
147
173
|
"translate_formula_cell",
|
|
148
174
|
]
|
|
@@ -17,7 +17,10 @@ from modelwright.references import WorkbookReference
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
JsonValue = str | int | float | bool | None | list[Any] | dict[str, Any]
|
|
20
|
+
DEFAULT_INLINE_PROVENANCE_COMMENT_LIMIT = 50_000
|
|
21
|
+
DEFAULT_INLINE_FORMULA_LAMBDA_LIMIT = 50_000
|
|
20
22
|
DiagnosticSeverity = Literal["info", "warning", "error"]
|
|
23
|
+
FormulaStorage = Literal["lambdas", "expression_source"]
|
|
21
24
|
GeneratedSymbolKind = Literal["input", "intermediate", "output"]
|
|
22
25
|
|
|
23
26
|
|
|
@@ -89,6 +92,7 @@ class GeneratedModuleContract:
|
|
|
89
92
|
output_refs: tuple[str, ...] = field(default_factory=tuple)
|
|
90
93
|
symbols: tuple[GeneratedSymbol, ...] = field(default_factory=tuple)
|
|
91
94
|
include_provenance_comments: bool = True
|
|
95
|
+
formula_storage: FormulaStorage = "lambdas"
|
|
92
96
|
|
|
93
97
|
@classmethod
|
|
94
98
|
def from_dict(cls, data: dict[str, Any]) -> "GeneratedModuleContract":
|
|
@@ -100,6 +104,7 @@ class GeneratedModuleContract:
|
|
|
100
104
|
output_refs=tuple(data.get("output_refs", [])),
|
|
101
105
|
symbols=tuple(GeneratedSymbol.from_dict(item) for item in data.get("symbols", [])),
|
|
102
106
|
include_provenance_comments=data.get("include_provenance_comments", True),
|
|
107
|
+
formula_storage=data.get("formula_storage", "lambdas"),
|
|
103
108
|
)
|
|
104
109
|
|
|
105
110
|
def to_dict(self) -> dict[str, JsonValue]:
|
|
@@ -111,6 +116,7 @@ class GeneratedModuleContract:
|
|
|
111
116
|
"output_refs": list(self.output_refs),
|
|
112
117
|
"symbols": [symbol.to_dict() for symbol in self.symbols],
|
|
113
118
|
"include_provenance_comments": self.include_provenance_comments,
|
|
119
|
+
"formula_storage": self.formula_storage,
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
|
|
@@ -186,6 +192,8 @@ def infer_generated_module_contract(
|
|
|
186
192
|
module_name: str,
|
|
187
193
|
input_refs: Sequence[str] = (),
|
|
188
194
|
progress: Callable[[str], None] | None = None,
|
|
195
|
+
inline_provenance_comment_limit: int | None = DEFAULT_INLINE_PROVENANCE_COMMENT_LIMIT,
|
|
196
|
+
inline_formula_lambda_limit: int | None = DEFAULT_INLINE_FORMULA_LAMBDA_LIMIT,
|
|
189
197
|
) -> GeneratedContractInferenceResult:
|
|
190
198
|
"""Infer a generated module contract by walking dependencies for selected outputs."""
|
|
191
199
|
|
|
@@ -242,7 +250,9 @@ def infer_generated_module_contract(
|
|
|
242
250
|
)
|
|
243
251
|
|
|
244
252
|
input_order: list[str] = []
|
|
253
|
+
input_seen: set[str] = set()
|
|
245
254
|
formula_order: list[str] = []
|
|
255
|
+
formula_seen: set[str] = set()
|
|
246
256
|
visiting: set[str] = set()
|
|
247
257
|
visited: set[str] = set()
|
|
248
258
|
circular_dependency_locations: set[str] = set()
|
|
@@ -255,12 +265,11 @@ def infer_generated_module_contract(
|
|
|
255
265
|
if isinstance(dependency, str):
|
|
256
266
|
refs.append(dependency)
|
|
257
267
|
continue
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
268
|
+
expanded = expanded_range_dependencies.get(dependency.normalized)
|
|
269
|
+
if expanded is None:
|
|
270
|
+
expanded = _expand_range_dependency(dependency)
|
|
271
|
+
expanded_range_dependencies[dependency.normalized] = expanded
|
|
272
|
+
refs.extend(expanded)
|
|
264
273
|
return tuple(refs)
|
|
265
274
|
|
|
266
275
|
def visit(root_ref: str) -> None:
|
|
@@ -273,8 +282,9 @@ def infer_generated_module_contract(
|
|
|
273
282
|
|
|
274
283
|
if dependencies_processed:
|
|
275
284
|
visiting.discard(cell_ref)
|
|
276
|
-
if cell_ref not in
|
|
285
|
+
if cell_ref not in formula_seen:
|
|
277
286
|
formula_order.append(cell_ref)
|
|
287
|
+
formula_seen.add(cell_ref)
|
|
278
288
|
visited.add(cell_ref)
|
|
279
289
|
continue
|
|
280
290
|
|
|
@@ -303,14 +313,16 @@ def infer_generated_module_contract(
|
|
|
303
313
|
|
|
304
314
|
cell = cell_by_ref.get(cell_ref)
|
|
305
315
|
if cell is None:
|
|
306
|
-
if cell_ref not in
|
|
316
|
+
if cell_ref not in input_seen:
|
|
307
317
|
input_order.append(cell_ref)
|
|
318
|
+
input_seen.add(cell_ref)
|
|
308
319
|
visited.add(cell_ref)
|
|
309
320
|
continue
|
|
310
321
|
|
|
311
322
|
if cell_ref in explicit_inputs or cell.formula is None:
|
|
312
|
-
if cell_ref not in
|
|
323
|
+
if cell_ref not in input_seen:
|
|
313
324
|
input_order.append(cell_ref)
|
|
325
|
+
input_seen.add(cell_ref)
|
|
314
326
|
visited.add(cell_ref)
|
|
315
327
|
continue
|
|
316
328
|
|
|
@@ -368,6 +380,12 @@ def infer_generated_module_contract(
|
|
|
368
380
|
input_refs=tuple(input_order),
|
|
369
381
|
output_refs=selected_outputs,
|
|
370
382
|
symbols=symbols,
|
|
383
|
+
include_provenance_comments=(
|
|
384
|
+
inline_provenance_comment_limit is None or len(formula_order) <= inline_provenance_comment_limit
|
|
385
|
+
),
|
|
386
|
+
formula_storage="lambdas"
|
|
387
|
+
if inline_formula_lambda_limit is None or len(formula_order) <= inline_formula_lambda_limit
|
|
388
|
+
else "expression_source",
|
|
371
389
|
)
|
|
372
390
|
return GeneratedContractInferenceResult(
|
|
373
391
|
contract=contract,
|
|
@@ -478,6 +496,44 @@ def _render_module(
|
|
|
478
496
|
'"""',
|
|
479
497
|
"",
|
|
480
498
|
"import fnmatch",
|
|
499
|
+
"from functools import lru_cache",
|
|
500
|
+
"",
|
|
501
|
+
"",
|
|
502
|
+
"class _SfRangeView:",
|
|
503
|
+
" def __init__(self, sheet, min_col, min_row, max_col, max_row, get_value):",
|
|
504
|
+
" self.sheet = sheet",
|
|
505
|
+
" self.min_col = min_col",
|
|
506
|
+
" self.min_row = min_row",
|
|
507
|
+
" self.max_col = max_col",
|
|
508
|
+
" self.max_row = max_row",
|
|
509
|
+
" self._get_value = get_value",
|
|
510
|
+
" self._values = None",
|
|
511
|
+
" self._lazy_values = None",
|
|
512
|
+
" self._value_calls = 0",
|
|
513
|
+
" self._lazy_value_calls = 0",
|
|
514
|
+
"",
|
|
515
|
+
" def _refs(self):",
|
|
516
|
+
" for row in range(self.min_row, self.max_row + 1):",
|
|
517
|
+
" for column in range(self.min_col, self.max_col + 1):",
|
|
518
|
+
" yield f'{self.sheet}!{_sf_column_name(column)}{row}'",
|
|
519
|
+
"",
|
|
520
|
+
" def values(self):",
|
|
521
|
+
" if self._values is not None:",
|
|
522
|
+
" return self._values",
|
|
523
|
+
" values = tuple(self._get_value(ref) for ref in self._refs())",
|
|
524
|
+
" self._value_calls += 1",
|
|
525
|
+
" if self._value_calls > 1:",
|
|
526
|
+
" self._values = values",
|
|
527
|
+
" return values",
|
|
528
|
+
"",
|
|
529
|
+
" def lazy_values(self):",
|
|
530
|
+
" if self._lazy_values is not None:",
|
|
531
|
+
" return self._lazy_values",
|
|
532
|
+
" values = tuple(lambda ref=ref: self._get_value(ref) for ref in self._refs())",
|
|
533
|
+
" self._lazy_value_calls += 1",
|
|
534
|
+
" if self._lazy_value_calls > 1:",
|
|
535
|
+
" self._lazy_values = values",
|
|
536
|
+
" return values",
|
|
481
537
|
"",
|
|
482
538
|
"",
|
|
483
539
|
"def _sf_column_name(index):",
|
|
@@ -490,6 +546,9 @@ def _render_module(
|
|
|
490
546
|
"",
|
|
491
547
|
"def _sf_flatten(values):",
|
|
492
548
|
" for value in values:",
|
|
549
|
+
" if isinstance(value, _SfRangeView):",
|
|
550
|
+
" yield from value.values()",
|
|
551
|
+
" continue",
|
|
493
552
|
" if isinstance(value, (list, tuple)):",
|
|
494
553
|
" yield from _sf_flatten(value)",
|
|
495
554
|
" else:",
|
|
@@ -498,6 +557,9 @@ def _render_module(
|
|
|
498
557
|
"",
|
|
499
558
|
"def _sf_flatten_lazy(values):",
|
|
500
559
|
" for value in values:",
|
|
560
|
+
" if isinstance(value, _SfRangeView):",
|
|
561
|
+
" yield from value.lazy_values()",
|
|
562
|
+
" continue",
|
|
501
563
|
" if isinstance(value, (list, tuple)):",
|
|
502
564
|
" yield from _sf_flatten_lazy(value)",
|
|
503
565
|
" else:",
|
|
@@ -525,6 +587,7 @@ def _render_module(
|
|
|
525
587
|
" return value",
|
|
526
588
|
"",
|
|
527
589
|
"",
|
|
590
|
+
"@lru_cache(maxsize=4096)",
|
|
528
591
|
"def _sf_numeric_value(value):",
|
|
529
592
|
" if isinstance(value, bool):",
|
|
530
593
|
" return None",
|
|
@@ -605,17 +668,20 @@ def _render_module(
|
|
|
605
668
|
" raise ValueError(f'unsupported criteria operator: {operator}')",
|
|
606
669
|
"",
|
|
607
670
|
"",
|
|
608
|
-
"def
|
|
671
|
+
"def _sf_criteria_matcher(criteria):",
|
|
609
672
|
" if isinstance(criteria, str):",
|
|
610
673
|
" for operator in ('>=', '<=', '<>', '>', '<', '='):",
|
|
611
674
|
" if criteria.startswith(operator):",
|
|
612
|
-
"
|
|
613
|
-
" return _sf_compare_criteria(value, operator,
|
|
675
|
+
" raw_expected = criteria[len(operator):]",
|
|
676
|
+
" return lambda value: _sf_compare_criteria(value, operator, _sf_coerce_criteria(raw_expected, value))",
|
|
614
677
|
" if '*' in criteria or '?' in criteria:",
|
|
615
|
-
" return fnmatch.fnmatchcase(str(value), criteria)",
|
|
616
|
-
"
|
|
617
|
-
"
|
|
618
|
-
"
|
|
678
|
+
" return lambda value: fnmatch.fnmatchcase(str(value), criteria)",
|
|
679
|
+
" return lambda value: _sf_compare_criteria(value, '=', _sf_coerce_criteria(criteria, value))",
|
|
680
|
+
" return lambda value: _sf_compare_criteria(value, '=', criteria)",
|
|
681
|
+
"",
|
|
682
|
+
"",
|
|
683
|
+
"def _sf_matches_criteria(value, criteria):",
|
|
684
|
+
" return _sf_criteria_matcher(criteria)(value)",
|
|
619
685
|
"",
|
|
620
686
|
"",
|
|
621
687
|
"def _sf_lookup_equal(left, right):",
|
|
@@ -627,24 +693,26 @@ def _render_module(
|
|
|
627
693
|
"def _sf_sumif(criteria_range, criteria, sum_range=None):",
|
|
628
694
|
" criteria_values = tuple(_sf_flatten((criteria_range,)))",
|
|
629
695
|
" sum_values = criteria_values if sum_range is None else tuple(_sf_flatten_lazy((sum_range,)))",
|
|
696
|
+
" matcher = _sf_criteria_matcher(criteria)",
|
|
630
697
|
" total = 0",
|
|
631
698
|
" for criteria_value, sum_value in zip(criteria_values, sum_values):",
|
|
632
|
-
" if
|
|
699
|
+
" if matcher(criteria_value):",
|
|
633
700
|
" total += _sf_sum_value(sum_value)",
|
|
634
701
|
" return total",
|
|
635
702
|
"",
|
|
636
703
|
"",
|
|
637
704
|
"def _sf_countif(criteria_range, criteria):",
|
|
638
|
-
"
|
|
705
|
+
" matcher = _sf_criteria_matcher(criteria)",
|
|
706
|
+
" return sum(1 for value in _sf_flatten((criteria_range,)) if matcher(value))",
|
|
639
707
|
"",
|
|
640
708
|
"",
|
|
641
709
|
"def _sf_sumifs(sum_range, *criteria_pairs):",
|
|
642
710
|
" sum_values = tuple(_sf_flatten_lazy((sum_range,)))",
|
|
643
711
|
" criteria_ranges = [tuple(_sf_flatten((criteria_range,))) for criteria_range, _criteria in criteria_pairs]",
|
|
644
|
-
"
|
|
712
|
+
" criteria_matchers = tuple(_sf_criteria_matcher(criteria) for _range, criteria in criteria_pairs)",
|
|
645
713
|
" total = 0",
|
|
646
714
|
" for index, sum_value in enumerate(sum_values):",
|
|
647
|
-
" if all(
|
|
715
|
+
" if all(matcher(criteria_range[index]) for criteria_range, matcher in zip(criteria_ranges, criteria_matchers)):",
|
|
648
716
|
" total += _sf_sum_value(sum_value)",
|
|
649
717
|
" return total",
|
|
650
718
|
"",
|
|
@@ -653,11 +721,11 @@ def _render_module(
|
|
|
653
721
|
" criteria_ranges = [tuple(_sf_flatten((criteria_range,))) for criteria_range, _criteria in criteria_pairs]",
|
|
654
722
|
" if not criteria_ranges:",
|
|
655
723
|
" return 0",
|
|
656
|
-
"
|
|
724
|
+
" criteria_matchers = tuple(_sf_criteria_matcher(criteria) for _range, criteria in criteria_pairs)",
|
|
657
725
|
" return sum(",
|
|
658
726
|
" 1",
|
|
659
727
|
" for index in range(len(criteria_ranges[0]))",
|
|
660
|
-
" if all(
|
|
728
|
+
" if all(matcher(criteria_range[index]) for criteria_range, matcher in zip(criteria_ranges, criteria_matchers))",
|
|
661
729
|
" )",
|
|
662
730
|
"",
|
|
663
731
|
"",
|
|
@@ -699,6 +767,7 @@ def _render_module(
|
|
|
699
767
|
f"def {contract.entrypoint}(inputs=None):",
|
|
700
768
|
" inputs = {} if inputs is None else dict(inputs)",
|
|
701
769
|
" _cache = {}",
|
|
770
|
+
" _range_cache = {}",
|
|
702
771
|
" _stack = []",
|
|
703
772
|
" _evaluated_count = 0",
|
|
704
773
|
" _constants = {",
|
|
@@ -731,7 +800,7 @@ def _render_module(
|
|
|
731
800
|
" raise RuntimeError('circular dependency during generated model execution: ' + ' -> '.join(cycle))",
|
|
732
801
|
" _stack.append(cell_ref)",
|
|
733
802
|
" try:",
|
|
734
|
-
" value = formula
|
|
803
|
+
" value = _evaluate_formula(cell_ref, formula)",
|
|
735
804
|
" finally:",
|
|
736
805
|
" _stack.pop()",
|
|
737
806
|
" _cache[cell_ref] = value",
|
|
@@ -750,11 +819,12 @@ def _render_module(
|
|
|
750
819
|
" return value",
|
|
751
820
|
"",
|
|
752
821
|
" def _range(sheet, min_col, min_row, max_col, max_row):",
|
|
753
|
-
"
|
|
754
|
-
"
|
|
755
|
-
"
|
|
756
|
-
"
|
|
757
|
-
"
|
|
822
|
+
" key = (sheet, min_col, min_row, max_col, max_row)",
|
|
823
|
+
" view = _range_cache.get(key)",
|
|
824
|
+
" if view is None:",
|
|
825
|
+
" view = _SfRangeView(sheet, min_col, min_row, max_col, max_row, _get)",
|
|
826
|
+
" _range_cache[key] = view",
|
|
827
|
+
" return view",
|
|
758
828
|
"",
|
|
759
829
|
" def _table(sheet, min_col, min_row, max_col, max_row):",
|
|
760
830
|
" return tuple(",
|
|
@@ -762,6 +832,36 @@ def _render_module(
|
|
|
762
832
|
" for row in range(min_row, max_row + 1)",
|
|
763
833
|
" )",
|
|
764
834
|
"",
|
|
835
|
+
]
|
|
836
|
+
)
|
|
837
|
+
if contract.formula_storage == "lambdas":
|
|
838
|
+
lines.extend(
|
|
839
|
+
[
|
|
840
|
+
" def _evaluate_formula(_cell_ref, formula):",
|
|
841
|
+
" return formula()",
|
|
842
|
+
"",
|
|
843
|
+
]
|
|
844
|
+
)
|
|
845
|
+
elif contract.formula_storage == "expression_source":
|
|
846
|
+
lines.extend(
|
|
847
|
+
[
|
|
848
|
+
" _formula_globals = dict(globals())",
|
|
849
|
+
" _formula_globals.update({",
|
|
850
|
+
" '_get': _get,",
|
|
851
|
+
" '_range': _range,",
|
|
852
|
+
" '_table': _table,",
|
|
853
|
+
" })",
|
|
854
|
+
"",
|
|
855
|
+
" def _evaluate_formula(cell_ref, formula):",
|
|
856
|
+
" code = compile(formula, f'<modelwright formula {cell_ref}>', 'eval')",
|
|
857
|
+
" return eval(code, _formula_globals)",
|
|
858
|
+
"",
|
|
859
|
+
]
|
|
860
|
+
)
|
|
861
|
+
else:
|
|
862
|
+
raise ValueError(f"unsupported formula storage: {contract.formula_storage}")
|
|
863
|
+
lines.extend(
|
|
864
|
+
[
|
|
765
865
|
" _formulas = {",
|
|
766
866
|
]
|
|
767
867
|
)
|
|
@@ -771,7 +871,11 @@ def _render_module(
|
|
|
771
871
|
lines.append(f" # {symbol.cell_ref}" + (f": {symbol.raw_formula}" if symbol.raw_formula else ""))
|
|
772
872
|
|
|
773
873
|
expression = expressions[symbol.cell_ref]
|
|
774
|
-
|
|
874
|
+
rendered_formula = _render_formula_root(expression.root)
|
|
875
|
+
if contract.formula_storage == "lambdas":
|
|
876
|
+
lines.append(f" {symbol.cell_ref!r}: lambda: {rendered_formula},")
|
|
877
|
+
else:
|
|
878
|
+
lines.append(f" {symbol.cell_ref!r}: {rendered_formula!r},")
|
|
775
879
|
|
|
776
880
|
if index == 1 or index % 10000 == 0 or index == len(formula_symbols):
|
|
777
881
|
_progress(
|
|
@@ -781,12 +885,17 @@ def _render_module(
|
|
|
781
885
|
lines.extend(
|
|
782
886
|
[
|
|
783
887
|
" }",
|
|
784
|
-
"
|
|
888
|
+
" _output_refs = (",
|
|
785
889
|
]
|
|
786
890
|
)
|
|
787
891
|
for output_ref in contract.output_refs:
|
|
788
|
-
lines.append(f" {output_ref!r}
|
|
789
|
-
lines.
|
|
892
|
+
lines.append(f" {output_ref!r},")
|
|
893
|
+
lines.extend(
|
|
894
|
+
[
|
|
895
|
+
" )",
|
|
896
|
+
" return {cell_ref: _get(cell_ref) for cell_ref in _output_refs}",
|
|
897
|
+
]
|
|
898
|
+
)
|
|
790
899
|
lines.append("")
|
|
791
900
|
return "\n".join(lines)
|
|
792
901
|
|