interlaced 2.6.0__tar.gz → 2.7.0__tar.gz
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.
- {interlaced-2.6.0/src/interlaced.egg-info → interlaced-2.7.0}/PKG-INFO +1 -1
- {interlaced-2.6.0 → interlaced-2.7.0}/pyproject.toml +1 -1
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cli/main.py +256 -76
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/exceptions.py +12 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/inspect.py +15 -2
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/mcp_server.py +24 -35
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/__init__.py +4 -0
- interlaced-2.7.0/src/interlace/plan/comment.py +62 -0
- interlaced-2.7.0/src/interlace/plan/orchestrate.py +180 -0
- interlaced-2.7.0/src/interlace/plan/table_diff.py +302 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/app.py +177 -574
- interlaced-2.7.0/src/interlace/service/types.py +425 -0
- {interlaced-2.6.0 → interlaced-2.7.0/src/interlaced.egg-info}/PKG-INFO +1 -1
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlaced.egg-info/SOURCES.txt +4 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/LICENSE +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/MANIFEST.in +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/README.md +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/setup.cfg +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cdc/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cdc/decode.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cdc/publish.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cdc/slot.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/checks/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/checks/builtin.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/checks/runner.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/checks/spec.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/cli/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/config/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/config/config.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/connections.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/contracts.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/dsl/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/dsl/decorators.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/dsl/discovery.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/dsl/dynamic.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/dsl/sql_config.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/adbc.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/base.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/bigquery.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/duckdb.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/postgres.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/quack.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/redshift.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/registry.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/snowflake.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/engines/spark.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/graph/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/graph/column_lineage.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/graph/dag.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/graph/project.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/graph/selectors.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/inputs.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/ir/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/ir/canonicalize.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/ir/fingerprint.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/ir/macros.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/ir/relation.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/annotate.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/ddl.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/drift.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/reconcile.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/physical/spec.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/apply.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/differ.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/plan.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/resolve.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/plan/run.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/project.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/py.typed +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/query.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/runtime/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/runtime/handles.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/runtime/python_model.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/scaffold.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/scheduler/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/scheduler/engine.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/scheduler/triggers.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/scheduler/worker.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/auth.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/app.css +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/favicon.svg +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/index.html +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/api.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/app.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/dag.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/timeline.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/ui.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/checks.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/environments.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/lineage.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/models.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/overview.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/plan.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/query.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/runs.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/streams.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/service/ui/js/views/system.js +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/sinks.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/sources/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/sources/auth.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/sources/rest.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/interval.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/janitor.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/locks.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/snapshot.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/state/store.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/append.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/base.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/full_merge.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/hash_merge.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/incremental.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/merge.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/replace.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/replace_in_place.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/scd.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/strategies/view.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/streaming/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/streaming/log.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/streaming/materializer.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/streaming/schema.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/README.md +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/generate.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/interlace.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/models/events.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/models/events_by_type.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/models/top_users.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/models/user_spend.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/events/template.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/github/README.md +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/github/interlace.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/github/models/github_issues.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/github/template.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/README.md +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/docker-compose.yml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/init/seed.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/interlace.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/models/orders.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/postgres/template.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/README.md +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/interlace.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/templates/quickstart/template.yaml +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/testing/__init__.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlace/testing/golden.py +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlaced.egg-info/dependency_links.txt +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlaced.egg-info/entry_points.txt +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlaced.egg-info/requires.txt +0 -0
- {interlaced-2.6.0 → interlaced-2.7.0}/src/interlaced.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: interlaced
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres.
|
|
5
5
|
Author-email: Mark <mark@interlace.sh>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "interlaced"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.7.0"
|
|
8
8
|
description = "Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -17,16 +17,23 @@ from rich.markup import escape
|
|
|
17
17
|
from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
|
|
18
18
|
from rich.table import Table
|
|
19
19
|
|
|
20
|
-
from interlace.
|
|
21
|
-
|
|
20
|
+
from interlace.exceptions import (
|
|
21
|
+
CheckError,
|
|
22
|
+
ConfigurationError,
|
|
23
|
+
InterlaceError,
|
|
24
|
+
LockError,
|
|
25
|
+
PlanError,
|
|
26
|
+
QueryError,
|
|
27
|
+
SelectionError,
|
|
28
|
+
)
|
|
22
29
|
from interlace.graph.column_lineage import column_impact, column_lineage, split_target
|
|
23
30
|
from interlace.graph.project import CompiledProject
|
|
24
|
-
from interlace.graph.selectors import select_models
|
|
25
|
-
from interlace.physical.annotate import annotate_plan
|
|
31
|
+
from interlace.graph.selectors import select_models
|
|
26
32
|
from interlace.plan.apply import ApplyResult
|
|
27
|
-
from interlace.plan.
|
|
33
|
+
from interlace.plan.comment import plan_markdown
|
|
34
|
+
from interlace.plan.orchestrate import compute_plan, plan_and_apply, resolve_selection, run_and_apply
|
|
28
35
|
from interlace.plan.plan import ChangeType, Plan
|
|
29
|
-
from interlace.plan.
|
|
36
|
+
from interlace.plan.table_diff import TableDiff
|
|
30
37
|
from interlace.project import Project
|
|
31
38
|
from interlace.scaffold import list_templates, scaffold_project
|
|
32
39
|
from interlace.scheduler.engine import TriggerEngine, build_triggers
|
|
@@ -243,11 +250,6 @@ def _selection(
|
|
|
243
250
|
raise typer.Exit(1) from exc
|
|
244
251
|
|
|
245
252
|
|
|
246
|
-
async def _promoted_if_needed(state: Any, environment: str, selectors: list[str]) -> dict[str, str] | None:
|
|
247
|
-
"""state:modified compares against the target environment's fingerprints."""
|
|
248
|
-
return await state.get_environment(environment) if wants_state(selectors) else None
|
|
249
|
-
|
|
250
|
-
|
|
251
253
|
@app.command()
|
|
252
254
|
def init(
|
|
253
255
|
path: Path = typer.Argument(Path("."), help="Directory to initialise."),
|
|
@@ -287,9 +289,10 @@ def plan(
|
|
|
287
289
|
select: list[str] = _SELECT,
|
|
288
290
|
forward_only: bool = _FORWARD_ONLY,
|
|
289
291
|
as_json: bool = _JSON,
|
|
292
|
+
markdown: bool = typer.Option(False, "--markdown", help="Emit a GitHub-flavoured plan comment (CI)."),
|
|
290
293
|
) -> None:
|
|
291
294
|
"""Show what apply would change in an environment."""
|
|
292
|
-
asyncio.run(_plan(environment, path, select, forward_only, as_json))
|
|
295
|
+
asyncio.run(_plan(environment, path, select, forward_only, as_json, markdown))
|
|
293
296
|
|
|
294
297
|
|
|
295
298
|
@app.command()
|
|
@@ -305,6 +308,22 @@ def apply(
|
|
|
305
308
|
asyncio.run(_apply(environment, path, select, forward_only, force, parallelism))
|
|
306
309
|
|
|
307
310
|
|
|
311
|
+
@app.command("diff")
|
|
312
|
+
def table_diff_cmd(
|
|
313
|
+
environment: str = _ENV,
|
|
314
|
+
path: Path = _PATH,
|
|
315
|
+
select: list[str] = _SELECT,
|
|
316
|
+
against: str = typer.Option("", "--against", help="Other environment (env mode)."),
|
|
317
|
+
source: str = typer.Option("", "--source", help="Left table, schema.table (table mode)."),
|
|
318
|
+
target: str = typer.Option("", "--target", help="Right table, schema.table (table mode)."),
|
|
319
|
+
on: list[str] = typer.Option([], "--on", help="Join key columns. Default: the model's key, else common columns."),
|
|
320
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Sample rows per category."),
|
|
321
|
+
as_json: bool = _JSON,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Compare a model across two environments, or two tables on the warehouse."""
|
|
324
|
+
asyncio.run(_table_diff(environment, path, select, against, source, target, on, limit, as_json))
|
|
325
|
+
|
|
326
|
+
|
|
308
327
|
@app.command()
|
|
309
328
|
def test(
|
|
310
329
|
path: Path = _PATH,
|
|
@@ -333,24 +352,31 @@ def test(
|
|
|
333
352
|
|
|
334
353
|
|
|
335
354
|
async def _plan(
|
|
336
|
-
environment: str,
|
|
355
|
+
environment: str,
|
|
356
|
+
path: Path,
|
|
357
|
+
select: list[str],
|
|
358
|
+
forward_only: bool = False,
|
|
359
|
+
as_json: bool = False,
|
|
360
|
+
markdown: bool = False,
|
|
337
361
|
) -> None:
|
|
338
362
|
project = Project.load(path)
|
|
339
363
|
compiled = project.compile()
|
|
340
364
|
state = await project.open_state()
|
|
341
365
|
engines = project.open_engines()
|
|
342
366
|
try:
|
|
343
|
-
|
|
344
|
-
result = await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if as_json:
|
|
367
|
+
selected = await resolve_selection(compiled, state, environment, select)
|
|
368
|
+
result = await compute_plan(compiled, environment, state, engines, select=selected, forward_only=forward_only)
|
|
369
|
+
if markdown:
|
|
370
|
+
typer.echo(plan_markdown(result, environment))
|
|
371
|
+
elif as_json:
|
|
349
372
|
_emit_json(_plan_dict(result, environment))
|
|
350
373
|
else:
|
|
351
374
|
_render(result, environment)
|
|
352
375
|
if result.blocking:
|
|
353
376
|
raise typer.Exit(1)
|
|
377
|
+
except SelectionError as exc:
|
|
378
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
379
|
+
raise typer.Exit(1) from exc
|
|
354
380
|
finally:
|
|
355
381
|
await state.close()
|
|
356
382
|
engines.close()
|
|
@@ -385,6 +411,132 @@ def _plan_dict(plan: Plan, environment: str) -> dict:
|
|
|
385
411
|
}
|
|
386
412
|
|
|
387
413
|
|
|
414
|
+
def _diff_dict(result: TableDiff) -> dict[str, object]:
|
|
415
|
+
schema = {
|
|
416
|
+
"added": [{"name": name, "type": dtype} for name, dtype in result.schema.added],
|
|
417
|
+
"removed": [{"name": name, "type": dtype} for name, dtype in result.schema.removed],
|
|
418
|
+
"type_changed": [
|
|
419
|
+
{"name": name, "left": left, "right": right} for name, left, right in result.schema.type_changed
|
|
420
|
+
],
|
|
421
|
+
}
|
|
422
|
+
rows = None
|
|
423
|
+
if result.rows is not None:
|
|
424
|
+
rows = {
|
|
425
|
+
"left_count": result.rows.left_count,
|
|
426
|
+
"right_count": result.rows.right_count,
|
|
427
|
+
"left_only": result.rows.left_only,
|
|
428
|
+
"right_only": result.rows.right_only,
|
|
429
|
+
"changed": result.rows.changed,
|
|
430
|
+
"matched": result.rows.matched,
|
|
431
|
+
"keys": result.rows.keys,
|
|
432
|
+
"left_only_sample": result.rows.left_only_sample,
|
|
433
|
+
"right_only_sample": result.rows.right_only_sample,
|
|
434
|
+
"changed_sample": result.rows.changed_sample,
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
"left": result.left,
|
|
438
|
+
"right": result.right,
|
|
439
|
+
"model": result.model,
|
|
440
|
+
"schema": schema,
|
|
441
|
+
"rows": rows,
|
|
442
|
+
"message": result.message,
|
|
443
|
+
"differs": result.differs,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _render_diffs(results: list[TableDiff]) -> None:
|
|
448
|
+
for result in results:
|
|
449
|
+
title = result.model or f"{result.left} vs {result.right}"
|
|
450
|
+
if result.message and result.rows is None and result.schema.empty:
|
|
451
|
+
console.print(f"[yellow]{title}[/yellow] {result.message}")
|
|
452
|
+
continue
|
|
453
|
+
table = _table(title)
|
|
454
|
+
table.add_column("Side")
|
|
455
|
+
table.add_column("Relation", style="dim")
|
|
456
|
+
table.add_row("left", result.left)
|
|
457
|
+
table.add_row("right", result.right)
|
|
458
|
+
console.print(table)
|
|
459
|
+
if result.schema.added or result.schema.removed or result.schema.type_changed:
|
|
460
|
+
for name, dtype in result.schema.added:
|
|
461
|
+
console.print(f" [green]+[/] column {name} {dtype}")
|
|
462
|
+
for name, dtype in result.schema.removed:
|
|
463
|
+
console.print(f" [red]-[/] column {name} {dtype}")
|
|
464
|
+
for name, left, right in result.schema.type_changed:
|
|
465
|
+
console.print(f" [yellow]~[/] column {name} {left} → {right}")
|
|
466
|
+
if result.message:
|
|
467
|
+
console.print(f" [yellow]{result.message}[/yellow]")
|
|
468
|
+
if result.rows is None:
|
|
469
|
+
continue
|
|
470
|
+
rows = result.rows
|
|
471
|
+
console.print(
|
|
472
|
+
f" rows left={rows.left_count} right={rows.right_count} "
|
|
473
|
+
f"only-left={rows.left_only} only-right={rows.right_only} changed={rows.changed} "
|
|
474
|
+
f"matched={rows.matched} keys={','.join(rows.keys) or '—'}"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
async def _table_diff(
|
|
479
|
+
environment: str,
|
|
480
|
+
path: Path,
|
|
481
|
+
select: list[str],
|
|
482
|
+
against: str,
|
|
483
|
+
source: str,
|
|
484
|
+
target: str,
|
|
485
|
+
on: list[str],
|
|
486
|
+
limit: int,
|
|
487
|
+
as_json: bool,
|
|
488
|
+
) -> None:
|
|
489
|
+
from interlace.plan.table_diff import diff_environments, diff_tables, parse_table_ref
|
|
490
|
+
|
|
491
|
+
if bool(source) != bool(target):
|
|
492
|
+
console.print("[red]table mode needs both --source and --target[/red]")
|
|
493
|
+
raise typer.Exit(2)
|
|
494
|
+
if not source and not against:
|
|
495
|
+
console.print("[red]env mode needs --against ENV (or pass --source/--target)[/red]")
|
|
496
|
+
raise typer.Exit(2)
|
|
497
|
+
project = Project.load(path)
|
|
498
|
+
compiled = project.compile()
|
|
499
|
+
engines = project.open_engines()
|
|
500
|
+
state = await project.open_state()
|
|
501
|
+
try:
|
|
502
|
+
if source:
|
|
503
|
+
result = await diff_tables(
|
|
504
|
+
engines.get(),
|
|
505
|
+
parse_table_ref(source),
|
|
506
|
+
parse_table_ref(target),
|
|
507
|
+
keys=on or None,
|
|
508
|
+
sample=limit,
|
|
509
|
+
)
|
|
510
|
+
results = [result]
|
|
511
|
+
else:
|
|
512
|
+
selected = await resolve_selection(compiled, state, environment, select)
|
|
513
|
+
results = await diff_environments(
|
|
514
|
+
compiled,
|
|
515
|
+
left_env=environment,
|
|
516
|
+
right_env=against,
|
|
517
|
+
store=state,
|
|
518
|
+
engines=engines,
|
|
519
|
+
select=selected,
|
|
520
|
+
keys=on or None,
|
|
521
|
+
sample=limit,
|
|
522
|
+
)
|
|
523
|
+
if as_json:
|
|
524
|
+
_emit_json([_diff_dict(item) for item in results])
|
|
525
|
+
else:
|
|
526
|
+
if not results:
|
|
527
|
+
console.print("No comparable models (ephemeral/file outputs are skipped).")
|
|
528
|
+
else:
|
|
529
|
+
_render_diffs(results)
|
|
530
|
+
if any(item.differs for item in results):
|
|
531
|
+
raise typer.Exit(1)
|
|
532
|
+
except PlanError as exc:
|
|
533
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
534
|
+
raise typer.Exit(1) from exc
|
|
535
|
+
finally:
|
|
536
|
+
await state.close()
|
|
537
|
+
engines.close()
|
|
538
|
+
|
|
539
|
+
|
|
388
540
|
async def _apply(
|
|
389
541
|
environment: str,
|
|
390
542
|
path: Path,
|
|
@@ -393,50 +545,64 @@ async def _apply(
|
|
|
393
545
|
force: bool = False,
|
|
394
546
|
parallelism: int = 0,
|
|
395
547
|
) -> None:
|
|
548
|
+
from interlace.exceptions import BreakingPlanError
|
|
549
|
+
|
|
396
550
|
project = Project.load(path)
|
|
397
551
|
compiled = project.compile()
|
|
398
552
|
engines = project.open_engines()
|
|
399
553
|
state = await project.open_state()
|
|
554
|
+
tracker: _BuildProgress | None = _BuildProgress() if console.is_terminal else None
|
|
555
|
+
started = False
|
|
400
556
|
try:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
557
|
+
|
|
558
|
+
async def prepare() -> None:
|
|
559
|
+
# Stream-fed projects must build without the daemon ever having run:
|
|
560
|
+
# declared stream tables are ensured (empty) so models reading them work.
|
|
561
|
+
if project.streams:
|
|
562
|
+
await ensure_stream_tables(project.streams, engines.get())
|
|
563
|
+
|
|
564
|
+
def on_plan(plan: Plan) -> None:
|
|
565
|
+
nonlocal started, tracker
|
|
566
|
+
_render(plan, environment)
|
|
567
|
+
if tracker is not None and not plan.backfills:
|
|
568
|
+
tracker = None
|
|
569
|
+
if tracker is not None:
|
|
570
|
+
tracker.progress.start()
|
|
571
|
+
started = True
|
|
572
|
+
|
|
573
|
+
def on_progress(model: str, event: str, detail: dict[str, Any]) -> None:
|
|
574
|
+
if tracker is not None:
|
|
575
|
+
tracker(model, event, detail)
|
|
576
|
+
|
|
420
577
|
try:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
578
|
+
_plan_result, result = await plan_and_apply(
|
|
579
|
+
compiled,
|
|
580
|
+
environment=environment,
|
|
581
|
+
project=project,
|
|
582
|
+
engines=engines,
|
|
583
|
+
state=state,
|
|
584
|
+
lock_owner=f"cli:{os.getpid()}:apply",
|
|
585
|
+
selectors=select,
|
|
586
|
+
forward_only=forward_only,
|
|
587
|
+
force=force,
|
|
588
|
+
parallelism=parallelism or None,
|
|
589
|
+
on_progress=on_progress,
|
|
590
|
+
on_plan=on_plan,
|
|
591
|
+
prepare=prepare,
|
|
592
|
+
)
|
|
593
|
+
except BreakingPlanError as exc:
|
|
594
|
+
console.print(
|
|
595
|
+
f"[red]plan has breaking changes ({', '.join(exc.names)}); re-run with --force to proceed[/red]"
|
|
596
|
+
)
|
|
597
|
+
raise typer.Exit(1) from exc
|
|
434
598
|
except CheckError as exc:
|
|
435
599
|
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
436
600
|
raise typer.Exit(1) from exc
|
|
437
601
|
except LockError as exc:
|
|
438
602
|
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
439
603
|
raise typer.Exit(1) from exc
|
|
604
|
+
if result is None:
|
|
605
|
+
return
|
|
440
606
|
_render_build_results(result, compiled)
|
|
441
607
|
_render_checks(result)
|
|
442
608
|
await _render_empty_incrementals(result, compiled, engines)
|
|
@@ -444,6 +610,8 @@ async def _apply(
|
|
|
444
610
|
f"[green]Built {len(set(result.built))} model(s); promoted {result.promoted} to '{environment}'.[/green]"
|
|
445
611
|
)
|
|
446
612
|
finally:
|
|
613
|
+
if started and tracker is not None:
|
|
614
|
+
tracker.progress.stop()
|
|
447
615
|
await state.close()
|
|
448
616
|
engines.close()
|
|
449
617
|
|
|
@@ -509,34 +677,44 @@ async def _execute(
|
|
|
509
677
|
compiled = project.compile()
|
|
510
678
|
engines = project.open_engines()
|
|
511
679
|
state = await project.open_state()
|
|
680
|
+
tracker: _BuildProgress | None = _BuildProgress() if console.is_terminal else None
|
|
681
|
+
started_bar = False
|
|
512
682
|
try:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
683
|
+
|
|
684
|
+
async def prepare() -> None:
|
|
685
|
+
if project.streams: # as in _apply: stream tables must exist daemon or not
|
|
686
|
+
await ensure_stream_tables(project.streams, engines.get())
|
|
687
|
+
|
|
688
|
+
def on_plan(plan: Plan) -> None:
|
|
689
|
+
nonlocal started_bar, tracker
|
|
690
|
+
if tracker is not None and not plan.backfills:
|
|
691
|
+
tracker = None
|
|
692
|
+
if tracker is not None:
|
|
693
|
+
tracker.progress.start()
|
|
694
|
+
started_bar = True
|
|
695
|
+
|
|
696
|
+
def on_progress(model: str, event: str, detail: dict[str, Any]) -> None:
|
|
697
|
+
if tracker is not None:
|
|
698
|
+
tracker(model, event, detail)
|
|
699
|
+
|
|
525
700
|
started = time.perf_counter()
|
|
526
701
|
try:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
702
|
+
plan_result, result = await run_and_apply(
|
|
703
|
+
compiled,
|
|
704
|
+
environment=environment,
|
|
705
|
+
project=project,
|
|
706
|
+
engines=engines,
|
|
707
|
+
state=state,
|
|
708
|
+
lock_owner=f"cli:{os.getpid()}:run",
|
|
709
|
+
selectors=select,
|
|
710
|
+
start=window_start,
|
|
711
|
+
end=window_end,
|
|
712
|
+
restate=restate,
|
|
713
|
+
parallelism=parallelism or None,
|
|
714
|
+
on_progress=on_progress,
|
|
715
|
+
on_plan=on_plan,
|
|
716
|
+
prepare=prepare,
|
|
717
|
+
)
|
|
540
718
|
except CheckError as exc:
|
|
541
719
|
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
542
720
|
raise typer.Exit(1) from exc
|
|
@@ -553,6 +731,8 @@ async def _execute(
|
|
|
553
731
|
f"promoted {result.promoted} to '{environment}'.[/green]"
|
|
554
732
|
)
|
|
555
733
|
finally:
|
|
734
|
+
if started_bar and tracker is not None:
|
|
735
|
+
tracker.progress.stop()
|
|
556
736
|
await state.close()
|
|
557
737
|
engines.close()
|
|
558
738
|
|
|
@@ -42,6 +42,18 @@ class PlanError(InterlaceError):
|
|
|
42
42
|
"""A plan could not be computed or applied."""
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
class BreakingPlanError(PlanError):
|
|
46
|
+
"""The plan has breaking changes and ``force`` was not set."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, names: list[str]) -> None:
|
|
49
|
+
listed = ", ".join(names)
|
|
50
|
+
super().__init__(
|
|
51
|
+
f"plan has breaking changes ({listed}); pass force to proceed",
|
|
52
|
+
details={"kind": "breaking", "names": names},
|
|
53
|
+
)
|
|
54
|
+
self.names = names
|
|
55
|
+
|
|
56
|
+
|
|
45
57
|
class ExecutionError(InterlaceError):
|
|
46
58
|
"""A model failed while building (its query, its Python function, or the load)."""
|
|
47
59
|
|
|
@@ -23,7 +23,12 @@ from interlace.sinks import target_ref
|
|
|
23
23
|
from interlace.state.snapshot import Snapshot
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class
|
|
26
|
+
class _RelationStore(Protocol):
|
|
27
|
+
async def get_environment(self, environment: str) -> dict[str, str]: ...
|
|
28
|
+
async def get_snapshot(self, name: str, fingerprint: str) -> Snapshot | None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _InspectStore(_RelationStore, Protocol):
|
|
27
32
|
"""The control-plane reads a preview needs, beyond the plan/apply store slice."""
|
|
28
33
|
|
|
29
34
|
async def latest_model_build(self, model: str) -> dict[str, object] | None: ...
|
|
@@ -118,7 +123,7 @@ async def last_build(store: _InspectStore, name: str) -> LastBuild | None:
|
|
|
118
123
|
|
|
119
124
|
|
|
120
125
|
async def _built_table(
|
|
121
|
-
model: CompiledModel, store:
|
|
126
|
+
model: CompiledModel, store: _RelationStore, engine: EngineAdapter, environment: str
|
|
122
127
|
) -> tuple[TableRef, str] | None:
|
|
123
128
|
"""The relation a preview should read, and why (environment view or snapshot)."""
|
|
124
129
|
if model.materialise == "ephemeral" or model.materialise == "file":
|
|
@@ -144,6 +149,14 @@ async def _built_table(
|
|
|
144
149
|
return None
|
|
145
150
|
|
|
146
151
|
|
|
152
|
+
async def model_relation(
|
|
153
|
+
model: CompiledModel, store: _RelationStore, engine: EngineAdapter, environment: str
|
|
154
|
+
) -> TableRef | None:
|
|
155
|
+
"""The table a preview or table-diff should read for ``model`` in ``environment``."""
|
|
156
|
+
found = await _built_table(model, store, engine, environment)
|
|
157
|
+
return None if found is None else found[0]
|
|
158
|
+
|
|
159
|
+
|
|
147
160
|
def _unavailable(model: CompiledModel) -> str:
|
|
148
161
|
if model.materialise == "ephemeral":
|
|
149
162
|
return "ephemeral models are inlined into the models that read them"
|
|
@@ -17,18 +17,13 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Any, cast
|
|
18
18
|
|
|
19
19
|
from interlace import __version__
|
|
20
|
-
from interlace.dsl.dynamic import apply_with_registrations
|
|
21
20
|
from interlace.exceptions import InterlaceError
|
|
22
21
|
from interlace.graph.column_lineage import column_lineage
|
|
23
22
|
from interlace.graph.project import CompiledProject
|
|
24
|
-
from interlace.graph.selectors import select_models, wants_state
|
|
25
23
|
from interlace.inspect import failing_rows, preview_model
|
|
26
|
-
from interlace.
|
|
27
|
-
from interlace.plan.differ import diff
|
|
24
|
+
from interlace.plan.orchestrate import compute_plan, plan_and_apply, resolve_selection
|
|
28
25
|
from interlace.project import Project
|
|
29
26
|
from interlace.query import prepare_readonly
|
|
30
|
-
from interlace.state.locks import hold_apply_lock
|
|
31
|
-
from interlace.state.snapshot import ChangeCategory
|
|
32
27
|
from interlace.streaming import ensure_stream_tables
|
|
33
28
|
|
|
34
29
|
_PROTOCOL = "2024-11-05"
|
|
@@ -158,10 +153,10 @@ async def _plan(path: Path, args: dict[str, Any]) -> Any:
|
|
|
158
153
|
try:
|
|
159
154
|
environment = _env(project, args)
|
|
160
155
|
selectors = [str(item) for item in args.get("selectors") or []]
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
selected = await resolve_selection(compiled, state, environment, selectors)
|
|
157
|
+
plan = await compute_plan(
|
|
158
|
+
compiled, environment, state, engines, select=selected, forward_only=bool(args.get("forward_only"))
|
|
159
|
+
)
|
|
165
160
|
finally:
|
|
166
161
|
await state.close()
|
|
167
162
|
engines.close()
|
|
@@ -187,34 +182,28 @@ async def _apply(path: Path, args: dict[str, Any]) -> Any:
|
|
|
187
182
|
project, compiled, state, engines = await _open(path)
|
|
188
183
|
try:
|
|
189
184
|
environment = _env(project, args)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
plan = await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
compiled=compiled,
|
|
208
|
-
engines=engines,
|
|
209
|
-
state=state,
|
|
210
|
-
base_path=project.root,
|
|
211
|
-
parallelism=project.config.parallelism,
|
|
212
|
-
connections=project.config.connections,
|
|
213
|
-
project=project,
|
|
214
|
-
)
|
|
185
|
+
|
|
186
|
+
async def prepare() -> None:
|
|
187
|
+
if project.streams:
|
|
188
|
+
await ensure_stream_tables(project.streams, engines.get())
|
|
189
|
+
|
|
190
|
+
plan, result = await plan_and_apply(
|
|
191
|
+
compiled,
|
|
192
|
+
environment=environment,
|
|
193
|
+
project=project,
|
|
194
|
+
engines=engines,
|
|
195
|
+
state=state,
|
|
196
|
+
lock_owner=f"mcp:{os.getpid()}",
|
|
197
|
+
selectors=[str(item) for item in args.get("selectors") or []],
|
|
198
|
+
forward_only=bool(args.get("forward_only")),
|
|
199
|
+
force=bool(args.get("force")),
|
|
200
|
+
prepare=prepare,
|
|
201
|
+
)
|
|
215
202
|
finally:
|
|
216
203
|
await state.close()
|
|
217
204
|
engines.close()
|
|
205
|
+
if result is None:
|
|
206
|
+
return {"environment": environment, "built": [], "promoted": 0}
|
|
218
207
|
return {
|
|
219
208
|
"environment": environment,
|
|
220
209
|
"built": result.built,
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from interlace.plan.apply import ApplyResult, apply
|
|
6
6
|
from interlace.plan.differ import diff, snapshot_of
|
|
7
|
+
from interlace.plan.orchestrate import compute_plan, plan_and_apply, run_and_apply
|
|
7
8
|
from interlace.plan.plan import BackfillTask, ChangeType, ModelChange, Plan, TransferEdge, ViewSwap, env_view
|
|
8
9
|
from interlace.plan.run import run_plan
|
|
9
10
|
|
|
@@ -16,8 +17,11 @@ __all__ = [
|
|
|
16
17
|
"TransferEdge",
|
|
17
18
|
"ViewSwap",
|
|
18
19
|
"apply",
|
|
20
|
+
"compute_plan",
|
|
19
21
|
"diff",
|
|
20
22
|
"env_view",
|
|
23
|
+
"plan_and_apply",
|
|
24
|
+
"run_and_apply",
|
|
21
25
|
"run_plan",
|
|
22
26
|
"snapshot_of",
|
|
23
27
|
]
|