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,135 @@
|
|
|
1
|
+
"""E: timezone-aware nanoseconds, NULL/NaN, duplicates and sequence semantics."""
|
|
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 pyarrow as pa
|
|
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", "null_nan", "duplicate_loss")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def declaration() -> ef.DiffContract:
|
|
22
|
+
return ef.sequence_contract(
|
|
23
|
+
(
|
|
24
|
+
column("sensor", ef.StringType()),
|
|
25
|
+
column("at", ef.TimestampType(unit="ns", timezone="UTC"), nullable=True),
|
|
26
|
+
column("reading", ef.FloatType(bits=64), nullable=True, unit="degrees_C"),
|
|
27
|
+
),
|
|
28
|
+
by=(ef.SortSpec(column="at", direction="ascending", nulls="last"),),
|
|
29
|
+
ties="preserve",
|
|
30
|
+
contract_id="journey.E.telemetry",
|
|
31
|
+
policy=ef.ComparisonPolicy(
|
|
32
|
+
physical_types="lossless",
|
|
33
|
+
justification="Zones may encode the same instant; retain nanoseconds exactly.",
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fixture(scenario: str) -> tuple[pa.Table, pa.Table]:
|
|
39
|
+
import pyarrow as pa
|
|
40
|
+
|
|
41
|
+
expected = pa.table(
|
|
42
|
+
{
|
|
43
|
+
"sensor": ["s1", "s1", "s1", "s2"],
|
|
44
|
+
"at": pa.array(
|
|
45
|
+
[1704067200000000001, 1704067200000000001, 1704067201000000002, None],
|
|
46
|
+
type=pa.timestamp("ns", tz="UTC"),
|
|
47
|
+
),
|
|
48
|
+
"reading": pa.array(
|
|
49
|
+
[1.5, 1.5, float("nan"), None], type=pa.float64(), from_pandas=False
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
candidate = pa.table(
|
|
54
|
+
{
|
|
55
|
+
"sensor": ["s1", "s1", "s1", "s2"],
|
|
56
|
+
"at": pa.array(
|
|
57
|
+
[1704067200000000001, 1704067200000000001, 1704067201000000002, None],
|
|
58
|
+
type=pa.timestamp("ns", tz="America/Guayaquil"),
|
|
59
|
+
),
|
|
60
|
+
"reading": pa.array(
|
|
61
|
+
[
|
|
62
|
+
1.5,
|
|
63
|
+
1.5,
|
|
64
|
+
None if scenario == "null_nan" else float("nan"),
|
|
65
|
+
float("nan") if scenario == "null_nan" else None,
|
|
66
|
+
],
|
|
67
|
+
type=pa.float64(),
|
|
68
|
+
from_pandas=False,
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
if scenario == "different":
|
|
73
|
+
candidate = candidate.take([2, 0, 1, 3])
|
|
74
|
+
elif scenario == "duplicate_loss":
|
|
75
|
+
candidate = candidate.take([0, 2, 3])
|
|
76
|
+
return candidate, expected
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
80
|
+
private = new_output(output, scenario, SCENARIOS)
|
|
81
|
+
candidate, expected = fixture(scenario)
|
|
82
|
+
declared = declaration()
|
|
83
|
+
result = ef.diff(
|
|
84
|
+
candidate,
|
|
85
|
+
expected,
|
|
86
|
+
contract=declared,
|
|
87
|
+
options=ef.ComparisonOptions(max_rows=1 if scenario == "not_evaluable" else 100),
|
|
88
|
+
work_dir=private,
|
|
89
|
+
)
|
|
90
|
+
# This Scan has no final Sort matching the significant-order contract.
|
|
91
|
+
schema = declared.spec.contract.output.schema_spec
|
|
92
|
+
contract = ef.Contract(
|
|
93
|
+
schema_version="1.0",
|
|
94
|
+
contract_id="journey.E.migration-limit",
|
|
95
|
+
revision=1,
|
|
96
|
+
family="dates",
|
|
97
|
+
inputs=(ef.TableSpec(name="telemetry", schema_spec=schema),),
|
|
98
|
+
output=ef.TableSpec(name="result", schema_spec=schema),
|
|
99
|
+
preconditions=(),
|
|
100
|
+
rules=(),
|
|
101
|
+
)
|
|
102
|
+
checks = ef.check_migration(
|
|
103
|
+
ef.MigrationSpec(
|
|
104
|
+
contract=contract,
|
|
105
|
+
source_engine="pandas",
|
|
106
|
+
target_engine="duckdb",
|
|
107
|
+
ir=ef.RelationalPlan(
|
|
108
|
+
nodes=(ef.Scan(node_id="scan", table="telemetry"),), output="scan"
|
|
109
|
+
),
|
|
110
|
+
require_rules=False,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
if any(check.supported for check in checks):
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Review the matching-final-Sort preflight contract before changing this example"
|
|
116
|
+
)
|
|
117
|
+
return write_summary(
|
|
118
|
+
output,
|
|
119
|
+
{
|
|
120
|
+
"journey": "E",
|
|
121
|
+
"synthetic": True,
|
|
122
|
+
"scenario": scenario,
|
|
123
|
+
"timezone_policy": "instant; UTC and America/Guayaquil",
|
|
124
|
+
"timestamp_unit": "ns",
|
|
125
|
+
"duplicate_policy": "preserve",
|
|
126
|
+
"null_nan_policy": "distinct",
|
|
127
|
+
"order_policy": "significant",
|
|
128
|
+
"migration_preflight": [check.model_dump(mode="json") for check in checks],
|
|
129
|
+
**comparison_record(result, output, "telemetry"),
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
raise SystemExit(cli(run, __doc__ or "E", SCENARIOS))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""F: prove feature preparation preserves records; do not infer anomaly quality."""
|
|
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", "dropped_row", "changed_id")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def materialize(scenario: str) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
source = pd.DataFrame(
|
|
25
|
+
{
|
|
26
|
+
"sample_id": pd.Series(["0001", "0002", "0003", "0004"], dtype="string[pyarrow]"),
|
|
27
|
+
"duration_ms": [1000, 2500, 0, 1000000],
|
|
28
|
+
"events": [2, 3, 0, 99],
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
# Explicit predetermined scale; it is not fitted using labels or test outputs.
|
|
32
|
+
candidate = source[["sample_id", "events"]].assign(duration_s=source["duration_ms"] / 1000.0)
|
|
33
|
+
candidate = candidate[["sample_id", "duration_s", "events"]]
|
|
34
|
+
# The large fourth value is intentionally conserved, not called an anomaly.
|
|
35
|
+
expected = pd.DataFrame(
|
|
36
|
+
{
|
|
37
|
+
"sample_id": pd.Series(["0001", "0002", "0003", "0004"], dtype="string[pyarrow]"),
|
|
38
|
+
"duration_s": [1.0, 2.5, 0.0, 1000.0],
|
|
39
|
+
"events": [2, 3, 0, 99],
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
if scenario == "different":
|
|
43
|
+
candidate["duration_s"] = candidate["duration_s"].clip(upper=100.0)
|
|
44
|
+
elif scenario == "dropped_row":
|
|
45
|
+
candidate = candidate.iloc[:3].reset_index(drop=True)
|
|
46
|
+
elif scenario == "changed_id":
|
|
47
|
+
candidate.loc[0, "sample_id"] = "1"
|
|
48
|
+
return candidate, expected
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
52
|
+
private = new_output(output, scenario, SCENARIOS)
|
|
53
|
+
candidate, expected = materialize(scenario)
|
|
54
|
+
declaration = ef.keyed_contract(
|
|
55
|
+
(
|
|
56
|
+
column("sample_id", ef.StringType()),
|
|
57
|
+
column("duration_s", ef.FloatType(bits=64), unit="seconds"),
|
|
58
|
+
column("events", ef.IntegerType(bits=64, signed=True), unit="events"),
|
|
59
|
+
),
|
|
60
|
+
keys=["sample_id"],
|
|
61
|
+
contract_id="journey.F.features",
|
|
62
|
+
)
|
|
63
|
+
result = ef.diff(
|
|
64
|
+
candidate,
|
|
65
|
+
expected,
|
|
66
|
+
contract=declaration,
|
|
67
|
+
options=ef.ComparisonOptions(max_rows=1 if scenario == "not_evaluable" else 100),
|
|
68
|
+
work_dir=private,
|
|
69
|
+
)
|
|
70
|
+
return write_summary(
|
|
71
|
+
output,
|
|
72
|
+
{
|
|
73
|
+
"journey": "F",
|
|
74
|
+
"synthetic": True,
|
|
75
|
+
"scenario": scenario,
|
|
76
|
+
"anomaly_detection": "not_requested",
|
|
77
|
+
"model_training": "not_requested",
|
|
78
|
+
**comparison_record(result, output, "features"),
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
raise SystemExit(cli(run, __doc__ or "F", SCENARIOS))
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""G: fixed-identity injected client, provenance and byte/schema integrity errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import pyarrow as pa
|
|
12
|
+
|
|
13
|
+
import equiframe as ef
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING or __package__:
|
|
16
|
+
from .common import cli, column, comparison_record, new_output, write_summary
|
|
17
|
+
else:
|
|
18
|
+
from common import cli, column, comparison_record, new_output, write_summary
|
|
19
|
+
|
|
20
|
+
SCENARIOS = ("good", "different", "error", "schema_error")
|
|
21
|
+
SNAPSHOT_ID = "synthetic.snapshot.20261006.v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemoryClient:
|
|
25
|
+
"""A trusted transport fixture. No HTTP request, credentials or hidden dataset."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, manifest: ef.SnapshotManifest, payload: bytes) -> None:
|
|
28
|
+
self.manifest = manifest
|
|
29
|
+
self.payload = payload
|
|
30
|
+
self.requests: list[str] = []
|
|
31
|
+
self.downloads = 0
|
|
32
|
+
|
|
33
|
+
def get_snapshot(self, snapshot_id: str) -> ef.SnapshotManifest:
|
|
34
|
+
if snapshot_id != SNAPSHOT_ID:
|
|
35
|
+
raise LookupError("Unknown fixed snapshot identity")
|
|
36
|
+
self.requests.append(snapshot_id)
|
|
37
|
+
return self.manifest
|
|
38
|
+
|
|
39
|
+
def download_artifact(self, ref: ef.ArtifactRef, destination: Path) -> None:
|
|
40
|
+
if ref != self.manifest.tables[0].artifact:
|
|
41
|
+
raise LookupError("Unknown artifact")
|
|
42
|
+
self.downloads += 1
|
|
43
|
+
with destination.open("xb") as stream:
|
|
44
|
+
stream.write(self.payload)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fixture(scenario: str) -> tuple[MemoryClient, pa.Table, ef.Contract]:
|
|
48
|
+
import pyarrow as pa
|
|
49
|
+
|
|
50
|
+
schema = pa.schema([("id", pa.int64()), ("amount", pa.decimal128(8, 2))])
|
|
51
|
+
expected = pa.Table.from_pylist(
|
|
52
|
+
[{"id": 1, "amount": Decimal("1.10")}, {"id": 2, "amount": None}], schema=schema
|
|
53
|
+
)
|
|
54
|
+
actual = pa.Table.from_pylist(
|
|
55
|
+
[
|
|
56
|
+
{"id": 1, "amount": Decimal("1.11" if scenario == "different" else "1.10")},
|
|
57
|
+
{"id": 2, "amount": None},
|
|
58
|
+
],
|
|
59
|
+
schema=schema,
|
|
60
|
+
)
|
|
61
|
+
sink = pa.BufferOutputStream()
|
|
62
|
+
with pa.ipc.new_file(sink, actual.schema) as writer:
|
|
63
|
+
writer.write_table(actual)
|
|
64
|
+
payload = sink.getvalue().to_pybytes()
|
|
65
|
+
kinds: tuple[ef.LogicalType, ...] = (
|
|
66
|
+
ef.IntegerType(bits=64, signed=True),
|
|
67
|
+
ef.DecimalType(precision=8, scale=2),
|
|
68
|
+
)
|
|
69
|
+
manifest = ef.SnapshotManifest(
|
|
70
|
+
snapshot_id=SNAPSHOT_ID,
|
|
71
|
+
domain_schema_revision="synthetic-ledger.1",
|
|
72
|
+
extraction_revision="synthetic-generator.1",
|
|
73
|
+
provenance=("Inline synthetic fixture authored for journey G; no live warehouse",),
|
|
74
|
+
tables=(
|
|
75
|
+
ef.SnapshotTable(
|
|
76
|
+
name="items",
|
|
77
|
+
artifact=ef.ArtifactRef(
|
|
78
|
+
schema_version="1.0",
|
|
79
|
+
uri="https://example.invalid/synthetic.arrow",
|
|
80
|
+
sha256=hashlib.sha256(payload).hexdigest(),
|
|
81
|
+
format="arrow",
|
|
82
|
+
media_type="application/vnd.apache.arrow.file",
|
|
83
|
+
size_bytes=len(payload),
|
|
84
|
+
),
|
|
85
|
+
row_count=2,
|
|
86
|
+
columns=tuple(
|
|
87
|
+
ef.SnapshotColumn(
|
|
88
|
+
name=field.name,
|
|
89
|
+
physical_type="int32"
|
|
90
|
+
if scenario == "schema_error" and field.name == "id"
|
|
91
|
+
else str(field.type),
|
|
92
|
+
logical_type=kind,
|
|
93
|
+
nullable=field.name == "amount",
|
|
94
|
+
)
|
|
95
|
+
for field, kind in zip(schema, kinds, strict=True)
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
# Alter one known synthetic byte after recording the manifest; preserve size.
|
|
101
|
+
if scenario == "error":
|
|
102
|
+
payload = bytes([payload[0] ^ 1]) + payload[1:]
|
|
103
|
+
client = MemoryClient(manifest, payload)
|
|
104
|
+
declaration = ef.keyed_contract(
|
|
105
|
+
(
|
|
106
|
+
column("id", kinds[0], unit="identifier"),
|
|
107
|
+
column("amount", kinds[1], nullable=True, unit="USD"),
|
|
108
|
+
),
|
|
109
|
+
keys=["id"],
|
|
110
|
+
)
|
|
111
|
+
output_schema = declaration.spec.contract.output.schema_spec
|
|
112
|
+
contract = ef.Contract(
|
|
113
|
+
schema_version="1.0",
|
|
114
|
+
contract_id="journey.G.snapshot",
|
|
115
|
+
revision=1,
|
|
116
|
+
family="nulls_types",
|
|
117
|
+
inputs=(ef.TableSpec(name="items", schema_spec=output_schema),),
|
|
118
|
+
output=ef.TableSpec(name="result", schema_spec=output_schema),
|
|
119
|
+
preconditions=(),
|
|
120
|
+
rules=(),
|
|
121
|
+
)
|
|
122
|
+
return client, expected, contract
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
126
|
+
private = new_output(output, scenario, SCENARIOS)
|
|
127
|
+
client, expected, contract = fixture(scenario)
|
|
128
|
+
source = ef.SnapshotDataSource(
|
|
129
|
+
SNAPSHOT_ID, client=client, expected_domain_schema_revision="synthetic-ledger.1"
|
|
130
|
+
)
|
|
131
|
+
registry = ef.TransformationRegistry()
|
|
132
|
+
registry.register(ef.DuckDBTransformation("identity", 'SELECT * FROM "items"', "journey.G.v1"))
|
|
133
|
+
execution = ef.execute(
|
|
134
|
+
ef.ExecutionPlan(
|
|
135
|
+
contract=contract,
|
|
136
|
+
engine="duckdb",
|
|
137
|
+
transformation_id="identity",
|
|
138
|
+
options=ef.ExecutionOptions(duckdb_threads=1),
|
|
139
|
+
),
|
|
140
|
+
source=source,
|
|
141
|
+
registry=registry,
|
|
142
|
+
work_dir=private,
|
|
143
|
+
)
|
|
144
|
+
configuration = {
|
|
145
|
+
parameter.name: parameter.value.value
|
|
146
|
+
for parameter in execution.effective_configuration
|
|
147
|
+
if isinstance(parameter.value, ef.StringValue)
|
|
148
|
+
}
|
|
149
|
+
provenance_preserved = configuration.get("snapshot_id") == SNAPSHOT_ID and configuration.get(
|
|
150
|
+
"snapshot_manifest_sha256"
|
|
151
|
+
) == ef.snapshot_fingerprint(client.manifest)
|
|
152
|
+
record: dict[str, object]
|
|
153
|
+
if execution.state == "completed":
|
|
154
|
+
comparison = ef.compare(
|
|
155
|
+
execution,
|
|
156
|
+
ef.ArrowInput(expected),
|
|
157
|
+
contract=contract,
|
|
158
|
+
reference="expected",
|
|
159
|
+
work_dir=private,
|
|
160
|
+
)
|
|
161
|
+
record = comparison_record(comparison, output, "snapshot")
|
|
162
|
+
else:
|
|
163
|
+
record = {
|
|
164
|
+
"accepted": False,
|
|
165
|
+
"exit_code": 2,
|
|
166
|
+
"state": "error",
|
|
167
|
+
"problem_code": execution.problem.code if execution.problem is not None else None,
|
|
168
|
+
}
|
|
169
|
+
if client.requests != [SNAPSHOT_ID] or client.downloads != 1 or not provenance_preserved:
|
|
170
|
+
raise RuntimeError("Snapshot identity/provenance was not retained")
|
|
171
|
+
return write_summary(
|
|
172
|
+
output,
|
|
173
|
+
{
|
|
174
|
+
"journey": "G",
|
|
175
|
+
"synthetic": True,
|
|
176
|
+
"scenario": scenario,
|
|
177
|
+
"snapshot_id": SNAPSHOT_ID,
|
|
178
|
+
"manifest_requests": len(client.requests),
|
|
179
|
+
"download_count": client.downloads,
|
|
180
|
+
"provenance_preserved": provenance_preserved,
|
|
181
|
+
"execution_state": execution.state,
|
|
182
|
+
"output_available": execution.output is not None,
|
|
183
|
+
"integrity": (
|
|
184
|
+
"passed"
|
|
185
|
+
if execution.state == "completed"
|
|
186
|
+
else "rejected"
|
|
187
|
+
if execution.problem is not None
|
|
188
|
+
and execution.problem.code == "artifact_integrity_error"
|
|
189
|
+
else "not_evaluable"
|
|
190
|
+
),
|
|
191
|
+
**record,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
raise SystemExit(cli(run, __doc__ or "G", SCENARIOS))
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""H: portable verified evidence, minimized report and a fail-closed CI gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import pyarrow as pa
|
|
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
|
+
SCENARIOS = ("good", "different", "not_evaluable", "error")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def capture_expected(table: pa.Table, directory: Path) -> ef.ArtifactRef:
|
|
23
|
+
import pyarrow as pa
|
|
24
|
+
|
|
25
|
+
path = directory / "literal-expected.arrow"
|
|
26
|
+
with pa.OSFile(str(path), "wb") as sink, pa.ipc.new_file(sink, table.schema) as writer:
|
|
27
|
+
writer.write_table(table)
|
|
28
|
+
return ef.LocalArtifactStore(directory / "expected-store").put(
|
|
29
|
+
path, media_type="application/vnd.apache.arrow.file"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(output: Path, scenario: str = "good") -> dict[str, object]:
|
|
34
|
+
import pyarrow as pa
|
|
35
|
+
|
|
36
|
+
private = new_output(output, scenario, SCENARIOS)
|
|
37
|
+
original = private / "original"
|
|
38
|
+
original.mkdir()
|
|
39
|
+
# Trusted synthetic SQL executes once in an owned/closed in-memory connection.
|
|
40
|
+
captured = ef.capture_duckdb(
|
|
41
|
+
"SELECT id, value FROM (VALUES ('SECRET_SYNTH_A', 10::BIGINT), "
|
|
42
|
+
"('SECRET_SYNTH_B', $amount::BIGINT)) AS t(id, value)",
|
|
43
|
+
parameters={"amount": 21 if scenario == "different" else 20},
|
|
44
|
+
work_dir=original / "capture",
|
|
45
|
+
)
|
|
46
|
+
expected = capture_expected(
|
|
47
|
+
pa.table({"id": ["SECRET_SYNTH_A", "SECRET_SYNTH_B"], "value": [10, 20]}), original
|
|
48
|
+
)
|
|
49
|
+
contract = ef.keyed_contract(
|
|
50
|
+
(
|
|
51
|
+
column("id", ef.StringType()),
|
|
52
|
+
column("value", ef.IntegerType(bits=64, signed=True), unit="items"),
|
|
53
|
+
),
|
|
54
|
+
keys=["id"],
|
|
55
|
+
contract_id="journey.H.portable",
|
|
56
|
+
).spec.contract
|
|
57
|
+
verified = ef.verify(
|
|
58
|
+
{"candidate": captured.data},
|
|
59
|
+
contract=contract,
|
|
60
|
+
expected=expected,
|
|
61
|
+
require_expected=True,
|
|
62
|
+
options=ef.ComparisonOptions(max_rows=1 if scenario == "not_evaluable" else 100),
|
|
63
|
+
work_dir=original / "verify",
|
|
64
|
+
)
|
|
65
|
+
portable = private / "portable"
|
|
66
|
+
bundle = ef.export_bundle(original / "verify" / verified.verification_id, portable)
|
|
67
|
+
manifest_sha256 = hashlib.sha256((portable / "bundle.json").read_bytes()).hexdigest()
|
|
68
|
+
# The old absolute artifact paths cease to exist before import or inspection.
|
|
69
|
+
original.rename(private / "retired-original")
|
|
70
|
+
imported = private / "imported"
|
|
71
|
+
imported_bundle = ef.import_bundle(portable, imported, expected_sha256=manifest_sha256)
|
|
72
|
+
if imported_bundle != bundle:
|
|
73
|
+
raise RuntimeError("Portable inventory changed during import")
|
|
74
|
+
if scenario == "error":
|
|
75
|
+
(imported / bundle.root_document).write_text("{}", encoding="utf-8")
|
|
76
|
+
try:
|
|
77
|
+
ef.read_bundle(imported, expected_sha256=manifest_sha256)
|
|
78
|
+
except ef.ArtifactIntegrityError:
|
|
79
|
+
return write_summary(
|
|
80
|
+
output,
|
|
81
|
+
{
|
|
82
|
+
"journey": "H",
|
|
83
|
+
"synthetic": True,
|
|
84
|
+
"scenario": scenario,
|
|
85
|
+
"accepted": False,
|
|
86
|
+
"exit_code": 2,
|
|
87
|
+
"integrity": "rejected",
|
|
88
|
+
"report_created": False,
|
|
89
|
+
"original_paths_available": original.exists(),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
raise RuntimeError("Synthetic corrupted bundle was unexpectedly accepted")
|
|
93
|
+
inspected = ef.inspect_bundle(imported, expected_sha256=manifest_sha256)
|
|
94
|
+
# read/inspect already verified the closed inventory and typed linked records.
|
|
95
|
+
# Reopen only its validated root to render the imported comparison models.
|
|
96
|
+
reread = ef.VerificationResult.model_validate_json(
|
|
97
|
+
(imported / bundle.root_document).read_bytes()
|
|
98
|
+
)
|
|
99
|
+
comparisons = [
|
|
100
|
+
comparison_record(comparison, output, f"comparison-{index}")
|
|
101
|
+
for index, comparison in enumerate(reread.comparisons)
|
|
102
|
+
]
|
|
103
|
+
accepted = (
|
|
104
|
+
reread.publishable
|
|
105
|
+
and bool(comparisons)
|
|
106
|
+
and all(record["accepted"] for record in comparisons)
|
|
107
|
+
)
|
|
108
|
+
exit_code = 0 if accepted else 1 if reread.operational_state == "failed" else 3
|
|
109
|
+
return write_summary(
|
|
110
|
+
output,
|
|
111
|
+
{
|
|
112
|
+
"journey": "H",
|
|
113
|
+
"synthetic": True,
|
|
114
|
+
"scenario": scenario,
|
|
115
|
+
"accepted": accepted,
|
|
116
|
+
"exit_code": exit_code,
|
|
117
|
+
"integrity": inspected["integrity"],
|
|
118
|
+
"operational_state": reread.operational_state,
|
|
119
|
+
"bundle_manifest_sha256": manifest_sha256,
|
|
120
|
+
"original_paths_available": original.exists(),
|
|
121
|
+
"report_created": True,
|
|
122
|
+
"independent_review": reread.independent_review.state,
|
|
123
|
+
"comparisons": comparisons,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
raise SystemExit(cli(run, __doc__ or "H", SCENARIOS))
|