equiframe 0.12.4__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.
- equiframe/__init__.py +832 -0
- equiframe/__main__.py +3 -0
- equiframe/_base.py +84 -0
- equiframe/_cli_support.py +125 -0
- equiframe/_comparison_io.py +161 -0
- equiframe/_dataset_provenance.py +146 -0
- equiframe/_decimal.py +22 -0
- equiframe/_diff_capture.py +433 -0
- equiframe/_doctor.py +123 -0
- equiframe/_document_io.py +61 -0
- equiframe/_documents.py +79 -0
- equiframe/_duckdb_transport.py +149 -0
- equiframe/_error_reporting.py +52 -0
- equiframe/_first_use.py +102 -0
- equiframe/_path_compat.py +26 -0
- equiframe/_physical.py +133 -0
- equiframe/_r_review.R +308 -0
- equiframe/_regular_io.py +167 -0
- equiframe/_report_format_v1.py +298 -0
- equiframe/_report_io.py +591 -0
- equiframe/_stability_inventory.py +1421 -0
- equiframe/_timezones.py +257 -0
- equiframe/_vendor/cyclonedx/LICENSE +201 -0
- equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
- equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
- equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
- equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
- equiframe/adapters.py +454 -0
- equiframe/assertion_types.py +122 -0
- equiframe/assertions.py +271 -0
- equiframe/benchmark.py +101 -0
- equiframe/bundle.py +240 -0
- equiframe/cli.py +693 -0
- equiframe/comparison.py +937 -0
- equiframe/comparison_report.py +660 -0
- equiframe/comparison_types.py +257 -0
- equiframe/compatibility.py +546 -0
- equiframe/contracts.py +879 -0
- equiframe/datasets.py +1301 -0
- equiframe/decision.py +34 -0
- equiframe/destinations.py +295 -0
- equiframe/detector_types.py +431 -0
- equiframe/detectors.py +389 -0
- equiframe/diff.py +192 -0
- equiframe/diff_contracts.py +523 -0
- equiframe/docs/adoption.md +329 -0
- equiframe/docs/api.md +534 -0
- equiframe/docs/arquitecto.md +143 -0
- equiframe/docs/choosing-api.md +84 -0
- equiframe/docs/cientifico.md +180 -0
- equiframe/docs/compatibility.md +254 -0
- equiframe/docs/contributing.md +171 -0
- equiframe/docs/dependencies.md +237 -0
- equiframe/docs/eda.md +166 -0
- equiframe/docs/extending.md +158 -0
- equiframe/docs/fast-ci.md +52 -0
- equiframe/docs/first-use.md +78 -0
- equiframe/docs/index.md +142 -0
- equiframe/docs/ingestion.md +145 -0
- equiframe/docs/inicio.md +129 -0
- equiframe/docs/installed-private.md +97 -0
- equiframe/docs/journeys.md +362 -0
- equiframe/docs/lectura-conversion.md +211 -0
- equiframe/docs/licensing.md +17 -0
- equiframe/docs/maintenance.md +233 -0
- equiframe/docs/migration-ir.md +333 -0
- equiframe/docs/option-audit.md +45 -0
- equiframe/docs/outcome-gates.md +134 -0
- equiframe/docs/performance.md +263 -0
- equiframe/docs/piloto-simulado.md +56 -0
- equiframe/docs/quickstart.md +156 -0
- equiframe/docs/r_review.md +212 -0
- equiframe/docs/recipes.md +248 -0
- equiframe/docs/releasing.md +183 -0
- equiframe/docs/security.md +218 -0
- equiframe/docs/semantics.md +274 -0
- equiframe/docs/sqlite-snapshots.md +262 -0
- equiframe/docs/universal-sources.md +49 -0
- equiframe/docs/universalidad-cinco-pasos.md +107 -0
- equiframe/docs/upgrading.md +538 -0
- equiframe/duckdb_capture.py +410 -0
- equiframe/eda/__init__.py +36 -0
- equiframe/eda/__main__.py +3 -0
- equiframe/eda/_backend.py +612 -0
- equiframe/eda/_inventory.py +49 -0
- equiframe/eda/_paths.py +49 -0
- equiframe/eda/_report.py +291 -0
- equiframe/eda/api.py +535 -0
- equiframe/eda/cli.py +102 -0
- equiframe/eda/models.py +239 -0
- equiframe/errors.py +83 -0
- equiframe/evidence.py +375 -0
- equiframe/examples/integrations/eda_generic.py +60 -0
- equiframe/examples/integrations/outcome_gates.py +94 -0
- equiframe/examples/integrations/sqlite_snapshot.py +246 -0
- equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
- equiframe/examples/integrations/universal_first_use.py +296 -0
- equiframe/examples/journeys/README.md +31 -0
- equiframe/examples/journeys/__init__.py +1 -0
- equiframe/examples/journeys/a_pandas_refactor.py +88 -0
- equiframe/examples/journeys/b_native_migration.py +181 -0
- equiframe/examples/journeys/c_retail_etl.py +460 -0
- equiframe/examples/journeys/common.py +136 -0
- equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
- equiframe/examples/journeys/e_telemetry.py +135 -0
- equiframe/examples/journeys/f_data_science_preparation.py +84 -0
- equiframe/examples/journeys/g_external_snapshots.py +197 -0
- equiframe/examples/journeys/h_portable_ci.py +129 -0
- equiframe/examples/journeys/requirements.lock +249 -0
- equiframe/examples/journeys/verify_journeys.py +117 -0
- equiframe/examples/load_convert_compare.py +85 -0
- equiframe/execution.py +355 -0
- equiframe/execution_types.py +177 -0
- equiframe/ingestion.py +744 -0
- equiframe/local.py +126 -0
- equiframe/migration/__init__.py +94 -0
- equiframe/migration/_physical.py +8 -0
- equiframe/migration/backends/_timestamp_transport.py +109 -0
- equiframe/migration/backends/duckdb.py +651 -0
- equiframe/migration/backends/duckdb_decimal.py +125 -0
- equiframe/migration/backends/duckdb_time.py +87 -0
- equiframe/migration/backends/pandas.py +544 -0
- equiframe/migration/backends/polars.py +915 -0
- equiframe/migration/backends/polars_decimal.py +231 -0
- equiframe/migration/io.py +198 -0
- equiframe/migration/ports.py +96 -0
- equiframe/migration/runner.py +545 -0
- equiframe/migration/runner_types.py +405 -0
- equiframe/migration/types.py +354 -0
- equiframe/migration/validation.py +619 -0
- equiframe/normalization.py +358 -0
- equiframe/pipeline.py +274 -0
- equiframe/pipeline_types.py +247 -0
- equiframe/ports.py +39 -0
- equiframe/preprocessing.py +110 -0
- equiframe/profile_types.py +343 -0
- equiframe/profiles.py +447 -0
- equiframe/py.typed +0 -0
- equiframe/r_review.py +706 -0
- equiframe/reduction.py +518 -0
- equiframe/registry.py +121 -0
- equiframe/report_rendering.py +366 -0
- equiframe/report_types.py +294 -0
- equiframe/reporting.py +26 -0
- equiframe/rule_types.py +178 -0
- equiframe/rules.py +819 -0
- equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
- equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
- equiframe/schemas/r-review-options-1.0.schema.json +38 -0
- equiframe/schemas/r-review-result-1.0.schema.json +207 -0
- equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
- equiframe/serialization.py +152 -0
- equiframe/snapshot.py +296 -0
- equiframe/sources.py +283 -0
- equiframe/tables.py +459 -0
- equiframe/types.py +71 -0
- equiframe/values.py +252 -0
- equiframe/verification_request.py +35 -0
- equiframe/verify.py +799 -0
- equiframe/verify_types.py +255 -0
- equiframe/workflow.py +510 -0
- equiframe/workflow_request.py +38 -0
- equiframe-0.12.4.dist-info/METADATA +268 -0
- equiframe-0.12.4.dist-info/RECORD +170 -0
- equiframe-0.12.4.dist-info/WHEEL +4 -0
- equiframe-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Exact native Decimal coefficient arithmetic with explicit overflow checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from equiframe.errors import CapabilityError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
from equiframe.execution import RunContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def divide(
|
|
16
|
+
frame: pl.DataFrame,
|
|
17
|
+
left: pl.Expr,
|
|
18
|
+
right: pl.Expr,
|
|
19
|
+
*,
|
|
20
|
+
left_scale: int,
|
|
21
|
+
right_scale: int,
|
|
22
|
+
precision: int,
|
|
23
|
+
scale: int,
|
|
24
|
+
rounding: str,
|
|
25
|
+
prefix: str,
|
|
26
|
+
context: RunContext,
|
|
27
|
+
) -> tuple[pl.DataFrame, pl.Expr]:
|
|
28
|
+
"""Materialize native quotient/remainder steps; never use floats or row callbacks.
|
|
29
|
+
|
|
30
|
+
Each operand coefficient fits 38 decimal digits. Modular doubling computes
|
|
31
|
+
the next digit without multiplying a potentially 38-digit remainder by ten.
|
|
32
|
+
Decimal(38,0) carries exact coefficient arithmetic: Polars 1.30 panics on
|
|
33
|
+
Int128 column/column addition and subtraction. Integer floor/modulo remain
|
|
34
|
+
native Int128 operations; every resulting coefficient still fits 38 digits.
|
|
35
|
+
Only finite native boolean reductions cross back to the coordinator to reject
|
|
36
|
+
invalid arithmetic; neither operands nor answers are computed in Python.
|
|
37
|
+
"""
|
|
38
|
+
import polars as pl
|
|
39
|
+
|
|
40
|
+
def col(name: str) -> pl.Expr:
|
|
41
|
+
return pl.col(prefix + name)
|
|
42
|
+
|
|
43
|
+
def named(value: pl.Expr, name: str) -> pl.Expr:
|
|
44
|
+
return value.alias(prefix + name)
|
|
45
|
+
|
|
46
|
+
wide = pl.Decimal(38, 0)
|
|
47
|
+
zero = pl.lit(0, dtype=wide)
|
|
48
|
+
|
|
49
|
+
def constant(value: int) -> pl.Expr:
|
|
50
|
+
return pl.lit(str(value)).cast(wide)
|
|
51
|
+
|
|
52
|
+
def floor(left: pl.Expr, right: pl.Expr) -> pl.Expr:
|
|
53
|
+
# The native direct Int128 -> Decimal cast rejects valid large values
|
|
54
|
+
# in Polars 1.30. Its native integer text and Decimal parsers are exact.
|
|
55
|
+
return (left.cast(pl.Int128) // right.cast(pl.Int128)).cast(pl.String).cast(wide)
|
|
56
|
+
|
|
57
|
+
def modulo(left: pl.Expr, right: pl.Expr) -> pl.Expr:
|
|
58
|
+
return (left.cast(pl.Int128) % right.cast(pl.Int128)).cast(pl.String).cast(wide)
|
|
59
|
+
|
|
60
|
+
frame = frame.with_columns(
|
|
61
|
+
named(left.is_not_null() & right.is_not_null(), "active"),
|
|
62
|
+
named(left.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "a"),
|
|
63
|
+
named(right.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "b"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def reject(condition: pl.Expr, reason: str) -> None:
|
|
67
|
+
context.check()
|
|
68
|
+
if frame.select((col("active") & condition).any()).item():
|
|
69
|
+
raise CapabilityError("Polars Decimal division: " + reason)
|
|
70
|
+
|
|
71
|
+
reject(col("b") == 0, "zero denominator")
|
|
72
|
+
frame = frame.with_columns(
|
|
73
|
+
named((col("a") < 0).xor(col("b") < 0), "negative"),
|
|
74
|
+
named(pl.when(col("active")).then(col("a").abs()).otherwise(zero), "a"),
|
|
75
|
+
named(
|
|
76
|
+
pl.when(col("active")).then(col("b").abs()).otherwise(constant(1)),
|
|
77
|
+
"b",
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
frame = frame.with_columns(
|
|
81
|
+
named(floor(col("a"), col("b")), "q"), named(modulo(col("a"), col("b")), "r")
|
|
82
|
+
)
|
|
83
|
+
exponent = scale + right_scale - left_scale
|
|
84
|
+
limit = 10**precision - 1
|
|
85
|
+
if exponent >= 0:
|
|
86
|
+
reject(col("q") > constant(limit), "output precision exceeded")
|
|
87
|
+
for _ in range(exponent):
|
|
88
|
+
reject(col("q") > constant(limit // 10), "output precision exceeded")
|
|
89
|
+
remainder, denominator = col("r"), col("b")
|
|
90
|
+
carry = remainder >= denominator - remainder
|
|
91
|
+
frame = frame.with_columns(
|
|
92
|
+
named(carry.cast(pl.Int64).cast(wide), "c2"),
|
|
93
|
+
named(
|
|
94
|
+
remainder
|
|
95
|
+
- pl.when(carry).then(denominator - remainder).otherwise(zero - remainder),
|
|
96
|
+
"r2",
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
for first, second in ((2, 4), (4, 8)):
|
|
100
|
+
remainder = col("r" + str(first))
|
|
101
|
+
carry = remainder >= denominator - remainder
|
|
102
|
+
frame = frame.with_columns(
|
|
103
|
+
named(
|
|
104
|
+
col("c" + str(first)) * constant(2) + carry.cast(pl.Int64).cast(wide),
|
|
105
|
+
"c" + str(second),
|
|
106
|
+
),
|
|
107
|
+
named(
|
|
108
|
+
remainder
|
|
109
|
+
- pl.when(carry).then(denominator - remainder).otherwise(zero - remainder),
|
|
110
|
+
"r" + str(second),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
r8, r2 = col("r8"), col("r2")
|
|
114
|
+
carry = r8 >= denominator - r2
|
|
115
|
+
frame = frame.with_columns(
|
|
116
|
+
named(
|
|
117
|
+
col("q") * constant(10)
|
|
118
|
+
+ col("c8")
|
|
119
|
+
+ col("c2")
|
|
120
|
+
+ carry.cast(pl.Int64).cast(wide),
|
|
121
|
+
"q",
|
|
122
|
+
),
|
|
123
|
+
named(r8 - pl.when(carry).then(denominator - r2).otherwise(zero - r2), "r"),
|
|
124
|
+
)
|
|
125
|
+
remainder = col("r")
|
|
126
|
+
exact = remainder == 0
|
|
127
|
+
above = remainder > col("b") - remainder
|
|
128
|
+
tie = remainder == col("b") - remainder
|
|
129
|
+
else:
|
|
130
|
+
factor = pl.lit(10 ** (-exponent), dtype=pl.Int128)
|
|
131
|
+
frame = frame.with_columns(
|
|
132
|
+
named(modulo(col("q"), factor), "dropped"), named(floor(col("q"), factor), "q")
|
|
133
|
+
)
|
|
134
|
+
dropped, remainder = col("dropped"), col("r")
|
|
135
|
+
half = constant(5 * 10 ** (-exponent - 1))
|
|
136
|
+
exact = (dropped == 0) & (remainder == 0)
|
|
137
|
+
above = (dropped > half) | ((dropped == half) & (remainder > 0))
|
|
138
|
+
tie = (dropped == half) & (remainder == 0)
|
|
139
|
+
if rounding == "exact":
|
|
140
|
+
reject(~exact, "declared exact scale is insufficient")
|
|
141
|
+
increment = pl.lit(False)
|
|
142
|
+
elif rounding == "half_even":
|
|
143
|
+
increment = above | (tie & (modulo(col("q"), constant(2)) == 1))
|
|
144
|
+
elif rounding == "half_away_from_zero":
|
|
145
|
+
increment = above | tie
|
|
146
|
+
else:
|
|
147
|
+
raise CapabilityError("unsupported Decimal division rounding")
|
|
148
|
+
reject(
|
|
149
|
+
(col("q") > constant(limit)) | ((col("q") == constant(limit)) & increment),
|
|
150
|
+
"output precision exceeded",
|
|
151
|
+
)
|
|
152
|
+
frame = frame.with_columns(named(col("q") + increment.cast(pl.Int64).cast(wide), "q"))
|
|
153
|
+
text = col("q").cast(pl.String)
|
|
154
|
+
if scale:
|
|
155
|
+
text = text.str.pad_start(scale + 1, "0")
|
|
156
|
+
text = pl.concat_str(
|
|
157
|
+
text.str.slice(0, text.str.len_chars() - scale), pl.lit("."), text.str.slice(-scale)
|
|
158
|
+
)
|
|
159
|
+
text = pl.concat_str(
|
|
160
|
+
pl.when(col("negative") & (col("q") != 0)).then(pl.lit("-")).otherwise(pl.lit("")),
|
|
161
|
+
text,
|
|
162
|
+
)
|
|
163
|
+
output = prefix + "result"
|
|
164
|
+
frame = frame.with_columns(
|
|
165
|
+
pl.when(col("active"))
|
|
166
|
+
.then(text.cast(pl.Decimal(precision, scale), strict=True))
|
|
167
|
+
.otherwise(None)
|
|
168
|
+
.alias(output)
|
|
169
|
+
)
|
|
170
|
+
return frame, pl.col(output)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def multiply(
|
|
174
|
+
frame: pl.DataFrame,
|
|
175
|
+
left: pl.Expr,
|
|
176
|
+
right: pl.Expr,
|
|
177
|
+
*,
|
|
178
|
+
precision: int,
|
|
179
|
+
scale: int,
|
|
180
|
+
prefix: str,
|
|
181
|
+
context: RunContext,
|
|
182
|
+
) -> tuple[pl.DataFrame, pl.Expr]:
|
|
183
|
+
"""Bound native coefficient multiplication before the product can overflow."""
|
|
184
|
+
import polars as pl
|
|
185
|
+
|
|
186
|
+
def col(name: str) -> pl.Expr:
|
|
187
|
+
return pl.col(prefix + name)
|
|
188
|
+
|
|
189
|
+
def named(value: pl.Expr, name: str) -> pl.Expr:
|
|
190
|
+
return value.alias(prefix + name)
|
|
191
|
+
|
|
192
|
+
wide = pl.Decimal(38, 0)
|
|
193
|
+
zero, one = pl.lit(0, dtype=wide), pl.lit(1, dtype=wide)
|
|
194
|
+
limit = pl.lit(str(10**precision - 1)).cast(wide)
|
|
195
|
+
context.check()
|
|
196
|
+
frame = frame.with_columns(
|
|
197
|
+
named(left.is_not_null() & right.is_not_null(), "active"),
|
|
198
|
+
named(left.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "a"),
|
|
199
|
+
named(right.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "b"),
|
|
200
|
+
).with_columns(
|
|
201
|
+
named((col("a") < 0).xor(col("b") < 0), "negative"),
|
|
202
|
+
named(pl.when(col("active")).then(col("a").abs()).otherwise(zero), "a"),
|
|
203
|
+
named(pl.when(col("active")).then(col("b").abs()).otherwise(zero), "b"),
|
|
204
|
+
)
|
|
205
|
+
denominator = pl.when(col("b") != 0).then(col("b")).otherwise(one)
|
|
206
|
+
bound = (limit.cast(pl.Int128) // denominator.cast(pl.Int128)).cast(pl.String).cast(wide)
|
|
207
|
+
too_large = col("active") & (col("b") != 0) & (col("a") > bound)
|
|
208
|
+
if frame.select(too_large.any()).item():
|
|
209
|
+
raise CapabilityError("Polars Decimal multiply: output precision exceeded")
|
|
210
|
+
context.check()
|
|
211
|
+
# Both magnitudes fit the declared coefficient bound before this product.
|
|
212
|
+
# No intermediate is scaled up to the final scale or widened through float.
|
|
213
|
+
frame = frame.with_columns(named(col("a") * col("b"), "value"))
|
|
214
|
+
text = col("value").cast(pl.String)
|
|
215
|
+
if scale:
|
|
216
|
+
text = text.str.pad_start(scale + 1, "0")
|
|
217
|
+
text = pl.concat_str(
|
|
218
|
+
text.str.slice(0, text.str.len_chars() - scale), pl.lit("."), text.str.slice(-scale)
|
|
219
|
+
)
|
|
220
|
+
text = pl.concat_str(
|
|
221
|
+
pl.when(col("negative") & (col("value") != 0)).then(pl.lit("-")).otherwise(pl.lit("")),
|
|
222
|
+
text,
|
|
223
|
+
)
|
|
224
|
+
output = prefix + "result"
|
|
225
|
+
frame = frame.with_columns(
|
|
226
|
+
pl.when(col("active"))
|
|
227
|
+
.then(text.cast(pl.Decimal(precision, scale), strict=True))
|
|
228
|
+
.otherwise(None)
|
|
229
|
+
.alias(output)
|
|
230
|
+
)
|
|
231
|
+
return frame, pl.col(output)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Read and verify migration evidence without executing ETL, compilers or plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from equiframe._physical import physical_size, verify_table_metadata
|
|
8
|
+
from equiframe._report_format_v1 import (
|
|
9
|
+
migration_html_v1,
|
|
10
|
+
migration_legacy_summary_v1,
|
|
11
|
+
migration_summary_v1,
|
|
12
|
+
)
|
|
13
|
+
from equiframe.comparison import iter_differences, read_comparison
|
|
14
|
+
from equiframe.errors import ArtifactIntegrityError
|
|
15
|
+
from equiframe.evidence import (
|
|
16
|
+
describe_file,
|
|
17
|
+
load_json,
|
|
18
|
+
parse_json,
|
|
19
|
+
read_pipeline,
|
|
20
|
+
verify_events,
|
|
21
|
+
verify_files,
|
|
22
|
+
)
|
|
23
|
+
from equiframe.local import local_path
|
|
24
|
+
from equiframe.types import ArtifactRef
|
|
25
|
+
|
|
26
|
+
from .runner_types import MigrationResult, MigrationSpec
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _legacy_summary(result: MigrationResult) -> dict[str, object]:
|
|
30
|
+
return migration_legacy_summary_v1(result)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def migration_summary(result: MigrationResult) -> dict[str, object]:
|
|
34
|
+
return migration_summary_v1(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _render_report(result: MigrationResult, *, legacy: bool = False) -> str:
|
|
38
|
+
return migration_html_v1(result, legacy=legacy)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render_migration_report(result: MigrationResult) -> str:
|
|
42
|
+
return _render_report(result)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_migration(path: Path, *, verify: bool = True) -> MigrationResult:
|
|
46
|
+
"""Check a closed bundle; explicit False reads metadata without byte verification."""
|
|
47
|
+
if type(verify) is not bool:
|
|
48
|
+
raise TypeError("verify must be a bool; only explicit False skips integrity checks")
|
|
49
|
+
path = path / "migration.json" if path.is_dir() else path
|
|
50
|
+
result = load_json(MigrationResult, path)
|
|
51
|
+
if not verify:
|
|
52
|
+
return result
|
|
53
|
+
directory = path.parent.resolve()
|
|
54
|
+
listed = {i.path: i for i in result.evidence}
|
|
55
|
+
verify_files(directory, result.evidence)
|
|
56
|
+
|
|
57
|
+
def bind(ref: ArtifactRef) -> None:
|
|
58
|
+
ref_path = local_path(ref).resolve()
|
|
59
|
+
if not ref_path.is_relative_to(directory):
|
|
60
|
+
raise ArtifactIntegrityError("migration artifact escapes its operation")
|
|
61
|
+
item = listed.get(ref_path.relative_to(directory).as_posix())
|
|
62
|
+
if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
|
|
63
|
+
raise ArtifactIntegrityError("migration artifact contradicts its inventory")
|
|
64
|
+
|
|
65
|
+
actual = {
|
|
66
|
+
p.relative_to(directory).as_posix()
|
|
67
|
+
for p in directory.rglob("*")
|
|
68
|
+
if p.is_file() and p != path.resolve()
|
|
69
|
+
}
|
|
70
|
+
if actual != set(listed):
|
|
71
|
+
raise ArtifactIntegrityError("migration files contradict its closed inventory")
|
|
72
|
+
for name in ("events.jsonl", "spec.json", "report.json", "report.html"):
|
|
73
|
+
if name not in listed:
|
|
74
|
+
raise ArtifactIntegrityError("required migration evidence is missing")
|
|
75
|
+
if load_json(MigrationSpec, directory / "spec.json") != result.spec:
|
|
76
|
+
raise ArtifactIntegrityError("migration configuration contradicts the manifest")
|
|
77
|
+
events = verify_events(directory / "events.jsonl", operation_id=result.migration_id)
|
|
78
|
+
if (
|
|
79
|
+
not events
|
|
80
|
+
or len(events) != result.event_count
|
|
81
|
+
or events[-1].sha256 != result.event_head_sha256
|
|
82
|
+
or (events[0].stage, events[0].state) != ("migration", "started")
|
|
83
|
+
or (events[-1].stage, events[-1].state) != ("migration", "finished")
|
|
84
|
+
):
|
|
85
|
+
raise ArtifactIntegrityError("migration journal contradicts its manifest")
|
|
86
|
+
terminal = tuple(
|
|
87
|
+
(e.stage, e.state, e.code)
|
|
88
|
+
for e in events
|
|
89
|
+
if e.stage != "migration" and e.state != "started"
|
|
90
|
+
)
|
|
91
|
+
if terminal != tuple((s.stage, s.state, s.code) for s in result.stages):
|
|
92
|
+
raise ArtifactIntegrityError("migration stage outcomes contradict its journal")
|
|
93
|
+
refs = []
|
|
94
|
+
for table in result.inputs:
|
|
95
|
+
bind(table.data)
|
|
96
|
+
if table.index:
|
|
97
|
+
bind(table.index)
|
|
98
|
+
verify_table_metadata(table)
|
|
99
|
+
refs.append(table.data)
|
|
100
|
+
if table.index:
|
|
101
|
+
refs.append(table.index)
|
|
102
|
+
if result.expected:
|
|
103
|
+
bind(result.expected)
|
|
104
|
+
refs.append(result.expected)
|
|
105
|
+
if result.stages[1].state == "completed":
|
|
106
|
+
if "capture.json" not in listed:
|
|
107
|
+
raise ArtifactIntegrityError("complete capture receipt is missing")
|
|
108
|
+
captured = parse_json((directory / "capture.json").read_bytes())
|
|
109
|
+
if (
|
|
110
|
+
not isinstance(captured, dict)
|
|
111
|
+
or set(captured)
|
|
112
|
+
!= {
|
|
113
|
+
"schema_version",
|
|
114
|
+
"inputs",
|
|
115
|
+
"expected",
|
|
116
|
+
"rows",
|
|
117
|
+
"arrow_buffer_bytes",
|
|
118
|
+
}
|
|
119
|
+
or (
|
|
120
|
+
captured.get("schema_version") != "1.0"
|
|
121
|
+
or captured.get("inputs") != [t.model_dump(mode="json") for t in result.inputs]
|
|
122
|
+
or captured.get("expected")
|
|
123
|
+
!= (result.expected.model_dump(mode="json") if result.expected else None)
|
|
124
|
+
)
|
|
125
|
+
):
|
|
126
|
+
raise ArtifactIntegrityError("capture receipt contradicts fixed inputs")
|
|
127
|
+
sizes = [physical_size(t.data) for t in result.inputs]
|
|
128
|
+
expected_size = physical_size(result.expected) if result.expected else (0, 0)
|
|
129
|
+
observed_rows = sum(n for n, _ in sizes) + expected_size[0]
|
|
130
|
+
observed_bytes = sum(n for _, n in sizes) + expected_size[1]
|
|
131
|
+
observed_bytes += sum(physical_size(t.index)[1] for t in result.inputs if t.index)
|
|
132
|
+
if (
|
|
133
|
+
type(captured.get("rows")) is not int
|
|
134
|
+
or type(captured.get("arrow_buffer_bytes")) is not int
|
|
135
|
+
or captured.get("rows") != observed_rows
|
|
136
|
+
or captured.get("arrow_buffer_bytes") != observed_bytes
|
|
137
|
+
or observed_rows > result.spec.options.max_capture_rows
|
|
138
|
+
or observed_bytes > result.spec.options.max_capture_bytes
|
|
139
|
+
):
|
|
140
|
+
raise ArtifactIntegrityError("capture physical statistics contradict the receipt")
|
|
141
|
+
for p in (result.source, result.target):
|
|
142
|
+
if p is not None and read_pipeline(directory / p.pipeline_id) != p:
|
|
143
|
+
raise ArtifactIntegrityError("native pipeline contradicts migration manifest")
|
|
144
|
+
if p is not None and p.execution is not None:
|
|
145
|
+
for t in (*p.execution.inputs, *((p.execution.output,) if p.execution.output else ())):
|
|
146
|
+
bind(t.data)
|
|
147
|
+
if t.index:
|
|
148
|
+
bind(t.index)
|
|
149
|
+
verify_table_metadata(t)
|
|
150
|
+
for c in (result.comparison, result.verification_source, result.verification_target):
|
|
151
|
+
if c is None:
|
|
152
|
+
continue
|
|
153
|
+
if read_comparison(directory / c.comparison_id / "comparison.json") != c:
|
|
154
|
+
raise ArtifactIntegrityError("comparison contradicts migration manifest")
|
|
155
|
+
bind(c.differences)
|
|
156
|
+
count = violations = 0
|
|
157
|
+
for difference in iter_differences(c.differences):
|
|
158
|
+
if count < len(c.examples) and difference != c.examples[count]:
|
|
159
|
+
raise ArtifactIntegrityError("comparison examples contradict complete differences")
|
|
160
|
+
count += 1
|
|
161
|
+
violations += int(difference.category in ("contract", "schema"))
|
|
162
|
+
if count != c.counts.difference_records or violations != c.counts.contract_violations:
|
|
163
|
+
raise ArtifactIntegrityError("comparison differences contradict recorded counts")
|
|
164
|
+
if len(c.examples) != min(count, c.options.max_examples):
|
|
165
|
+
raise ArtifactIntegrityError("comparison example count contradicts configured budget")
|
|
166
|
+
refs.append(c.differences)
|
|
167
|
+
refs.extend(s.data for s in (c.left, c.right) if s is not None)
|
|
168
|
+
for ref in refs:
|
|
169
|
+
ref_path = local_path(ref).resolve()
|
|
170
|
+
if not ref_path.is_relative_to(directory):
|
|
171
|
+
raise ArtifactIntegrityError("migration artifact escapes its operation")
|
|
172
|
+
item = listed.get(ref_path.relative_to(directory).as_posix())
|
|
173
|
+
if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
|
|
174
|
+
raise ArtifactIntegrityError("migration artifact contradicts its inventory")
|
|
175
|
+
if describe_file(ref_path, directory) != item:
|
|
176
|
+
raise ArtifactIntegrityError("migration artifact changed")
|
|
177
|
+
stored_summary = parse_json((directory / "report.json").read_bytes())
|
|
178
|
+
legacy = stored_summary == migration_legacy_summary_v1(result)
|
|
179
|
+
if not legacy and stored_summary != migration_summary_v1(result):
|
|
180
|
+
raise ArtifactIntegrityError("migration report contradicts its manifest")
|
|
181
|
+
if (directory / "report.html").read_bytes() != migration_html_v1(result, legacy=legacy).encode(
|
|
182
|
+
"utf-8"
|
|
183
|
+
):
|
|
184
|
+
raise ArtifactIntegrityError("migration HTML contradicts its manifest")
|
|
185
|
+
if result.detection_requested:
|
|
186
|
+
from equiframe.detector_types import DetectorBundle
|
|
187
|
+
|
|
188
|
+
if "detector-bundle.json" not in listed:
|
|
189
|
+
raise ArtifactIntegrityError("migration detector bundle is missing")
|
|
190
|
+
bundle = load_json(DetectorBundle, directory / "detector-bundle.json")
|
|
191
|
+
for p in (result.source, result.target):
|
|
192
|
+
if (
|
|
193
|
+
p is not None
|
|
194
|
+
and load_json(DetectorBundle, directory / p.pipeline_id / "detector-bundle.json")
|
|
195
|
+
!= bundle
|
|
196
|
+
):
|
|
197
|
+
raise ArtifactIntegrityError("native pipelines used different detector bundles")
|
|
198
|
+
return result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Explicit compiler registry; JSON never imports or registers a plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from equiframe.contracts import Contract
|
|
9
|
+
from equiframe.errors import CapabilityError
|
|
10
|
+
from equiframe.registry import Transformation
|
|
11
|
+
|
|
12
|
+
from .types import CompilationCheck, CompilationDiagnostic, RelationalPlan
|
|
13
|
+
from .validation import ValidatedIR, validate_ir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CompiledPlan:
|
|
18
|
+
"""Lower-level native implementation for already validated physical inputs.
|
|
19
|
+
|
|
20
|
+
Compilation checks IR/contract metadata, not data. The caller must establish
|
|
21
|
+
exact input types and value constraints before executing this transformation;
|
|
22
|
+
``migrate`` performs that admission automatically. Passing the transformation
|
|
23
|
+
directly to ``execute`` uses native conversion behavior and records it, but
|
|
24
|
+
completion does not establish input conformance or semantic equivalence.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
engine: str
|
|
28
|
+
transformation: Transformation
|
|
29
|
+
ir_sha256: str
|
|
30
|
+
contract_sha256: str
|
|
31
|
+
check: CompilationCheck
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlanCompiler(Protocol):
|
|
35
|
+
"""Compile trusted IR metadata; actual input admission belongs to the caller.
|
|
36
|
+
|
|
37
|
+
``check`` and ``compile`` do not load or validate tables. Use ``migrate`` for
|
|
38
|
+
checked input capture, or independently validate exact physical input types
|
|
39
|
+
and constraints before using a returned ``CompiledPlan`` with ``execute``.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def engine(self) -> str: ...
|
|
44
|
+
|
|
45
|
+
def check(self, plan: RelationalPlan, contract: Contract) -> CompilationCheck: ...
|
|
46
|
+
|
|
47
|
+
def compile(self, plan: RelationalPlan, contract: Contract) -> CompiledPlan: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompilerRegistry:
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self._compilers: dict[str, PlanCompiler] = {}
|
|
53
|
+
|
|
54
|
+
def register(self, compiler: PlanCompiler) -> None:
|
|
55
|
+
if not compiler.engine or compiler.engine in self._compilers:
|
|
56
|
+
raise ValueError("compiler engine must be nonempty and registered once")
|
|
57
|
+
self._compilers[compiler.engine] = compiler
|
|
58
|
+
|
|
59
|
+
def resolve(self, engine: str) -> PlanCompiler:
|
|
60
|
+
if engine not in self._compilers:
|
|
61
|
+
raise CapabilityError("no explicitly registered compiler for engine: " + engine)
|
|
62
|
+
return self._compilers[engine]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compiler_check(engine: str, plan: RelationalPlan, contract: Contract) -> CompilationCheck:
|
|
66
|
+
try:
|
|
67
|
+
validate_ir(plan, contract)
|
|
68
|
+
except (ValueError, CapabilityError) as error:
|
|
69
|
+
return CompilationCheck(
|
|
70
|
+
engine=engine,
|
|
71
|
+
supported=False,
|
|
72
|
+
diagnostics=(
|
|
73
|
+
CompilationDiagnostic(
|
|
74
|
+
code="unsupported_ir", message=str(error).strip() or "invalid IR"
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
return CompilationCheck(engine=engine, supported=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def require_supported(engine: str, plan: RelationalPlan, contract: Contract) -> ValidatedIR:
|
|
82
|
+
check = compiler_check(engine, plan, contract)
|
|
83
|
+
if not check.supported:
|
|
84
|
+
raise CapabilityError(check.diagnostics[0].message)
|
|
85
|
+
return validate_ir(plan, contract)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def native_compiler_registry() -> CompilerRegistry:
|
|
89
|
+
from .backends.duckdb import DuckDBCompiler
|
|
90
|
+
from .backends.pandas import PandasCompiler
|
|
91
|
+
from .backends.polars import PolarsCompiler
|
|
92
|
+
|
|
93
|
+
registry = CompilerRegistry()
|
|
94
|
+
for compiler in (PandasCompiler(), PolarsCompiler(), DuckDBCompiler()):
|
|
95
|
+
registry.register(compiler)
|
|
96
|
+
return registry
|