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
equiframe/values.py ADDED
@@ -0,0 +1,252 @@
1
+ """Lossless, tagged contract literals; these are metadata, not table encoders."""
2
+
3
+ import math
4
+ import re
5
+ from datetime import date, datetime, timedelta
6
+ from decimal import Decimal, InvalidOperation, localcontext
7
+ from typing import Annotated, Literal
8
+
9
+ from pydantic import (
10
+ AfterValidator,
11
+ BeforeValidator,
12
+ Field,
13
+ PlainSerializer,
14
+ WithJsonSchema,
15
+ model_validator,
16
+ )
17
+ from typing_extensions import Self
18
+
19
+ from equiframe._base import FrozenModel, Text, fail
20
+ from equiframe._timezones import timezone
21
+
22
+
23
+ def _decimal(value: object) -> Decimal:
24
+ if isinstance(value, Decimal):
25
+ result = value
26
+ elif isinstance(value, str) and re.fullmatch(
27
+ r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?", value
28
+ ):
29
+ try:
30
+ result = Decimal(value)
31
+ except InvalidOperation as exc:
32
+ raise ValueError("invalid decimal text") from exc
33
+ else:
34
+ raise ValueError("decimal requires a Decimal or exact decimal string; numbers are rejected")
35
+ if not result.is_finite():
36
+ raise ValueError("decimal must be finite; use tagged float special values")
37
+ return result
38
+
39
+
40
+ def _decimal_text(value: Decimal) -> str:
41
+ # str(Decimal) consults context.capitals even though it never rounds.
42
+ with localcontext() as decimal_context:
43
+ decimal_context.capitals = 1
44
+ return str(value)
45
+
46
+
47
+ ExactDecimal = Annotated[
48
+ Decimal,
49
+ BeforeValidator(_decimal),
50
+ PlainSerializer(_decimal_text, return_type=str),
51
+ WithJsonSchema(
52
+ {
53
+ "type": "string",
54
+ "pattern": r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$",
55
+ }
56
+ ),
57
+ ]
58
+
59
+
60
+ def _zone(value: str) -> str:
61
+ timezone(value)
62
+ return value
63
+
64
+
65
+ TimeZone = Annotated[Text, AfterValidator(_zone)]
66
+ TimeUnit = Literal["s", "ms", "us", "ns"]
67
+
68
+
69
+ class NullValue(FrozenModel):
70
+ kind: Literal["null"] = "null"
71
+
72
+
73
+ class NaNValue(FrozenModel):
74
+ kind: Literal["nan"] = "nan"
75
+
76
+
77
+ class InfinityValue(FrozenModel):
78
+ kind: Literal["infinity"] = "infinity"
79
+ sign: Literal["positive", "negative"]
80
+
81
+
82
+ class BooleanValue(FrozenModel):
83
+ kind: Literal["boolean"] = "boolean"
84
+ value: bool
85
+
86
+
87
+ class IntegerValue(FrozenModel):
88
+ kind: Literal["integer"] = "integer"
89
+ value: int
90
+
91
+
92
+ class StringValue(FrozenModel):
93
+ kind: Literal["string"] = "string"
94
+ value: str
95
+
96
+ @model_validator(mode="after")
97
+ def valid_unicode(self) -> Self:
98
+ if any(0xD800 <= ord(char) <= 0xDFFF for char in self.value):
99
+ fail("invalid_unicode", "string contains a surrogate code point", "value")
100
+ return self
101
+
102
+
103
+ class DecimalValue(FrozenModel):
104
+ kind: Literal["decimal"] = "decimal"
105
+ value: ExactDecimal
106
+
107
+
108
+ def _float(value: object) -> float:
109
+ if isinstance(value, bool) or not isinstance(value, (float, int)):
110
+ raise ValueError("float literal requires a JSON number")
111
+ try:
112
+ result = float(value)
113
+ except OverflowError as exc:
114
+ raise ValueError("float literal exceeds representable range") from exc
115
+ if not math.isfinite(result):
116
+ raise ValueError("nonfinite floats require a tagged special value")
117
+ if isinstance(value, int) and int(result) != value:
118
+ raise ValueError("integer cannot be represented as a float without loss")
119
+ return result
120
+
121
+
122
+ class FloatValue(FrozenModel):
123
+ kind: Literal["float"] = "float"
124
+ value: Annotated[float, BeforeValidator(_float)]
125
+
126
+
127
+ class DateValue(FrozenModel):
128
+ kind: Literal["date"] = "date"
129
+ value: str = Field(pattern=r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
130
+
131
+ @model_validator(mode="after")
132
+ def valid_date(self) -> Self:
133
+ try:
134
+ date.fromisoformat(self.value)
135
+ except ValueError:
136
+ fail("invalid_date", "invalid calendar date", "value")
137
+ return self
138
+
139
+
140
+ _TIMESTAMP = re.compile(
141
+ r"(?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2})T"
142
+ r"(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})"
143
+ r"(?P<fraction>\.[0-9]{1,9})?(?P<offset>Z|[+-][0-9]{2}:[0-9]{2})?"
144
+ )
145
+
146
+
147
+ def _timestamp_datetime(value: str) -> datetime:
148
+ """Calendar/offset validation without relying on the Python 3.11 ISO parser.
149
+
150
+ The declared literal grammar admits 1-9 fraction digits. datetime stores only
151
+ microseconds; exact nanosecond ordering is calculated separately from the
152
+ original unmodified literal by TimestampValue.nanoseconds.
153
+ """
154
+ match = _TIMESTAMP.fullmatch(value)
155
+ if match is None:
156
+ raise ValueError("invalid declared timestamp grammar")
157
+ fraction = (match["fraction"] or "").removeprefix(".")
158
+ microseconds = ("." + fraction[:6].ljust(6, "0")) if fraction else ""
159
+ offset = "+00:00" if match["offset"] == "Z" else (match["offset"] or "")
160
+ calendar = (
161
+ f"{match['date']}T{match['hour']}:{match['minute']}:{match['second']}{microseconds}{offset}"
162
+ )
163
+ return datetime.fromisoformat(calendar)
164
+
165
+
166
+ class TimestampValue(FrozenModel):
167
+ kind: Literal["timestamp"] = "timestamp"
168
+ value: str
169
+ zone: TimeZone | None = None
170
+
171
+ @model_validator(mode="after")
172
+ def valid_timestamp(self) -> Self:
173
+ match = _TIMESTAMP.fullmatch(self.value)
174
+ if match is None:
175
+ fail(
176
+ "invalid_timestamp",
177
+ "expected ISO timestamp with at most 9 fractional digits",
178
+ "value",
179
+ )
180
+ offset = match["offset"]
181
+ if offset == "-00:00":
182
+ fail(
183
+ "unknown_offset", "-00:00 denotes an unknown offset and is not an instant", "value"
184
+ )
185
+ if offset not in (None, "Z") and (int(offset[1:3]) > 23 or int(offset[4:6]) > 59):
186
+ fail("invalid_offset", "invalid timestamp offset", "value")
187
+ try:
188
+ instant = _timestamp_datetime(self.value)
189
+ except ValueError:
190
+ fail("invalid_timestamp", "invalid calendar date or clock time", "value")
191
+ if self.zone is not None:
192
+ if instant.tzinfo is None:
193
+ fail("missing_offset", "a zoned literal requires an explicit offset", "value")
194
+ try:
195
+ actual_offset = instant.astimezone(timezone(self.zone)).utcoffset()
196
+ except (OverflowError, ValueError):
197
+ fail("timestamp_range", "zoned instant exceeds supported calendar range", "value")
198
+ if actual_offset != instant.utcoffset():
199
+ fail("zone_offset_mismatch", "offset disagrees with the declared IANA zone", "zone")
200
+ return self
201
+
202
+ @property
203
+ def has_offset(self) -> bool:
204
+ return bool(re.search(r"(?:Z|[+-][0-9]{2}:[0-9]{2})$", self.value))
205
+
206
+ @property
207
+ def fractional_digits(self) -> int:
208
+ match = _TIMESTAMP.fullmatch(self.value)
209
+ if not (match is not None):
210
+ raise AssertionError("Internal invariant check failed.")
211
+ return len((match["fraction"] or "")[1:])
212
+
213
+ def nanoseconds(self, *, instant: bool) -> int:
214
+ """Exact ordering key; no float timestamp or sub-microsecond truncation."""
215
+ match = _TIMESTAMP.fullmatch(self.value)
216
+ if not (match is not None):
217
+ raise AssertionError("Internal invariant check failed.")
218
+ day = date.fromisoformat(match["date"]).toordinal()
219
+ seconds = day * 86400 + int(match["hour"]) * 3600
220
+ seconds += int(match["minute"]) * 60 + int(match["second"])
221
+ offset = match["offset"]
222
+ if instant and not self.has_offset:
223
+ raise ValueError("an instant ordering key requires an explicit offset")
224
+ if instant and offset not in (None, "Z"):
225
+ shift = int(offset[1:3]) * 3600 + int(offset[4:6]) * 60
226
+ seconds -= shift if offset[0] == "+" else -shift
227
+ return seconds * 10**9 + int((match["fraction"] or "")[1:].ljust(9, "0") or "0")
228
+
229
+ def in_zone(self, zone: str, *, fold: int) -> datetime:
230
+ """Used only to validate an explicitly declared naive-time policy."""
231
+ return _timestamp_datetime(self.value).replace(tzinfo=timezone(zone), fold=fold)
232
+
233
+
234
+ ScalarValue = Annotated[
235
+ NullValue
236
+ | NaNValue
237
+ | InfinityValue
238
+ | BooleanValue
239
+ | IntegerValue
240
+ | StringValue
241
+ | DecimalValue
242
+ | FloatValue
243
+ | DateValue
244
+ | TimestampValue,
245
+ Field(discriminator="kind"),
246
+ ]
247
+
248
+
249
+ def offset_nanoseconds(offset: timedelta | None) -> int:
250
+ if offset is None:
251
+ raise ValueError("missing UTC offset")
252
+ return (offset.days * 86400 + offset.seconds) * 10**9 + offset.microseconds * 1000
@@ -0,0 +1,35 @@
1
+ """Data-only requests for verification of explicitly supplied external artifacts."""
2
+
3
+ from pydantic import model_validator
4
+ from typing_extensions import Self
5
+
6
+ from equiframe._base import FrozenModel
7
+ from equiframe.comparison_types import ComparisonOptions, ComparisonPolicy
8
+ from equiframe.contracts import Contract
9
+ from equiframe.pipeline_types import PipelineInput
10
+ from equiframe.types import ArtifactRef, Sha256
11
+ from equiframe.verify_types import ExpectedProvenance, IndependentReviewEvidence
12
+
13
+
14
+ class VerificationRequest(FrozenModel):
15
+ contract: Contract
16
+ outputs: tuple[PipelineInput, ...]
17
+ expected: ArtifactRef | None = None
18
+ provenance: ExpectedProvenance | None = None
19
+ reviews: tuple[IndependentReviewEvidence, ...] = ()
20
+ snapshots: tuple[PipelineInput, ...] = ()
21
+ plan_sha256: Sha256 | None = None
22
+ policy: ComparisonPolicy = ComparisonPolicy()
23
+ options: ComparisonOptions = ComparisonOptions()
24
+ require_expected: bool = False
25
+ require_rules: bool = False
26
+ request_review: bool = False
27
+
28
+ @model_validator(mode="after")
29
+ def names(self) -> Self:
30
+ if not self.outputs:
31
+ raise ValueError("at least one output is required")
32
+ for entries in (self.outputs, self.snapshots):
33
+ if len({item.name for item in entries}) != len(entries):
34
+ raise ValueError("artifact binding names must be unique")
35
+ return self