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,354 @@
1
+ """Closed, immutable tabular IR; documents never contain executable code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any, Literal, cast
6
+
7
+ from pydantic import Field, SerializerFunctionWrapHandler, model_serializer, model_validator
8
+ from typing_extensions import Self
9
+
10
+ from equiframe._base import FrozenModel, Identifier, Text, _ExplicitOutcome
11
+ from equiframe.contracts import DecimalType, LogicalType
12
+ from equiframe.values import ScalarValue, TimeZone
13
+
14
+ Engine = Literal["pandas", "polars", "duckdb"]
15
+
16
+
17
+ class Column(FrozenModel):
18
+ kind: Literal["column"] = "column"
19
+ name: Text
20
+
21
+
22
+ class LiteralExpression(FrozenModel):
23
+ kind: Literal["literal"] = "literal"
24
+ logical_type: LogicalType
25
+ value: ScalarValue
26
+ unit: Text | None = None
27
+
28
+
29
+ class Compare(FrozenModel):
30
+ kind: Literal["compare"] = "compare"
31
+ operation: Literal["eq", "ne", "lt", "le", "gt", "ge"]
32
+ left: Expression
33
+ right: Expression
34
+ nan: Literal["unordered"] = "unordered"
35
+
36
+
37
+ class Boolean(FrozenModel):
38
+ kind: Literal["boolean"] = "boolean"
39
+ operation: Literal["and", "or"]
40
+ arguments: Annotated[tuple[Expression, ...], Field(min_length=2, max_length=32)]
41
+
42
+
43
+ class Not(FrozenModel):
44
+ kind: Literal["not"] = "not"
45
+ argument: Expression
46
+
47
+
48
+ class IsNull(FrozenModel):
49
+ kind: Literal["is_null"] = "is_null"
50
+ argument: Expression
51
+
52
+
53
+ class IsNaN(FrozenModel):
54
+ kind: Literal["is_nan"] = "is_nan"
55
+ argument: Expression
56
+
57
+
58
+ class Arithmetic(FrozenModel):
59
+ kind: Literal["arithmetic"] = "arithmetic"
60
+ operation: Literal["add", "subtract", "multiply", "divide"]
61
+ left: Expression
62
+ right: Expression
63
+ logical_type: LogicalType
64
+ unit: Text
65
+ overflow: Literal["reject"] = "reject"
66
+ rounding: Literal["exact", "half_even", "half_away_from_zero"] = "exact"
67
+
68
+ @model_serializer(mode="wrap")
69
+ def historical_defaults(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
70
+ result = cast("dict[str, Any]", handler(self))
71
+ if self.rounding == "exact":
72
+ result.pop("rounding", None)
73
+ return result
74
+
75
+ @model_validator(mode="after")
76
+ def rounding_scope(self) -> Self:
77
+ if self.operation != "divide" and self.rounding != "exact":
78
+ raise ValueError("only division declares rounding; use DecimalRound explicitly")
79
+ return self
80
+
81
+
82
+ class DecimalRound(FrozenModel):
83
+ kind: Literal["decimal_round"] = "decimal_round"
84
+ argument: Expression
85
+ logical_type: DecimalType
86
+ rounding: Literal["half_even", "half_away_from_zero"]
87
+ overflow: Literal["reject"] = "reject"
88
+
89
+
90
+ class LocalizeTimestamp(FrozenModel):
91
+ kind: Literal["localize_timestamp"] = "localize_timestamp"
92
+ argument: Expression
93
+ timezone: TimeZone
94
+ ambiguous: Literal["reject", "earlier", "later"]
95
+ nonexistent: Literal["reject"] = "reject"
96
+
97
+
98
+ class Coalesce(FrozenModel):
99
+ kind: Literal["coalesce"] = "coalesce"
100
+ arguments: Annotated[tuple[Expression, ...], Field(min_length=2, max_length=32)]
101
+
102
+
103
+ class Conditional(FrozenModel):
104
+ kind: Literal["conditional"] = "conditional"
105
+ predicate: Expression
106
+ then: Expression
107
+ otherwise: Expression
108
+ unknown: Literal["otherwise"] = "otherwise"
109
+
110
+
111
+ Expression = Annotated[
112
+ Column
113
+ | LiteralExpression
114
+ | Compare
115
+ | Boolean
116
+ | Not
117
+ | IsNull
118
+ | IsNaN
119
+ | Arithmetic
120
+ | DecimalRound
121
+ | LocalizeTimestamp
122
+ | Coalesce
123
+ | Conditional,
124
+ Field(discriminator="kind"),
125
+ ]
126
+ for _model in (
127
+ Compare,
128
+ Boolean,
129
+ Not,
130
+ IsNull,
131
+ IsNaN,
132
+ Arithmetic,
133
+ DecimalRound,
134
+ LocalizeTimestamp,
135
+ Coalesce,
136
+ Conditional,
137
+ ):
138
+ _model.model_rebuild()
139
+
140
+
141
+ class NamedExpression(FrozenModel):
142
+ name: Text
143
+ expression: Expression
144
+
145
+
146
+ class Scan(FrozenModel):
147
+ kind: Literal["scan"] = "scan"
148
+ node_id: Identifier
149
+ table: Text
150
+
151
+
152
+ class Project(FrozenModel):
153
+ kind: Literal["project"] = "project"
154
+ node_id: Identifier
155
+ source: Identifier
156
+ columns: Annotated[tuple[NamedExpression, ...], Field(min_length=1, max_length=256)]
157
+
158
+
159
+ class Filter(FrozenModel):
160
+ kind: Literal["filter"] = "filter"
161
+ node_id: Identifier
162
+ source: Identifier
163
+ predicate: Expression
164
+
165
+
166
+ class CastColumn(FrozenModel):
167
+ column: Text
168
+ logical_type: LogicalType
169
+
170
+
171
+ class Cast(FrozenModel):
172
+ kind: Literal["cast"] = "cast"
173
+ node_id: Identifier
174
+ source: Identifier
175
+ columns: Annotated[tuple[CastColumn, ...], Field(min_length=1, max_length=256)]
176
+ loss: Literal["reject"] = "reject"
177
+
178
+
179
+ class JoinKey(FrozenModel):
180
+ left: Text
181
+ right: Text
182
+
183
+
184
+ class EquiJoin(FrozenModel):
185
+ kind: Literal["join"] = "join"
186
+ node_id: Identifier
187
+ left: Identifier
188
+ right: Identifier
189
+ keys: Annotated[tuple[JoinKey, ...], Field(min_length=1, max_length=32)]
190
+ how: Literal["inner", "left", "right", "full", "semi", "anti"]
191
+ null_keys: Literal["equal", "never"]
192
+ right_prefix: Text = "right."
193
+ cardinality: Literal["one_to_one", "one_to_many", "many_to_one", "many_to_many"] = (
194
+ "many_to_many"
195
+ )
196
+
197
+ @model_serializer(mode="wrap")
198
+ def historical_defaults(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
199
+ result = cast("dict[str, Any]", handler(self))
200
+ if self.cardinality == "many_to_many":
201
+ result.pop("cardinality", None)
202
+ return result
203
+
204
+
205
+ class Measure(FrozenModel):
206
+ name: Text
207
+ operation: Literal["count_rows", "count_valid", "sum", "min", "max"]
208
+ column: Text | None = None
209
+ logical_type: LogicalType
210
+ unit: Text | None = None
211
+
212
+ @model_validator(mode="after")
213
+ def column_required(self) -> Self:
214
+ if (self.operation == "count_rows") != (self.column is None):
215
+ raise ValueError("count_rows has no column; other measures require a column")
216
+ return self
217
+
218
+
219
+ class Aggregate(FrozenModel):
220
+ kind: Literal["aggregate"] = "aggregate"
221
+ node_id: Identifier
222
+ source: Identifier
223
+ group_by: Annotated[tuple[Text, ...], Field(max_length=32)] = ()
224
+ measures: Annotated[tuple[Measure, ...], Field(min_length=1, max_length=128)]
225
+ null_groups: Literal["retain", "drop"]
226
+ empty_sum: Literal["null", "zero"]
227
+
228
+
229
+ class SortKey(FrozenModel):
230
+ column: Text
231
+ direction: Literal["ascending", "descending"]
232
+ nulls: Literal["first", "last"]
233
+
234
+
235
+ class Sort(FrozenModel):
236
+ kind: Literal["sort"] = "sort"
237
+ node_id: Identifier
238
+ source: Identifier
239
+ by: Annotated[tuple[SortKey, ...], Field(min_length=1, max_length=32)]
240
+ ties: Literal["unspecified", "identical_rows", "reject"]
241
+
242
+
243
+ class WindowMeasure(FrozenModel):
244
+ name: Text
245
+ operation: Literal["row_number", "lag", "lead"]
246
+ column: Text | None = None
247
+ logical_type: LogicalType
248
+ unit: Text | None = None
249
+ offset: int = Field(default=1, ge=1, le=1_000_000)
250
+
251
+ @model_validator(mode="after")
252
+ def column_required(self) -> Self:
253
+ if (self.operation == "row_number") != (self.column is None):
254
+ raise ValueError("row_number has no column; lag/lead require a column")
255
+ if self.operation == "row_number" and self.offset != 1:
256
+ raise ValueError("offset applies only to lag/lead")
257
+ return self
258
+
259
+
260
+ class Window(FrozenModel):
261
+ kind: Literal["window"] = "window"
262
+ node_id: Identifier
263
+ source: Identifier
264
+ partition_by: Annotated[tuple[Text, ...], Field(max_length=32)] = ()
265
+ by: Annotated[tuple[SortKey, ...], Field(min_length=1, max_length=32)]
266
+ measures: Annotated[tuple[WindowMeasure, ...], Field(min_length=1, max_length=128)]
267
+ ties: Literal["reject"] = "reject"
268
+
269
+
270
+ class Concatenate(FrozenModel):
271
+ kind: Literal["concatenate"] = "concatenate"
272
+ node_id: Identifier
273
+ sources: Annotated[tuple[Identifier, ...], Field(min_length=2, max_length=32)]
274
+ schema_policy: Literal["identical"] = "identical"
275
+
276
+
277
+ class Deduplicate(FrozenModel):
278
+ kind: Literal["deduplicate"] = "deduplicate"
279
+ node_id: Identifier
280
+ source: Identifier
281
+ keys: Annotated[tuple[Text, ...], Field(min_length=1, max_length=256)]
282
+ keep: Literal["identical", "first"] = "identical"
283
+ nulls: Literal["equal"] = "equal"
284
+ by: Annotated[tuple[SortKey, ...], Field(max_length=32)] = ()
285
+ ties: Literal["reject"] = "reject"
286
+
287
+ @model_serializer(mode="wrap")
288
+ def historical_defaults(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
289
+ result = cast("dict[str, Any]", handler(self))
290
+ if not self.by:
291
+ result.pop("by", None)
292
+ result.pop("ties", None)
293
+ return result
294
+
295
+ @model_validator(mode="after")
296
+ def deterministic_selection(self) -> Self:
297
+ if (self.keep == "first") != bool(self.by):
298
+ raise ValueError("keep=first requires by; keep=identical forbids by")
299
+ return self
300
+
301
+
302
+ Node = Annotated[
303
+ Scan
304
+ | Project
305
+ | Filter
306
+ | Cast
307
+ | EquiJoin
308
+ | Aggregate
309
+ | Sort
310
+ | Concatenate
311
+ | Deduplicate
312
+ | Window,
313
+ Field(discriminator="kind"),
314
+ ]
315
+
316
+
317
+ class RelationalPlan(FrozenModel):
318
+ schema_version: Literal["1.0"] = "1.0"
319
+ nodes: Annotated[tuple[Node, ...], Field(min_length=1, max_length=128)]
320
+ output: Identifier
321
+
322
+
323
+ class IRColumn(FrozenModel):
324
+ name: Text
325
+ logical_type: LogicalType
326
+ nullable: bool
327
+ unit: Text | None = None
328
+
329
+
330
+ class NodeSchema(FrozenModel):
331
+ node_id: Identifier
332
+ columns: tuple[IRColumn, ...]
333
+
334
+
335
+ class CompilationDiagnostic(FrozenModel):
336
+ code: Identifier
337
+ message: Text
338
+ node_id: Identifier | None = None
339
+
340
+
341
+ class CompilationCheck(_ExplicitOutcome, FrozenModel):
342
+ _truth_value_guidance = (
343
+ "inspect .supported explicitly; preflight support does not establish equivalence"
344
+ )
345
+
346
+ engine: Text
347
+ supported: bool
348
+ diagnostics: tuple[CompilationDiagnostic, ...] = ()
349
+
350
+ @model_validator(mode="after")
351
+ def coherent(self) -> Self:
352
+ if self.supported == bool(self.diagnostics):
353
+ raise ValueError("supported checks have no diagnostics; unsupported checks require one")
354
+ return self