interlaced 2.0.0a2__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 (74) hide show
  1. interlace/__init__.py +26 -0
  2. interlace/checks/__init__.py +10 -0
  3. interlace/checks/builtin.py +152 -0
  4. interlace/checks/runner.py +103 -0
  5. interlace/checks/spec.py +113 -0
  6. interlace/cli/__init__.py +7 -0
  7. interlace/cli/main.py +1072 -0
  8. interlace/config/__init__.py +7 -0
  9. interlace/config/config.py +172 -0
  10. interlace/contracts.py +35 -0
  11. interlace/dsl/__init__.py +16 -0
  12. interlace/dsl/decorators.py +222 -0
  13. interlace/dsl/discovery.py +75 -0
  14. interlace/dsl/sql_config.py +47 -0
  15. interlace/engines/__init__.py +16 -0
  16. interlace/engines/base.py +86 -0
  17. interlace/engines/duckdb.py +249 -0
  18. interlace/engines/postgres.py +157 -0
  19. interlace/engines/quack.py +113 -0
  20. interlace/engines/registry.py +125 -0
  21. interlace/exceptions.py +66 -0
  22. interlace/exports.py +145 -0
  23. interlace/graph/__init__.py +17 -0
  24. interlace/graph/column_lineage.py +69 -0
  25. interlace/graph/dag.py +81 -0
  26. interlace/graph/project.py +242 -0
  27. interlace/graph/selectors.py +45 -0
  28. interlace/ir/__init__.py +24 -0
  29. interlace/ir/canonicalize.py +71 -0
  30. interlace/ir/fingerprint.py +55 -0
  31. interlace/ir/relation.py +71 -0
  32. interlace/ir/schema.py +23 -0
  33. interlace/plan/__init__.py +23 -0
  34. interlace/plan/apply.py +527 -0
  35. interlace/plan/differ.py +211 -0
  36. interlace/plan/plan.py +177 -0
  37. interlace/plan/resolve.py +74 -0
  38. interlace/plan/run.py +79 -0
  39. interlace/project.py +278 -0
  40. interlace/py.typed +0 -0
  41. interlace/runtime/__init__.py +6 -0
  42. interlace/runtime/handles.py +48 -0
  43. interlace/runtime/python_model.py +175 -0
  44. interlace/scaffold.py +83 -0
  45. interlace/scheduler/__init__.py +17 -0
  46. interlace/scheduler/engine.py +66 -0
  47. interlace/scheduler/triggers.py +84 -0
  48. interlace/scheduler/worker.py +154 -0
  49. interlace/service/__init__.py +7 -0
  50. interlace/service/app.py +836 -0
  51. interlace/service/auth.py +41 -0
  52. interlace/state/__init__.py +18 -0
  53. interlace/state/interval.py +134 -0
  54. interlace/state/janitor.py +142 -0
  55. interlace/state/snapshot.py +47 -0
  56. interlace/state/store.py +906 -0
  57. interlace/strategies/__init__.py +64 -0
  58. interlace/strategies/base.py +71 -0
  59. interlace/strategies/full.py +38 -0
  60. interlace/strategies/full_merge.py +98 -0
  61. interlace/strategies/incremental_by_time.py +65 -0
  62. interlace/strategies/merge_by_key.py +69 -0
  63. interlace/strategies/scd_type_2.py +122 -0
  64. interlace/strategies/view.py +33 -0
  65. interlace/streaming/__init__.py +18 -0
  66. interlace/streaming/log.py +298 -0
  67. interlace/streaming/materializer.py +166 -0
  68. interlace/streaming/schema.py +218 -0
  69. interlaced-2.0.0a2.dist-info/METADATA +216 -0
  70. interlaced-2.0.0a2.dist-info/RECORD +74 -0
  71. interlaced-2.0.0a2.dist-info/WHEEL +5 -0
  72. interlaced-2.0.0a2.dist-info/entry_points.txt +2 -0
  73. interlaced-2.0.0a2.dist-info/licenses/LICENSE +21 -0
  74. interlaced-2.0.0a2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ """Intermediate representation: the canonical sqlglot AST + Arrow contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from interlace.ir.canonicalize import parse, resolve_references, table_references
6
+ from interlace.ir.fingerprint import canonical_sql, data_fingerprint, metadata_fingerprint
7
+ from interlace.ir.relation import EngineRef, Relation, SqlRelation, TableRef
8
+ from interlace.ir.schema import ArrowSchema, empty_schema, schema_from_fields
9
+
10
+ __all__ = [
11
+ "ArrowSchema",
12
+ "EngineRef",
13
+ "Relation",
14
+ "SqlRelation",
15
+ "TableRef",
16
+ "canonical_sql",
17
+ "data_fingerprint",
18
+ "empty_schema",
19
+ "metadata_fingerprint",
20
+ "parse",
21
+ "resolve_references",
22
+ "schema_from_fields",
23
+ "table_references",
24
+ ]
@@ -0,0 +1,71 @@
1
+ """Parsing and static analysis of SQL into the canonical IR.
2
+
3
+ Phase 1 covers parsing (dialect-aware) and table-reference extraction for
4
+ implicit dependency discovery. Qualification and type annotation
5
+ (``sqlglot.optimizer.qualify`` against the project schema graph) land alongside
6
+ column lineage in a later phase.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sqlglot
12
+ from sqlglot import exp
13
+
14
+ from interlace.exceptions import CompilationError
15
+ from interlace.ir.relation import TableRef
16
+
17
+
18
+ def parse(sql: str, dialect: str = "duckdb") -> exp.Expression:
19
+ """Parse a single SQL statement in the given dialect into a sqlglot AST."""
20
+ try:
21
+ parsed = sqlglot.parse_one(sql, dialect=dialect)
22
+ except Exception as exc: # sqlglot raises ParseError and friends
23
+ raise CompilationError(f"failed to parse SQL ({dialect})", details={"sql": sql, "error": str(exc)}) from exc
24
+ if parsed is None:
25
+ raise CompilationError("empty SQL statement", details={"sql": sql})
26
+ return parsed
27
+
28
+
29
+ def table_references(ast: exp.Expression) -> list[str]:
30
+ """Return the distinct real table references in an AST, excluding CTE names.
31
+
32
+ Each reference is keyed as ``db.name`` (or ``name`` when unqualified). CTE
33
+ aliases are filtered out — they are local names, not dependencies.
34
+ """
35
+ cte_names = {cte.alias_or_name for cte in ast.find_all(exp.CTE)}
36
+ refs: list[str] = []
37
+ seen: set[str] = set()
38
+ for table in ast.find_all(exp.Table):
39
+ name = table.name
40
+ if not name or name in cte_names:
41
+ continue
42
+ key = f"{table.db}.{name}" if table.db else name
43
+ if key not in seen:
44
+ seen.add(key)
45
+ refs.append(key)
46
+ return refs
47
+
48
+
49
+ def resolve_references(ast: exp.Expression, mapping: dict[str, TableRef]) -> exp.Expression:
50
+ """Rewrite model-name table references to their physical tables (returns a new AST).
51
+
52
+ ``mapping`` is keyed by dependency model name; a reference matches by its
53
+ ``db.name`` key or, failing that, its bare name (so ``main.orders`` resolves
54
+ to model ``orders``). Aliases are preserved; the input AST is not mutated.
55
+ """
56
+ if not mapping:
57
+ return ast
58
+
59
+ def rewrite(node: exp.Expression) -> exp.Expression:
60
+ if isinstance(node, exp.Table):
61
+ key = f"{node.db}.{node.name}" if node.db else node.name
62
+ target = mapping.get(key) or mapping.get(node.name)
63
+ if target is not None:
64
+ if not node.alias: # keep qualified column refs (b.x) resolving after the rename
65
+ node.set("alias", exp.TableAlias(this=exp.to_identifier(node.name)))
66
+ node.set("this", exp.to_identifier(target.name))
67
+ node.set("db", exp.to_identifier(target.schema) if target.schema else None)
68
+ node.set("catalog", exp.to_identifier(target.catalog) if target.catalog else None)
69
+ return node
70
+
71
+ return ast.transform(rewrite)
@@ -0,0 +1,55 @@
1
+ """Snapshot fingerprinting.
2
+
3
+ A model's ``data`` fingerprint is a hash of its normalised SQL AST, its
4
+ materialisation/strategy config, and the sorted fingerprints of its upstreams —
5
+ so any change that affects results (here or upstream) yields a new fingerprint
6
+ and triggers a rebuild. A separate ``metadata`` fingerprint covers comments,
7
+ owner, and tags, which must never trigger a rebuild.
8
+
9
+ This mirrors sqlmesh's snapshot model. The hash is deliberately short (16 hex
10
+ chars) because it becomes part of physical table names.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ from typing import Any
18
+
19
+ from sqlglot import exp
20
+
21
+ _FP_LEN = 16
22
+
23
+
24
+ def canonical_sql(ast: exp.Expression) -> str:
25
+ """Render an AST to a stable, comment-free, normalised string for hashing."""
26
+ return ast.sql(comments=False, normalize=True, pretty=False)
27
+
28
+
29
+ def _stable_json(value: Any) -> str:
30
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
31
+
32
+
33
+ def _digest(*parts: str) -> str:
34
+ h = hashlib.sha256("\x00".join(parts).encode("utf-8"))
35
+ return h.hexdigest()[:_FP_LEN]
36
+
37
+
38
+ def data_fingerprint(
39
+ *,
40
+ query: str | exp.Expression,
41
+ strategy_config: dict[str, Any],
42
+ upstream_fingerprints: list[str],
43
+ ) -> str:
44
+ """Fingerprint that changes whenever the model's output could change.
45
+
46
+ ``query`` is the canonical SQL for SQL models, or for Python models the
47
+ dedented source plus closure bytecode digest produced by the caller.
48
+ """
49
+ sql = canonical_sql(query) if isinstance(query, exp.Expression) else query
50
+ return _digest(sql, _stable_json(strategy_config), *sorted(upstream_fingerprints))
51
+
52
+
53
+ def metadata_fingerprint(metadata: dict[str, Any]) -> str:
54
+ """Fingerprint over non-semantic metadata (comments, owner, tags)."""
55
+ return _digest(_stable_json(metadata))
@@ -0,0 +1,71 @@
1
+ """The universal model-output contract.
2
+
3
+ Every model produces a :class:`Relation`. A relation is *logical* until a sink
4
+ forces it: a :class:`SqlRelation` carries a sqlglot AST that the owning engine
5
+ evaluates natively (zero rows enter Python). Python models move data as plain
6
+ Arrow ``RecordBatchReader`` handles (see ``runtime/``), not as relations.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import ClassVar, Protocol, runtime_checkable
13
+
14
+ from sqlglot import exp
15
+
16
+ from interlace.ir.schema import ArrowSchema
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class TableRef:
21
+ """A fully-qualified table identifier, dialect-agnostic until transpiled."""
22
+
23
+ schema: str
24
+ name: str
25
+ catalog: str | None = None
26
+
27
+ def to_expr(self) -> exp.Table:
28
+ """This table as a sqlglot Table node (identifier-safe, dialect-agnostic)."""
29
+ return exp.table_(self.name, db=self.schema, catalog=self.catalog)
30
+
31
+ def qualified(self) -> str:
32
+ parts = [p for p in (self.catalog, self.schema, self.name) if p]
33
+ return ".".join(parts)
34
+
35
+ def __str__(self) -> str:
36
+ return self.qualified()
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class EngineRef:
41
+ """Identifies which engine (gateway) can evaluate a relation, and its dialect."""
42
+
43
+ name: str # connection/gateway name from config
44
+ dialect: str # sqlglot dialect name
45
+
46
+
47
+ @runtime_checkable
48
+ class Relation(Protocol):
49
+ """What every model produces. Logical until a sink forces it."""
50
+
51
+ @property
52
+ def schema(self) -> ArrowSchema: ...
53
+
54
+ @property
55
+ def plane(self) -> str: ...
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class SqlRelation:
60
+ """Logical plane: a sqlglot AST bound to the engine that can evaluate it.
61
+
62
+ The AST is canonical (qualified, type-annotated, dialect-neutral); the engine
63
+ adapter transpiles it at execution time. Composing SQL models never leaves
64
+ this plane, so no data is materialised until a sink runs a single statement.
65
+ """
66
+
67
+ ast: exp.Expression
68
+ engine: EngineRef
69
+ schema: ArrowSchema
70
+
71
+ plane: ClassVar[str] = "logical"
interlace/ir/schema.py ADDED
@@ -0,0 +1,23 @@
1
+ """Canonical schema representation.
2
+
3
+ Apache Arrow is the single source of truth for column names and types across the
4
+ IR. Engine adapters map this to and from their native catalogs; nothing else in
5
+ the framework invents its own type system.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import pyarrow as pa
11
+
12
+ # The canonical schema type used throughout the IR.
13
+ type ArrowSchema = pa.Schema
14
+
15
+
16
+ def schema_from_fields(fields: dict[str, pa.DataType]) -> ArrowSchema:
17
+ """Build an Arrow schema from an ordered mapping of name to Arrow type."""
18
+ return pa.schema([pa.field(name, dtype) for name, dtype in fields.items()])
19
+
20
+
21
+ def empty_schema() -> ArrowSchema:
22
+ """An empty schema (no columns) — used when a relation's shape is not yet known."""
23
+ return pa.schema([])
@@ -0,0 +1,23 @@
1
+ """Plan/apply: diff models, classify changes, compute backfills, swap views."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from interlace.plan.apply import ApplyResult, apply
6
+ from interlace.plan.differ import diff, snapshot_of
7
+ from interlace.plan.plan import BackfillTask, ChangeType, ModelChange, Plan, TransferEdge, ViewSwap, env_view
8
+ from interlace.plan.run import run_plan
9
+
10
+ __all__ = [
11
+ "ApplyResult",
12
+ "BackfillTask",
13
+ "ChangeType",
14
+ "ModelChange",
15
+ "Plan",
16
+ "TransferEdge",
17
+ "ViewSwap",
18
+ "apply",
19
+ "diff",
20
+ "env_view",
21
+ "run_plan",
22
+ "snapshot_of",
23
+ ]