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.
Files changed (170) hide show
  1. equiframe/__init__.py +832 -0
  2. equiframe/__main__.py +3 -0
  3. equiframe/_base.py +84 -0
  4. equiframe/_cli_support.py +125 -0
  5. equiframe/_comparison_io.py +161 -0
  6. equiframe/_dataset_provenance.py +146 -0
  7. equiframe/_decimal.py +22 -0
  8. equiframe/_diff_capture.py +433 -0
  9. equiframe/_doctor.py +123 -0
  10. equiframe/_document_io.py +61 -0
  11. equiframe/_documents.py +79 -0
  12. equiframe/_duckdb_transport.py +149 -0
  13. equiframe/_error_reporting.py +52 -0
  14. equiframe/_first_use.py +102 -0
  15. equiframe/_path_compat.py +26 -0
  16. equiframe/_physical.py +133 -0
  17. equiframe/_r_review.R +308 -0
  18. equiframe/_regular_io.py +167 -0
  19. equiframe/_report_format_v1.py +298 -0
  20. equiframe/_report_io.py +591 -0
  21. equiframe/_stability_inventory.py +1421 -0
  22. equiframe/_timezones.py +257 -0
  23. equiframe/_vendor/cyclonedx/LICENSE +201 -0
  24. equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
  25. equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
  26. equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
  27. equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
  28. equiframe/adapters.py +454 -0
  29. equiframe/assertion_types.py +122 -0
  30. equiframe/assertions.py +271 -0
  31. equiframe/benchmark.py +101 -0
  32. equiframe/bundle.py +240 -0
  33. equiframe/cli.py +693 -0
  34. equiframe/comparison.py +937 -0
  35. equiframe/comparison_report.py +660 -0
  36. equiframe/comparison_types.py +257 -0
  37. equiframe/compatibility.py +546 -0
  38. equiframe/contracts.py +879 -0
  39. equiframe/datasets.py +1301 -0
  40. equiframe/decision.py +34 -0
  41. equiframe/destinations.py +295 -0
  42. equiframe/detector_types.py +431 -0
  43. equiframe/detectors.py +389 -0
  44. equiframe/diff.py +192 -0
  45. equiframe/diff_contracts.py +523 -0
  46. equiframe/docs/adoption.md +329 -0
  47. equiframe/docs/api.md +534 -0
  48. equiframe/docs/arquitecto.md +143 -0
  49. equiframe/docs/choosing-api.md +84 -0
  50. equiframe/docs/cientifico.md +180 -0
  51. equiframe/docs/compatibility.md +254 -0
  52. equiframe/docs/contributing.md +171 -0
  53. equiframe/docs/dependencies.md +237 -0
  54. equiframe/docs/eda.md +166 -0
  55. equiframe/docs/extending.md +158 -0
  56. equiframe/docs/fast-ci.md +52 -0
  57. equiframe/docs/first-use.md +78 -0
  58. equiframe/docs/index.md +142 -0
  59. equiframe/docs/ingestion.md +145 -0
  60. equiframe/docs/inicio.md +129 -0
  61. equiframe/docs/installed-private.md +97 -0
  62. equiframe/docs/journeys.md +362 -0
  63. equiframe/docs/lectura-conversion.md +211 -0
  64. equiframe/docs/licensing.md +17 -0
  65. equiframe/docs/maintenance.md +233 -0
  66. equiframe/docs/migration-ir.md +333 -0
  67. equiframe/docs/option-audit.md +45 -0
  68. equiframe/docs/outcome-gates.md +134 -0
  69. equiframe/docs/performance.md +263 -0
  70. equiframe/docs/piloto-simulado.md +56 -0
  71. equiframe/docs/quickstart.md +156 -0
  72. equiframe/docs/r_review.md +212 -0
  73. equiframe/docs/recipes.md +248 -0
  74. equiframe/docs/releasing.md +183 -0
  75. equiframe/docs/security.md +218 -0
  76. equiframe/docs/semantics.md +274 -0
  77. equiframe/docs/sqlite-snapshots.md +262 -0
  78. equiframe/docs/universal-sources.md +49 -0
  79. equiframe/docs/universalidad-cinco-pasos.md +107 -0
  80. equiframe/docs/upgrading.md +538 -0
  81. equiframe/duckdb_capture.py +410 -0
  82. equiframe/eda/__init__.py +36 -0
  83. equiframe/eda/__main__.py +3 -0
  84. equiframe/eda/_backend.py +612 -0
  85. equiframe/eda/_inventory.py +49 -0
  86. equiframe/eda/_paths.py +49 -0
  87. equiframe/eda/_report.py +291 -0
  88. equiframe/eda/api.py +535 -0
  89. equiframe/eda/cli.py +102 -0
  90. equiframe/eda/models.py +239 -0
  91. equiframe/errors.py +83 -0
  92. equiframe/evidence.py +375 -0
  93. equiframe/examples/integrations/eda_generic.py +60 -0
  94. equiframe/examples/integrations/outcome_gates.py +94 -0
  95. equiframe/examples/integrations/sqlite_snapshot.py +246 -0
  96. equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
  97. equiframe/examples/integrations/universal_first_use.py +296 -0
  98. equiframe/examples/journeys/README.md +31 -0
  99. equiframe/examples/journeys/__init__.py +1 -0
  100. equiframe/examples/journeys/a_pandas_refactor.py +88 -0
  101. equiframe/examples/journeys/b_native_migration.py +181 -0
  102. equiframe/examples/journeys/c_retail_etl.py +460 -0
  103. equiframe/examples/journeys/common.py +136 -0
  104. equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
  105. equiframe/examples/journeys/e_telemetry.py +135 -0
  106. equiframe/examples/journeys/f_data_science_preparation.py +84 -0
  107. equiframe/examples/journeys/g_external_snapshots.py +197 -0
  108. equiframe/examples/journeys/h_portable_ci.py +129 -0
  109. equiframe/examples/journeys/requirements.lock +249 -0
  110. equiframe/examples/journeys/verify_journeys.py +117 -0
  111. equiframe/examples/load_convert_compare.py +85 -0
  112. equiframe/execution.py +355 -0
  113. equiframe/execution_types.py +177 -0
  114. equiframe/ingestion.py +744 -0
  115. equiframe/local.py +126 -0
  116. equiframe/migration/__init__.py +94 -0
  117. equiframe/migration/_physical.py +8 -0
  118. equiframe/migration/backends/_timestamp_transport.py +109 -0
  119. equiframe/migration/backends/duckdb.py +651 -0
  120. equiframe/migration/backends/duckdb_decimal.py +125 -0
  121. equiframe/migration/backends/duckdb_time.py +87 -0
  122. equiframe/migration/backends/pandas.py +544 -0
  123. equiframe/migration/backends/polars.py +915 -0
  124. equiframe/migration/backends/polars_decimal.py +231 -0
  125. equiframe/migration/io.py +198 -0
  126. equiframe/migration/ports.py +96 -0
  127. equiframe/migration/runner.py +545 -0
  128. equiframe/migration/runner_types.py +405 -0
  129. equiframe/migration/types.py +354 -0
  130. equiframe/migration/validation.py +619 -0
  131. equiframe/normalization.py +358 -0
  132. equiframe/pipeline.py +274 -0
  133. equiframe/pipeline_types.py +247 -0
  134. equiframe/ports.py +39 -0
  135. equiframe/preprocessing.py +110 -0
  136. equiframe/profile_types.py +343 -0
  137. equiframe/profiles.py +447 -0
  138. equiframe/py.typed +0 -0
  139. equiframe/r_review.py +706 -0
  140. equiframe/reduction.py +518 -0
  141. equiframe/registry.py +121 -0
  142. equiframe/report_rendering.py +366 -0
  143. equiframe/report_types.py +294 -0
  144. equiframe/reporting.py +26 -0
  145. equiframe/rule_types.py +178 -0
  146. equiframe/rules.py +819 -0
  147. equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
  148. equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
  149. equiframe/schemas/r-review-options-1.0.schema.json +38 -0
  150. equiframe/schemas/r-review-result-1.0.schema.json +207 -0
  151. equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
  152. equiframe/serialization.py +152 -0
  153. equiframe/snapshot.py +296 -0
  154. equiframe/sources.py +283 -0
  155. equiframe/tables.py +459 -0
  156. equiframe/types.py +71 -0
  157. equiframe/values.py +252 -0
  158. equiframe/verification_request.py +35 -0
  159. equiframe/verify.py +799 -0
  160. equiframe/verify_types.py +255 -0
  161. equiframe/workflow.py +510 -0
  162. equiframe/workflow_request.py +38 -0
  163. equiframe-0.12.4.dist-info/METADATA +268 -0
  164. equiframe-0.12.4.dist-info/RECORD +170 -0
  165. equiframe-0.12.4.dist-info/WHEEL +4 -0
  166. equiframe-0.12.4.dist-info/entry_points.txt +3 -0
  167. equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
  168. equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
  169. equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
  170. 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})"""