interlaced 2.8.1__tar.gz → 2.8.2__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.8.1/src/interlaced.egg-info → interlaced-2.8.2}/PKG-INFO +1 -1
- {interlaced-2.8.1 → interlaced-2.8.2}/pyproject.toml +1 -1
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/cdc/publish.py +2 -2
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/checks/runner.py +47 -2
- interlaced-2.8.2/src/interlace/cli/build.py +496 -0
- interlaced-2.8.2/src/interlace/cli/inspect.py +522 -0
- interlaced-2.8.2/src/interlace/cli/main.py +29 -0
- interlaced-2.8.2/src/interlace/cli/operate.py +578 -0
- interlaced-2.8.2/src/interlace/cli/support.py +310 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/dsl/decorators.py +3 -2
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/base.py +25 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/duckdb.py +8 -1
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/quack.py +5 -1
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/spark.py +2 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/inspect.py +38 -0
- interlaced-2.8.2/src/interlace/ir/layout.py +30 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/mcp_server.py +10 -14
- interlaced-2.8.2/src/interlace/physical/changes.py +45 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/physical/drift.py +8 -3
- {interlaced-2.8.1/src/interlace/physical → interlaced-2.8.2/src/interlace/plan}/annotate.py +3 -2
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/apply.py +237 -174
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/differ.py +9 -11
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/orchestrate.py +1 -1
- interlaced-2.8.2/src/interlace/plan/payload.py +86 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/plan.py +41 -64
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/run.py +11 -24
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/project.py +24 -2
- interlaced-2.8.2/src/interlace/scheduler/daemon.py +217 -0
- interlaced-2.8.2/src/interlace/service/app.py +383 -0
- interlaced-2.8.2/src/interlace/service/present.py +235 -0
- interlaced-2.8.2/src/interlace/service/routes/__init__.py +1 -0
- interlaced-2.8.2/src/interlace/service/routes/admin.py +259 -0
- interlaced-2.8.2/src/interlace/service/routes/catalog.py +243 -0
- interlaced-2.8.2/src/interlace/service/routes/environments.py +75 -0
- interlaced-2.8.2/src/interlace/service/routes/models.py +183 -0
- interlaced-2.8.2/src/interlace/service/routes/plan.py +187 -0
- interlaced-2.8.2/src/interlace/service/routes/runs.py +149 -0
- interlaced-2.8.2/src/interlace/service/routes/streams.py +242 -0
- interlaced-2.8.2/src/interlace/state/advisory.py +84 -0
- interlaced-2.8.2/src/interlace/state/cdc.py +70 -0
- interlaced-2.8.2/src/interlace/state/checks.py +62 -0
- interlaced-2.8.2/src/interlace/state/codec.py +127 -0
- interlaced-2.8.2/src/interlace/state/conn.py +40 -0
- interlaced-2.8.2/src/interlace/state/events.py +129 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/state/interval.py +13 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/state/janitor.py +31 -6
- interlaced-2.8.2/src/interlace/state/keys.py +92 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/state/locks.py +4 -4
- interlaced-2.8.2/src/interlace/state/queue.py +291 -0
- interlaced-2.8.2/src/interlace/state/schema.py +182 -0
- interlaced-2.8.2/src/interlace/state/snapshots.py +353 -0
- interlaced-2.8.2/src/interlace/state/store.py +377 -0
- interlaced-2.8.2/src/interlace/state/triggers.py +35 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/__init__.py +19 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/append.py +3 -7
- interlaced-2.8.2/src/interlace/strategies/base.py +141 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/full_merge.py +43 -27
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/hash_merge.py +5 -7
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/incremental.py +7 -9
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/merge.py +10 -14
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/replace.py +8 -11
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/replace_in_place.py +3 -7
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/scd.py +10 -10
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/strategies/view.py +7 -7
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/streaming/materializer.py +43 -27
- {interlaced-2.8.1 → interlaced-2.8.2/src/interlaced.egg-info}/PKG-INFO +1 -1
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlaced.egg-info/SOURCES.txt +29 -1
- interlaced-2.8.1/src/interlace/cli/main.py +0 -1867
- interlaced-2.8.1/src/interlace/service/app.py +0 -1988
- interlaced-2.8.1/src/interlace/state/store.py +0 -1413
- interlaced-2.8.1/src/interlace/strategies/base.py +0 -89
- {interlaced-2.8.1 → interlaced-2.8.2}/LICENSE +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/MANIFEST.in +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/setup.cfg +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/cdc/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/cdc/decode.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/cdc/slot.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/checks/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/checks/builtin.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/checks/spec.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/cli/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/config/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/config/config.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/connections.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/contracts.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/dsl/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/dsl/discovery.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/dsl/dynamic.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/dsl/sql_config.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/adbc.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/bigquery.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/postgres.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/redshift.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/registry.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/engines/snowflake.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/exceptions.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/graph/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/graph/column_lineage.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/graph/dag.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/graph/project.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/graph/selectors.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/inputs.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/ir/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/ir/canonicalize.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/ir/fingerprint.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/ir/macros.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/ir/relation.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/physical/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/physical/ddl.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/physical/reconcile.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/physical/spec.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/comment.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/resolve.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/plan/table_diff.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/py.typed +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/query.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/runtime/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/runtime/handles.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/runtime/python_model.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/scaffold.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/scheduler/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/scheduler/engine.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/scheduler/triggers.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/scheduler/worker.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/auth.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/types.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/app.css +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/favicon.svg +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/index.html +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/api.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/app.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/dag.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/timeline.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/ui.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/checks.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/environments.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/lineage.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/models.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/overview.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/plan.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/query.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/runs.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/streams.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/service/ui/js/views/system.js +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/sinks.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/sources/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/sources/auth.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/sources/rest.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/state/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/state/snapshot.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/streaming/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/streaming/log.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/streaming/schema.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/docker-compose.yml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/init/seed.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/interlace.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/models/orders.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/models/orders_by_status.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/models/orders_stream.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/push.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/cdc/template.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/generate.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/interlace.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/models/events.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_type.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/models/top_users.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/models/user_spend.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/events/template.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/github/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/github/interlace.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/github/models/github_issues.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/github/template.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/docker-compose.yml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/init/seed.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/interlace.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/postgres/template.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/README.md +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/interlace.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/templates/quickstart/template.yaml +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/testing/__init__.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlace/testing/golden.py +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlaced.egg-info/dependency_links.txt +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlaced.egg-info/entry_points.txt +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/src/interlaced.egg-info/requires.txt +0 -0
- {interlaced-2.8.1 → interlaced-2.8.2}/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.8.
|
|
3
|
+
Version: 2.8.2
|
|
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.8.
|
|
7
|
+
version = "2.8.2"
|
|
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"
|
|
@@ -18,10 +18,10 @@ async def publish_changes(log: StreamLog, store: SqliteStateStore, stream: str,
|
|
|
18
18
|
stream,
|
|
19
19
|
[Event(payload=payload_of(change), idempotency_key=f"cdc:{change.lsn}") for change in changes],
|
|
20
20
|
)
|
|
21
|
-
await store.cdc_note_pending(stream, list(zip(result.offsets, (change.lsn for change in changes), strict=True)))
|
|
21
|
+
await store.cdc.cdc_note_pending(stream, list(zip(result.offsets, (change.lsn for change in changes), strict=True)))
|
|
22
22
|
return result.offsets
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
async def confirm_flushed(store: SqliteStateStore, stream: str, watermark: int) -> str | None:
|
|
26
26
|
"""Advance the stored LSN up to the offset ``flush_streams`` has committed."""
|
|
27
|
-
return await store.cdc_advance(stream, watermark)
|
|
27
|
+
return await store.cdc.cdc_advance(stream, watermark)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import inspect
|
|
14
|
-
from collections.abc import Mapping
|
|
14
|
+
from collections.abc import Mapping, Sequence
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
17
|
import pyarrow as pa
|
|
@@ -21,10 +21,13 @@ from interlace.checks.builtin import build_check_query
|
|
|
21
21
|
from interlace.checks.spec import CheckSpec
|
|
22
22
|
from interlace.dsl.decorators import CheckDef
|
|
23
23
|
from interlace.engines.base import EngineAdapter
|
|
24
|
-
from interlace.
|
|
24
|
+
from interlace.engines.registry import EngineRegistry
|
|
25
|
+
from interlace.exceptions import DefinitionError, PlanError
|
|
25
26
|
from interlace.graph.project import CompiledModel, CompiledProject
|
|
26
27
|
from interlace.ir.relation import TableRef
|
|
27
28
|
from interlace.runtime.handles import RelationHandle
|
|
29
|
+
from interlace.sinks import target_ref
|
|
30
|
+
from interlace.state.store import SqliteStateStore
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
@dataclass(frozen=True)
|
|
@@ -101,3 +104,45 @@ async def run_checks(
|
|
|
101
104
|
outcomes = [await _run_declared(spec, model, compiled, engine, table, physical) for spec in model.checks]
|
|
102
105
|
outcomes += [await _run_python(check, engine, table, model.name) for check in python_checks]
|
|
103
106
|
return outcomes
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def run_promoted_checks(
|
|
110
|
+
compiled: CompiledProject,
|
|
111
|
+
store: SqliteStateStore,
|
|
112
|
+
engines: EngineRegistry,
|
|
113
|
+
environment: str,
|
|
114
|
+
selectors: Sequence[str] = (),
|
|
115
|
+
) -> tuple[list[CheckOutcome], list[str]]:
|
|
116
|
+
"""Re-check an environment's promoted tables. ``state:`` selectors see the ledger.
|
|
117
|
+
|
|
118
|
+
Returns ``(outcomes, skipped)``. Models that were never promoted in this
|
|
119
|
+
environment are skipped. A missing environment is a plan error.
|
|
120
|
+
"""
|
|
121
|
+
from interlace.plan.orchestrate import resolve_selection
|
|
122
|
+
|
|
123
|
+
chosen = await resolve_selection(compiled, store, environment, selectors)
|
|
124
|
+
promoted = await store.get_environment(environment)
|
|
125
|
+
if not promoted:
|
|
126
|
+
raise PlanError(f"no environment {environment!r} — run `interlace apply` first")
|
|
127
|
+
snapshots = await store.get_snapshots(promoted.items())
|
|
128
|
+
physical = {name: snapshot.physical_table for (name, _), snapshot in snapshots.items()}
|
|
129
|
+
outcomes: list[CheckOutcome] = []
|
|
130
|
+
skipped: list[str] = []
|
|
131
|
+
for name, model in compiled.models.items():
|
|
132
|
+
if chosen is not None and name not in chosen:
|
|
133
|
+
continue
|
|
134
|
+
if not model.checks and not compiled.python_checks.get(name):
|
|
135
|
+
continue
|
|
136
|
+
if model.materialise == "file" or (model.is_terminal and environment not in model.environments):
|
|
137
|
+
continue
|
|
138
|
+
snapshot = snapshots.get((name, promoted.get(name, "")))
|
|
139
|
+
if snapshot is None:
|
|
140
|
+
skipped.append(name)
|
|
141
|
+
continue
|
|
142
|
+
engine = engines.require(snapshot.engine, model=name)
|
|
143
|
+
check_table = target_ref(model.target or "") if model.materialise == "table" else snapshot.physical_table
|
|
144
|
+
results = await run_checks(model, compiled, engine, check_table, compiled.python_checks.get(name, ()), physical)
|
|
145
|
+
if results:
|
|
146
|
+
await store.record_check_results(environment, snapshot.fingerprint, results)
|
|
147
|
+
outcomes.extend(results)
|
|
148
|
+
return outcomes, skipped
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Plan, apply, run, diff, test, and init."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.markup import escape
|
|
13
|
+
|
|
14
|
+
from interlace.cli.support import (
|
|
15
|
+
_END,
|
|
16
|
+
_ENV,
|
|
17
|
+
_FORWARD_ONLY,
|
|
18
|
+
_JSON,
|
|
19
|
+
_PARALLELISM,
|
|
20
|
+
_PATH,
|
|
21
|
+
_SELECT,
|
|
22
|
+
_START,
|
|
23
|
+
_BuildProgress,
|
|
24
|
+
_emit_json,
|
|
25
|
+
_render,
|
|
26
|
+
_render_build_results,
|
|
27
|
+
_render_checks,
|
|
28
|
+
_render_empty_incrementals,
|
|
29
|
+
_render_warnings,
|
|
30
|
+
_selection,
|
|
31
|
+
_table,
|
|
32
|
+
_window,
|
|
33
|
+
app,
|
|
34
|
+
console,
|
|
35
|
+
)
|
|
36
|
+
from interlace.exceptions import (
|
|
37
|
+
CheckError,
|
|
38
|
+
ConfigurationError,
|
|
39
|
+
InterlaceError,
|
|
40
|
+
LockError,
|
|
41
|
+
PlanError,
|
|
42
|
+
SelectionError,
|
|
43
|
+
)
|
|
44
|
+
from interlace.plan.comment import plan_markdown
|
|
45
|
+
from interlace.plan.orchestrate import compute_plan, plan_and_apply, resolve_selection, run_and_apply
|
|
46
|
+
from interlace.plan.plan import Plan
|
|
47
|
+
from interlace.plan.table_diff import TableDiff
|
|
48
|
+
from interlace.project import Project, open_session
|
|
49
|
+
from interlace.scaffold import list_templates, scaffold_project
|
|
50
|
+
from interlace.streaming import ensure_stream_tables
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def init(
|
|
55
|
+
path: Path = typer.Argument(Path("."), help="Directory to initialise."),
|
|
56
|
+
name: str = typer.Option("", "--name", "-n", help="Project name (defaults to the directory name)."),
|
|
57
|
+
template: str = typer.Option("quickstart", "--template", "-t", help="Which starter to scaffold (see --list)."),
|
|
58
|
+
show_list: bool = typer.Option(False, "--list", help="List available templates and exit."),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Scaffold a new interlace project from a template."""
|
|
61
|
+
if show_list:
|
|
62
|
+
table = _table("Templates")
|
|
63
|
+
table.add_column("Template")
|
|
64
|
+
table.add_column("Description", style="dim")
|
|
65
|
+
table.add_column("Needs", style="dim")
|
|
66
|
+
for info in list_templates():
|
|
67
|
+
# escape: a description may contain [sources]-style brackets Rich would eat as markup
|
|
68
|
+
table.add_row(info.name, escape(info.description), ", ".join(info.requires_env) or "—")
|
|
69
|
+
console.print(table)
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
written = scaffold_project(path, name or None, template)
|
|
73
|
+
except ConfigurationError as exc:
|
|
74
|
+
console.print(f"[red]{escape(exc.message)}[/red] ({exc.details.get('path', '')})")
|
|
75
|
+
raise typer.Exit(1) from exc
|
|
76
|
+
console.print(f"[green]Initialised interlace project in {path}[/green] [dim](template: {template})[/dim]")
|
|
77
|
+
for written_path in written:
|
|
78
|
+
console.print(f" + {written_path}")
|
|
79
|
+
needs = next((t.requires_env for t in list_templates() if t.name == template), ())
|
|
80
|
+
if needs:
|
|
81
|
+
console.print(f"\n[yellow]Set before applying:[/yellow] {', '.join(needs)}")
|
|
82
|
+
console.print("\nNext: [bold]interlace apply[/bold] (or --env dev for a sandbox)")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command()
|
|
86
|
+
def plan(
|
|
87
|
+
environment: str = _ENV,
|
|
88
|
+
path: Path = _PATH,
|
|
89
|
+
select: list[str] = _SELECT,
|
|
90
|
+
forward_only: bool = _FORWARD_ONLY,
|
|
91
|
+
as_json: bool = _JSON,
|
|
92
|
+
markdown: bool = typer.Option(False, "--markdown", help="Emit a GitHub-flavoured plan comment (CI)."),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Show what apply would change in an environment."""
|
|
95
|
+
asyncio.run(_plan(environment, path, select, forward_only, as_json, markdown))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command()
|
|
99
|
+
def apply(
|
|
100
|
+
environment: str = _ENV,
|
|
101
|
+
path: Path = _PATH,
|
|
102
|
+
select: list[str] = _SELECT,
|
|
103
|
+
forward_only: bool = _FORWARD_ONLY,
|
|
104
|
+
force: bool = typer.Option(False, "--force", help="Proceed even when the plan contains breaking changes."),
|
|
105
|
+
parallelism: int = _PARALLELISM,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Build changed models and promote the environment."""
|
|
108
|
+
asyncio.run(_apply(environment, path, select, forward_only, force, parallelism))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("diff")
|
|
112
|
+
def table_diff_cmd(
|
|
113
|
+
environment: str = _ENV,
|
|
114
|
+
path: Path = _PATH,
|
|
115
|
+
select: list[str] = _SELECT,
|
|
116
|
+
against: str = typer.Option("", "--against", help="Other environment (env mode)."),
|
|
117
|
+
source: str = typer.Option("", "--source", help="Left table, schema.table (table mode)."),
|
|
118
|
+
target: str = typer.Option("", "--target", help="Right table, schema.table (table mode)."),
|
|
119
|
+
on: list[str] = typer.Option([], "--on", help="Join key columns. Default: the model's key, else common columns."),
|
|
120
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Sample rows per category."),
|
|
121
|
+
as_json: bool = _JSON,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Compare a model across two environments, or two tables on the warehouse."""
|
|
124
|
+
asyncio.run(_table_diff(environment, path, select, against, source, target, on, limit, as_json))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def test(
|
|
129
|
+
path: Path = _PATH,
|
|
130
|
+
select: list[str] = _SELECT,
|
|
131
|
+
update_golden: bool = typer.Option(False, "--update-golden", help="Rewrite tests/golden from the actual result."),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Build selected models in an ephemeral DuckDB and diff tests/golden."""
|
|
134
|
+
project = Project.load(path)
|
|
135
|
+
compiled = project.compile()
|
|
136
|
+
from interlace.testing.golden import run_fixture_tests
|
|
137
|
+
|
|
138
|
+
chosen = _selection(compiled, select) if select else None
|
|
139
|
+
try:
|
|
140
|
+
report = run_fixture_tests(compiled, project.root, select=chosen, update=update_golden)
|
|
141
|
+
except InterlaceError as exc:
|
|
142
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
143
|
+
raise typer.Exit(1) from exc
|
|
144
|
+
if update_golden:
|
|
145
|
+
console.print(f"[green]updated {len(report.passed)} golden file(s)[/green]")
|
|
146
|
+
return
|
|
147
|
+
for message in report.messages:
|
|
148
|
+
console.print(f"[red]{escape(message)}[/red]")
|
|
149
|
+
if report.messages:
|
|
150
|
+
raise typer.Exit(1)
|
|
151
|
+
console.print(f"[green]{len(report.passed)} golden test(s) passed[/green]")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _plan(
|
|
155
|
+
environment: str,
|
|
156
|
+
path: Path,
|
|
157
|
+
select: list[str],
|
|
158
|
+
forward_only: bool = False,
|
|
159
|
+
as_json: bool = False,
|
|
160
|
+
markdown: bool = False,
|
|
161
|
+
) -> None:
|
|
162
|
+
async with open_session(path) as (_project, compiled, state, engines):
|
|
163
|
+
try:
|
|
164
|
+
selected = await resolve_selection(compiled, state, environment, select)
|
|
165
|
+
result = await compute_plan(
|
|
166
|
+
compiled, environment, state, engines, select=selected, forward_only=forward_only
|
|
167
|
+
)
|
|
168
|
+
if markdown:
|
|
169
|
+
typer.echo(plan_markdown(result, environment))
|
|
170
|
+
elif as_json:
|
|
171
|
+
previous = await state.get_snapshots(
|
|
172
|
+
(change.name, change.previous_fingerprint)
|
|
173
|
+
for change in result.changes
|
|
174
|
+
if change.previous_fingerprint is not None
|
|
175
|
+
)
|
|
176
|
+
from interlace.plan.payload import plan_document
|
|
177
|
+
|
|
178
|
+
_emit_json(plan_document(result, compiled, previous, environment).as_dict())
|
|
179
|
+
else:
|
|
180
|
+
_render(result, environment)
|
|
181
|
+
if result.blocking:
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
except SelectionError as exc:
|
|
184
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
185
|
+
raise typer.Exit(1) from exc
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _diff_dict(result: TableDiff) -> dict[str, object]:
|
|
189
|
+
schema = {
|
|
190
|
+
"added": [{"name": name, "type": dtype} for name, dtype in result.schema.added],
|
|
191
|
+
"removed": [{"name": name, "type": dtype} for name, dtype in result.schema.removed],
|
|
192
|
+
"type_changed": [
|
|
193
|
+
{"name": name, "left": left, "right": right} for name, left, right in result.schema.type_changed
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
rows = None
|
|
197
|
+
if result.rows is not None:
|
|
198
|
+
rows = {
|
|
199
|
+
"left_count": result.rows.left_count,
|
|
200
|
+
"right_count": result.rows.right_count,
|
|
201
|
+
"left_only": result.rows.left_only,
|
|
202
|
+
"right_only": result.rows.right_only,
|
|
203
|
+
"changed": result.rows.changed,
|
|
204
|
+
"matched": result.rows.matched,
|
|
205
|
+
"keys": result.rows.keys,
|
|
206
|
+
"left_only_sample": result.rows.left_only_sample,
|
|
207
|
+
"right_only_sample": result.rows.right_only_sample,
|
|
208
|
+
"changed_sample": result.rows.changed_sample,
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
"left": result.left,
|
|
212
|
+
"right": result.right,
|
|
213
|
+
"model": result.model,
|
|
214
|
+
"schema": schema,
|
|
215
|
+
"rows": rows,
|
|
216
|
+
"message": result.message,
|
|
217
|
+
"differs": result.differs,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _render_diffs(results: list[TableDiff]) -> None:
|
|
222
|
+
for result in results:
|
|
223
|
+
title = result.model or f"{result.left} vs {result.right}"
|
|
224
|
+
if result.message and result.rows is None and result.schema.empty:
|
|
225
|
+
console.print(f"[yellow]{title}[/yellow] {result.message}")
|
|
226
|
+
continue
|
|
227
|
+
table = _table(title)
|
|
228
|
+
table.add_column("Side")
|
|
229
|
+
table.add_column("Relation", style="dim")
|
|
230
|
+
table.add_row("left", result.left)
|
|
231
|
+
table.add_row("right", result.right)
|
|
232
|
+
console.print(table)
|
|
233
|
+
if result.schema.added or result.schema.removed or result.schema.type_changed:
|
|
234
|
+
for name, dtype in result.schema.added:
|
|
235
|
+
console.print(f" [green]+[/] column {name} {dtype}")
|
|
236
|
+
for name, dtype in result.schema.removed:
|
|
237
|
+
console.print(f" [red]-[/] column {name} {dtype}")
|
|
238
|
+
for name, left, right in result.schema.type_changed:
|
|
239
|
+
console.print(f" [yellow]~[/] column {name} {left} → {right}")
|
|
240
|
+
if result.message:
|
|
241
|
+
console.print(f" [yellow]{result.message}[/yellow]")
|
|
242
|
+
if result.rows is None:
|
|
243
|
+
continue
|
|
244
|
+
rows = result.rows
|
|
245
|
+
console.print(
|
|
246
|
+
f" rows left={rows.left_count} right={rows.right_count} "
|
|
247
|
+
f"only-left={rows.left_only} only-right={rows.right_only} changed={rows.changed} "
|
|
248
|
+
f"matched={rows.matched} keys={','.join(rows.keys) or '—'}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def _table_diff(
|
|
253
|
+
environment: str,
|
|
254
|
+
path: Path,
|
|
255
|
+
select: list[str],
|
|
256
|
+
against: str,
|
|
257
|
+
source: str,
|
|
258
|
+
target: str,
|
|
259
|
+
on: list[str],
|
|
260
|
+
limit: int,
|
|
261
|
+
as_json: bool,
|
|
262
|
+
) -> None:
|
|
263
|
+
from interlace.plan.table_diff import diff_environments, diff_tables, parse_table_ref
|
|
264
|
+
|
|
265
|
+
if bool(source) != bool(target):
|
|
266
|
+
console.print("[red]table mode needs both --source and --target[/red]")
|
|
267
|
+
raise typer.Exit(2)
|
|
268
|
+
if not source and not against:
|
|
269
|
+
console.print("[red]env mode needs --against ENV (or pass --source/--target)[/red]")
|
|
270
|
+
raise typer.Exit(2)
|
|
271
|
+
project = Project.load(path)
|
|
272
|
+
compiled = project.compile()
|
|
273
|
+
engines = project.open_engines()
|
|
274
|
+
state = await project.open_state()
|
|
275
|
+
try:
|
|
276
|
+
if source:
|
|
277
|
+
result = await diff_tables(
|
|
278
|
+
engines.get(),
|
|
279
|
+
parse_table_ref(source),
|
|
280
|
+
parse_table_ref(target),
|
|
281
|
+
keys=on or None,
|
|
282
|
+
sample=limit,
|
|
283
|
+
)
|
|
284
|
+
results = [result]
|
|
285
|
+
else:
|
|
286
|
+
selected = await resolve_selection(compiled, state, environment, select)
|
|
287
|
+
results = await diff_environments(
|
|
288
|
+
compiled,
|
|
289
|
+
left_env=environment,
|
|
290
|
+
right_env=against,
|
|
291
|
+
store=state,
|
|
292
|
+
engines=engines,
|
|
293
|
+
select=selected,
|
|
294
|
+
keys=on or None,
|
|
295
|
+
sample=limit,
|
|
296
|
+
)
|
|
297
|
+
if as_json:
|
|
298
|
+
_emit_json([_diff_dict(item) for item in results])
|
|
299
|
+
else:
|
|
300
|
+
if not results:
|
|
301
|
+
console.print("No comparable models (ephemeral/file outputs are skipped).")
|
|
302
|
+
else:
|
|
303
|
+
_render_diffs(results)
|
|
304
|
+
if any(item.differs for item in results):
|
|
305
|
+
raise typer.Exit(1)
|
|
306
|
+
except PlanError as exc:
|
|
307
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
308
|
+
raise typer.Exit(1) from exc
|
|
309
|
+
finally:
|
|
310
|
+
await state.close()
|
|
311
|
+
engines.close()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _apply(
|
|
315
|
+
environment: str,
|
|
316
|
+
path: Path,
|
|
317
|
+
select: list[str],
|
|
318
|
+
forward_only: bool = False,
|
|
319
|
+
force: bool = False,
|
|
320
|
+
parallelism: int = 0,
|
|
321
|
+
) -> None:
|
|
322
|
+
from interlace.exceptions import BreakingPlanError
|
|
323
|
+
|
|
324
|
+
project = Project.load(path)
|
|
325
|
+
compiled = project.compile()
|
|
326
|
+
engines = project.open_engines()
|
|
327
|
+
state = await project.open_state()
|
|
328
|
+
tracker: _BuildProgress | None = _BuildProgress() if console.is_terminal else None
|
|
329
|
+
started = False
|
|
330
|
+
try:
|
|
331
|
+
|
|
332
|
+
async def prepare() -> None:
|
|
333
|
+
# Stream-fed projects must build without the daemon ever having run:
|
|
334
|
+
# declared stream tables are ensured (empty) so models reading them work.
|
|
335
|
+
if project.streams:
|
|
336
|
+
await ensure_stream_tables(project.streams, engines.get())
|
|
337
|
+
|
|
338
|
+
def on_plan(plan: Plan) -> None:
|
|
339
|
+
nonlocal started, tracker
|
|
340
|
+
_render(plan, environment)
|
|
341
|
+
if tracker is not None and not plan.backfills:
|
|
342
|
+
tracker = None
|
|
343
|
+
if tracker is not None:
|
|
344
|
+
tracker.progress.start()
|
|
345
|
+
started = True
|
|
346
|
+
|
|
347
|
+
def on_progress(model: str, event: str, detail: dict[str, Any]) -> None:
|
|
348
|
+
if tracker is not None:
|
|
349
|
+
tracker(model, event, detail)
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
_plan_result, result = await plan_and_apply(
|
|
353
|
+
compiled,
|
|
354
|
+
environment=environment,
|
|
355
|
+
project=project,
|
|
356
|
+
engines=engines,
|
|
357
|
+
state=state,
|
|
358
|
+
lock_owner=f"cli:{os.getpid()}:apply",
|
|
359
|
+
selectors=select,
|
|
360
|
+
forward_only=forward_only,
|
|
361
|
+
force=force,
|
|
362
|
+
parallelism=parallelism or None,
|
|
363
|
+
on_progress=on_progress,
|
|
364
|
+
on_plan=on_plan,
|
|
365
|
+
prepare=prepare,
|
|
366
|
+
)
|
|
367
|
+
except BreakingPlanError as exc:
|
|
368
|
+
console.print(
|
|
369
|
+
f"[red]plan has breaking changes ({', '.join(exc.names)}); re-run with --force to proceed[/red]"
|
|
370
|
+
)
|
|
371
|
+
raise typer.Exit(1) from exc
|
|
372
|
+
except CheckError as exc:
|
|
373
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
374
|
+
raise typer.Exit(1) from exc
|
|
375
|
+
except LockError as exc:
|
|
376
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
377
|
+
raise typer.Exit(1) from exc
|
|
378
|
+
if result is None:
|
|
379
|
+
return
|
|
380
|
+
_render_build_results(result, compiled)
|
|
381
|
+
_render_checks(result)
|
|
382
|
+
await _render_empty_incrementals(result, compiled, engines)
|
|
383
|
+
console.print(
|
|
384
|
+
f"[green]Built {len(set(result.built))} model(s); promoted {result.promoted} to '{environment}'.[/green]"
|
|
385
|
+
)
|
|
386
|
+
finally:
|
|
387
|
+
if started and tracker is not None:
|
|
388
|
+
tracker.progress.stop()
|
|
389
|
+
await state.close()
|
|
390
|
+
engines.close()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@app.command()
|
|
394
|
+
def run(
|
|
395
|
+
environment: str = _ENV,
|
|
396
|
+
path: Path = _PATH,
|
|
397
|
+
select: list[str] = _SELECT,
|
|
398
|
+
start: str = _START,
|
|
399
|
+
end: str = _END,
|
|
400
|
+
parallelism: int = _PARALLELISM,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""Force-build models and promote, ignoring change detection.
|
|
403
|
+
|
|
404
|
+
For incremental models, --start/--end set the catchup window
|
|
405
|
+
(default: the latest grain interval).
|
|
406
|
+
"""
|
|
407
|
+
asyncio.run(_execute(environment, path, select, start, end, restate=False, parallelism=parallelism))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@app.command()
|
|
411
|
+
def restate(
|
|
412
|
+
environment: str = _ENV,
|
|
413
|
+
path: Path = _PATH,
|
|
414
|
+
select: list[str] = _SELECT,
|
|
415
|
+
start: str = _START,
|
|
416
|
+
end: str = _END,
|
|
417
|
+
parallelism: int = _PARALLELISM,
|
|
418
|
+
) -> None:
|
|
419
|
+
"""Reprocess incremental models over a window, ignoring the ledger (vs run, which skips filled)."""
|
|
420
|
+
asyncio.run(_execute(environment, path, select, start, end, restate=True, parallelism=parallelism))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def _execute(
|
|
424
|
+
environment: str,
|
|
425
|
+
path: Path,
|
|
426
|
+
select: list[str],
|
|
427
|
+
start: str,
|
|
428
|
+
end: str,
|
|
429
|
+
*,
|
|
430
|
+
restate: bool,
|
|
431
|
+
parallelism: int = 0,
|
|
432
|
+
) -> None:
|
|
433
|
+
window_start = _window(start, "--start")
|
|
434
|
+
window_end = _window(end, "--end")
|
|
435
|
+
project = Project.load(path)
|
|
436
|
+
compiled = project.compile()
|
|
437
|
+
engines = project.open_engines()
|
|
438
|
+
state = await project.open_state()
|
|
439
|
+
tracker: _BuildProgress | None = _BuildProgress() if console.is_terminal else None
|
|
440
|
+
started_bar = False
|
|
441
|
+
try:
|
|
442
|
+
|
|
443
|
+
async def prepare() -> None:
|
|
444
|
+
if project.streams: # as in _apply: stream tables must exist daemon or not
|
|
445
|
+
await ensure_stream_tables(project.streams, engines.get())
|
|
446
|
+
|
|
447
|
+
def on_plan(plan: Plan) -> None:
|
|
448
|
+
nonlocal started_bar, tracker
|
|
449
|
+
if tracker is not None and not plan.backfills:
|
|
450
|
+
tracker = None
|
|
451
|
+
if tracker is not None:
|
|
452
|
+
tracker.progress.start()
|
|
453
|
+
started_bar = True
|
|
454
|
+
|
|
455
|
+
def on_progress(model: str, event: str, detail: dict[str, Any]) -> None:
|
|
456
|
+
if tracker is not None:
|
|
457
|
+
tracker(model, event, detail)
|
|
458
|
+
|
|
459
|
+
started = time.perf_counter()
|
|
460
|
+
try:
|
|
461
|
+
plan_result, result = await run_and_apply(
|
|
462
|
+
compiled,
|
|
463
|
+
environment=environment,
|
|
464
|
+
project=project,
|
|
465
|
+
engines=engines,
|
|
466
|
+
state=state,
|
|
467
|
+
lock_owner=f"cli:{os.getpid()}:run",
|
|
468
|
+
selectors=select,
|
|
469
|
+
start=window_start,
|
|
470
|
+
end=window_end,
|
|
471
|
+
restate=restate,
|
|
472
|
+
parallelism=parallelism or None,
|
|
473
|
+
on_progress=on_progress,
|
|
474
|
+
on_plan=on_plan,
|
|
475
|
+
prepare=prepare,
|
|
476
|
+
)
|
|
477
|
+
except CheckError as exc:
|
|
478
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
479
|
+
raise typer.Exit(1) from exc
|
|
480
|
+
except LockError as exc:
|
|
481
|
+
console.print(f"[red]{escape(exc.message)}[/red]")
|
|
482
|
+
raise typer.Exit(1) from exc
|
|
483
|
+
_render_build_results(result, compiled)
|
|
484
|
+
_render_checks(result)
|
|
485
|
+
_render_warnings(plan_result)
|
|
486
|
+
await _render_empty_incrementals(result, compiled, engines)
|
|
487
|
+
verb = "Restated" if restate else "Ran"
|
|
488
|
+
console.print(
|
|
489
|
+
f"[green]{verb} {len(set(result.built))} model(s) in {time.perf_counter() - started:.2f}s; "
|
|
490
|
+
f"promoted {result.promoted} to '{environment}'.[/green]"
|
|
491
|
+
)
|
|
492
|
+
finally:
|
|
493
|
+
if started_bar and tracker is not None:
|
|
494
|
+
tracker.progress.stop()
|
|
495
|
+
await state.close()
|
|
496
|
+
engines.close()
|