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,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
|