Skip to content

Commit f857f68

Browse files
committed
Fix(sqlmesh_dbt): Allow global options to be specified in both top-level and subcommand positions
1 parent 2dd01a4 commit f857f68

File tree

4 files changed

+232
-137
lines changed

4 files changed

+232
-137
lines changed

sqlmesh/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,20 @@ def configure_logging(
232232
logger.addHandler(file_handler)
233233

234234
if debug:
235-
import faulthandler
235+
import faulthandler, io
236236

237237
enable_debug_mode()
238238

239239
# Enable threadumps.
240-
faulthandler.enable()
240+
try:
241+
faulthandler.enable()
241242

242-
# Windows doesn't support register so we check for it here
243-
if hasattr(faulthandler, "register"):
244-
from signal import SIGUSR1
243+
# Windows doesn't support register so we check for it here
244+
if hasattr(faulthandler, "register"):
245+
from signal import SIGUSR1
245246

246-
faulthandler.register(SIGUSR1.value)
247+
faulthandler.register(SIGUSR1.value)
248+
except io.UnsupportedOperation as e:
249+
# this can fail if sys.stderr isnt a proper file handle, which can happen in unit test / notebook environments
250+
# rather than prevent SQLMesh from working because we couldnt register the faulthandler, we just log a warning and carry on
251+
logger.warning("Unable to enable faulthandler for thread dumps", exc_info=e)

sqlmesh_dbt/cli.py

Lines changed: 53 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
11
import typing as t
22
import sys
33
import click
4+
from click.core import ParameterSource
45
from sqlmesh_dbt.operations import DbtOperations, create
56
from sqlmesh_dbt.error import cli_global_error_handler, ErrorHandlingGroup
67
from pathlib import Path
7-
from sqlmesh_dbt.options import YamlParamType
8-
import functools
8+
from sqlmesh_dbt.options import global_options, run_options, list_options
99

1010

1111
def _get_dbt_operations(
12-
ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], threads: t.Optional[int] = None
13-
) -> DbtOperations:
14-
if not isinstance(ctx.obj, functools.partial):
12+
ctx: click.Context, **kwargs: t.Dict[str, t.Any]
13+
) -> t.Tuple[DbtOperations, t.Dict[str, t.Any]]:
14+
if not isinstance(ctx.obj, dict):
1515
raise ValueError(f"Unexpected click context object: {type(ctx.obj)}")
1616

17-
dbt_operations = ctx.obj(vars=vars, threads=threads)
17+
all_options = {
18+
**kwargs,
19+
# ctx.obj only contains global options that a user specifically provided, so these values take precedence
20+
# over **kwargs which contains all options (plus Click's defaults) whether or not they were explicitly set by a user
21+
**ctx.obj,
22+
}
23+
24+
T = t.TypeVar("T")
25+
26+
def _pop_global_option(name: str, expected_type: t.Type[T]) -> t.Optional[T]:
27+
value = all_options.pop(name, None)
28+
if value is not None and not isinstance(value, expected_type):
29+
raise ValueError(
30+
f"Expecting option '{name}' to be type '{expected_type}', however it was '{type(value)}'"
31+
)
32+
return value
33+
34+
dbt_operations = create(
35+
project_dir=_pop_global_option("project_dir", Path),
36+
profiles_dir=_pop_global_option("profiles_dir", Path),
37+
profile=_pop_global_option("profile", str),
38+
target=_pop_global_option("target", str),
39+
vars=_pop_global_option("vars", dict),
40+
threads=_pop_global_option("threads", int),
41+
debug=_pop_global_option("debug", bool) or False,
42+
log_level=_pop_global_option("log_level", str),
43+
)
1844

1945
if not isinstance(dbt_operations, DbtOperations):
2046
raise ValueError(f"Unexpected dbt operations type: {type(dbt_operations)}")
@@ -23,88 +49,15 @@ def _get_dbt_operations(
2349
def _cleanup() -> None:
2450
dbt_operations.close()
2551

26-
return dbt_operations
27-
28-
29-
vars_option = click.option(
30-
"--vars",
31-
type=YamlParamType(),
32-
help="Supply variables to the project. This argument overrides variables defined in your dbt_project.yml file. This argument should be a YAML string, eg. '{my_variable: my_value}'",
33-
)
34-
35-
36-
select_option = click.option(
37-
"-s",
38-
"--select",
39-
multiple=True,
40-
help="Specify the nodes to include.",
41-
)
42-
model_option = click.option(
43-
"-m",
44-
"--models",
45-
"--model",
46-
multiple=True,
47-
help="Specify the model nodes to include; other nodes are excluded.",
48-
)
49-
exclude_option = click.option("--exclude", multiple=True, help="Specify the nodes to exclude.")
50-
51-
# TODO: expand this out into --resource-type/--resource-types and --exclude-resource-type/--exclude-resource-types
52-
resource_types = [
53-
"metric",
54-
"semantic_model",
55-
"saved_query",
56-
"source",
57-
"analysis",
58-
"model",
59-
"test",
60-
"unit_test",
61-
"exposure",
62-
"snapshot",
63-
"seed",
64-
"default",
65-
"all",
66-
]
67-
resource_type_option = click.option(
68-
"--resource-type", type=click.Choice(resource_types, case_sensitive=False)
69-
)
52+
# at this point, :all_options just contains what's left because we popped the global options
53+
return dbt_operations, all_options
7054

7155

7256
@click.group(cls=ErrorHandlingGroup, invoke_without_command=True)
73-
@click.option("--profile", help="Which existing profile to load. Overrides output.profile")
74-
@click.option("-t", "--target", help="Which target to load for the given profile")
75-
@click.option(
76-
"-d",
77-
"--debug/--no-debug",
78-
default=False,
79-
help="Display debug logging during dbt execution. Useful for debugging and making bug reports events to help when debugging.",
80-
)
81-
@click.option(
82-
"--log-level",
83-
default="info",
84-
type=click.Choice(["debug", "info", "warn", "error", "none"]),
85-
help="Specify the minimum severity of events that are logged to the console and the log file.",
86-
)
87-
@click.option(
88-
"--profiles-dir",
89-
type=click.Path(exists=True, file_okay=False, path_type=Path),
90-
help="Which directory to look in for the profiles.yml file. If not set, dbt will look in the current working directory first, then HOME/.dbt/",
91-
)
92-
@click.option(
93-
"--project-dir",
94-
type=click.Path(exists=True, file_okay=False, path_type=Path),
95-
help="Which directory to look in for the dbt_project.yml file. Default is the current working directory and its parents.",
96-
)
57+
@global_options
9758
@click.pass_context
9859
@cli_global_error_handler
99-
def dbt(
100-
ctx: click.Context,
101-
profile: t.Optional[str] = None,
102-
target: t.Optional[str] = None,
103-
debug: bool = False,
104-
log_level: t.Optional[str] = None,
105-
profiles_dir: t.Optional[Path] = None,
106-
project_dir: t.Optional[Path] = None,
107-
) -> None:
60+
def dbt(ctx: click.Context, **kwargs: t.Any) -> None:
10861
"""
10962
An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine.
11063
"""
@@ -113,76 +66,45 @@ def dbt(
11366
# we dont need to import sqlmesh/load the project for CLI help
11467
return
11568

116-
# we have a partially applied function here because subcommands might set extra options like --vars
117-
# that need to be known before we attempt to load the project
118-
ctx.obj = functools.partial(
119-
create,
120-
project_dir=project_dir,
121-
profiles_dir=profiles_dir,
122-
profile=profile,
123-
target=target,
124-
debug=debug,
125-
log_level=log_level,
126-
)
69+
# only keep track of the global options that have been explicitly set
70+
# the subcommand handlers get invoked with the default values of the global options (even if the option is specified top level)
71+
# so we capture any explicitly set options to use as overrides in the subcommand handlers
72+
ctx.obj = {
73+
k: v for k, v in kwargs.items() if ctx.get_parameter_source(k) != ParameterSource.DEFAULT
74+
}
12775

12876
if not ctx.invoked_subcommand:
129-
if profile or target:
77+
if "profile" in ctx.obj or "target" in ctx.obj:
13078
# trigger a project load to validate the specified profile / target
131-
ctx.obj()
79+
_get_dbt_operations(ctx)
13280

13381
click.echo(
13482
f"No command specified. Run `{ctx.info_name} --help` to see the available commands."
13583
)
13684

13785

13886
@dbt.command()
139-
@select_option
140-
@model_option
141-
@exclude_option
142-
@resource_type_option
143-
@click.option(
144-
"-f",
145-
"--full-refresh",
146-
is_flag=True,
147-
default=False,
148-
help="If specified, sqlmesh will drop incremental models and fully-recalculate the incremental table from the model definition.",
149-
)
150-
@click.option(
151-
"--env",
152-
"--environment",
153-
help="Run against a specific Virtual Data Environment (VDE) instead of the main environment",
154-
)
155-
@click.option(
156-
"--empty/--no-empty", default=False, help="If specified, limit input refs and sources"
157-
)
158-
@click.option(
159-
"--threads",
160-
type=int,
161-
help="Specify number of threads to use while executing models. Overrides settings in profiles.yml.",
162-
)
163-
@vars_option
87+
@global_options
88+
@run_options
16489
@click.pass_context
16590
def run(
16691
ctx: click.Context,
167-
vars: t.Optional[t.Dict[str, t.Any]],
168-
threads: t.Optional[int],
16992
env: t.Optional[str] = None,
17093
**kwargs: t.Any,
17194
) -> None:
17295
"""Compile SQL and execute against the current target database."""
173-
_get_dbt_operations(ctx, vars, threads).run(environment=env, **kwargs)
96+
ops, remaining_kwargs = _get_dbt_operations(ctx, **kwargs)
97+
ops.run(environment=env, **remaining_kwargs)
17498

17599

176100
@dbt.command(name="list")
177-
@select_option
178-
@model_option
179-
@exclude_option
180-
@resource_type_option
181-
@vars_option
101+
@global_options
102+
@list_options
182103
@click.pass_context
183-
def list_(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], **kwargs: t.Any) -> None:
104+
def list_(ctx: click.Context, **kwargs: t.Any) -> None:
184105
"""List the resources in your project"""
185-
_get_dbt_operations(ctx, vars).list_(**kwargs)
106+
ops, remaining_kwargs = _get_dbt_operations(ctx, **kwargs)
107+
ops.list_(**remaining_kwargs)
186108

187109

188110
@dbt.command(name="ls", hidden=True) # hidden alias for list

0 commit comments

Comments
 (0)