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,247 @@
|
|
|
1
|
+
"""Versioned orchestration contracts; technical completion is distinct from alerts."""
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, model_validator
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from equiframe._base import FrozenModel, Identifier, NonNegativeInt, Text, _ExplicitOutcome
|
|
10
|
+
from equiframe.comparison_types import ComparisonOptions, ComparisonPolicy, ComparisonResult
|
|
11
|
+
from equiframe.decision import combine_decisions
|
|
12
|
+
from equiframe.detector_types import ComponentDecision, DetectionResult, HybridDecision
|
|
13
|
+
from equiframe.execution_types import ExecutionPlan, ExecutionResult
|
|
14
|
+
from equiframe.profile_types import ProfileOptions, ProfileResult, ProfileSpec, profile_digest
|
|
15
|
+
from equiframe.rule_types import RuleEvaluation, RuleOptions
|
|
16
|
+
from equiframe.types import ArtifactRef, Sha256
|
|
17
|
+
|
|
18
|
+
StageName = Literal["execution", "rules", "profile", "detection", "comparison", "hybrid"]
|
|
19
|
+
StageState = Literal["completed", "not_evaluable", "failed", "skipped"]
|
|
20
|
+
STAGES: tuple[StageName, ...] = (
|
|
21
|
+
"execution",
|
|
22
|
+
"rules",
|
|
23
|
+
"profile",
|
|
24
|
+
"detection",
|
|
25
|
+
"comparison",
|
|
26
|
+
"hybrid",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PipelineSpec(FrozenModel):
|
|
31
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
32
|
+
execution: ExecutionPlan
|
|
33
|
+
profile: ProfileSpec | None = None
|
|
34
|
+
rule_options: RuleOptions = RuleOptions()
|
|
35
|
+
profile_options: ProfileOptions = ProfileOptions()
|
|
36
|
+
comparison_policy: ComparisonPolicy = ComparisonPolicy()
|
|
37
|
+
comparison_options: ComparisonOptions = ComparisonOptions()
|
|
38
|
+
comparison_reference: Literal["peer", "expected"] = "peer"
|
|
39
|
+
|
|
40
|
+
@model_validator(mode="after")
|
|
41
|
+
def consistent(self) -> Self:
|
|
42
|
+
if self.profile and self.profile.family != self.execution.contract.family:
|
|
43
|
+
raise ValueError("profile and execution families must agree")
|
|
44
|
+
# Authentication belongs in the injected provider, never in persisted ETL parameters.
|
|
45
|
+
forbidden = {"password", "passwd", "secret", "token", "authorization", "credentials"}
|
|
46
|
+
for parameter in self.execution.parameters:
|
|
47
|
+
name = parameter.name.lower().replace("-", "_").replace(".", "_")
|
|
48
|
+
if forbidden.intersection(name.split("_")) or name in {"api_key", "access_key"}:
|
|
49
|
+
raise ValueError("credential parameters must be supplied outside the pipeline plan")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PipelineInput(FrozenModel):
|
|
54
|
+
name: Identifier
|
|
55
|
+
artifact: ArtifactRef
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PipelineRequest(FrozenModel):
|
|
59
|
+
"""CLI data/configuration only. Executable registrations are supplied separately."""
|
|
60
|
+
|
|
61
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
62
|
+
spec: PipelineSpec
|
|
63
|
+
inputs: tuple[PipelineInput, ...]
|
|
64
|
+
comparison: ArtifactRef | None = None
|
|
65
|
+
|
|
66
|
+
@model_validator(mode="after")
|
|
67
|
+
def consistent(self) -> Self:
|
|
68
|
+
names = tuple(item.name for item in self.inputs)
|
|
69
|
+
if len(set(names)) != len(names) or set(names) != {
|
|
70
|
+
item.name for item in self.spec.execution.contract.inputs
|
|
71
|
+
}:
|
|
72
|
+
raise ValueError("input bindings must cover exactly the declared tables")
|
|
73
|
+
if any(item.artifact.format not in ("arrow", "parquet") for item in self.inputs):
|
|
74
|
+
raise ValueError("pipeline inputs require Arrow or Parquet")
|
|
75
|
+
if self.comparison and self.comparison.format not in ("arrow", "parquet"):
|
|
76
|
+
raise ValueError("comparison requires Arrow or Parquet")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StageRecord(FrozenModel):
|
|
81
|
+
stage: StageName
|
|
82
|
+
state: StageState
|
|
83
|
+
elapsed_seconds: Annotated[float, Field(ge=0)]
|
|
84
|
+
code: Identifier
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class EvidenceFile(FrozenModel):
|
|
88
|
+
path: Text
|
|
89
|
+
sha256: Sha256
|
|
90
|
+
size_bytes: NonNegativeInt
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def local_relative(self) -> Self:
|
|
94
|
+
path = PurePosixPath(self.path)
|
|
95
|
+
if (
|
|
96
|
+
path.is_absolute()
|
|
97
|
+
or ".." in path.parts
|
|
98
|
+
or ":" in self.path
|
|
99
|
+
or "\\" in self.path
|
|
100
|
+
or path.as_posix() != self.path
|
|
101
|
+
or self.path == "."
|
|
102
|
+
):
|
|
103
|
+
raise ValueError("evidence path must be a normalized relative file path")
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PipelineResult(_ExplicitOutcome, FrozenModel):
|
|
108
|
+
_truth_value_guidance = (
|
|
109
|
+
"inspect .complete, .exit_code and the requested component decisions explicitly"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
113
|
+
pipeline_id: Identifier
|
|
114
|
+
spec: PipelineSpec
|
|
115
|
+
spec_sha256: Sha256
|
|
116
|
+
started_at: Text
|
|
117
|
+
finished_at: Text
|
|
118
|
+
elapsed_seconds: Annotated[float, Field(ge=0)]
|
|
119
|
+
detection_requested: bool
|
|
120
|
+
comparison_requested: bool
|
|
121
|
+
cancelled: bool
|
|
122
|
+
stages: tuple[StageRecord, ...]
|
|
123
|
+
execution: ExecutionResult | None
|
|
124
|
+
rules: RuleEvaluation | None
|
|
125
|
+
profile: ProfileResult | None
|
|
126
|
+
detection: DetectionResult | None
|
|
127
|
+
comparison: ComparisonResult | None
|
|
128
|
+
hybrid: HybridDecision
|
|
129
|
+
evidence: tuple[EvidenceFile, ...]
|
|
130
|
+
event_count: Annotated[int, Field(ge=2)]
|
|
131
|
+
event_head_sha256: Sha256
|
|
132
|
+
|
|
133
|
+
@model_validator(mode="after")
|
|
134
|
+
def consistent(self) -> Self:
|
|
135
|
+
if self.spec_sha256 != profile_digest(self.spec.model_dump(mode="json")):
|
|
136
|
+
raise ValueError("pipeline specification fingerprint mismatch")
|
|
137
|
+
if tuple(stage.stage for stage in self.stages) != STAGES:
|
|
138
|
+
raise ValueError("pipeline must record each stage exactly once in order")
|
|
139
|
+
if len({item.path for item in self.evidence}) != len(self.evidence):
|
|
140
|
+
raise ValueError("duplicate evidence paths")
|
|
141
|
+
if self.execution is not None and self.execution.plan != self.spec.execution:
|
|
142
|
+
raise ValueError("execution does not belong to this pipeline plan")
|
|
143
|
+
if self.rules is not None:
|
|
144
|
+
if self.execution is None or (
|
|
145
|
+
self.rules.execution_id != self.execution.execution_id
|
|
146
|
+
or self.rules.contract_sha256 != self.execution.contract_sha256
|
|
147
|
+
or self.rules.run_id != self.execution.run_id
|
|
148
|
+
or self.rules.options != self.spec.rule_options
|
|
149
|
+
):
|
|
150
|
+
raise ValueError("rules do not belong to this execution")
|
|
151
|
+
if self.hybrid.r != combine_decisions(self.rules, self.hybrid.m).r:
|
|
152
|
+
raise ValueError("hybrid must preserve the rule decision")
|
|
153
|
+
elif self.hybrid.r.decision != "not_evaluable":
|
|
154
|
+
raise ValueError("missing rules cannot produce a rule decision")
|
|
155
|
+
if self.profile is not None and (
|
|
156
|
+
self.execution is None
|
|
157
|
+
or self.profile.vector.spec != self.spec.profile
|
|
158
|
+
or self.profile.metadata.execution_id != self.execution.execution_id
|
|
159
|
+
or self.profile.metadata.run_id != self.execution.run_id
|
|
160
|
+
or self.profile.metadata.contract_sha256 != self.execution.contract_sha256
|
|
161
|
+
or self.profile.metadata.artifacts
|
|
162
|
+
!= tuple(
|
|
163
|
+
reference
|
|
164
|
+
for table in (
|
|
165
|
+
*self.execution.inputs,
|
|
166
|
+
*((self.execution.output,) if self.execution.output else ()),
|
|
167
|
+
)
|
|
168
|
+
for reference in (table.data, *((table.index,) if table.index else ()))
|
|
169
|
+
)
|
|
170
|
+
or self.profile.metadata.execution_sha256
|
|
171
|
+
!= profile_digest(self.execution.model_dump(mode="json"))
|
|
172
|
+
or self.profile.options != self.spec.profile_options
|
|
173
|
+
):
|
|
174
|
+
raise ValueError("profile does not belong to the declared execution/features")
|
|
175
|
+
if self.detection is not None:
|
|
176
|
+
if not self.detection_requested or self.profile is None or not self.profile.complete:
|
|
177
|
+
raise ValueError("detection requires a requested, complete profile")
|
|
178
|
+
if len(self.detection.m) != 1 or self.hybrid.m != self.detection.m[0].result:
|
|
179
|
+
raise ValueError("detection and hybrid must describe one identical profile")
|
|
180
|
+
elif self.hybrid.m.decision != "not_evaluable":
|
|
181
|
+
raise ValueError("missing detector cannot produce an M decision")
|
|
182
|
+
if self.comparison is not None and not self.comparison_requested:
|
|
183
|
+
raise ValueError("unexpected comparison")
|
|
184
|
+
if self.comparison is not None and (
|
|
185
|
+
self.comparison.spec.contract != self.spec.execution.contract
|
|
186
|
+
or self.comparison.spec.policy != self.spec.comparison_policy
|
|
187
|
+
or self.comparison.options != self.spec.comparison_options
|
|
188
|
+
or self.comparison.reference != self.spec.comparison_reference
|
|
189
|
+
or (
|
|
190
|
+
self.comparison.left is not None
|
|
191
|
+
and self.comparison.left.execution != self.execution
|
|
192
|
+
)
|
|
193
|
+
):
|
|
194
|
+
raise ValueError("comparison does not belong to the configured execution/policy")
|
|
195
|
+
values = (self.execution, self.rules, self.profile, self.detection, self.comparison)
|
|
196
|
+
for stage, value in zip(self.stages[:5], values, strict=True):
|
|
197
|
+
if stage.state == "completed" and value is None:
|
|
198
|
+
raise ValueError("completed stage requires its result")
|
|
199
|
+
if stage.state in ("failed", "skipped") and value is not None:
|
|
200
|
+
raise ValueError("failed/skipped stage cannot claim a result")
|
|
201
|
+
if self.execution is not None and (self.stages[0].state == "completed") != (
|
|
202
|
+
self.execution.state == "completed"
|
|
203
|
+
):
|
|
204
|
+
raise ValueError("execution stage contradicts its result")
|
|
205
|
+
for stage, value in ((self.stages[1], self.rules), (self.stages[2], self.profile)):
|
|
206
|
+
if value is not None and (stage.state == "completed") != value.complete:
|
|
207
|
+
raise ValueError("stage completeness contradicts its result")
|
|
208
|
+
if self.detection is not None and (self.stages[3].state == "completed") != all(
|
|
209
|
+
d.result.complete for d in (*self.detection.m, *self.detection.b)
|
|
210
|
+
):
|
|
211
|
+
raise ValueError("detector stage completeness contradicts its result")
|
|
212
|
+
if (
|
|
213
|
+
self.comparison is not None
|
|
214
|
+
and (self.stages[4].state == "completed") != self.comparison.complete
|
|
215
|
+
):
|
|
216
|
+
raise ValueError("comparison stage completeness contradicts its result")
|
|
217
|
+
if (self.stages[5].state == "completed") != self.hybrid.result.complete:
|
|
218
|
+
raise ValueError("hybrid stage completeness contradicts its result")
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def complete(self) -> bool:
|
|
223
|
+
required: set[StageName] = {"execution", "rules"}
|
|
224
|
+
if self.spec.profile:
|
|
225
|
+
required.add("profile")
|
|
226
|
+
if self.detection_requested:
|
|
227
|
+
required.update(("detection", "hybrid"))
|
|
228
|
+
if self.comparison_requested:
|
|
229
|
+
required.add("comparison")
|
|
230
|
+
return not self.cancelled and all(
|
|
231
|
+
stage.state == "completed" for stage in self.stages if stage.stage in required
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def exit_code(self) -> int:
|
|
236
|
+
if self.execution is not None and self.execution.state == "invalid_input":
|
|
237
|
+
return 2
|
|
238
|
+
if not self.complete:
|
|
239
|
+
return 3
|
|
240
|
+
alert = self.hybrid.r.decision == "alert" or self.hybrid.result.decision == "alert"
|
|
241
|
+
if self.detection is not None:
|
|
242
|
+
alert |= any(d.result.decision == "alert" for d in self.detection.b)
|
|
243
|
+
return int(alert or (self.comparison is not None and self.comparison.state == "different"))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def unavailable(reason: str) -> ComponentDecision:
|
|
247
|
+
return ComponentDecision(decision="not_evaluable", complete=False, reason=reason)
|
equiframe/ports.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Complete structural integration port; implementation belongs to later steps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Protocol
|
|
8
|
+
|
|
9
|
+
from equiframe.types import ArtifactRef
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from equiframe.execution import RunContext
|
|
13
|
+
from equiframe.execution_types import EngineCapabilities
|
|
14
|
+
from equiframe.registry import Transformation
|
|
15
|
+
from equiframe.tables import LoadedTable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArtifactStore(Protocol):
|
|
19
|
+
def put(self, path: Path, *, media_type: str) -> ArtifactRef: ...
|
|
20
|
+
|
|
21
|
+
def get(self, ref: ArtifactRef, destination: Path) -> Path: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DataSource(Protocol):
|
|
25
|
+
@property
|
|
26
|
+
def table_names(self) -> tuple[str, ...]: ...
|
|
27
|
+
|
|
28
|
+
def resolve(self, name: str, *, context: RunContext) -> LoadedTable: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EngineAdapter(Protocol):
|
|
32
|
+
@property
|
|
33
|
+
def engine(self) -> str: ...
|
|
34
|
+
|
|
35
|
+
def capabilities(self) -> EngineCapabilities: ...
|
|
36
|
+
|
|
37
|
+
def execute(
|
|
38
|
+
self, transformation: Transformation, inputs: Mapping[str, LoadedTable], context: RunContext
|
|
39
|
+
) -> LoadedTable: ...
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Fit once on explicitly selected normal training profiles; one matrix for M and B."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from statistics import median
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from equiframe._document_io import read_document as _read_document
|
|
8
|
+
from equiframe._document_io import write_document as _write_document
|
|
9
|
+
from equiframe.profile_types import (
|
|
10
|
+
FeatureFit,
|
|
11
|
+
FeatureMatrix,
|
|
12
|
+
NormalTrainingProfiles,
|
|
13
|
+
PreprocessorBundle,
|
|
14
|
+
ProfileSpec,
|
|
15
|
+
ProfileVector,
|
|
16
|
+
profile_digest,
|
|
17
|
+
profile_spec_fingerprint,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compatible(profile: ProfileVector, spec: ProfileSpec) -> ProfileVector:
|
|
22
|
+
profile = ProfileVector.model_validate(profile)
|
|
23
|
+
if profile.spec_sha256 != profile_spec_fingerprint(spec):
|
|
24
|
+
raise ValueError("incompatible family, feature version, definitions or order")
|
|
25
|
+
if any(o.state == "not_evaluable" for o in profile.observations):
|
|
26
|
+
raise ValueError("failed measurements cannot be imputed as undefined values")
|
|
27
|
+
return profile
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fit_preprocessor(
|
|
31
|
+
training: NormalTrainingProfiles,
|
|
32
|
+
*,
|
|
33
|
+
spec: ProfileSpec,
|
|
34
|
+
empty_policy: Literal["drop", "not_estimable"] = "drop",
|
|
35
|
+
) -> PreprocessorBundle:
|
|
36
|
+
"""The caller must supply normal TRAINING selection, backed by its external manifest."""
|
|
37
|
+
training = NormalTrainingProfiles.model_validate(training)
|
|
38
|
+
spec = ProfileSpec.model_validate(spec)
|
|
39
|
+
profiles = tuple(_compatible(p, spec) for p in training.profiles)
|
|
40
|
+
fits: list[FeatureFit] = []
|
|
41
|
+
for index, feature in enumerate(spec.features):
|
|
42
|
+
observations = [p.observations[index] for p in profiles]
|
|
43
|
+
valid = [o.value for o in observations if o.value is not None]
|
|
44
|
+
reason: Literal["no_valid_training_values", "not_applicable"] | None = None
|
|
45
|
+
if not valid:
|
|
46
|
+
reason = (
|
|
47
|
+
"not_applicable"
|
|
48
|
+
if observations and all(o.state == "not_applicable" for o in observations)
|
|
49
|
+
else "no_valid_training_values"
|
|
50
|
+
)
|
|
51
|
+
fits.append(
|
|
52
|
+
FeatureFit(
|
|
53
|
+
name=feature.name,
|
|
54
|
+
valid_count=len(valid),
|
|
55
|
+
median=float(median(valid)) if valid else None,
|
|
56
|
+
reason=reason,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
ready = any(f.median is not None for f in fits) and not (
|
|
60
|
+
empty_policy == "not_estimable"
|
|
61
|
+
and any(f.reason == "no_valid_training_values" for f in fits)
|
|
62
|
+
)
|
|
63
|
+
return PreprocessorBundle(
|
|
64
|
+
spec=spec,
|
|
65
|
+
spec_sha256=profile_spec_fingerprint(spec),
|
|
66
|
+
selection_manifest_sha256=training.selection_manifest_sha256,
|
|
67
|
+
training_sha256=profile_digest([p.model_dump(mode="json") for p in profiles]),
|
|
68
|
+
training_rows=len(profiles),
|
|
69
|
+
empty_policy=empty_policy,
|
|
70
|
+
features=tuple(fits),
|
|
71
|
+
state="ready" if ready else "not_estimable",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def transform_profiles(
|
|
76
|
+
bundle: PreprocessorBundle, profiles: tuple[ProfileVector, ...]
|
|
77
|
+
) -> FeatureMatrix:
|
|
78
|
+
"""No fit, labels, partitions or engine metadata enter this numeric matrix."""
|
|
79
|
+
bundle = PreprocessorBundle.model_validate(bundle)
|
|
80
|
+
if bundle.state != "ready":
|
|
81
|
+
raise ValueError("preprocessor is not estimable")
|
|
82
|
+
rows: list[tuple[float, ...]] = []
|
|
83
|
+
for profile in profiles:
|
|
84
|
+
profile = _compatible(profile, bundle.spec)
|
|
85
|
+
row: list[float] = []
|
|
86
|
+
for fitted, observed in zip(bundle.features, profile.observations, strict=True):
|
|
87
|
+
if fitted.median is None:
|
|
88
|
+
continue
|
|
89
|
+
row.extend(
|
|
90
|
+
(
|
|
91
|
+
fitted.median if observed.value is None else observed.value,
|
|
92
|
+
float(observed.state == "undefined"),
|
|
93
|
+
float(observed.state == "not_applicable"),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
rows.append(tuple(row))
|
|
97
|
+
return FeatureMatrix(
|
|
98
|
+
preprocessor_sha256=profile_digest(bundle.model_dump(mode="json")),
|
|
99
|
+
columns=bundle.columns,
|
|
100
|
+
values=tuple(rows),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def write_preprocessor(bundle: PreprocessorBundle, path: Path) -> None:
|
|
105
|
+
bundle = PreprocessorBundle.model_validate(bundle)
|
|
106
|
+
_write_document(path, bundle.model_dump_json().encode("utf-8"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_preprocessor(path: Path) -> PreprocessorBundle:
|
|
110
|
+
return PreprocessorBundle.model_validate_json(_read_document(path))
|