interlaced 2.0.0a2__tar.gz → 2.0.0a3__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.
Files changed (122) hide show
  1. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/PKG-INFO +1 -1
  2. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/pyproject.toml +1 -1
  3. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/__init__.py +1 -1
  4. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/config/config.py +49 -9
  5. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/PKG-INFO +1 -1
  6. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_warehouse_config.py +21 -0
  7. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/LICENSE +0 -0
  8. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/README.md +0 -0
  9. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/setup.cfg +0 -0
  10. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/checks/__init__.py +0 -0
  11. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/checks/builtin.py +0 -0
  12. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/checks/runner.py +0 -0
  13. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/checks/spec.py +0 -0
  14. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/cli/__init__.py +0 -0
  15. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/cli/main.py +0 -0
  16. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/config/__init__.py +0 -0
  17. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/contracts.py +0 -0
  18. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/dsl/__init__.py +0 -0
  19. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/dsl/decorators.py +0 -0
  20. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/dsl/discovery.py +0 -0
  21. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/dsl/sql_config.py +0 -0
  22. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/__init__.py +0 -0
  23. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/base.py +0 -0
  24. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/duckdb.py +0 -0
  25. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/postgres.py +0 -0
  26. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/quack.py +0 -0
  27. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/engines/registry.py +0 -0
  28. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/exceptions.py +0 -0
  29. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/exports.py +0 -0
  30. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/graph/__init__.py +0 -0
  31. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/graph/column_lineage.py +0 -0
  32. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/graph/dag.py +0 -0
  33. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/graph/project.py +0 -0
  34. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/graph/selectors.py +0 -0
  35. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/ir/__init__.py +0 -0
  36. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/ir/canonicalize.py +0 -0
  37. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/ir/fingerprint.py +0 -0
  38. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/ir/relation.py +0 -0
  39. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/ir/schema.py +0 -0
  40. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/__init__.py +0 -0
  41. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/apply.py +0 -0
  42. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/differ.py +0 -0
  43. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/plan.py +0 -0
  44. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/resolve.py +0 -0
  45. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/plan/run.py +0 -0
  46. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/project.py +0 -0
  47. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/py.typed +0 -0
  48. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/runtime/__init__.py +0 -0
  49. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/runtime/handles.py +0 -0
  50. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/runtime/python_model.py +0 -0
  51. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/scaffold.py +0 -0
  52. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/scheduler/__init__.py +0 -0
  53. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/scheduler/engine.py +0 -0
  54. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/scheduler/triggers.py +0 -0
  55. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/scheduler/worker.py +0 -0
  56. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/service/__init__.py +0 -0
  57. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/service/app.py +0 -0
  58. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/service/auth.py +0 -0
  59. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/state/__init__.py +0 -0
  60. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/state/interval.py +0 -0
  61. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/state/janitor.py +0 -0
  62. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/state/snapshot.py +0 -0
  63. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/state/store.py +0 -0
  64. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/__init__.py +0 -0
  65. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/base.py +0 -0
  66. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/full.py +0 -0
  67. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/full_merge.py +0 -0
  68. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/incremental_by_time.py +0 -0
  69. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/merge_by_key.py +0 -0
  70. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/scd_type_2.py +0 -0
  71. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/strategies/view.py +0 -0
  72. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/streaming/__init__.py +0 -0
  73. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/streaming/log.py +0 -0
  74. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/streaming/materializer.py +0 -0
  75. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlace/streaming/schema.py +0 -0
  76. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/SOURCES.txt +0 -0
  77. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/dependency_links.txt +0 -0
  78. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/entry_points.txt +0 -0
  79. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/requires.txt +0 -0
  80. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/src/interlaced.egg-info/top_level.txt +0 -0
  81. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_apply.py +0 -0
  82. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_checks.py +0 -0
  83. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_cli.py +0 -0
  84. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_cli_inspect.py +0 -0
  85. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_column_lineage.py +0 -0
  86. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_compile.py +0 -0
  87. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_contracts.py +0 -0
  88. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_dag.py +0 -0
  89. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_differ.py +0 -0
  90. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_discovery.py +0 -0
  91. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_dsl.py +0 -0
  92. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_duckdb_adapter.py +0 -0
  93. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_ephemeral.py +0 -0
  94. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_example.py +0 -0
  95. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_example_benchmark.py +0 -0
  96. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_example_platform_tour.py +0 -0
  97. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_exports.py +0 -0
  98. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_fingerprint.py +0 -0
  99. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_forward_only.py +0 -0
  100. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_incremental.py +0 -0
  101. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_incremental_python.py +0 -0
  102. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_init.py +0 -0
  103. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_interval.py +0 -0
  104. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_janitor.py +0 -0
  105. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_multi_engine.py +0 -0
  106. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_postgres_engine.py +0 -0
  107. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_python_models.py +0 -0
  108. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_rebuild_skip.py +0 -0
  109. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_run.py +0 -0
  110. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_scd2.py +0 -0
  111. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_scheduler.py +0 -0
  112. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_schema_evolution.py +0 -0
  113. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_selectors.py +0 -0
  114. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_service.py +0 -0
  115. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_sql_config.py +0 -0
  116. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_state_store.py +0 -0
  117. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_storage.py +0 -0
  118. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_strategies.py +0 -0
  119. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_streaming.py +0 -0
  120. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_table_sinks.py +0 -0
  121. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_transfers.py +0 -0
  122. {interlaced-2.0.0a2 → interlaced-2.0.0a3}/tests/test_worker_leases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlaced
3
- Version: 2.0.0a2
3
+ Version: 2.0.0a3
4
4
  Summary: Python/SQL-first data platform: transformation, built-in orchestration, and durable streaming ingestion
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.0.0a2"
7
+ version = "2.0.0a3"
8
8
  description = "Python/SQL-first data platform: transformation, built-in orchestration, and durable streaming ingestion"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  from interlace.dsl.decorators import check, model, stream
14
14
  from interlace.ir.relation import EngineRef, SqlRelation, TableRef
15
15
 
16
- __version__ = "2.0.0a2"
16
+ __version__ = "2.0.0a3"
17
17
 
18
18
  __all__ = [
19
19
  "EngineRef",
@@ -3,9 +3,12 @@
3
3
  A small pydantic model with sensible defaults so a project works with no config
4
4
  file at all. Paths are relative to the project root.
5
5
 
6
- ``${VAR}`` references anywhere in the YAML are substituted from the environment
7
- before parsing (unset variables are left literal), so DSNs and secret values
8
- never need to be committed: ``database: "ducklake:postgres:${WAREHOUSE_DSN}"``.
6
+ ``${VAR}`` references anywhere in the YAML are substituted before parsing, so
7
+ DSNs and secret values never need to be committed:
8
+ ``database: "ducklake:postgres:${WAREHOUSE_DSN}"``. Values come from the real
9
+ environment first, then from a ``.env`` file next to the config (dotenv
10
+ KEY=VALUE lines; the process environment always wins). Unset variables are
11
+ left literal so a missing one surfaces as an obvious ``${VAR}`` in errors.
9
12
  """
10
13
 
11
14
  from __future__ import annotations
@@ -22,6 +25,7 @@ from interlace.exceptions import ConfigurationError
22
25
  CONFIG_FILE = "interlace.yaml"
23
26
 
24
27
  _ENV_REF = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
28
+ _ENV_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
25
29
 
26
30
 
27
31
  class SecretConfig(BaseModel):
@@ -150,18 +154,54 @@ def _default_engine_from_top_level(config: ProjectConfig) -> EngineConfig:
150
154
  )
151
155
 
152
156
 
153
- def _interpolate_env(text: str) -> str:
154
- """Replace ``${VAR}`` with the environment value; unset vars stay literal (so a
155
- missing variable surfaces as an obvious ``${VAR}`` in errors, never silently '')."""
156
- return _ENV_REF.sub(lambda m: os.environ.get(m.group(1), m.group(0)), text)
157
+ def _interpolate_env(text: str, fallback: dict[str, str] | None = None) -> str:
158
+ """Replace ``${VAR}`` from the process environment, then the ``.env`` fallback;
159
+ unset vars stay literal (so a missing variable surfaces as an obvious ``${VAR}``
160
+ in errors, never silently '')."""
161
+ extra = fallback or {}
162
+
163
+ def resolve(match: re.Match[str]) -> str:
164
+ name = match.group(1)
165
+ if name in os.environ:
166
+ return os.environ[name]
167
+ return extra.get(name, match.group(0))
168
+
169
+ return _ENV_REF.sub(resolve, text)
170
+
171
+
172
+ def load_dotenv(path: Path) -> dict[str, str]:
173
+ """KEY=VALUE pairs from a ``.env`` file (missing file = empty). Supports the
174
+ common dotenv subset: comments and blank lines skipped, optional ``export``
175
+ prefix, optional single/double quotes around the value. Never mutates the
176
+ process environment — values only feed ``${VAR}`` interpolation."""
177
+ values: dict[str, str] = {}
178
+ if not path.exists():
179
+ return values
180
+ for line in path.read_text().splitlines():
181
+ stripped = line.strip()
182
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
183
+ continue
184
+ key, _, raw = stripped.removeprefix("export ").partition("=")
185
+ key = key.strip()
186
+ if not _ENV_KEY.fullmatch(key):
187
+ continue
188
+ value = raw.strip()
189
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
190
+ value = value[1:-1]
191
+ values[key] = value
192
+ return values
157
193
 
158
194
 
159
195
  def load_config(path: Path) -> ProjectConfig:
160
- """Load and validate config, returning defaults when the file is absent."""
196
+ """Load and validate config, returning defaults when the file is absent.
197
+
198
+ ``${VAR}`` references resolve from the process environment first, then from
199
+ a ``.env`` file sitting next to the config."""
161
200
  if not path.exists():
162
201
  return ProjectConfig()
202
+ dotenv = load_dotenv(path.parent / ".env")
163
203
  try:
164
- data = yaml.safe_load(_interpolate_env(path.read_text())) or {}
204
+ data = yaml.safe_load(_interpolate_env(path.read_text(), dotenv)) or {}
165
205
  except yaml.YAMLError as exc:
166
206
  raise ConfigurationError("could not parse config", details={"path": str(path), "error": str(exc)}) from exc
167
207
  if not isinstance(data, dict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlaced
3
- Version: 2.0.0a2
3
+ Version: 2.0.0a3
4
4
  Summary: Python/SQL-first data platform: transformation, built-in orchestration, and durable streaming ingestion
5
5
  Author-email: Mark <mark@interlace.sh>
6
6
  License-Expression: MIT
@@ -236,3 +236,24 @@ def test_pg_guard_accepts_libpq_conventions(monkeypatch: pytest.MonkeyPatch) ->
236
236
  _require_explicit_pg_host("service=warehouse", "t")
237
237
  monkeypatch.setenv("PGHOST", "db.internal")
238
238
  _require_explicit_pg_host("dbname=analytics", "t") # target named via libpq env
239
+
240
+
241
+ def test_dotenv_feeds_interpolation(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
242
+ """${VAR} resolves from .env next to the config; the process environment wins."""
243
+ monkeypatch.delenv("WH_PATH", raising=False)
244
+ (tmp_path / ".env").write_text('# warehouse\nexport WH_PATH="wh-from-dotenv.duckdb"\nIGNORED\n')
245
+ (tmp_path / "interlace.yaml").write_text("name: dotenv\ndatabase: ${WH_PATH}\n")
246
+ assert load_config(tmp_path / "interlace.yaml").database == "wh-from-dotenv.duckdb"
247
+
248
+ monkeypatch.setenv("WH_PATH", "wh-from-env.duckdb") # real environment beats .env
249
+ assert load_config(tmp_path / "interlace.yaml").database == "wh-from-env.duckdb"
250
+
251
+
252
+ def test_dotenv_never_mutates_the_process_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
253
+ import os
254
+
255
+ monkeypatch.delenv("DOTENV_ONLY", raising=False)
256
+ (tmp_path / ".env").write_text("DOTENV_ONLY=secret\n")
257
+ (tmp_path / "interlace.yaml").write_text("name: pure\ndatabase: ${DOTENV_ONLY}\n")
258
+ assert load_config(tmp_path / "interlace.yaml").database == "secret"
259
+ assert "DOTENV_ONLY" not in os.environ
File without changes
File without changes
File without changes