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,257 @@
1
+ """Comparison document v1; independent of unchanged contract/execution format 1.0."""
2
+
3
+ import math
4
+ import re
5
+ from decimal import Decimal, InvalidOperation
6
+ from typing import Annotated, Literal
7
+
8
+ from pydantic import Field, model_validator
9
+ from typing_extensions import Self
10
+
11
+ from equiframe._base import FrozenModel, Identifier, NonNegativeInt, PositiveInt, Text, fail
12
+ from equiframe.contracts import Contract
13
+ from equiframe.execution_types import ExecutionResult, TableArtifact, VersionInfo
14
+ from equiframe.types import ArtifactRef, ComparisonState, Sha256
15
+
16
+ Side = Literal["left", "right"]
17
+
18
+
19
+ class ComparisonPolicy(FrozenModel):
20
+ column_order: Literal["significant", "ignored"] = "significant"
21
+ physical_types: Literal["exact", "lossless"] = "exact"
22
+ index: Literal["reject", "ignore"] = "reject"
23
+ justification: Text | None = None
24
+
25
+ @model_validator(mode="after")
26
+ def explicit_authorization(self) -> Self:
27
+ if (
28
+ self.column_order == "ignored"
29
+ or self.physical_types == "lossless"
30
+ or self.index == "ignore"
31
+ ) and self.justification is None:
32
+ fail(
33
+ "comparison_authorization",
34
+ "relaxed policies require justification",
35
+ "justification",
36
+ )
37
+ return self
38
+
39
+
40
+ class ComparisonSpec(FrozenModel):
41
+ schema_version: Literal["1.0"] = "1.0"
42
+ contract: Contract
43
+ policy: ComparisonPolicy = Field(default_factory=ComparisonPolicy)
44
+
45
+
46
+ class ComparisonOptions(FrozenModel):
47
+ batch_rows: PositiveInt = 4096
48
+ max_batch_bytes: PositiveInt = 64 * 1024 * 1024
49
+ max_row_bytes: PositiveInt = 1024 * 1024
50
+ partitions: Annotated[int, Field(ge=1, le=4096)] = 16
51
+ sqlite_cache_kib: PositiveInt = 4096
52
+ max_rows: PositiveInt = 10_000_000
53
+ max_work: PositiveInt = 100_000_000
54
+ max_disk_bytes: PositiveInt = 4 * 1024**3
55
+ max_evidence_bytes: PositiveInt = 512 * 1024**2
56
+ max_matching_rows: PositiveInt = 2000
57
+ max_matching_bytes: PositiveInt = 32 * 1024**2
58
+ max_pair_checks: PositiveInt = 2_000_000
59
+ max_numeric_digits: PositiveInt = 4096
60
+ max_examples: NonNegativeInt = 20
61
+ timeout_seconds: Annotated[float, Field(gt=0)] | None = None
62
+
63
+
64
+ class ComparisonValue(FrozenModel):
65
+ """Lossless scalar evidence. Numeric text; dates/instants use integer epochs."""
66
+
67
+ kind: Literal[
68
+ "null",
69
+ "nan",
70
+ "infinity",
71
+ "boolean",
72
+ "integer",
73
+ "string",
74
+ "decimal",
75
+ "float",
76
+ "date",
77
+ "timestamp",
78
+ ]
79
+ value: str | None = None
80
+ unit: Literal["day", "s", "ms", "us", "ns"] | None = None
81
+ timezone: str | None = None
82
+
83
+ @model_validator(mode="after")
84
+ def tagged(self) -> Self:
85
+ if (self.kind in ("null", "nan")) != (self.value is None):
86
+ fail("comparison_value", "only null/NaN omit value", "value")
87
+ if self.kind not in ("date", "timestamp") and (self.unit or self.timezone):
88
+ fail("comparison_value", "only temporal values carry units/zones", "unit")
89
+ if self.kind in ("integer", "date", "timestamp") and not re.fullmatch(
90
+ r"-?[0-9]+", self.value or ""
91
+ ):
92
+ fail("comparison_value", "integer text required", "value")
93
+ if self.kind == "boolean" and self.value not in ("true", "false"):
94
+ fail("comparison_value", "boolean text required", "value")
95
+ if self.kind == "infinity" and self.value not in ("positive", "negative"):
96
+ fail("comparison_value", "infinity sign required", "value")
97
+ if self.kind in ("decimal", "float"):
98
+ try:
99
+ valid = Decimal(self.value or "").is_finite()
100
+ if self.kind == "float":
101
+ valid = valid and math.isfinite(float(self.value or ""))
102
+ except (InvalidOperation, ValueError):
103
+ valid = False
104
+ if not valid:
105
+ fail("comparison_value", "finite numeric text required", "value")
106
+ if self.kind == "date" and (self.unit not in ("day", "ms") or self.timezone):
107
+ fail("comparison_value", "date requires day/ms without timezone", "unit")
108
+ if self.kind == "timestamp" and self.unit not in ("s", "ms", "us", "ns"):
109
+ fail("comparison_value", "timestamp requires an epoch unit", "unit")
110
+ return self
111
+
112
+
113
+ class ComparisonDifference(FrozenModel):
114
+ schema_version: Literal["1.0"] = "1.0"
115
+ category: Literal["contract", "content", "schema"]
116
+ code: Text
117
+ side: Side | None = None
118
+ position: NonNegativeInt | None = None
119
+ other_position: NonNegativeInt | None = None
120
+ column: str | None = None
121
+ key: tuple[ComparisonValue, ...] = ()
122
+ left: tuple[ComparisonValue, ...] = ()
123
+ right: tuple[ComparisonValue, ...] = ()
124
+ left_count: NonNegativeInt = 0
125
+ right_count: NonNegativeInt = 0
126
+ message: Text
127
+
128
+
129
+ class ComparisonDiagnostic(FrozenModel):
130
+ code: Text
131
+ message: Text
132
+ side: Side | None = None
133
+ exception_type: Text | None = None
134
+
135
+
136
+ class ComparisonSource(FrozenModel):
137
+ data: ArtifactRef
138
+ table: TableArtifact | None = None
139
+ execution: ExecutionResult | None = None
140
+ observed_schema: tuple[tuple[str, str], ...] = ()
141
+
142
+
143
+ class AppliedPolicy(FrozenModel):
144
+ side: Side
145
+ column: str | None = None
146
+ operation: Text
147
+ count: NonNegativeInt
148
+
149
+
150
+ class ComparisonCounts(FrozenModel):
151
+ left_rows: NonNegativeInt = 0
152
+ right_rows: NonNegativeInt = 0
153
+ matched_rows: NonNegativeInt = 0
154
+ difference_records: NonNegativeInt = 0
155
+ contract_violations: NonNegativeInt = 0
156
+ final: bool = False
157
+
158
+
159
+ class ComparisonResult(FrozenModel):
160
+ schema_version: Literal["1.0"] = "1.0"
161
+ scope: Literal["output_schema"] = "output_schema"
162
+ comparison_id: Identifier
163
+ spec: ComparisonSpec
164
+ contract_sha256: Sha256
165
+ spec_sha256: Sha256
166
+ state: ComparisonState
167
+ complete: bool
168
+ reference: Literal["peer", "expected"]
169
+ strategy: Literal["pending", "sequence", "key", "multiset", "matching"]
170
+ selected_key: str | None = None
171
+ left: ComparisonSource | None
172
+ right: ComparisonSource | None
173
+ counts: ComparisonCounts
174
+ options: ComparisonOptions
175
+ applied_policies: tuple[AppliedPolicy, ...]
176
+ diagnostics: tuple[ComparisonDiagnostic, ...]
177
+ examples: tuple[ComparisonDifference, ...]
178
+ differences: ArtifactRef
179
+ started_at: Text
180
+ elapsed_seconds: Annotated[float, Field(ge=0)]
181
+ work_units: NonNegativeInt
182
+ pair_checks: NonNegativeInt
183
+ peak_disk_bytes: NonNegativeInt
184
+ versions: tuple[VersionInfo, ...]
185
+
186
+ def __bool__(self) -> bool:
187
+ """Object existence must never imply observed equivalence."""
188
+ raise TypeError(
189
+ "ComparisonResult has no truth value; use assert_equivalent(result) "
190
+ "or inspect result.state and result.complete explicitly"
191
+ )
192
+
193
+ @model_validator(mode="after")
194
+ def consistent(self) -> Self:
195
+ from equiframe.comparison import comparison_fingerprint
196
+ from equiframe.serialization import contract_fingerprint
197
+
198
+ if self.contract_sha256 != contract_fingerprint(self.spec.contract):
199
+ fail("comparison_fingerprint", "contract fingerprint mismatch", "contract_sha256")
200
+ if self.spec_sha256 != comparison_fingerprint(self.spec):
201
+ fail("comparison_fingerprint", "spec fingerprint mismatch", "spec_sha256")
202
+ if self.counts.final != self.complete:
203
+ fail("comparison_completeness", "count completeness mismatch", "counts")
204
+ if self.complete and (
205
+ self.left is None or self.right is None or self.strategy == "pending"
206
+ ):
207
+ fail(
208
+ "comparison_completeness", "complete requires both sources and strategy", "complete"
209
+ )
210
+ if self.complete:
211
+ for source, rows in (
212
+ (self.left, self.counts.left_rows),
213
+ (self.right, self.counts.right_rows),
214
+ ):
215
+ if (
216
+ source is not None
217
+ and source.table is not None
218
+ and source.table.row_count != rows
219
+ ):
220
+ fail(
221
+ "comparison_counts",
222
+ "final row count differs from captured table metadata",
223
+ "counts",
224
+ )
225
+ if self.counts.matched_rows > min(self.counts.left_rows, self.counts.right_rows):
226
+ fail("comparison_counts", "matched rows exceed observed rows", "counts")
227
+ if self.counts.contract_violations > self.counts.difference_records:
228
+ fail("comparison_counts", "violations exceed difference records", "counts")
229
+ if (self.strategy == "key") != (self.selected_key is not None):
230
+ fail("comparison_strategy", "selected key must match key strategy", "selected_key")
231
+ if self.selected_key is not None and self.selected_key not in {
232
+ key.name for key in self.spec.contract.output.schema_spec.keys if key.unique
233
+ }:
234
+ fail("comparison_strategy", "selected key is not a declared unique key", "selected_key")
235
+ if len(self.examples) > min(self.options.max_examples, self.counts.difference_records):
236
+ fail(
237
+ "comparison_examples", "examples exceed their budget or recorded count", "examples"
238
+ )
239
+ if self.differences.format != "jsonl":
240
+ fail("comparison_evidence", "difference artifact must be JSONL", "differences")
241
+ if self.state == "equivalent" and (
242
+ not self.complete
243
+ or self.counts.difference_records
244
+ or self.diagnostics
245
+ or self.counts.matched_rows != self.counts.left_rows
246
+ or self.counts.matched_rows != self.counts.right_rows
247
+ ):
248
+ fail(
249
+ "false_equivalent",
250
+ "equivalent requires complete checks without differences",
251
+ "state",
252
+ )
253
+ if self.state == "different" and (not self.complete or not self.counts.difference_records):
254
+ fail("comparison_state", "different requires completed checks and differences", "state")
255
+ if self.state == "not_evaluable" and not self.diagnostics:
256
+ fail("comparison_diagnostic", "not_evaluable requires a diagnostic", "diagnostics")
257
+ return self