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,296 @@
|
|
|
1
|
+
"""One exact contract, six input variants and three independent native engines.
|
|
2
|
+
|
|
3
|
+
All data and the injected API response are synthetic. No remote service, paid
|
|
4
|
+
resource, credentials, benchmark or GitHub Actions are required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import hashlib
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
from contextlib import closing
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pyarrow as pa
|
|
19
|
+
import pyarrow.parquet as pq
|
|
20
|
+
|
|
21
|
+
import equiframe as ef
|
|
22
|
+
|
|
23
|
+
INT = ef.IntegerType(bits=64, signed=True)
|
|
24
|
+
MONEY = ef.DecimalType(precision=12, scale=2)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def column(
|
|
28
|
+
name: str, dtype: ef.LogicalType, *, nullable: bool = True, unit: str | None = None
|
|
29
|
+
) -> ef.ColumnSpec:
|
|
30
|
+
return ef.ColumnSpec(
|
|
31
|
+
name=name,
|
|
32
|
+
logical_type=dtype,
|
|
33
|
+
nullable=nullable,
|
|
34
|
+
null_policy=ef.NullPolicy(representations=("null",)),
|
|
35
|
+
numeric_policy=ef.NumericPolicy(comparison="exact", unit=unit)
|
|
36
|
+
if unit is not None
|
|
37
|
+
else None,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def declarations() -> tuple[ef.Contract, ef.RelationalPlan, ef.IngestionSpec]:
|
|
42
|
+
columns = (
|
|
43
|
+
column("id", ef.StringType(), nullable=False),
|
|
44
|
+
column("sequence", INT, nullable=False, unit="sequence"),
|
|
45
|
+
column("amount", MONEY, unit="USD"),
|
|
46
|
+
)
|
|
47
|
+
outputs = (
|
|
48
|
+
*columns,
|
|
49
|
+
column("position", INT, nullable=False, unit="rows"),
|
|
50
|
+
column("previous_amount", MONEY, unit="USD"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def table(name: str, fields: tuple[ef.ColumnSpec, ...]) -> ef.TableSpec:
|
|
54
|
+
return ef.TableSpec(
|
|
55
|
+
name=name,
|
|
56
|
+
schema_spec=ef.TableSchema(
|
|
57
|
+
columns=fields, keys=(), multiplicity="preserve", order=ef.IgnoredOrder()
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
contract = ef.Contract(
|
|
62
|
+
schema_version="1.0",
|
|
63
|
+
contract_id="universal.synthetic",
|
|
64
|
+
revision=1,
|
|
65
|
+
family="nulls_types",
|
|
66
|
+
inputs=(table("input", columns),),
|
|
67
|
+
output=table("output", outputs),
|
|
68
|
+
preconditions=(),
|
|
69
|
+
rules=(),
|
|
70
|
+
)
|
|
71
|
+
plan = ef.RelationalPlan(
|
|
72
|
+
nodes=(
|
|
73
|
+
ef.Scan(node_id="s", table="input"),
|
|
74
|
+
ef.Window(
|
|
75
|
+
node_id="w",
|
|
76
|
+
source="s",
|
|
77
|
+
partition_by=("id",),
|
|
78
|
+
by=(ef.SortKey(column="sequence", direction="ascending", nulls="last"),),
|
|
79
|
+
measures=(
|
|
80
|
+
ef.WindowMeasure(
|
|
81
|
+
name="position", operation="row_number", logical_type=INT, unit="rows"
|
|
82
|
+
),
|
|
83
|
+
ef.WindowMeasure(
|
|
84
|
+
name="previous_amount", operation="lag", column="amount", logical_type=MONEY
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
output="w",
|
|
90
|
+
)
|
|
91
|
+
spec = ef.IngestionSpec(
|
|
92
|
+
format="jsonl",
|
|
93
|
+
fields=tuple(ef.FieldMapping(column=field, path=(field.name,)) for field in columns),
|
|
94
|
+
)
|
|
95
|
+
return contract, plan, spec
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def source_table() -> pa.Table:
|
|
99
|
+
return pa.table(
|
|
100
|
+
{
|
|
101
|
+
"id": ["001", "001", "002", "001"],
|
|
102
|
+
"sequence": [2, 1, 1, 3],
|
|
103
|
+
"amount": pa.array(
|
|
104
|
+
[None, Decimal("12.30"), Decimal("-0.01"), Decimal("2.00")],
|
|
105
|
+
type=pa.decimal128(12, 2),
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def expected_output() -> pa.Table:
|
|
112
|
+
"""Literal expected authored independently of source loading/native execution."""
|
|
113
|
+
return pa.table(
|
|
114
|
+
{
|
|
115
|
+
"id": ["001", "001", "001", "002"],
|
|
116
|
+
"sequence": [1, 2, 3, 1],
|
|
117
|
+
"amount": pa.array(
|
|
118
|
+
[Decimal("12.30"), None, Decimal("2.00"), Decimal("-0.01")],
|
|
119
|
+
type=pa.decimal128(12, 2),
|
|
120
|
+
),
|
|
121
|
+
"position": [1, 2, 3, 1],
|
|
122
|
+
"previous_amount": pa.array(
|
|
123
|
+
[None, Decimal("12.30"), None, None], type=pa.decimal128(12, 2)
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def inputs(root: Path, spec: ef.IngestionSpec, case: str) -> dict[str, ef.DataSource]:
|
|
130
|
+
root.mkdir(exist_ok=False)
|
|
131
|
+
csv = root / "input.csv"
|
|
132
|
+
csv.write_text(
|
|
133
|
+
"id,sequence,amount\n001,2,NULL\n001,1,12.30\n002,1,-0.01\n001,3,2.00\n", encoding="utf-8"
|
|
134
|
+
)
|
|
135
|
+
csv_spec = ef.IngestionSpec(
|
|
136
|
+
format="csv",
|
|
137
|
+
fields=tuple(
|
|
138
|
+
mapping.model_copy(update={"null_tokens": ("NULL",)})
|
|
139
|
+
if mapping.column.nullable
|
|
140
|
+
else mapping
|
|
141
|
+
for mapping in spec.fields
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
database = root / "input.sqlite"
|
|
145
|
+
with sqlite3.connect(database) as db:
|
|
146
|
+
db.execute("CREATE TABLE entries (id TEXT, sequence INTEGER, amount TEXT, cohort TEXT)")
|
|
147
|
+
db.executemany(
|
|
148
|
+
"INSERT INTO entries VALUES (?, ?, ?, ?)",
|
|
149
|
+
[
|
|
150
|
+
("001", 2, None, "demo"),
|
|
151
|
+
("001", 1, "12.31" if case == "different" else "12.30", "demo"),
|
|
152
|
+
("002", 1, "-0.01", "demo"),
|
|
153
|
+
("001", 3, "2.00", "demo"),
|
|
154
|
+
("excluded", 1, "99.99", "other"),
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
records = [
|
|
158
|
+
{"id": "001", "sequence": 2, "amount": None},
|
|
159
|
+
{"id": "001", "sequence": 1, "amount": "12.30"},
|
|
160
|
+
{"id": "002", "sequence": 1, "amount": "-0.01"},
|
|
161
|
+
{"id": "001", "sequence": 3, "amount": "2.00"},
|
|
162
|
+
]
|
|
163
|
+
json_path = root / "input.json"
|
|
164
|
+
json_path.write_text(json.dumps({"records": records}), encoding="utf-8")
|
|
165
|
+
json_spec = ef.IngestionSpec(format="json", records_path=("records", "*"), fields=spec.fields)
|
|
166
|
+
response = ("\n".join(json.dumps(row) for row in records) + "\n").encode()
|
|
167
|
+
response_sha = "0" * 64 if case == "corrupt" else hashlib.sha256(response).hexdigest()
|
|
168
|
+
raw = source_table()
|
|
169
|
+
parquet = root / "input.parquet"
|
|
170
|
+
pq.write_table(raw, parquet)
|
|
171
|
+
ref = ef.LocalArtifactStore(root / "artifacts").put(
|
|
172
|
+
parquet, media_type="application/vnd.apache.parquet"
|
|
173
|
+
)
|
|
174
|
+
batch_input = ef.RecordBatchInput(
|
|
175
|
+
lambda: closing(
|
|
176
|
+
pa.RecordBatchReader.from_batches(raw.schema, raw.to_batches(max_chunksize=2))
|
|
177
|
+
),
|
|
178
|
+
raw.schema,
|
|
179
|
+
)
|
|
180
|
+
items = {
|
|
181
|
+
"csv": ef.FileInput(csv, csv_spec, batch_rows=2),
|
|
182
|
+
"sqlite": ef.SQLiteInput(
|
|
183
|
+
database,
|
|
184
|
+
"SELECT id, sequence, amount FROM entries WHERE cohort = ?",
|
|
185
|
+
spec,
|
|
186
|
+
("demo",),
|
|
187
|
+
batch_rows=2,
|
|
188
|
+
),
|
|
189
|
+
"json": ef.FileInput(json_path, json_spec),
|
|
190
|
+
"api_response": ef.StreamInput(lambda: closing(io.BytesIO(response)), spec, response_sha),
|
|
191
|
+
"parquet": ef.ArtifactInput(ref),
|
|
192
|
+
"arrow_batches": batch_input,
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
name: ef.CompositeDataSource(
|
|
196
|
+
{"input": (ef.LocalDataSource({"provider_table": item}), "provider_table")}
|
|
197
|
+
)
|
|
198
|
+
for name, item in items.items()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_demo(output: Path, case: str = "good") -> int:
|
|
203
|
+
if case not in ("good", "different", "corrupt"):
|
|
204
|
+
raise ValueError("unknown synthetic case")
|
|
205
|
+
output = output.resolve()
|
|
206
|
+
output.mkdir(parents=True, exist_ok=False)
|
|
207
|
+
contract, plan, spec = declarations()
|
|
208
|
+
sources = inputs(output / "inputs", spec, case)
|
|
209
|
+
receipts: list[dict[str, object]] = []
|
|
210
|
+
exit_code = 0
|
|
211
|
+
for engine in ("pandas", "polars", "duckdb"):
|
|
212
|
+
compiled = ef.native_compiler_registry().resolve(engine).compile(plan, contract)
|
|
213
|
+
registry = ef.TransformationRegistry()
|
|
214
|
+
registry.register(compiled.transformation)
|
|
215
|
+
for name, source in sources.items():
|
|
216
|
+
directory = output / engine / name
|
|
217
|
+
execution = ef.execute(
|
|
218
|
+
ef.ExecutionPlan(
|
|
219
|
+
contract=contract,
|
|
220
|
+
engine=engine,
|
|
221
|
+
transformation_id=compiled.transformation.transformation_id,
|
|
222
|
+
options=ef.ExecutionOptions(
|
|
223
|
+
max_input_rows=100,
|
|
224
|
+
max_output_rows=100,
|
|
225
|
+
max_transport_bytes=1_000_000,
|
|
226
|
+
timeout_seconds=30,
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
source=source,
|
|
230
|
+
registry=registry,
|
|
231
|
+
work_dir=directory / "execution",
|
|
232
|
+
)
|
|
233
|
+
receipt: dict[str, object] = {
|
|
234
|
+
"engine": engine,
|
|
235
|
+
"source": name,
|
|
236
|
+
"execution_state": execution.state,
|
|
237
|
+
"comparison_state": None,
|
|
238
|
+
"problem": execution.problem.model_dump(mode="json") if execution.problem else None,
|
|
239
|
+
}
|
|
240
|
+
if execution.state != "completed":
|
|
241
|
+
exit_code = 3
|
|
242
|
+
else:
|
|
243
|
+
comparison = ef.compare(
|
|
244
|
+
execution,
|
|
245
|
+
ef.ArrowInput(expected_output()),
|
|
246
|
+
contract=contract,
|
|
247
|
+
reference="expected",
|
|
248
|
+
work_dir=directory / "comparison",
|
|
249
|
+
policy=ef.ComparisonPolicy(
|
|
250
|
+
index="ignore",
|
|
251
|
+
justification=(
|
|
252
|
+
"This declared relational bag has no native index identity."
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
ef.write_comparison_report(comparison, directory / "report")
|
|
257
|
+
receipt["comparison_state"] = comparison.state
|
|
258
|
+
receipt["comparison_complete"] = comparison.complete
|
|
259
|
+
try:
|
|
260
|
+
ef.assert_equivalent(comparison)
|
|
261
|
+
except ef.EquivalenceDifferentError:
|
|
262
|
+
exit_code = max(exit_code, 1)
|
|
263
|
+
except ef.EquivalenceNotEvaluableError:
|
|
264
|
+
exit_code = 3
|
|
265
|
+
receipts.append(receipt)
|
|
266
|
+
print(
|
|
267
|
+
f"{engine}/{name}: execution={execution.state}; "
|
|
268
|
+
f"comparison={receipt['comparison_state']}"
|
|
269
|
+
)
|
|
270
|
+
(output / "receipt.json").write_text(
|
|
271
|
+
json.dumps(
|
|
272
|
+
{
|
|
273
|
+
"equiframe": ef.__version__,
|
|
274
|
+
"synthetic": True,
|
|
275
|
+
"case": case,
|
|
276
|
+
"exit_code": exit_code,
|
|
277
|
+
"runs": receipts,
|
|
278
|
+
},
|
|
279
|
+
indent=2,
|
|
280
|
+
)
|
|
281
|
+
+ "\n",
|
|
282
|
+
encoding="utf-8",
|
|
283
|
+
)
|
|
284
|
+
return exit_code
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def main() -> None:
|
|
288
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
289
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
290
|
+
parser.add_argument("--case", choices=("good", "different", "corrupt"), default="good")
|
|
291
|
+
args = parser.parse_args()
|
|
292
|
+
raise SystemExit(run_demo(args.output, args.case))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Eight complete synthetic journeys
|
|
2
|
+
|
|
3
|
+
Read the [packaged journey guide](../../docs/journeys.md) for installation,
|
|
4
|
+
independent expected values, acceptance criteria, artifacts, privacy and limits.
|
|
5
|
+
Everything is inline synthetic data; no credentials or network data sources.
|
|
6
|
+
|
|
7
|
+
After installing an identified private wheel and the exact dependencies in
|
|
8
|
+
`requirements.lock`, copy this entire directory outside the checkout:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python journeys/a_pandas_refactor.py --case good --output evidence-a
|
|
12
|
+
python journeys/verify_journeys.py --output all-journey-evidence
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The first command is an equivalence gate. The second is an **acceptance harness**:
|
|
16
|
+
it expects both good cases and intentional failures, and returns 0 only when all
|
|
17
|
+
31 scenario exits match their declared meaning. It does not approve every input.
|
|
18
|
+
|
|
19
|
+
When these examples are installed inside the candidate wheel, the isolated route
|
|
20
|
+
requires no copied scripts:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -I -m equiframe.examples.journeys.a_pandas_refactor --case good --output evidence-a
|
|
24
|
+
python -I -m equiframe.examples.journeys.verify_journeys --output all-journey-evidence
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use a fresh output directory every time. `private/` or `raw/` contains raw
|
|
28
|
+
captures. Only the `reports/` or `public_reports/` subtree is data-minimized.
|
|
29
|
+
A portable bundle, `summary.json`, and `environment.json` are not redacted sharing
|
|
30
|
+
packages. Local Parquet output is local file publication, never package/service
|
|
31
|
+
publication. Keep raw captures and local evidence out of public commits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runnable synthetic product journeys; not an additional public core API."""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""A: pandas refactor, exact money, composite keys and visible duplicate failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
import equiframe as ef
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING or __package__:
|
|
14
|
+
from .common import cli, column, comparison_record, new_output, write_summary
|
|
15
|
+
else:
|
|
16
|
+
from common import cli, column, comparison_record, new_output, write_summary
|
|
17
|
+
|
|
18
|
+
SCENARIOS = ("good", "different", "not_evaluable", "duplicate")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def materialize(scenario: str) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
source = pd.DataFrame(
|
|
25
|
+
{
|
|
26
|
+
"order_id": pd.Series(["001", "001", "002"], dtype="string[pyarrow]"),
|
|
27
|
+
"line": [1, 2, 1],
|
|
28
|
+
"quantity": [2, 1, 3],
|
|
29
|
+
"unit_cents": [125, 50, 100],
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
# Independent literal, not computed using old or optimized transformation.
|
|
33
|
+
expected = pd.DataFrame(
|
|
34
|
+
{
|
|
35
|
+
"order_id": pd.Series(["001", "001", "002"], dtype="string[pyarrow]"),
|
|
36
|
+
"line": [1, 2, 1],
|
|
37
|
+
"total_cents": [250, 50, 300],
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
old = source[["order_id", "line"]].copy()
|
|
41
|
+
old["total_cents"] = [
|
|
42
|
+
int(quantity) * int(price)
|
|
43
|
+
for quantity, price in zip(source["quantity"], source["unit_cents"], strict=True)
|
|
44
|
+
]
|
|
45
|
+
pd.testing.assert_frame_equal(old, expected)
|
|
46
|
+
candidate = source[["order_id", "line"]].assign(
|
|
47
|
+
total_cents=source["quantity"] * source["unit_cents"]
|
|
48
|
+
)
|
|
49
|
+
if scenario == "different":
|
|
50
|
+
candidate["total_cents"] += candidate["order_id"].eq("002").astype("int64")
|
|
51
|
+
if scenario == "duplicate":
|
|
52
|
+
candidate = pd.concat([candidate, candidate.iloc[[0]]], ignore_index=True)
|
|
53
|
+
else:
|
|
54
|
+
candidate = candidate.iloc[[2, 0, 1]].reset_index(drop=True)
|
|
55
|
+
return candidate, expected
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def declaration() -> ef.DiffContract:
|
|
59
|
+
return ef.keyed_contract(
|
|
60
|
+
(
|
|
61
|
+
column("order_id", ef.StringType()),
|
|
62
|
+
column("line", ef.IntegerType(bits=64, signed=True), unit="line_identifier"),
|
|
63
|
+
column("total_cents", ef.IntegerType(bits=64, signed=True), unit="USD_cent"),
|
|
64
|
+
),
|
|
65
|
+
keys=["order_id", "line"],
|
|
66
|
+
contract_id="journey.A.refactor",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
71
|
+
private = new_output(output, scenario, SCENARIOS)
|
|
72
|
+
candidate, expected = materialize(scenario)
|
|
73
|
+
result = ef.diff(
|
|
74
|
+
candidate,
|
|
75
|
+
expected,
|
|
76
|
+
contract=declaration(),
|
|
77
|
+
options=ef.ComparisonOptions(max_rows=1 if scenario == "not_evaluable" else 100),
|
|
78
|
+
work_dir=private,
|
|
79
|
+
)
|
|
80
|
+
record = comparison_record(result, output, "refactor")
|
|
81
|
+
return write_summary(
|
|
82
|
+
output,
|
|
83
|
+
{"journey": "A", "synthetic": True, "scenario": scenario, **record},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
raise SystemExit(cli(run, __doc__ or "A", SCENARIOS))
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""B: real pandas and DuckDB compilation, one input capture, independent expected."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
import equiframe as ef
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING or __package__:
|
|
15
|
+
from .common import cli, column, comparison_record, new_output, write_summary
|
|
16
|
+
else:
|
|
17
|
+
from common import cli, column, comparison_record, new_output, write_summary
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CountedSource:
|
|
21
|
+
"""Observe actual source resolution without changing the production source port."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, source: ef.LocalDataSource) -> None:
|
|
24
|
+
self.source = source
|
|
25
|
+
self.calls = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def table_names(self) -> tuple[str, ...]:
|
|
29
|
+
return self.source.table_names
|
|
30
|
+
|
|
31
|
+
def resolve(self, name: str, *, context: ef.RunContext) -> ef.LoadedTable:
|
|
32
|
+
self.calls += 1
|
|
33
|
+
return self.source.resolve(name, context=context)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def configuration(scenario: str) -> ef.MigrationSpec:
|
|
37
|
+
currency = column("currency", ef.StringType())
|
|
38
|
+
amount = column(
|
|
39
|
+
"amount", ef.DecimalType(precision=18, scale=2), nullable=True, unit="currency_amount"
|
|
40
|
+
)
|
|
41
|
+
total = column(
|
|
42
|
+
"total", ef.DecimalType(precision=20, scale=2), nullable=True, unit="currency_amount"
|
|
43
|
+
)
|
|
44
|
+
output_schema = ef.keyed_contract(
|
|
45
|
+
(currency, total), keys=["currency"]
|
|
46
|
+
).spec.contract.output.schema_spec
|
|
47
|
+
contract = ef.Contract(
|
|
48
|
+
schema_version="1.0",
|
|
49
|
+
contract_id="journey.B.native-totals",
|
|
50
|
+
revision=1,
|
|
51
|
+
family="aggregations",
|
|
52
|
+
inputs=(
|
|
53
|
+
ef.TableSpec(
|
|
54
|
+
name="sales",
|
|
55
|
+
schema_spec=ef.bag_contract((currency, amount)).spec.contract.output.schema_spec,
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
output=ef.TableSpec(name="result", schema_spec=output_schema),
|
|
59
|
+
preconditions=(),
|
|
60
|
+
rules=(
|
|
61
|
+
ef.RuleSpec(
|
|
62
|
+
rule_id="schema",
|
|
63
|
+
revision=1,
|
|
64
|
+
severity="error",
|
|
65
|
+
required=True,
|
|
66
|
+
specification=ef.SchemaCheck(table="result"),
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
return ef.MigrationSpec(
|
|
71
|
+
contract=contract,
|
|
72
|
+
source_engine="pandas",
|
|
73
|
+
target_engine="duckdb",
|
|
74
|
+
ir=ef.RelationalPlan(
|
|
75
|
+
nodes=(
|
|
76
|
+
ef.Scan(node_id="source", table="sales"),
|
|
77
|
+
ef.Aggregate(
|
|
78
|
+
node_id="totals",
|
|
79
|
+
source="source",
|
|
80
|
+
group_by=("currency",),
|
|
81
|
+
measures=(
|
|
82
|
+
ef.Measure(
|
|
83
|
+
name="total",
|
|
84
|
+
operation="sum",
|
|
85
|
+
column="amount",
|
|
86
|
+
logical_type=ef.DecimalType(precision=20, scale=2),
|
|
87
|
+
unit="currency_amount",
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
null_groups="retain",
|
|
91
|
+
empty_sum="zero" if scenario == "different" else "null",
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
output="totals",
|
|
95
|
+
),
|
|
96
|
+
comparison_policy=ef.ComparisonPolicy(
|
|
97
|
+
index="ignore", justification="Native generated RangeIndex is not row identity."
|
|
98
|
+
),
|
|
99
|
+
comparison_options=ef.ComparisonOptions(max_rows=1 if scenario == "not_evaluable" else 100),
|
|
100
|
+
execution_options=ef.ExecutionOptions(
|
|
101
|
+
duckdb_threads=1, duckdb_timezone="UTC", max_input_rows=100, max_output_rows=100
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
107
|
+
import pyarrow as pa
|
|
108
|
+
|
|
109
|
+
private = new_output(output, scenario)
|
|
110
|
+
source = CountedSource(
|
|
111
|
+
ef.LocalDataSource(
|
|
112
|
+
{
|
|
113
|
+
"sales": ef.ArrowInput(
|
|
114
|
+
pa.table(
|
|
115
|
+
{
|
|
116
|
+
"currency": ["USD", "USD", "EUR", "EUR"],
|
|
117
|
+
"amount": pa.array(
|
|
118
|
+
[Decimal("1.10"), Decimal("2.20"), None, None],
|
|
119
|
+
type=pa.decimal128(18, 2),
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
# This literal is independent of both native implementations and their IR.
|
|
128
|
+
expected = pa.table(
|
|
129
|
+
{
|
|
130
|
+
"currency": ["USD", "EUR"],
|
|
131
|
+
"total": pa.array([Decimal("3.30"), None], type=pa.decimal128(20, 2)),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
spec = configuration(scenario)
|
|
135
|
+
checks = ef.check_migration(spec)
|
|
136
|
+
if source.calls != 0 or not all(check.supported for check in checks):
|
|
137
|
+
raise RuntimeError("Preflight unexpectedly read input or refused the documented plan")
|
|
138
|
+
result = ef.migrate(spec, source=source, expected=ef.ArrowInput(expected), work_dir=private)
|
|
139
|
+
if ef.read_migration(private / result.migration_id) != result:
|
|
140
|
+
raise RuntimeError("Migration readback failed")
|
|
141
|
+
comparisons = {}
|
|
142
|
+
for label, comparison in (
|
|
143
|
+
("peers", result.comparison),
|
|
144
|
+
("pandas-expected", result.verification_source),
|
|
145
|
+
("duckdb-expected", result.verification_target),
|
|
146
|
+
):
|
|
147
|
+
if comparison is not None:
|
|
148
|
+
comparisons[label] = comparison_record(comparison, output, label)
|
|
149
|
+
executions = [
|
|
150
|
+
pipeline.execution
|
|
151
|
+
for pipeline in (result.source, result.target)
|
|
152
|
+
if pipeline is not None and pipeline.execution is not None
|
|
153
|
+
]
|
|
154
|
+
same_input = (
|
|
155
|
+
len(executions) == 2
|
|
156
|
+
and executions[0].inputs[0].data.sha256 == executions[1].inputs[0].data.sha256
|
|
157
|
+
)
|
|
158
|
+
accepted = result.exit_code == 0 and source.calls == 1 and same_input
|
|
159
|
+
return write_summary(
|
|
160
|
+
output,
|
|
161
|
+
{
|
|
162
|
+
"journey": "B",
|
|
163
|
+
"synthetic": True,
|
|
164
|
+
"scenario": scenario,
|
|
165
|
+
"preflight": [check.model_dump(mode="json") for check in checks],
|
|
166
|
+
"source_resolutions": source.calls,
|
|
167
|
+
"same_input_sha256": same_input,
|
|
168
|
+
"native_engines": [execution.plan.engine for execution in executions],
|
|
169
|
+
"execution_states": [execution.state for execution in executions],
|
|
170
|
+
"state": result.state,
|
|
171
|
+
"verification": result.verification,
|
|
172
|
+
"complete": result.complete,
|
|
173
|
+
"accepted": accepted,
|
|
174
|
+
"exit_code": result.exit_code if not (result.exit_code == 0 and not accepted) else 3,
|
|
175
|
+
"comparisons": comparisons,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
raise SystemExit(cli(run, __doc__ or "B"))
|