equiframe 0.12.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. equiframe/__init__.py +832 -0
  2. equiframe/__main__.py +3 -0
  3. equiframe/_base.py +84 -0
  4. equiframe/_cli_support.py +125 -0
  5. equiframe/_comparison_io.py +161 -0
  6. equiframe/_dataset_provenance.py +146 -0
  7. equiframe/_decimal.py +22 -0
  8. equiframe/_diff_capture.py +433 -0
  9. equiframe/_doctor.py +123 -0
  10. equiframe/_document_io.py +61 -0
  11. equiframe/_documents.py +79 -0
  12. equiframe/_duckdb_transport.py +149 -0
  13. equiframe/_error_reporting.py +52 -0
  14. equiframe/_first_use.py +102 -0
  15. equiframe/_path_compat.py +26 -0
  16. equiframe/_physical.py +133 -0
  17. equiframe/_r_review.R +308 -0
  18. equiframe/_regular_io.py +167 -0
  19. equiframe/_report_format_v1.py +298 -0
  20. equiframe/_report_io.py +591 -0
  21. equiframe/_stability_inventory.py +1421 -0
  22. equiframe/_timezones.py +257 -0
  23. equiframe/_vendor/cyclonedx/LICENSE +201 -0
  24. equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
  25. equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
  26. equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
  27. equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
  28. equiframe/adapters.py +454 -0
  29. equiframe/assertion_types.py +122 -0
  30. equiframe/assertions.py +271 -0
  31. equiframe/benchmark.py +101 -0
  32. equiframe/bundle.py +240 -0
  33. equiframe/cli.py +693 -0
  34. equiframe/comparison.py +937 -0
  35. equiframe/comparison_report.py +660 -0
  36. equiframe/comparison_types.py +257 -0
  37. equiframe/compatibility.py +546 -0
  38. equiframe/contracts.py +879 -0
  39. equiframe/datasets.py +1301 -0
  40. equiframe/decision.py +34 -0
  41. equiframe/destinations.py +295 -0
  42. equiframe/detector_types.py +431 -0
  43. equiframe/detectors.py +389 -0
  44. equiframe/diff.py +192 -0
  45. equiframe/diff_contracts.py +523 -0
  46. equiframe/docs/adoption.md +329 -0
  47. equiframe/docs/api.md +534 -0
  48. equiframe/docs/arquitecto.md +143 -0
  49. equiframe/docs/choosing-api.md +84 -0
  50. equiframe/docs/cientifico.md +180 -0
  51. equiframe/docs/compatibility.md +254 -0
  52. equiframe/docs/contributing.md +171 -0
  53. equiframe/docs/dependencies.md +237 -0
  54. equiframe/docs/eda.md +166 -0
  55. equiframe/docs/extending.md +158 -0
  56. equiframe/docs/fast-ci.md +52 -0
  57. equiframe/docs/first-use.md +78 -0
  58. equiframe/docs/index.md +142 -0
  59. equiframe/docs/ingestion.md +145 -0
  60. equiframe/docs/inicio.md +129 -0
  61. equiframe/docs/installed-private.md +97 -0
  62. equiframe/docs/journeys.md +362 -0
  63. equiframe/docs/lectura-conversion.md +211 -0
  64. equiframe/docs/licensing.md +17 -0
  65. equiframe/docs/maintenance.md +233 -0
  66. equiframe/docs/migration-ir.md +333 -0
  67. equiframe/docs/option-audit.md +45 -0
  68. equiframe/docs/outcome-gates.md +134 -0
  69. equiframe/docs/performance.md +263 -0
  70. equiframe/docs/piloto-simulado.md +56 -0
  71. equiframe/docs/quickstart.md +156 -0
  72. equiframe/docs/r_review.md +212 -0
  73. equiframe/docs/recipes.md +248 -0
  74. equiframe/docs/releasing.md +183 -0
  75. equiframe/docs/security.md +218 -0
  76. equiframe/docs/semantics.md +274 -0
  77. equiframe/docs/sqlite-snapshots.md +262 -0
  78. equiframe/docs/universal-sources.md +49 -0
  79. equiframe/docs/universalidad-cinco-pasos.md +107 -0
  80. equiframe/docs/upgrading.md +538 -0
  81. equiframe/duckdb_capture.py +410 -0
  82. equiframe/eda/__init__.py +36 -0
  83. equiframe/eda/__main__.py +3 -0
  84. equiframe/eda/_backend.py +612 -0
  85. equiframe/eda/_inventory.py +49 -0
  86. equiframe/eda/_paths.py +49 -0
  87. equiframe/eda/_report.py +291 -0
  88. equiframe/eda/api.py +535 -0
  89. equiframe/eda/cli.py +102 -0
  90. equiframe/eda/models.py +239 -0
  91. equiframe/errors.py +83 -0
  92. equiframe/evidence.py +375 -0
  93. equiframe/examples/integrations/eda_generic.py +60 -0
  94. equiframe/examples/integrations/outcome_gates.py +94 -0
  95. equiframe/examples/integrations/sqlite_snapshot.py +246 -0
  96. equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
  97. equiframe/examples/integrations/universal_first_use.py +296 -0
  98. equiframe/examples/journeys/README.md +31 -0
  99. equiframe/examples/journeys/__init__.py +1 -0
  100. equiframe/examples/journeys/a_pandas_refactor.py +88 -0
  101. equiframe/examples/journeys/b_native_migration.py +181 -0
  102. equiframe/examples/journeys/c_retail_etl.py +460 -0
  103. equiframe/examples/journeys/common.py +136 -0
  104. equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
  105. equiframe/examples/journeys/e_telemetry.py +135 -0
  106. equiframe/examples/journeys/f_data_science_preparation.py +84 -0
  107. equiframe/examples/journeys/g_external_snapshots.py +197 -0
  108. equiframe/examples/journeys/h_portable_ci.py +129 -0
  109. equiframe/examples/journeys/requirements.lock +249 -0
  110. equiframe/examples/journeys/verify_journeys.py +117 -0
  111. equiframe/examples/load_convert_compare.py +85 -0
  112. equiframe/execution.py +355 -0
  113. equiframe/execution_types.py +177 -0
  114. equiframe/ingestion.py +744 -0
  115. equiframe/local.py +126 -0
  116. equiframe/migration/__init__.py +94 -0
  117. equiframe/migration/_physical.py +8 -0
  118. equiframe/migration/backends/_timestamp_transport.py +109 -0
  119. equiframe/migration/backends/duckdb.py +651 -0
  120. equiframe/migration/backends/duckdb_decimal.py +125 -0
  121. equiframe/migration/backends/duckdb_time.py +87 -0
  122. equiframe/migration/backends/pandas.py +544 -0
  123. equiframe/migration/backends/polars.py +915 -0
  124. equiframe/migration/backends/polars_decimal.py +231 -0
  125. equiframe/migration/io.py +198 -0
  126. equiframe/migration/ports.py +96 -0
  127. equiframe/migration/runner.py +545 -0
  128. equiframe/migration/runner_types.py +405 -0
  129. equiframe/migration/types.py +354 -0
  130. equiframe/migration/validation.py +619 -0
  131. equiframe/normalization.py +358 -0
  132. equiframe/pipeline.py +274 -0
  133. equiframe/pipeline_types.py +247 -0
  134. equiframe/ports.py +39 -0
  135. equiframe/preprocessing.py +110 -0
  136. equiframe/profile_types.py +343 -0
  137. equiframe/profiles.py +447 -0
  138. equiframe/py.typed +0 -0
  139. equiframe/r_review.py +706 -0
  140. equiframe/reduction.py +518 -0
  141. equiframe/registry.py +121 -0
  142. equiframe/report_rendering.py +366 -0
  143. equiframe/report_types.py +294 -0
  144. equiframe/reporting.py +26 -0
  145. equiframe/rule_types.py +178 -0
  146. equiframe/rules.py +819 -0
  147. equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
  148. equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
  149. equiframe/schemas/r-review-options-1.0.schema.json +38 -0
  150. equiframe/schemas/r-review-result-1.0.schema.json +207 -0
  151. equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
  152. equiframe/serialization.py +152 -0
  153. equiframe/snapshot.py +296 -0
  154. equiframe/sources.py +283 -0
  155. equiframe/tables.py +459 -0
  156. equiframe/types.py +71 -0
  157. equiframe/values.py +252 -0
  158. equiframe/verification_request.py +35 -0
  159. equiframe/verify.py +799 -0
  160. equiframe/verify_types.py +255 -0
  161. equiframe/workflow.py +510 -0
  162. equiframe/workflow_request.py +38 -0
  163. equiframe-0.12.4.dist-info/METADATA +268 -0
  164. equiframe-0.12.4.dist-info/RECORD +170 -0
  165. equiframe-0.12.4.dist-info/WHEEL +4 -0
  166. equiframe-0.12.4.dist-info/entry_points.txt +3 -0
  167. equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
  168. equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
  169. equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
  170. equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
@@ -0,0 +1,343 @@
1
+ """Versioned, closed profile formulas and numeric-only preprocessing formats."""
2
+
3
+ import hashlib
4
+ import json
5
+ import math
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 (
12
+ FrozenModel,
13
+ Identifier,
14
+ NonNegativeInt,
15
+ PositiveInt,
16
+ Text,
17
+ _ExplicitOutcome,
18
+ unique,
19
+ )
20
+ from equiframe.contracts import ColumnSpec, Family
21
+ from equiframe.types import ArtifactRef, Sha256
22
+
23
+ ProfileMetric = Literal[
24
+ "log_filas",
25
+ "razon_salida_entrada",
26
+ "tasa_nulos",
27
+ "tasa_nan",
28
+ "prop_duplicados",
29
+ "cardinalidad_rel",
30
+ "expansion_union",
31
+ "claves_sin_pareja",
32
+ "conversiones_fallidas",
33
+ "fechas_invalidas",
34
+ ]
35
+ FeatureState = Literal["available", "undefined", "not_applicable", "not_evaluable"]
36
+
37
+
38
+ def profile_digest(value: object) -> str:
39
+ return hashlib.sha256(
40
+ json.dumps(
41
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
42
+ ).encode("ascii")
43
+ ).hexdigest()
44
+
45
+
46
+ class ProfileFeature(FrozenModel):
47
+ """A semantic slot, never an arbitrary numeric value or metadata expression."""
48
+
49
+ name: Identifier
50
+ metric: ProfileMetric
51
+ table: Identifier
52
+ columns: tuple[Text, ...] = ()
53
+ denominator_table: Identifier | None = None
54
+ right_table: Identifier | None = None
55
+ right_columns: tuple[Text, ...] = ()
56
+ null_matches: Literal["never"] | None = None
57
+ target: ColumnSpec | None = None
58
+
59
+ @model_validator(mode="after")
60
+ def consistent(self) -> Self:
61
+ unique(self.columns, "columns")
62
+ unique(self.right_columns, "right_columns")
63
+ rows = self.metric in ("log_filas", "razon_salida_entrada", "expansion_union")
64
+ multiple = self.metric in ("prop_duplicados", "claves_sin_pareja")
65
+ if (rows and self.columns) or (not rows and not self.columns):
66
+ raise ValueError("formula has an invalid column selection")
67
+ if not rows and not multiple and len(self.columns) != 1:
68
+ raise ValueError("column rates require exactly one column")
69
+ ratio = self.metric in ("razon_salida_entrada", "expansion_union")
70
+ if ratio != (self.denominator_table is not None):
71
+ raise ValueError("only row ratios require a denominator table")
72
+ join = self.metric == "claves_sin_pareja"
73
+ if join:
74
+ if (
75
+ not self.right_table
76
+ or len(self.columns) != len(self.right_columns)
77
+ or self.null_matches is None
78
+ ):
79
+ raise ValueError("join requires paired columns and an explicit NULL policy")
80
+ elif self.right_table or self.right_columns or self.null_matches is not None:
81
+ raise ValueError("right side and NULL matching are only valid for joins")
82
+ conversion = self.metric in ("conversiones_fallidas", "fechas_invalidas")
83
+ if conversion != (self.target is not None):
84
+ raise ValueError("conversion/date formulas require an explicit target column policy")
85
+ if (
86
+ self.metric == "fechas_invalidas"
87
+ and self.target is not None
88
+ and self.target.logical_type.kind not in ("date", "timestamp")
89
+ ):
90
+ raise ValueError("date validity requires a temporal target")
91
+ return self
92
+
93
+
94
+ class ProfileSpec(FrozenModel):
95
+ schema_version: Literal["1.0"] = "1.0"
96
+ feature_version: Literal["1.0"] = "1.0"
97
+ family: Family
98
+ features: Annotated[tuple[ProfileFeature, ...], Field(min_length=1)]
99
+
100
+ @model_validator(mode="after")
101
+ def consistent(self) -> Self:
102
+ unique(tuple(f.name for f in self.features), "features")
103
+ unique(
104
+ tuple(
105
+ name
106
+ for f in self.features
107
+ for name in (f.name, f.name + ".undefined", f.name + ".not_applicable")
108
+ ),
109
+ "matrix_columns",
110
+ )
111
+ formulas: set[str] = set()
112
+ for feature in self.features:
113
+ formula = feature.model_dump(mode="json", exclude={"name"})
114
+ if feature.metric == "expansion_union":
115
+ formula["metric"] = "razon_salida_entrada"
116
+ fingerprint = profile_digest(formula)
117
+ if fingerprint in formulas:
118
+ raise ValueError("duplicate formula (including expansion/row ratio alias)")
119
+ formulas.add(fingerprint)
120
+ return self
121
+
122
+
123
+ def profile_spec_fingerprint(spec: ProfileSpec) -> str:
124
+ return profile_digest(ProfileSpec.model_validate(spec).model_dump(mode="json"))
125
+
126
+
127
+ class ProfileOptions(FrozenModel):
128
+ max_rows: PositiveInt = 1_000_000
129
+ max_artifact_bytes: PositiveInt = 128 * 1024 * 1024
130
+ max_work: PositiveInt = 10_000_000
131
+ max_distinct: PositiveInt = 1_000_000
132
+ timeout_seconds: Annotated[float, Field(gt=0)] | None = None
133
+
134
+
135
+ class ConversionReceipt(FrozenModel):
136
+ """Instrumented attempts from the same transformation; NULL output is not a receipt."""
137
+
138
+ schema_version: Literal["1.0"] = "1.0"
139
+ feature: Identifier
140
+ source_sha256: Sha256
141
+ source_column: Text
142
+ output_sha256: Sha256
143
+ target: ColumnSpec
144
+ attempted_rows: tuple[NonNegativeInt, ...]
145
+ failed_rows: tuple[NonNegativeInt, ...]
146
+ producer_version: Text
147
+
148
+ @model_validator(mode="after")
149
+ def consistent(self) -> Self:
150
+ if len(set(self.attempted_rows)) != len(self.attempted_rows):
151
+ raise ValueError("conversion attempts must identify unique source rows")
152
+ if len(set(self.failed_rows)) != len(self.failed_rows):
153
+ raise ValueError("conversion failures must identify unique source rows")
154
+ if not set(self.failed_rows) <= set(self.attempted_rows):
155
+ raise ValueError("failed rows must be attempted rows")
156
+ return self
157
+
158
+
159
+ class FeatureSupport(FrozenModel):
160
+ rows: NonNegativeInt = 0
161
+ missing: NonNegativeInt = 0
162
+ nan: NonNegativeInt = 0
163
+ empty_strings: NonNegativeInt = 0
164
+ zeros: NonNegativeInt = 0
165
+ incomplete_keys: NonNegativeInt = 0
166
+
167
+ @model_validator(mode="after")
168
+ def consistent(self) -> Self:
169
+ if self.missing + self.nan + self.empty_strings + self.zeros > self.rows:
170
+ raise ValueError("disjoint scalar support counts exceed rows")
171
+ if self.incomplete_keys > self.rows:
172
+ raise ValueError("incomplete keys exceed rows")
173
+ return self
174
+
175
+
176
+ class FeatureObservation(FrozenModel):
177
+ name: Identifier
178
+ state: FeatureState
179
+ value: Annotated[float, Field(ge=0)] | None = None
180
+ numerator: NonNegativeInt | None = None
181
+ denominator: NonNegativeInt | None = None
182
+ reason: Text | None = None
183
+ support: FeatureSupport = FeatureSupport()
184
+
185
+ @model_validator(mode="after")
186
+ def consistent(self) -> Self:
187
+ if self.state == "available":
188
+ if self.value is None or self.reason is not None or self.numerator is None:
189
+ raise ValueError("available measurement requires value and numerator, no reason")
190
+ if self.denominator is not None and (
191
+ self.denominator == 0 or self.value != self.numerator / self.denominator
192
+ ):
193
+ raise ValueError("value must equal its documented ratio")
194
+ elif self.value is not None or self.reason is None:
195
+ raise ValueError("unavailable measurement requires reason and no value")
196
+ if self.state == "undefined" and (
197
+ self.denominator != 0 or self.numerator is None or self.reason != "empty_denominator"
198
+ ):
199
+ raise ValueError("undefined formula must preserve its empty denominator")
200
+ if self.state in ("not_applicable", "not_evaluable") and (
201
+ self.numerator is not None or self.denominator is not None
202
+ ):
203
+ raise ValueError("no partial counts for unavailable formulas")
204
+ return self
205
+
206
+
207
+ class ProfileVector(FrozenModel):
208
+ """Statistical input: formulas and measurements, no run metadata or labels."""
209
+
210
+ schema_version: Literal["1.0"] = "1.0"
211
+ spec: ProfileSpec
212
+ spec_sha256: Sha256
213
+ observations: tuple[FeatureObservation, ...]
214
+
215
+ @model_validator(mode="after")
216
+ def consistent(self) -> Self:
217
+ if self.spec_sha256 != profile_spec_fingerprint(self.spec):
218
+ raise ValueError("profile feature fingerprint mismatch")
219
+ if tuple(o.name for o in self.observations) != tuple(f.name for f in self.spec.features):
220
+ raise ValueError("profile observations must match the fixed feature order")
221
+ for feature, observation in zip(self.spec.features, self.observations, strict=True):
222
+ if observation.state == "available":
223
+ if feature.metric == "log_filas":
224
+ if observation.denominator is not None or observation.value != math.log1p(
225
+ observation.numerator or 0
226
+ ):
227
+ raise ValueError("invalid log_filas measurement")
228
+ elif observation.denominator is None:
229
+ raise ValueError("rate must retain its denominator")
230
+ elif (
231
+ feature.metric not in ("razon_salida_entrada", "expansion_union")
232
+ and (observation.numerator or 0) > observation.denominator
233
+ ):
234
+ raise ValueError("proportion cannot exceed one")
235
+ return self
236
+
237
+
238
+ class ProfileMetadata(FrozenModel):
239
+ profile_id: Identifier
240
+ run_id: Identifier
241
+ execution_id: Identifier
242
+ contract_sha256: Sha256
243
+ execution_sha256: Sha256
244
+ artifacts: tuple[ArtifactRef, ...]
245
+ conversion_receipts: tuple[ConversionReceipt, ...] = ()
246
+ implementation_version: Literal["equiframe-profiles-1.0"] = "equiframe-profiles-1.0"
247
+
248
+
249
+ class ProfileResult(_ExplicitOutcome, FrozenModel):
250
+ _truth_value_guidance = (
251
+ "inspect .complete and feature availability explicitly; a profile is not a decision"
252
+ )
253
+
254
+ schema_version: Literal["1.0"] = "1.0"
255
+ metadata: ProfileMetadata
256
+ vector: ProfileVector
257
+ options: ProfileOptions
258
+ complete: bool
259
+ elapsed_seconds: Annotated[float, Field(ge=0)]
260
+ work_units: NonNegativeInt
261
+
262
+ @model_validator(mode="after")
263
+ def consistent(self) -> Self:
264
+ if self.complete != all(o.state != "not_evaluable" for o in self.vector.observations):
265
+ raise ValueError("profile completeness contradicts observations")
266
+ return self
267
+
268
+
269
+ class NormalTrainingProfiles(FrozenModel):
270
+ """Caller selection boundary; the external manifest proves eligibility, not this type."""
271
+
272
+ purpose: Literal["normal_training"]
273
+ selection_manifest_sha256: Sha256
274
+ profiles: tuple[ProfileVector, ...]
275
+
276
+
277
+ class FeatureFit(FrozenModel):
278
+ name: Identifier
279
+ valid_count: NonNegativeInt
280
+ median: Annotated[float, Field(ge=0)] | None
281
+ reason: Literal["no_valid_training_values", "not_applicable"] | None = None
282
+
283
+ @model_validator(mode="after")
284
+ def consistent(self) -> Self:
285
+ if self.valid_count == 0:
286
+ if self.median is not None or self.reason is None:
287
+ raise ValueError("unsupported feature needs a documented removal")
288
+ elif self.median is None or self.reason is not None:
289
+ raise ValueError("supported feature requires its fitted median")
290
+ return self
291
+
292
+
293
+ class PreprocessorBundle(FrozenModel):
294
+ schema_version: Literal["1.0"] = "1.0"
295
+ algorithm: Literal["family-median-with-indicators-1.0"] = "family-median-with-indicators-1.0"
296
+ spec: ProfileSpec
297
+ spec_sha256: Sha256
298
+ selection_manifest_sha256: Sha256
299
+ training_sha256: Sha256
300
+ training_rows: NonNegativeInt
301
+ empty_policy: Literal["drop", "not_estimable"]
302
+ features: tuple[FeatureFit, ...]
303
+ state: Literal["ready", "not_estimable"]
304
+
305
+ @model_validator(mode="after")
306
+ def consistent(self) -> Self:
307
+ if self.spec_sha256 != profile_spec_fingerprint(self.spec):
308
+ raise ValueError("bundle feature fingerprint mismatch")
309
+ if tuple(f.name for f in self.features) != tuple(f.name for f in self.spec.features):
310
+ raise ValueError("bundle order mismatch")
311
+ if any(f.valid_count > self.training_rows for f in self.features):
312
+ raise ValueError("feature support exceeds training rows")
313
+ usable = any(f.median is not None for f in self.features)
314
+ unsupported = any(f.reason == "no_valid_training_values" for f in self.features)
315
+ ready = usable and not (unsupported and self.empty_policy == "not_estimable")
316
+ if (self.state == "ready") != ready:
317
+ raise ValueError("bundle state contradicts training support")
318
+ return self
319
+
320
+ @property
321
+ def columns(self) -> tuple[str, ...]:
322
+ return tuple(
323
+ name
324
+ for f in self.features
325
+ if f.median is not None
326
+ for name in (f.name, f.name + ".undefined", f.name + ".not_applicable")
327
+ )
328
+
329
+
330
+ class FeatureMatrix(FrozenModel):
331
+ """The sole matrix for both M and B. Row metadata is deliberately absent."""
332
+
333
+ schema_version: Literal["1.0"] = "1.0"
334
+ preprocessor_sha256: Sha256
335
+ columns: tuple[Text, ...]
336
+ values: tuple[tuple[float, ...], ...]
337
+
338
+ @model_validator(mode="after")
339
+ def consistent(self) -> Self:
340
+ unique(self.columns, "columns")
341
+ if not self.columns or any(len(row) != len(self.columns) for row in self.values):
342
+ raise ValueError("matrix requires a nonempty schema and equal-width numeric rows")
343
+ return self