Skip to content

Commit e574577

Browse files
authored
Feat(dbt_cli): Add support for '--profile' and '--target' (#5174)
1 parent 59f8b2e commit e574577

File tree

9 files changed

+124
-8
lines changed

9 files changed

+124
-8
lines changed

sqlmesh/core/config/loader.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def load_configs(
3232
paths: t.Union[str | Path, t.Iterable[str | Path]],
3333
sqlmesh_path: t.Optional[Path] = None,
3434
dotenv_path: t.Optional[Path] = None,
35+
**kwargs: t.Any,
3536
) -> t.Dict[Path, C]:
3637
sqlmesh_path = sqlmesh_path or c.SQLMESH_PATH
3738
config = config or "config"
@@ -70,6 +71,7 @@ def load_configs(
7071
project_paths=[path / name for name in ALL_CONFIG_FILENAMES],
7172
personal_paths=personal_paths,
7273
config_name=config,
74+
**kwargs,
7375
)
7476
for path in absolute_paths
7577
}
@@ -81,6 +83,7 @@ def load_config_from_paths(
8183
personal_paths: t.Optional[t.List[Path]] = None,
8284
config_name: str = "config",
8385
load_from_env: bool = True,
86+
**kwargs: t.Any,
8487
) -> C:
8588
project_paths = project_paths or []
8689
personal_paths = personal_paths or []
@@ -168,7 +171,11 @@ def load_config_from_paths(
168171
if dbt_project_file:
169172
from sqlmesh.dbt.loader import sqlmesh_config
170173

171-
dbt_python_config = sqlmesh_config(project_root=dbt_project_file.parent)
174+
dbt_python_config = sqlmesh_config(
175+
project_root=dbt_project_file.parent,
176+
dbt_profile_name=kwargs.pop("profile", None),
177+
dbt_target_name=kwargs.pop("target", None),
178+
)
172179
if type(dbt_python_config) != config_type:
173180
dbt_python_config = convert_config_type(dbt_python_config, config_type)
174181

sqlmesh/core/context.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,12 @@ def __init__(
367367
loader: t.Optional[t.Type[Loader]] = None,
368368
load: bool = True,
369369
users: t.Optional[t.List[User]] = None,
370+
config_loader_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
370371
):
371372
self.configs = (
372-
config if isinstance(config, dict) else load_configs(config, self.CONFIG_TYPE, paths)
373+
config
374+
if isinstance(config, dict)
375+
else load_configs(config, self.CONFIG_TYPE, paths, **(config_loader_kwargs or {}))
373376
)
374377
self._projects = {config.project for config in self.configs.values()}
375378
self.dag: DAG[str] = DAG()

sqlmesh/dbt/loader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,14 @@
4444
def sqlmesh_config(
4545
project_root: t.Optional[Path] = None,
4646
state_connection: t.Optional[ConnectionConfig] = None,
47+
dbt_profile_name: t.Optional[str] = None,
4748
dbt_target_name: t.Optional[str] = None,
4849
variables: t.Optional[t.Dict[str, t.Any]] = None,
4950
register_comments: t.Optional[bool] = None,
5051
**kwargs: t.Any,
5152
) -> Config:
5253
project_root = project_root or Path()
53-
context = DbtContext(project_root=project_root)
54+
context = DbtContext(project_root=project_root, profile_name=dbt_profile_name)
5455
profile = Profile.load(context, target_name=dbt_target_name)
5556
model_defaults = kwargs.pop("model_defaults", ModelDefaultsConfig())
5657
if model_defaults.dialect is None:

sqlmesh/dbt/profile.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def _read_profile(
101101
target_name = context.render(project_data.get("target"))
102102

103103
if target_name not in outputs:
104+
target_names = "\n".join(f"- {name}" for name in outputs)
104105
raise ConfigError(
105-
f"Target '{target_name}' not specified in profiles for '{context.profile_name}'."
106+
f"Target '{target_name}' not specified in profiles for '{context.profile_name}'. "
107+
f"The valid target names for this profile are:\n{target_names}"
106108
)
107109

108110
target_fields = load_yaml(context.render(yaml.dump(outputs[target_name])))

sqlmesh_dbt/cli.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import sys
33
import click
44
from sqlmesh_dbt.operations import DbtOperations, create
5+
from sqlmesh_dbt.error import cli_global_error_handler
6+
from pathlib import Path
57

68

79
def _get_dbt_operations(ctx: click.Context) -> DbtOperations:
@@ -10,9 +12,14 @@ def _get_dbt_operations(ctx: click.Context) -> DbtOperations:
1012
return ctx.obj
1113

1214

13-
@click.group()
15+
@click.group(invoke_without_command=True)
16+
@click.option("--profile", help="Which existing profile to load. Overrides output.profile")
17+
@click.option("-t", "--target", help="Which target to load for the given profile")
1418
@click.pass_context
15-
def dbt(ctx: click.Context) -> None:
19+
@cli_global_error_handler
20+
def dbt(
21+
ctx: click.Context, profile: t.Optional[str] = None, target: t.Optional[str] = None
22+
) -> None:
1623
"""
1724
An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine.
1825
"""
@@ -22,7 +29,12 @@ def dbt(ctx: click.Context) -> None:
2229
return
2330

2431
# TODO: conditionally call create() if there are times we dont want/need to import sqlmesh and load a project
25-
ctx.obj = create()
32+
ctx.obj = create(project_dir=Path.cwd(), profile=profile, target=target)
33+
34+
if not ctx.invoked_subcommand:
35+
click.echo(
36+
f"No command specified. Run `{ctx.info_name} --help` to see the available commands."
37+
)
2638

2739

2840
@dbt.command()

sqlmesh_dbt/error.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import typing as t
2+
import logging
3+
from functools import wraps
4+
import click
5+
import sys
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def cli_global_error_handler(
11+
func: t.Callable[..., t.Any],
12+
) -> t.Callable[..., t.Any]:
13+
@wraps(func)
14+
def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any:
15+
try:
16+
return func(*args, **kwargs)
17+
except Exception as ex:
18+
# these imports are deliberately deferred to avoid the penalty of importing the `sqlmesh`
19+
# package up front for every CLI command
20+
from sqlmesh.utils.errors import SQLMeshError
21+
from sqlglot.errors import SqlglotError
22+
23+
if isinstance(ex, (SQLMeshError, SqlglotError, ValueError)):
24+
click.echo(click.style("Error: " + str(ex), fg="red"))
25+
sys.exit(1)
26+
else:
27+
raise
28+
finally:
29+
context_or_obj = args[0]
30+
sqlmesh_context = (
31+
context_or_obj.obj if isinstance(context_or_obj, click.Context) else context_or_obj
32+
)
33+
if sqlmesh_context is not None:
34+
# important to import this only if a context was created
35+
# otherwise something like `sqlmesh_dbt run --help` will trigger this import because it's in the finally: block
36+
from sqlmesh import Context
37+
38+
if isinstance(sqlmesh_context, Context):
39+
sqlmesh_context.close()
40+
41+
return wrapper

sqlmesh_dbt/operations.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ def console(self) -> DbtCliConsole:
5353

5454

5555
def create(
56-
project_dir: t.Optional[Path] = None, profiles_dir: t.Optional[Path] = None, debug: bool = False
56+
project_dir: t.Optional[Path] = None,
57+
profile: t.Optional[str] = None,
58+
target: t.Optional[str] = None,
59+
debug: bool = False,
5760
) -> DbtOperations:
5861
with Progress(transient=True) as progress:
5962
# Indeterminate progress bar before SQLMesh import to provide feedback to the user that something is indeed happening
@@ -76,6 +79,7 @@ def create(
7679

7780
sqlmesh_context = Context(
7881
paths=[project_dir],
82+
config_loader_kwargs=dict(profile=profile, target=target),
7983
load=True,
8084
)
8185

tests/dbt/cli/test_global_flags.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import typing as t
2+
from pathlib import Path
3+
import pytest
4+
from click.testing import Result
5+
6+
pytestmark = pytest.mark.slow
7+
8+
9+
def test_profile_and_target(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]):
10+
# profile doesnt exist - error
11+
result = invoke_cli(["--profile", "nonexist"])
12+
assert result.exit_code == 1
13+
assert "Profile 'nonexist' not found in profiles" in result.output
14+
15+
# profile exists - successful load with default target
16+
result = invoke_cli(["--profile", "jaffle_shop"])
17+
assert result.exit_code == 0
18+
assert "No command specified" in result.output
19+
20+
# profile exists but target doesnt - error
21+
result = invoke_cli(["--profile", "jaffle_shop", "--target", "nonexist"])
22+
assert result.exit_code == 1
23+
assert "Target 'nonexist' not specified in profiles" in result.output
24+
assert "valid target names for this profile are" in result.output
25+
assert "- dev" in result.output
26+
27+
# profile exists and so does target - successful load with specified target
28+
result = invoke_cli(["--profile", "jaffle_shop", "--target", "dev"])
29+
assert result.exit_code == 0
30+
assert "No command specified" in result.output

tests/dbt/cli/test_operations.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from sqlmesh_dbt.operations import create
44
from sqlmesh.utils import yaml
5+
from sqlmesh.utils.errors import SQLMeshError
56
import time_machine
67

78
pytestmark = pytest.mark.slow
@@ -53,3 +54,18 @@ def test_create_uses_configured_start_date_if_supplied(jaffle_shop_duckdb: Path)
5354
for model in operations.context.models.values()
5455
if not model.kind.is_seed
5556
)
57+
58+
59+
def test_create_can_specify_profile_and_target(jaffle_shop_duckdb: Path):
60+
with pytest.raises(SQLMeshError, match=r"Profile 'foo' not found"):
61+
create(profile="foo")
62+
63+
with pytest.raises(
64+
SQLMeshError, match=r"Target 'prod' not specified in profiles for 'jaffle_shop'"
65+
):
66+
create(profile="jaffle_shop", target="prod")
67+
68+
dbt_project = create(profile="jaffle_shop", target="dev").project
69+
70+
assert dbt_project.context.profile_name == "jaffle_shop"
71+
assert dbt_project.context.target_name == "dev"

0 commit comments

Comments
 (0)