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,125 @@
|
|
|
1
|
+
"""Exact Decimal division compiled to native HUGEINT SQL, never DOUBLE."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from equiframe.errors import CapabilityError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def divide(
|
|
9
|
+
left: str,
|
|
10
|
+
right: str,
|
|
11
|
+
*,
|
|
12
|
+
left_scale: int,
|
|
13
|
+
right_scale: int,
|
|
14
|
+
precision: int,
|
|
15
|
+
scale: int,
|
|
16
|
+
rounding: str,
|
|
17
|
+
columns: Sequence[str],
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Bound each remainder below the denominator using modular doubling.
|
|
20
|
+
|
|
21
|
+
A correlated recursive CTE materializes at most 76 decimal-digit steps.
|
|
22
|
+
Internal names cannot shadow any outer input name, including quoted hostile
|
|
23
|
+
names. Every numerical computation remains a native SQL integer operation.
|
|
24
|
+
"""
|
|
25
|
+
prefix = "_equiframe_decimal_division_"
|
|
26
|
+
while any(name.casefold().startswith(prefix) for name in columns):
|
|
27
|
+
prefix += "_"
|
|
28
|
+
|
|
29
|
+
def name(short: str) -> str:
|
|
30
|
+
return '"' + prefix + short + '"'
|
|
31
|
+
|
|
32
|
+
a0, b0, active, negative = map(name, ("a0", "b0", "active", "negative"))
|
|
33
|
+
a, b, q, r, i, value, dropped = map(name, ("a", "b", "q", "r", "i", "value", "dropped"))
|
|
34
|
+
raw, operands, division, final, rounded = map(
|
|
35
|
+
name, ("raw", "operands", "division", "final", "rounded")
|
|
36
|
+
)
|
|
37
|
+
c2, c4, c8, r2, r4, r8 = map(name, ("c2", "c4", "c8", "r2", "r4", "r8"))
|
|
38
|
+
zero = "0::HUGEINT"
|
|
39
|
+
|
|
40
|
+
def doubled(remainder: str) -> tuple[str, str]:
|
|
41
|
+
carry = f"({remainder} >= {b}-({remainder}))"
|
|
42
|
+
result = (
|
|
43
|
+
f"({remainder} - CASE WHEN {carry} THEN {b}-({remainder}) "
|
|
44
|
+
f"ELSE {zero}-({remainder}) END)"
|
|
45
|
+
)
|
|
46
|
+
return carry, result
|
|
47
|
+
|
|
48
|
+
k2, rem2 = doubled(r)
|
|
49
|
+
k4, rem4 = doubled(r2)
|
|
50
|
+
k8, rem8 = doubled(r4)
|
|
51
|
+
exponent = scale + right_scale - left_scale
|
|
52
|
+
limit = 10**precision - 1
|
|
53
|
+
initial = f"""{raw} AS MATERIALIZED (
|
|
54
|
+
SELECT CAST(REPLACE(CAST({left} AS VARCHAR),'.','') AS HUGEINT) AS {a0},
|
|
55
|
+
CAST(REPLACE(CAST({right} AS VARCHAR),'.','') AS HUGEINT) AS {b0},
|
|
56
|
+
({left} IS NOT NULL AND {right} IS NOT NULL) AS {active}
|
|
57
|
+
), {operands} AS MATERIALIZED (
|
|
58
|
+
SELECT CASE WHEN {active} THEN abs({a0}) ELSE {zero} END AS {a},
|
|
59
|
+
CASE WHEN {active} AND {b0}=0 THEN error('Decimal division: zero denominator')
|
|
60
|
+
WHEN {active} THEN abs({b0}) ELSE 1::HUGEINT END AS {b},
|
|
61
|
+
{active}, (({a0}<0) <> ({b0}<0)) AS {negative} FROM {raw}
|
|
62
|
+
)"""
|
|
63
|
+
if exponent >= 0:
|
|
64
|
+
body = f""", {division}({i},{q},{r}) AS (
|
|
65
|
+
SELECT 0,CASE WHEN {a}//{b}>{limit}::HUGEINT
|
|
66
|
+
THEN error('Decimal division: output precision exceeded') ELSE {a}//{b} END,
|
|
67
|
+
{a}%{b} FROM {operands}
|
|
68
|
+
UNION ALL
|
|
69
|
+
SELECT {i}+1,
|
|
70
|
+
CASE WHEN {q}>{limit // 10}::HUGEINT
|
|
71
|
+
THEN error('Decimal division: output precision exceeded')
|
|
72
|
+
ELSE {q}*10+{c8}+{c2}+CAST(({r8}>={b}-{r2}) AS HUGEINT) END,
|
|
73
|
+
{r8}-CASE WHEN {r8}>={b}-{r2} THEN {b}-{r2} ELSE {zero}-{r2} END
|
|
74
|
+
FROM {division} CROSS JOIN {operands}
|
|
75
|
+
CROSS JOIN LATERAL (SELECT CAST({k2} AS HUGEINT) AS {c2},{rem2} AS {r2}) {name("t2")}
|
|
76
|
+
CROSS JOIN LATERAL (
|
|
77
|
+
SELECT 2*{c2}+CAST({k4} AS HUGEINT) AS {c4},{rem4} AS {r4}) {name("t4")}
|
|
78
|
+
CROSS JOIN LATERAL (
|
|
79
|
+
SELECT 2*{c4}+CAST({k8} AS HUGEINT) AS {c8},{rem8} AS {r8}) {name("t8")}
|
|
80
|
+
WHERE {i}<{exponent}
|
|
81
|
+
), {final} AS (
|
|
82
|
+
SELECT {q},{r},{b},{active},{negative} FROM {division}
|
|
83
|
+
CROSS JOIN {operands} WHERE {i}={exponent}
|
|
84
|
+
)"""
|
|
85
|
+
exact = f"{r}=0"
|
|
86
|
+
above = f"{r}>{b}-{r}"
|
|
87
|
+
tie = f"{r}={b}-{r}"
|
|
88
|
+
else:
|
|
89
|
+
factor = 10 ** (-exponent)
|
|
90
|
+
half = factor // 2
|
|
91
|
+
body = f""", {final} AS (
|
|
92
|
+
SELECT ({a}//{b})//{factor}::HUGEINT AS {q},
|
|
93
|
+
({a}//{b})%{factor}::HUGEINT AS {dropped},{a}%{b} AS {r},
|
|
94
|
+
{b},{active},{negative} FROM {operands}
|
|
95
|
+
)"""
|
|
96
|
+
exact = f"{dropped}=0 AND {r}=0"
|
|
97
|
+
above = f"({dropped}>{half}::HUGEINT OR ({dropped}={half}::HUGEINT AND {r}>0))"
|
|
98
|
+
tie = f"({dropped}={half}::HUGEINT AND {r}=0)"
|
|
99
|
+
if rounding == "exact":
|
|
100
|
+
result = (
|
|
101
|
+
f"CASE WHEN {q}>{limit}::HUGEINT "
|
|
102
|
+
"THEN error('Decimal division: output precision exceeded') "
|
|
103
|
+
f"WHEN {exact} THEN {q} "
|
|
104
|
+
"ELSE error('Decimal division: declared exact scale is insufficient') END"
|
|
105
|
+
)
|
|
106
|
+
elif rounding in ("half_even", "half_away_from_zero"):
|
|
107
|
+
increment = f"({above}) OR (({tie})" + (
|
|
108
|
+
f" AND {q}%2=1)" if rounding == "half_even" else ")"
|
|
109
|
+
)
|
|
110
|
+
result = (
|
|
111
|
+
f"CASE WHEN {q}>{limit}::HUGEINT OR ({q}={limit}::HUGEINT AND ({increment})) "
|
|
112
|
+
"THEN error('Decimal division: output precision exceeded') "
|
|
113
|
+
f"ELSE {q}+CAST(({increment}) AS HUGEINT) END"
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
raise CapabilityError("unsupported Decimal division rounding")
|
|
117
|
+
text = f"CAST({value} AS VARCHAR)"
|
|
118
|
+
if scale:
|
|
119
|
+
text = f"lpad({text},CAST(greatest(length({text}),{scale + 1}) AS INTEGER),'0')"
|
|
120
|
+
text = f"substring({text},1,length({text})-{scale}) || '.' || right({text},{scale})"
|
|
121
|
+
return f"""(WITH RECURSIVE {initial}{body},
|
|
122
|
+
{rounded} AS (SELECT {result} AS {value},{active},{negative} FROM {final})
|
|
123
|
+
SELECT CASE WHEN NOT {active} THEN NULL ELSE CAST(
|
|
124
|
+
CASE WHEN {negative} AND {value}<>0 THEN '-' ELSE '' END || ({text})
|
|
125
|
+
AS DECIMAL({precision},{scale})) END FROM {rounded})"""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Native ICU localization with explicit fold/gap policy and exact tick arithmetic."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from equiframe._timezones import native_timezone_key
|
|
6
|
+
from equiframe.errors import CapabilityError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def localize(
|
|
10
|
+
argument: str,
|
|
11
|
+
*,
|
|
12
|
+
unit: str,
|
|
13
|
+
timezone: str,
|
|
14
|
+
ambiguous: str,
|
|
15
|
+
columns: Sequence[str],
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Enumerate integer-second UTC offsets, validating every native round trip.
|
|
18
|
+
|
|
19
|
+
IANA offsets use integral seconds. The IR admits displacement of at most one
|
|
20
|
+
day. Searching that complete interval finds gaps, ordinary instants and all
|
|
21
|
+
valid fold candidates, including historical second offsets and date-line
|
|
22
|
+
changes. No default native ambiguity policy or Python row callback chooses
|
|
23
|
+
the result. A scalar correlated query preserves multiplicity in the caller.
|
|
24
|
+
"""
|
|
25
|
+
if unit not in ("s", "ms", "us", "ns") or ambiguous not in ("reject", "earlier", "later"):
|
|
26
|
+
raise CapabilityError("unsupported timestamp localization unit or ambiguity policy")
|
|
27
|
+
prefix = "_equiframe_timestamp_localization_"
|
|
28
|
+
while any(name.casefold().startswith(prefix) for name in columns):
|
|
29
|
+
prefix += "_"
|
|
30
|
+
|
|
31
|
+
def name(short: str) -> str:
|
|
32
|
+
return '"' + prefix + short + '"'
|
|
33
|
+
|
|
34
|
+
raw, wall, candidates, matches, totals, selected = map(
|
|
35
|
+
name, ("raw", "wall", "candidates", "matches", "totals", "selected")
|
|
36
|
+
)
|
|
37
|
+
tick, microseconds, fraction, offset, instant, count, chosen, result = map(
|
|
38
|
+
name, ("tick", "microseconds", "fraction", "offset", "instant", "count", "chosen", "result")
|
|
39
|
+
)
|
|
40
|
+
factor = {"s": 1_000_000, "ms": 1000, "us": 1, "ns": 1}[unit]
|
|
41
|
+
if unit == "ns":
|
|
42
|
+
# Native // truncates towards zero. Use floor so the residual is 0..999,
|
|
43
|
+
# including negative ticks preceding the Unix epoch.
|
|
44
|
+
micros = f"({tick}//1000-CASE WHEN {tick}<0 AND {tick}%1000<>0 THEN 1 ELSE 0 END)"
|
|
45
|
+
residual = f"({tick}-({micros})*1000)"
|
|
46
|
+
else:
|
|
47
|
+
micros, residual = f"({tick}*{factor})", "0::HUGEINT"
|
|
48
|
+
# Keep both candidate construction and native round trips away from DuckDB's
|
|
49
|
+
# reserved finite/infinite boundaries. Out-of-range localization fails before
|
|
50
|
+
# timezone evaluation; raw timestamp transport does not need this restriction.
|
|
51
|
+
margin = 2 * 86400 * 1_000_000
|
|
52
|
+
lower, upper = -(2**63) + margin, 2**63 - 1 - margin
|
|
53
|
+
zone = "'" + native_timezone_key(timezone).replace("'", "''") + "'"
|
|
54
|
+
chooser = "max" if ambiguous == "later" else "min"
|
|
55
|
+
reject_fold = (
|
|
56
|
+
f"WHEN {count}>1 THEN error('Timestamp localization: ambiguous wall time') "
|
|
57
|
+
if ambiguous == "reject"
|
|
58
|
+
else ""
|
|
59
|
+
)
|
|
60
|
+
encoded = f"({chosen}*1000+{fraction})" if unit == "ns" else f"({chosen}//{factor})"
|
|
61
|
+
return f"""(WITH {raw} AS MATERIALIZED (
|
|
62
|
+
SELECT CAST({argument} AS HUGEINT) AS {tick}
|
|
63
|
+
), {wall} AS MATERIALIZED (
|
|
64
|
+
SELECT {tick},CASE WHEN {tick} IS NULL THEN NULL
|
|
65
|
+
WHEN {micros} BETWEEN {lower}::HUGEINT AND {upper}::HUGEINT
|
|
66
|
+
THEN CAST({micros} AS BIGINT)
|
|
67
|
+
ELSE error('Timestamp localization: native microsecond range exceeded')
|
|
68
|
+
END AS {microseconds},
|
|
69
|
+
{residual} AS {fraction} FROM {raw}
|
|
70
|
+
), {candidates} AS MATERIALIZED (
|
|
71
|
+
SELECT make_timestamptz(CAST(CAST({microseconds} AS HUGEINT)-{offset}*1000000 AS BIGINT))
|
|
72
|
+
AS {instant} FROM {wall}
|
|
73
|
+
CROSS JOIN range(-86400,86401) AS {name("offsets")}({offset})
|
|
74
|
+
WHERE {tick} IS NOT NULL
|
|
75
|
+
), {matches} AS (
|
|
76
|
+
SELECT {instant} FROM {candidates} CROSS JOIN {wall}
|
|
77
|
+
WHERE timezone({zone},{instant})=make_timestamp({microseconds})
|
|
78
|
+
), {totals} AS (
|
|
79
|
+
SELECT count(*) AS {count},{chooser}(CAST(epoch_us({instant}) AS HUGEINT)) AS {chosen}
|
|
80
|
+
FROM {matches}
|
|
81
|
+
), {selected} AS (
|
|
82
|
+
SELECT {tick},{count},{encoded} AS {result} FROM {totals} CROSS JOIN {wall}
|
|
83
|
+
) SELECT CASE WHEN {tick} IS NULL THEN NULL
|
|
84
|
+
WHEN {count}=0 THEN error('Timestamp localization: nonexistent wall time')
|
|
85
|
+
{reject_fold}WHEN {result} BETWEEN {-(2**63)}::HUGEINT AND {2**63 - 1}::HUGEINT
|
|
86
|
+
THEN CAST({result} AS BIGINT)
|
|
87
|
+
ELSE error('Timestamp localization: output tick range exceeded') END FROM {selected})"""
|