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/local.py ADDED
@@ -0,0 +1,126 @@
1
+ """Local-only artifact storage; verified, exclusive copies without overwrites."""
2
+
3
+ import hashlib
4
+ import os
5
+ from collections.abc import Iterator, Mapping
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
8
+ from pathlib import Path
9
+ from urllib.parse import unquote, urlsplit
10
+ from urllib.request import url2pathname
11
+ from uuid import uuid4
12
+
13
+ from equiframe._base import Text
14
+ from equiframe._regular_io import (
15
+ local_input_path,
16
+ new_directory_path,
17
+ open_regular_file,
18
+ regular_directory,
19
+ )
20
+ from equiframe.errors import ArtifactIntegrityError, ArtifactIOError, CapabilityError
21
+ from equiframe.types import ArtifactRef
22
+
23
+ _FORMATS = {".json": "json", ".jsonl": "jsonl", ".arrow": "arrow", ".parquet": "parquet"}
24
+ _BUNDLE_PATHS: ContextVar[Mapping[str, Path] | None] = ContextVar("bundle_paths", default=None)
25
+
26
+
27
+ @contextmanager
28
+ def _bundle_paths(paths: Mapping[str, Path]) -> Iterator[None]:
29
+ """Resolve only verified bundle members, restoring scope even after a failure."""
30
+ token = _BUNDLE_PATHS.set(dict(paths))
31
+ try:
32
+ yield
33
+ finally:
34
+ _BUNDLE_PATHS.reset(token)
35
+
36
+
37
+ def local_path(ref: ArtifactRef) -> Path:
38
+ mapping = _BUNDLE_PATHS.get()
39
+ if mapping is not None:
40
+ if ref.uri not in mapping:
41
+ raise ArtifactIntegrityError("artifact reference is outside the portable inventory")
42
+ return mapping[ref.uri]
43
+ parts = urlsplit(ref.uri)
44
+ if parts.scheme != "file" or parts.netloc not in ("", "localhost"):
45
+ raise CapabilityError("only local file URIs are supported; remote access is unavailable")
46
+ decoded = unquote(parts.path)
47
+ if decoded.startswith("//") or "\\" in decoded:
48
+ raise CapabilityError("network/UNC paths are not supported")
49
+ return Path(url2pathname(parts.path))
50
+
51
+
52
+ def _copy(source: Path, destination: Path, expected: ArtifactRef | None = None) -> tuple[str, int]:
53
+ source = local_input_path(source)
54
+ destination = local_input_path(destination)
55
+ new_directory_path(destination.parent)
56
+ temporary = destination.with_name(destination.name + "." + uuid4().hex + ".part")
57
+ digest = hashlib.sha256()
58
+ size = 0
59
+ try:
60
+ with open_regular_file(source) as reader:
61
+ destination.parent.mkdir(parents=True, exist_ok=True)
62
+ regular_directory(destination.parent)
63
+ with temporary.open("xb") as writer:
64
+ while True:
65
+ capacity = 1024 * 1024
66
+ if expected is not None:
67
+ capacity = min(capacity, expected.size_bytes - size + 1)
68
+ block = reader.read(capacity)
69
+ if not block:
70
+ break
71
+ if expected is not None and size + len(block) > expected.size_bytes:
72
+ raise ArtifactIntegrityError("artifact exceeds its declared byte size")
73
+ writer.write(block)
74
+ digest.update(block)
75
+ size += len(block)
76
+ writer.flush()
77
+ os.fsync(writer.fileno())
78
+ if expected is not None and (
79
+ digest.hexdigest() != expected.sha256 or size != expected.size_bytes
80
+ ):
81
+ raise ArtifactIntegrityError("artifact bytes do not match declared SHA-256/size")
82
+ # Atomic publication that fails if destination exists, including concurrent writers.
83
+ os.link(temporary, destination)
84
+ return digest.hexdigest(), size
85
+ except (OSError, ValueError) as error:
86
+ raise ArtifactIOError(
87
+ "cannot copy/publish local artifact: " + type(error).__name__
88
+ ) from error
89
+ finally:
90
+ temporary.unlink(missing_ok=True)
91
+
92
+
93
+ class LocalArtifactStore:
94
+ def __init__(self, root: Path) -> None:
95
+ if not isinstance(root, Path):
96
+ raise TypeError("artifact store root must be a Path")
97
+ self.root = new_directory_path(local_input_path(root))
98
+
99
+ def put(self, path: Path, *, media_type: str) -> ArtifactRef:
100
+ from pydantic import TypeAdapter
101
+
102
+ if not isinstance(path, Path):
103
+ raise TypeError("artifact input must be a Path")
104
+ media_type = TypeAdapter(Text).validate_python(media_type, strict=True)
105
+ format_name = _FORMATS.get(path.suffix.lower())
106
+ if format_name is None:
107
+ raise CapabilityError("supported artifact suffixes: .json, .jsonl, .arrow, .parquet")
108
+ destination = self.root / (uuid4().hex + path.suffix.lower())
109
+ sha256, size = _copy(path, destination)
110
+ return ArtifactRef.model_validate(
111
+ {
112
+ "schema_version": "1.0",
113
+ "uri": destination.as_uri(),
114
+ "sha256": sha256,
115
+ "format": format_name,
116
+ "media_type": media_type,
117
+ "size_bytes": size,
118
+ }
119
+ )
120
+
121
+ def get(self, ref: ArtifactRef, destination: Path) -> Path:
122
+ if not isinstance(destination, Path):
123
+ raise TypeError("artifact destination must be a Path")
124
+ ref = ArtifactRef.model_validate(ref)
125
+ _copy(local_path(ref), destination, ref)
126
+ return destination
@@ -0,0 +1,94 @@
1
+ """Extensible declarative ETL migration; native engines remain optional imports."""
2
+
3
+ from .io import migration_summary, read_migration, render_migration_report
4
+ from .ports import CompiledPlan, CompilerRegistry, PlanCompiler, native_compiler_registry
5
+ from .runner import ExpectedInput, check_migration, migrate
6
+ from .runner_types import (
7
+ CompilerRecord,
8
+ MigrationOptions,
9
+ MigrationRequest,
10
+ MigrationResult,
11
+ MigrationSpec,
12
+ MigrationStage,
13
+ )
14
+ from .types import (
15
+ Aggregate,
16
+ Boolean,
17
+ Cast,
18
+ CastColumn,
19
+ Column,
20
+ Compare,
21
+ CompilationCheck,
22
+ CompilationDiagnostic,
23
+ DecimalRound,
24
+ EquiJoin,
25
+ Filter,
26
+ IRColumn,
27
+ IsNaN,
28
+ IsNull,
29
+ JoinKey,
30
+ LiteralExpression,
31
+ LocalizeTimestamp,
32
+ Measure,
33
+ NamedExpression,
34
+ NodeSchema,
35
+ Not,
36
+ Project,
37
+ RelationalPlan,
38
+ Scan,
39
+ Sort,
40
+ SortKey,
41
+ Window,
42
+ WindowMeasure,
43
+ )
44
+ from .validation import ValidatedIR, ir_fingerprint, validate_ir
45
+
46
+ __all__ = [
47
+ "Aggregate",
48
+ "Boolean",
49
+ "Cast",
50
+ "CastColumn",
51
+ "Column",
52
+ "Compare",
53
+ "CompilationCheck",
54
+ "CompilationDiagnostic",
55
+ "CompiledPlan",
56
+ "CompilerRecord",
57
+ "CompilerRegistry",
58
+ "DecimalRound",
59
+ "EquiJoin",
60
+ "ExpectedInput",
61
+ "Filter",
62
+ "IRColumn",
63
+ "IsNaN",
64
+ "IsNull",
65
+ "JoinKey",
66
+ "LiteralExpression",
67
+ "LocalizeTimestamp",
68
+ "Measure",
69
+ "MigrationOptions",
70
+ "MigrationRequest",
71
+ "MigrationResult",
72
+ "MigrationSpec",
73
+ "MigrationStage",
74
+ "NamedExpression",
75
+ "NodeSchema",
76
+ "Not",
77
+ "PlanCompiler",
78
+ "Project",
79
+ "RelationalPlan",
80
+ "Scan",
81
+ "Sort",
82
+ "SortKey",
83
+ "ValidatedIR",
84
+ "Window",
85
+ "WindowMeasure",
86
+ "check_migration",
87
+ "ir_fingerprint",
88
+ "migrate",
89
+ "migration_summary",
90
+ "native_compiler_registry",
91
+ "read_migration",
92
+ "render_migration_report",
93
+ "validate_ir",
94
+ ]
@@ -0,0 +1,8 @@
1
+ """Compatibility aliases for the shared captured-table implementation.
2
+
3
+ Physical table verification belongs to captured evidence, not migration. The
4
+ aliases retain internal consumer identity while formats and behavior stay intact.
5
+ """
6
+
7
+ from equiframe._physical import physical_size as physical_size
8
+ from equiframe._physical import verify_table_metadata as verify_table_metadata
@@ -0,0 +1,109 @@
1
+ """Explicit timestamp tick encoding for trusted IR implementations.
2
+
3
+ This codec changes representation only. It never localizes a wall time, chooses
4
+ an offset, changes a tick, or converts a wrongly typed native result to fit a
5
+ contract. Each compiler remains responsible for its native computation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass, replace
12
+ from typing import TYPE_CHECKING
13
+
14
+ from equiframe.contracts import Contract, TimestampType
15
+ from equiframe.errors import CapabilityError
16
+ from equiframe.registry import DuckDBTransformation, PolarsTransformation
17
+ from equiframe.tables import LoadedTable, check_arrow
18
+
19
+ if TYPE_CHECKING:
20
+ import pyarrow as pa
21
+
22
+ _TimestampColumns = tuple[tuple[str, TimestampType], ...]
23
+
24
+
25
+ def _view_columns(table: pa.Table, columns: _TimestampColumns, *, encode: bool) -> pa.Table:
26
+ """Reinterpret exact 64-bit buffers, retaining values, masks and metadata."""
27
+ import pyarrow as pa
28
+
29
+ check_arrow(table)
30
+ result: pa.Table = table
31
+ for name, logical in columns:
32
+ position = result.schema.get_field_index(name)
33
+ if position < 0:
34
+ raise CapabilityError("IR timestamp transport column is missing")
35
+ field = result.schema.field(position)
36
+ timestamp = pa.timestamp(logical.unit, tz=logical.timezone)
37
+ required, target = (timestamp, pa.int64()) if encode else (pa.int64(), timestamp)
38
+ if field.type != required:
39
+ raise CapabilityError("IR timestamp transport received an unexpected physical type")
40
+ array: pa.ChunkedArray = pa.chunked_array(
41
+ [chunk.view(target) for chunk in result.column(position).chunks], type=target
42
+ )
43
+ result = result.set_column(
44
+ position,
45
+ pa.field(name, target, nullable=field.nullable, metadata=field.metadata),
46
+ array,
47
+ )
48
+ check_arrow(result)
49
+ return result
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class _TimestampCodec:
54
+ inputs: tuple[tuple[str, _TimestampColumns], ...] = ()
55
+ output: _TimestampColumns = ()
56
+
57
+ @classmethod
58
+ def for_contract(cls, contract: Contract) -> _TimestampCodec:
59
+ """Describe the encoding only after the compiler validates its IR schema."""
60
+ return cls(
61
+ inputs=tuple(
62
+ (
63
+ table.name,
64
+ tuple(
65
+ (column.name, column.logical_type)
66
+ for column in table.schema_spec.columns
67
+ if isinstance(column.logical_type, TimestampType)
68
+ ),
69
+ )
70
+ for table in contract.inputs
71
+ if any(isinstance(c.logical_type, TimestampType) for c in table.schema_spec.columns)
72
+ ),
73
+ output=tuple(
74
+ (column.name, column.logical_type)
75
+ for column in contract.output.schema_spec.columns
76
+ if isinstance(column.logical_type, TimestampType)
77
+ ),
78
+ )
79
+
80
+ def encode_input(self, name: str, loaded: LoadedTable) -> LoadedTable:
81
+ columns = dict(self.inputs).get(name, ())
82
+ if not columns:
83
+ return loaded
84
+ return replace(
85
+ loaded,
86
+ table=_view_columns(loaded.table, columns, encode=True),
87
+ native=None,
88
+ native_types=(),
89
+ notes=(*loaded.notes, "IR internal timestamp encoding: unchanged signed int64 ticks."),
90
+ )
91
+
92
+ def encode_inputs(self, inputs: Mapping[str, LoadedTable]) -> dict[str, LoadedTable]:
93
+ descriptors = dict(self.inputs)
94
+ if not descriptors.keys() <= inputs.keys():
95
+ raise CapabilityError("IR timestamp transport input is missing")
96
+ return {name: self.encode_input(name, loaded) for name, loaded in inputs.items()}
97
+
98
+ def decode_output(self, table: pa.Table) -> pa.Table:
99
+ return _view_columns(table, self.output, encode=False)
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class _TimestampDuckDBTransformation(DuckDBTransformation):
104
+ timestamp_codec: _TimestampCodec | None = None
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class _TimestampPolarsTransformation(PolarsTransformation):
109
+ timestamp_codec: _TimestampCodec | None = None