Skip to content

Commit 1cc9da4

Browse files
authored
Feat: support dynamic blueprinting for Python models, add docs (#4177)
1 parent 46eaf49 commit 1cc9da4

5 files changed

Lines changed: 62 additions & 7 deletions

File tree

docs/concepts/models/python_models.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@ def entrypoint(
367367
)
368368
```
369369

370+
!!! note
371+
372+
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
373+
370374
## Examples
371375
### Basic
372376
The following is an example of a Python model returning a static Pandas DataFrame.

docs/concepts/models/sql_models.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ SELECT
175175
FROM customer2.some_source
176176
```
177177

178+
!!! note
179+
180+
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints @gen_blueprints()`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
181+
178182
## Python-based definition
179183

180184
The Python-based definition of SQL models consists of a single python function, decorated with SQLMesh's `@model` [decorator](https://wiki.python.org/moin/PythonDecorators). The decorator is required to have the `is_sql` keyword argument set to `True` to distinguish it from [Python models](./python_models.md) that return DataFrame instances.
@@ -258,6 +262,10 @@ def entrypoint(evaluator: MacroEvaluator) -> str | exp.Expression:
258262

259263
The two models produced from this template are the same as in the [example](#SQL-model-blueprinting) for SQL-based blueprinting.
260264

265+
!!! note
266+
267+
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
268+
261269
## Automatic dependencies
262270

263271
SQLMesh parses your SQL, so it understands what the code does and how it relates to other models. There is no need for you to manually specify dependencies to other models with special tags or commands.

sqlmesh/core/model/decorator.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from sqlmesh.core.model.kind import ModelKindName, _ModelKind
2626
from sqlmesh.utils import registry_decorator, DECORATOR_RETURN_TYPE
27-
from sqlmesh.utils.errors import ConfigError
27+
from sqlmesh.utils.errors import ConfigError, raise_config_error
2828
from sqlmesh.utils.metaprogramming import build_env, serialize_env
2929

3030

@@ -96,9 +96,32 @@ def models(
9696
default_catalog_per_gateway: t.Optional[t.Dict[str, str]] = None,
9797
**loader_kwargs: t.Any,
9898
) -> t.List[Model]:
99+
blueprints = self.kwargs.pop("blueprints", None)
100+
101+
if isinstance(blueprints, str):
102+
blueprints = parse_one(blueprints, dialect=dialect)
103+
104+
if isinstance(blueprints, MacroFunc):
105+
from sqlmesh.core.model.definition import render_expression
106+
107+
blueprints = render_expression(
108+
expression=blueprints,
109+
module_path=module_path,
110+
macros=loader_kwargs.get("macros"),
111+
jinja_macros=loader_kwargs.get("jinja_macros"),
112+
variables=get_variables(None),
113+
path=path,
114+
dialect=dialect,
115+
default_catalog=loader_kwargs.get("default_catalog"),
116+
)
117+
if not blueprints:
118+
raise_config_error("Failed to render blueprints property", path)
119+
120+
blueprints = blueprints[0]
121+
99122
return create_models_from_blueprints(
100123
gateway=self.kwargs.get("gateway"),
101-
blueprints=self.kwargs.pop("blueprints", None),
124+
blueprints=blueprints,
102125
get_variables=get_variables,
103126
loader=self.model,
104127
path=path,

sqlmesh/core/model/definition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1849,7 +1849,7 @@ def _extract_blueprints(blueprints: t.Any, path: Path) -> t.List[t.Any]:
18491849
return blueprints
18501850

18511851
raise_config_error(
1852-
"Expected a list or tuple consisting of key-value mappings for"
1852+
"Expected a list or tuple consisting of key-value mappings for "
18531853
f"the 'blueprints' property, got '{blueprints}' instead",
18541854
path,
18551855
)

tests/core/test_model.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8312,9 +8312,9 @@ def entrypoint(evaluator):
83128312
def test_dynamic_blueprinting(tmp_path: Path) -> None:
83138313
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
83148314

8315-
dynamic_template = tmp_path / "models/dynamic_template.sql"
8316-
dynamic_template.parent.mkdir(parents=True, exist_ok=True)
8317-
dynamic_template.write_text(
8315+
dynamic_template_sql = tmp_path / "models/dynamic_template.sql"
8316+
dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True)
8317+
dynamic_template_sql.write_text(
83188318
"""
83198319
MODEL (
83208320
name @customer.some_table,
@@ -8330,6 +8330,24 @@ def test_dynamic_blueprinting(tmp_path: Path) -> None:
83308330
"""
83318331
)
83328332

8333+
dynamic_template_py = tmp_path / "models/dynamic_template.py"
8334+
dynamic_template_py.parent.mkdir(parents=True, exist_ok=True)
8335+
dynamic_template_py.write_text(
8336+
"""
8337+
from sqlmesh import model
8338+
8339+
@model(
8340+
"@{customer}.some_other_table",
8341+
kind="FULL",
8342+
blueprints="@gen_blueprints()",
8343+
is_sql=True,
8344+
)
8345+
def entrypoint(evaluator):
8346+
field_a = evaluator.blueprint_var("field_a")
8347+
return f"SELECT {field_a}, @BLUEPRINT_VAR('field_b') AS field_b FROM @customer.some_source"
8348+
"""
8349+
)
8350+
83338351
gen_blueprints = tmp_path / "macros/gen_blueprints.py"
83348352
gen_blueprints.parent.mkdir(parents=True, exist_ok=True)
83358353
gen_blueprints.write_text(
@@ -8347,9 +8365,11 @@ def gen_blueprints(evaluator):
83478365
config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")), paths=tmp_path
83488366
)
83498367

8350-
assert len(ctx.models) == 2
8368+
assert len(ctx.models) == 4
83518369
assert '"memory"."customer1"."some_table"' in ctx.models
83528370
assert '"memory"."customer2"."some_table"' in ctx.models
8371+
assert '"memory"."customer1"."some_other_table"' in ctx.models
8372+
assert '"memory"."customer2"."some_other_table"' in ctx.models
83538373

83548374

83558375
def test_single_blueprint(tmp_path: Path) -> None:

0 commit comments

Comments
 (0)