dvt-core 0.52.2__cp310-cp310-macosx_10_9_x86_64.whl

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.

Potentially problematic release.


This version of dvt-core might be problematic. Click here for more details.

Files changed (275) hide show
  1. dbt/__init__.py +7 -0
  2. dbt/_pydantic_shim.py +26 -0
  3. dbt/artifacts/__init__.py +0 -0
  4. dbt/artifacts/exceptions/__init__.py +1 -0
  5. dbt/artifacts/exceptions/schemas.py +31 -0
  6. dbt/artifacts/resources/__init__.py +116 -0
  7. dbt/artifacts/resources/base.py +67 -0
  8. dbt/artifacts/resources/types.py +93 -0
  9. dbt/artifacts/resources/v1/analysis.py +10 -0
  10. dbt/artifacts/resources/v1/catalog.py +23 -0
  11. dbt/artifacts/resources/v1/components.py +274 -0
  12. dbt/artifacts/resources/v1/config.py +277 -0
  13. dbt/artifacts/resources/v1/documentation.py +11 -0
  14. dbt/artifacts/resources/v1/exposure.py +51 -0
  15. dbt/artifacts/resources/v1/function.py +52 -0
  16. dbt/artifacts/resources/v1/generic_test.py +31 -0
  17. dbt/artifacts/resources/v1/group.py +21 -0
  18. dbt/artifacts/resources/v1/hook.py +11 -0
  19. dbt/artifacts/resources/v1/macro.py +29 -0
  20. dbt/artifacts/resources/v1/metric.py +172 -0
  21. dbt/artifacts/resources/v1/model.py +145 -0
  22. dbt/artifacts/resources/v1/owner.py +10 -0
  23. dbt/artifacts/resources/v1/saved_query.py +111 -0
  24. dbt/artifacts/resources/v1/seed.py +41 -0
  25. dbt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  26. dbt/artifacts/resources/v1/semantic_model.py +314 -0
  27. dbt/artifacts/resources/v1/singular_test.py +14 -0
  28. dbt/artifacts/resources/v1/snapshot.py +91 -0
  29. dbt/artifacts/resources/v1/source_definition.py +84 -0
  30. dbt/artifacts/resources/v1/sql_operation.py +10 -0
  31. dbt/artifacts/resources/v1/unit_test_definition.py +77 -0
  32. dbt/artifacts/schemas/__init__.py +0 -0
  33. dbt/artifacts/schemas/base.py +191 -0
  34. dbt/artifacts/schemas/batch_results.py +24 -0
  35. dbt/artifacts/schemas/catalog/__init__.py +11 -0
  36. dbt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  37. dbt/artifacts/schemas/catalog/v1/catalog.py +59 -0
  38. dbt/artifacts/schemas/freshness/__init__.py +1 -0
  39. dbt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  40. dbt/artifacts/schemas/freshness/v3/freshness.py +158 -0
  41. dbt/artifacts/schemas/manifest/__init__.py +2 -0
  42. dbt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  43. dbt/artifacts/schemas/manifest/v12/manifest.py +211 -0
  44. dbt/artifacts/schemas/results.py +147 -0
  45. dbt/artifacts/schemas/run/__init__.py +2 -0
  46. dbt/artifacts/schemas/run/v5/__init__.py +0 -0
  47. dbt/artifacts/schemas/run/v5/run.py +184 -0
  48. dbt/artifacts/schemas/upgrades/__init__.py +4 -0
  49. dbt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  50. dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  51. dbt/artifacts/utils/validation.py +153 -0
  52. dbt/cli/__init__.py +1 -0
  53. dbt/cli/context.py +17 -0
  54. dbt/cli/exceptions.py +57 -0
  55. dbt/cli/flags.py +560 -0
  56. dbt/cli/main.py +2039 -0
  57. dbt/cli/option_types.py +121 -0
  58. dbt/cli/options.py +80 -0
  59. dbt/cli/params.py +804 -0
  60. dbt/cli/requires.py +490 -0
  61. dbt/cli/resolvers.py +50 -0
  62. dbt/cli/types.py +40 -0
  63. dbt/clients/__init__.py +0 -0
  64. dbt/clients/checked_load.py +83 -0
  65. dbt/clients/git.py +164 -0
  66. dbt/clients/jinja.py +206 -0
  67. dbt/clients/jinja_static.py +245 -0
  68. dbt/clients/registry.py +192 -0
  69. dbt/clients/yaml_helper.py +68 -0
  70. dbt/compilation.py +876 -0
  71. dbt/compute/__init__.py +14 -0
  72. dbt/compute/engines/__init__.py +12 -0
  73. dbt/compute/engines/spark_engine.py +624 -0
  74. dbt/compute/federated_executor.py +837 -0
  75. dbt/compute/filter_pushdown.cpython-310-darwin.so +0 -0
  76. dbt/compute/filter_pushdown.py +273 -0
  77. dbt/compute/jar_provisioning.cpython-310-darwin.so +0 -0
  78. dbt/compute/jar_provisioning.py +255 -0
  79. dbt/compute/java_compat.cpython-310-darwin.so +0 -0
  80. dbt/compute/java_compat.py +689 -0
  81. dbt/compute/jdbc_utils.cpython-310-darwin.so +0 -0
  82. dbt/compute/jdbc_utils.py +678 -0
  83. dbt/compute/smart_selector.cpython-310-darwin.so +0 -0
  84. dbt/compute/smart_selector.py +311 -0
  85. dbt/compute/strategies/__init__.py +54 -0
  86. dbt/compute/strategies/base.py +165 -0
  87. dbt/compute/strategies/dataproc.py +207 -0
  88. dbt/compute/strategies/emr.py +203 -0
  89. dbt/compute/strategies/local.py +364 -0
  90. dbt/compute/strategies/standalone.py +262 -0
  91. dbt/config/__init__.py +4 -0
  92. dbt/config/catalogs.py +94 -0
  93. dbt/config/compute.cpython-310-darwin.so +0 -0
  94. dbt/config/compute.py +547 -0
  95. dbt/config/dvt_profile.cpython-310-darwin.so +0 -0
  96. dbt/config/dvt_profile.py +342 -0
  97. dbt/config/profile.py +422 -0
  98. dbt/config/project.py +873 -0
  99. dbt/config/project_utils.py +28 -0
  100. dbt/config/renderer.py +231 -0
  101. dbt/config/runtime.py +553 -0
  102. dbt/config/selectors.py +208 -0
  103. dbt/config/utils.py +77 -0
  104. dbt/constants.py +28 -0
  105. dbt/context/__init__.py +0 -0
  106. dbt/context/base.py +745 -0
  107. dbt/context/configured.py +135 -0
  108. dbt/context/context_config.py +382 -0
  109. dbt/context/docs.py +82 -0
  110. dbt/context/exceptions_jinja.py +178 -0
  111. dbt/context/macro_resolver.py +195 -0
  112. dbt/context/macros.py +171 -0
  113. dbt/context/manifest.py +72 -0
  114. dbt/context/providers.py +2249 -0
  115. dbt/context/query_header.py +13 -0
  116. dbt/context/secret.py +58 -0
  117. dbt/context/target.py +74 -0
  118. dbt/contracts/__init__.py +0 -0
  119. dbt/contracts/files.py +413 -0
  120. dbt/contracts/graph/__init__.py +0 -0
  121. dbt/contracts/graph/manifest.py +1904 -0
  122. dbt/contracts/graph/metrics.py +97 -0
  123. dbt/contracts/graph/model_config.py +70 -0
  124. dbt/contracts/graph/node_args.py +42 -0
  125. dbt/contracts/graph/nodes.py +1806 -0
  126. dbt/contracts/graph/semantic_manifest.py +232 -0
  127. dbt/contracts/graph/unparsed.py +811 -0
  128. dbt/contracts/project.py +417 -0
  129. dbt/contracts/results.py +53 -0
  130. dbt/contracts/selection.py +23 -0
  131. dbt/contracts/sql.py +85 -0
  132. dbt/contracts/state.py +68 -0
  133. dbt/contracts/util.py +46 -0
  134. dbt/deprecations.py +346 -0
  135. dbt/deps/__init__.py +0 -0
  136. dbt/deps/base.py +152 -0
  137. dbt/deps/git.py +195 -0
  138. dbt/deps/local.py +79 -0
  139. dbt/deps/registry.py +130 -0
  140. dbt/deps/resolver.py +149 -0
  141. dbt/deps/tarball.py +120 -0
  142. dbt/docs/source/_ext/dbt_click.py +119 -0
  143. dbt/docs/source/conf.py +32 -0
  144. dbt/env_vars.py +64 -0
  145. dbt/event_time/event_time.py +40 -0
  146. dbt/event_time/sample_window.py +60 -0
  147. dbt/events/__init__.py +15 -0
  148. dbt/events/base_types.py +36 -0
  149. dbt/events/core_types_pb2.py +2 -0
  150. dbt/events/logging.py +108 -0
  151. dbt/events/types.py +2516 -0
  152. dbt/exceptions.py +1486 -0
  153. dbt/flags.py +89 -0
  154. dbt/graph/__init__.py +11 -0
  155. dbt/graph/cli.py +247 -0
  156. dbt/graph/graph.py +172 -0
  157. dbt/graph/queue.py +214 -0
  158. dbt/graph/selector.py +374 -0
  159. dbt/graph/selector_methods.py +975 -0
  160. dbt/graph/selector_spec.py +222 -0
  161. dbt/graph/thread_pool.py +18 -0
  162. dbt/hooks.py +21 -0
  163. dbt/include/README.md +49 -0
  164. dbt/include/__init__.py +3 -0
  165. dbt/include/starter_project/.gitignore +4 -0
  166. dbt/include/starter_project/README.md +15 -0
  167. dbt/include/starter_project/__init__.py +3 -0
  168. dbt/include/starter_project/analyses/.gitkeep +0 -0
  169. dbt/include/starter_project/dbt_project.yml +36 -0
  170. dbt/include/starter_project/macros/.gitkeep +0 -0
  171. dbt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
  172. dbt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
  173. dbt/include/starter_project/models/example/schema.yml +21 -0
  174. dbt/include/starter_project/seeds/.gitkeep +0 -0
  175. dbt/include/starter_project/snapshots/.gitkeep +0 -0
  176. dbt/include/starter_project/tests/.gitkeep +0 -0
  177. dbt/internal_deprecations.py +26 -0
  178. dbt/jsonschemas/__init__.py +3 -0
  179. dbt/jsonschemas/jsonschemas.py +309 -0
  180. dbt/jsonschemas/project/0.0.110.json +4717 -0
  181. dbt/jsonschemas/project/0.0.85.json +2015 -0
  182. dbt/jsonschemas/resources/0.0.110.json +2636 -0
  183. dbt/jsonschemas/resources/0.0.85.json +2536 -0
  184. dbt/jsonschemas/resources/latest.json +6773 -0
  185. dbt/links.py +4 -0
  186. dbt/materializations/__init__.py +0 -0
  187. dbt/materializations/incremental/__init__.py +0 -0
  188. dbt/materializations/incremental/microbatch.py +236 -0
  189. dbt/mp_context.py +8 -0
  190. dbt/node_types.py +37 -0
  191. dbt/parser/__init__.py +23 -0
  192. dbt/parser/analysis.py +21 -0
  193. dbt/parser/base.py +548 -0
  194. dbt/parser/common.py +266 -0
  195. dbt/parser/docs.py +52 -0
  196. dbt/parser/fixtures.py +51 -0
  197. dbt/parser/functions.py +30 -0
  198. dbt/parser/generic_test.py +100 -0
  199. dbt/parser/generic_test_builders.py +333 -0
  200. dbt/parser/hooks.py +118 -0
  201. dbt/parser/macros.py +137 -0
  202. dbt/parser/manifest.py +2204 -0
  203. dbt/parser/models.py +573 -0
  204. dbt/parser/partial.py +1178 -0
  205. dbt/parser/read_files.py +445 -0
  206. dbt/parser/schema_generic_tests.py +422 -0
  207. dbt/parser/schema_renderer.py +111 -0
  208. dbt/parser/schema_yaml_readers.py +935 -0
  209. dbt/parser/schemas.py +1466 -0
  210. dbt/parser/search.py +149 -0
  211. dbt/parser/seeds.py +28 -0
  212. dbt/parser/singular_test.py +20 -0
  213. dbt/parser/snapshots.py +44 -0
  214. dbt/parser/sources.py +558 -0
  215. dbt/parser/sql.py +62 -0
  216. dbt/parser/unit_tests.py +621 -0
  217. dbt/plugins/__init__.py +20 -0
  218. dbt/plugins/contracts.py +9 -0
  219. dbt/plugins/exceptions.py +2 -0
  220. dbt/plugins/manager.py +163 -0
  221. dbt/plugins/manifest.py +21 -0
  222. dbt/profiler.py +20 -0
  223. dbt/py.typed +1 -0
  224. dbt/query_analyzer.cpython-310-darwin.so +0 -0
  225. dbt/query_analyzer.py +410 -0
  226. dbt/runners/__init__.py +2 -0
  227. dbt/runners/exposure_runner.py +7 -0
  228. dbt/runners/no_op_runner.py +45 -0
  229. dbt/runners/saved_query_runner.py +7 -0
  230. dbt/selected_resources.py +8 -0
  231. dbt/task/__init__.py +0 -0
  232. dbt/task/base.py +503 -0
  233. dbt/task/build.py +197 -0
  234. dbt/task/clean.py +56 -0
  235. dbt/task/clone.py +161 -0
  236. dbt/task/compile.py +150 -0
  237. dbt/task/compute.py +454 -0
  238. dbt/task/debug.py +505 -0
  239. dbt/task/deps.py +280 -0
  240. dbt/task/docs/__init__.py +3 -0
  241. dbt/task/docs/generate.py +660 -0
  242. dbt/task/docs/index.html +250 -0
  243. dbt/task/docs/serve.py +29 -0
  244. dbt/task/freshness.py +322 -0
  245. dbt/task/function.py +121 -0
  246. dbt/task/group_lookup.py +46 -0
  247. dbt/task/init.py +553 -0
  248. dbt/task/java.py +316 -0
  249. dbt/task/list.py +236 -0
  250. dbt/task/printer.py +175 -0
  251. dbt/task/retry.py +175 -0
  252. dbt/task/run.py +1306 -0
  253. dbt/task/run_operation.py +141 -0
  254. dbt/task/runnable.py +758 -0
  255. dbt/task/seed.py +103 -0
  256. dbt/task/show.py +149 -0
  257. dbt/task/snapshot.py +56 -0
  258. dbt/task/spark.py +414 -0
  259. dbt/task/sql.py +110 -0
  260. dbt/task/target_sync.py +759 -0
  261. dbt/task/test.py +464 -0
  262. dbt/tests/fixtures/__init__.py +1 -0
  263. dbt/tests/fixtures/project.py +620 -0
  264. dbt/tests/util.py +651 -0
  265. dbt/tracking.py +529 -0
  266. dbt/utils/__init__.py +3 -0
  267. dbt/utils/artifact_upload.py +151 -0
  268. dbt/utils/utils.py +408 -0
  269. dbt/version.py +268 -0
  270. dvt_cli/__init__.py +72 -0
  271. dvt_core-0.52.2.dist-info/METADATA +286 -0
  272. dvt_core-0.52.2.dist-info/RECORD +275 -0
  273. dvt_core-0.52.2.dist-info/WHEEL +5 -0
  274. dvt_core-0.52.2.dist-info/entry_points.txt +2 -0
  275. dvt_core-0.52.2.dist-info/top_level.txt +2 -0
dbt/cli/main.py ADDED
@@ -0,0 +1,2039 @@
1
+ import functools
2
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
3
+ from copy import copy
4
+ from dataclasses import dataclass
5
+ import re
6
+ import time
7
+ from typing import Callable, List, Optional, Union
8
+
9
+ import click
10
+ from click.exceptions import BadOptionUsage
11
+ from click.exceptions import Exit as ClickExit
12
+ from click.exceptions import NoSuchOption, UsageError
13
+
14
+ from dbt.adapters.factory import register_adapter
15
+ from dbt.artifacts.schemas.catalog import CatalogArtifact
16
+ from dbt.artifacts.schemas.run import RunExecutionResult
17
+ from dbt.cli import params as p
18
+ from dbt.cli import requires
19
+ from dbt.cli.exceptions import DbtInternalException, DbtUsageException
20
+ from dbt.cli.requires import setup_manifest
21
+ from dbt.contracts.graph.manifest import Manifest
22
+ from dbt.mp_context import get_mp_context
23
+ from dbt_common.events.base_types import EventMsg
24
+
25
+
26
+ @dataclass
27
+ class dbtRunnerResult:
28
+ """Contains the result of an invocation of the dbtRunner"""
29
+
30
+ success: bool
31
+
32
+ exception: Optional[BaseException] = None
33
+ result: Union[
34
+ bool, # debug
35
+ CatalogArtifact, # docs generate
36
+ List[str], # list/ls
37
+ Manifest, # parse
38
+ None, # clean, deps, init, source
39
+ RunExecutionResult, # build, compile, run, seed, snapshot, test, run-operation
40
+ ] = None
41
+
42
+
43
+ # Programmatic invocation
44
+ class dbtRunner:
45
+ def __init__(
46
+ self,
47
+ manifest: Optional[Manifest] = None,
48
+ callbacks: Optional[List[Callable[[EventMsg], None]]] = None,
49
+ ) -> None:
50
+ self.manifest = manifest
51
+
52
+ if callbacks is None:
53
+ callbacks = []
54
+ self.callbacks = callbacks
55
+
56
+ def invoke(self, args: List[str], **kwargs) -> dbtRunnerResult:
57
+ try:
58
+ dbt_ctx = cli.make_context(cli.name, args.copy())
59
+ dbt_ctx.obj = {
60
+ "manifest": self.manifest,
61
+ "callbacks": self.callbacks,
62
+ "dbt_runner_command_args": args,
63
+ }
64
+
65
+ for key, value in kwargs.items():
66
+ dbt_ctx.params[key] = value
67
+ # Hack to set parameter source to custom string
68
+ dbt_ctx.set_parameter_source(key, "kwargs") # type: ignore
69
+
70
+ result, success = cli.invoke(dbt_ctx)
71
+ return dbtRunnerResult(
72
+ result=result,
73
+ success=success,
74
+ )
75
+ except requires.ResultExit as e:
76
+ return dbtRunnerResult(
77
+ result=e.result,
78
+ success=False,
79
+ )
80
+ except requires.ExceptionExit as e:
81
+ return dbtRunnerResult(
82
+ exception=e.exception,
83
+ success=False,
84
+ )
85
+ except (BadOptionUsage, NoSuchOption, UsageError) as e:
86
+ return dbtRunnerResult(
87
+ exception=DbtUsageException(e.message),
88
+ success=False,
89
+ )
90
+ except ClickExit as e:
91
+ if e.exit_code == 0:
92
+ return dbtRunnerResult(success=True)
93
+ return dbtRunnerResult(
94
+ exception=DbtInternalException(f"unhandled exit code {e.exit_code}"),
95
+ success=False,
96
+ )
97
+ except BaseException as e:
98
+ return dbtRunnerResult(
99
+ exception=e,
100
+ success=False,
101
+ )
102
+
103
+
104
+ # approach from https://github.com/pallets/click/issues/108#issuecomment-280489786
105
+ def global_flags(func):
106
+ @p.cache_selected_only
107
+ @p.debug
108
+ @p.defer
109
+ @p.deprecated_defer
110
+ @p.defer_state
111
+ @p.deprecated_favor_state
112
+ @p.deprecated_print
113
+ @p.deprecated_state
114
+ @p.fail_fast
115
+ @p.favor_state
116
+ @p.indirect_selection
117
+ @p.log_cache_events
118
+ @p.log_file_max_bytes
119
+ @p.log_format
120
+ @p.log_format_file
121
+ @p.log_level
122
+ @p.log_level_file
123
+ @p.log_path
124
+ @p.macro_debugging
125
+ @p.partial_parse
126
+ @p.partial_parse_file_path
127
+ @p.partial_parse_file_diff
128
+ @p.populate_cache
129
+ @p.print
130
+ @p.printer_width
131
+ @p.profile
132
+ @p.quiet
133
+ @p.record_timing_info
134
+ @p.send_anonymous_usage_stats
135
+ @p.single_threaded
136
+ @p.show_all_deprecations
137
+ @p.state
138
+ @p.static_parser
139
+ @p.target
140
+ @p.target_compute
141
+ @p.use_colors
142
+ @p.use_colors_file
143
+ @p.use_experimental_parser
144
+ @p.version
145
+ @p.version_check
146
+ @p.warn_error
147
+ @p.warn_error_options
148
+ @p.write_json
149
+ @p.use_fast_test_edges
150
+ @p.upload_artifacts
151
+ @functools.wraps(func)
152
+ def wrapper(*args, **kwargs):
153
+ return func(*args, **kwargs)
154
+
155
+ return wrapper
156
+
157
+
158
+ # dbt
159
+ @click.group(
160
+ context_settings={"help_option_names": ["-h", "--help"]},
161
+ invoke_without_command=True,
162
+ no_args_is_help=True,
163
+ epilog="Specify one of these sub-commands and you can find more help from there.",
164
+ )
165
+ @click.pass_context
166
+ @global_flags
167
+ @p.show_resource_report
168
+ def cli(ctx, **kwargs):
169
+ """An ELT tool for managing your SQL transformations and data models.
170
+ For more documentation on these commands, visit: docs.getdbt.com
171
+ """
172
+
173
+
174
+ # dbt build
175
+ @cli.command("build")
176
+ @click.pass_context
177
+ @global_flags
178
+ @p.empty
179
+ @p.event_time_start
180
+ @p.event_time_end
181
+ @p.exclude
182
+ @p.export_saved_queries
183
+ @p.full_refresh
184
+ @p.deprecated_include_saved_query
185
+ @p.profiles_dir
186
+ @p.project_dir
187
+ @p.resource_type
188
+ @p.exclude_resource_type
189
+ @p.sample
190
+ @p.select
191
+ @p.selector
192
+ @p.show
193
+ @p.store_failures
194
+ @p.target_path
195
+ @p.threads
196
+ @p.vars
197
+ @requires.postflight
198
+ @requires.preflight
199
+ @requires.profile
200
+ @requires.project
201
+ @requires.catalogs
202
+ @requires.runtime_config
203
+ @requires.manifest
204
+ def build(ctx, **kwargs):
205
+ """Run all seeds, models, snapshots, and tests in DAG order"""
206
+ from dbt.task.build import BuildTask
207
+
208
+ task = BuildTask(
209
+ ctx.obj["flags"],
210
+ ctx.obj["runtime_config"],
211
+ ctx.obj["manifest"],
212
+ )
213
+
214
+ results = task.run()
215
+ success = task.interpret_results(results)
216
+ return results, success
217
+
218
+
219
+ # dbt clean
220
+ @cli.command("clean")
221
+ @click.pass_context
222
+ @global_flags
223
+ @p.clean_project_files_only
224
+ @p.profiles_dir
225
+ @p.project_dir
226
+ @p.target_path
227
+ @p.vars
228
+ @requires.postflight
229
+ @requires.preflight
230
+ @requires.unset_profile
231
+ @requires.project
232
+ def clean(ctx, **kwargs):
233
+ """Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)"""
234
+ from dbt.task.clean import CleanTask
235
+
236
+ with CleanTask(ctx.obj["flags"], ctx.obj["project"]) as task:
237
+ results = task.run()
238
+ success = task.interpret_results(results)
239
+ return results, success
240
+
241
+
242
+ # dbt docs
243
+ @cli.group()
244
+ @click.pass_context
245
+ @global_flags
246
+ def docs(ctx, **kwargs):
247
+ """Generate or serve the documentation website for your project"""
248
+
249
+
250
+ # dbt docs generate
251
+ @docs.command("generate")
252
+ @click.pass_context
253
+ @global_flags
254
+ @p.compile_docs
255
+ @p.exclude
256
+ @p.profiles_dir
257
+ @p.project_dir
258
+ @p.select
259
+ @p.selector
260
+ @p.empty_catalog
261
+ @p.static
262
+ @p.target_path
263
+ @p.threads
264
+ @p.vars
265
+ @requires.postflight
266
+ @requires.preflight
267
+ @requires.profile
268
+ @requires.project
269
+ @requires.runtime_config
270
+ @requires.manifest(write=False)
271
+ def docs_generate(ctx, **kwargs):
272
+ """Generate the documentation website for your project"""
273
+ from dbt.task.docs.generate import GenerateTask
274
+
275
+ task = GenerateTask(
276
+ ctx.obj["flags"],
277
+ ctx.obj["runtime_config"],
278
+ ctx.obj["manifest"],
279
+ )
280
+
281
+ results = task.run()
282
+ success = task.interpret_results(results)
283
+ return results, success
284
+
285
+
286
+ # dbt docs serve
287
+ @docs.command("serve")
288
+ @click.pass_context
289
+ @global_flags
290
+ @p.browser
291
+ @p.host
292
+ @p.port
293
+ @p.profiles_dir
294
+ @p.project_dir
295
+ @p.target_path
296
+ @p.vars
297
+ @requires.postflight
298
+ @requires.preflight
299
+ @requires.profile
300
+ @requires.project
301
+ @requires.runtime_config
302
+ def docs_serve(ctx, **kwargs):
303
+ """Serve the documentation website for your project"""
304
+ from dbt.task.docs.serve import ServeTask
305
+
306
+ task = ServeTask(
307
+ ctx.obj["flags"],
308
+ ctx.obj["runtime_config"],
309
+ )
310
+
311
+ results = task.run()
312
+ success = task.interpret_results(results)
313
+ return results, success
314
+
315
+
316
+ # dbt compile
317
+ @cli.command("compile")
318
+ @click.pass_context
319
+ @global_flags
320
+ @p.exclude
321
+ @p.full_refresh
322
+ @p.show_output_format
323
+ @p.introspect
324
+ @p.profiles_dir
325
+ @p.project_dir
326
+ @p.empty
327
+ @p.select
328
+ @p.selector
329
+ @p.inline
330
+ @p.compile_inject_ephemeral_ctes
331
+ @p.target_path
332
+ @p.threads
333
+ @p.vars
334
+ @requires.postflight
335
+ @requires.preflight
336
+ @requires.profile
337
+ @requires.project
338
+ @requires.runtime_config
339
+ @requires.manifest
340
+ def compile(ctx, **kwargs):
341
+ """Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the
342
+ target/ directory."""
343
+ from dbt.task.compile import CompileTask
344
+
345
+ task = CompileTask(
346
+ ctx.obj["flags"],
347
+ ctx.obj["runtime_config"],
348
+ ctx.obj["manifest"],
349
+ )
350
+
351
+ results = task.run()
352
+ success = task.interpret_results(results)
353
+ return results, success
354
+
355
+
356
+ # dbt show
357
+ @cli.command("show")
358
+ @click.pass_context
359
+ @global_flags
360
+ @p.exclude
361
+ @p.full_refresh
362
+ @p.show_output_format
363
+ @p.show_limit
364
+ @p.introspect
365
+ @p.profiles_dir
366
+ @p.project_dir
367
+ @p.select
368
+ @p.selector
369
+ @p.inline
370
+ @p.inline_direct
371
+ @p.target_path
372
+ @p.threads
373
+ @p.vars
374
+ @requires.postflight
375
+ @requires.preflight
376
+ @requires.profile
377
+ @requires.project
378
+ @requires.runtime_config
379
+ def show(ctx, **kwargs):
380
+ """Generates executable SQL for a named resource or inline query, runs that SQL, and returns a preview of the
381
+ results. Does not materialize anything to the warehouse."""
382
+ from dbt.task.show import ShowTask, ShowTaskDirect
383
+
384
+ if ctx.obj["flags"].inline_direct:
385
+ # Issue the inline query directly, with no templating. Does not require
386
+ # loading the manifest.
387
+ register_adapter(ctx.obj["runtime_config"], get_mp_context())
388
+ task = ShowTaskDirect(
389
+ ctx.obj["flags"],
390
+ ctx.obj["runtime_config"],
391
+ )
392
+ else:
393
+ setup_manifest(ctx)
394
+ task = ShowTask(
395
+ ctx.obj["flags"],
396
+ ctx.obj["runtime_config"],
397
+ ctx.obj["manifest"],
398
+ )
399
+
400
+ results = task.run()
401
+ success = task.interpret_results(results)
402
+ return results, success
403
+
404
+
405
+ # dbt debug
406
+ @cli.command("debug")
407
+ @click.pass_context
408
+ @global_flags
409
+ @p.debug_connection
410
+ @p.config_dir
411
+ @p.profiles_dir_exists_false
412
+ @p.project_dir
413
+ @p.vars
414
+ @requires.postflight
415
+ @requires.preflight
416
+ def debug(ctx, **kwargs):
417
+ """Show information on the current dbt environment and check dependencies, then test the database connection. Not to be confused with the --debug option which increases verbosity."""
418
+ from dbt.task.debug import DebugTask
419
+
420
+ task = DebugTask(
421
+ ctx.obj["flags"],
422
+ )
423
+
424
+ results = task.run()
425
+ success = task.interpret_results(results)
426
+ return results, success
427
+
428
+
429
+ # dbt deps
430
+ @cli.command("deps")
431
+ @click.pass_context
432
+ @global_flags
433
+ @p.profiles_dir_exists_false
434
+ @p.project_dir
435
+ @p.vars
436
+ @p.source
437
+ @p.lock
438
+ @p.upgrade
439
+ @p.add_package
440
+ @requires.postflight
441
+ @requires.preflight
442
+ @requires.unset_profile
443
+ @requires.project
444
+ def deps(ctx, **kwargs):
445
+ """Install dbt packages specified.
446
+ In the following case, a new `package-lock.yml` will be generated and the packages are installed:
447
+ - user updated the packages.yml
448
+ - user specify the flag --update, which means for packages that are specified as a
449
+ range, dbt-core will try to install the newer version
450
+ Otherwise, deps will use `package-lock.yml` as source of truth to install packages.
451
+
452
+ There is a way to add new packages by providing an `--add-package` flag to deps command
453
+ which will allow user to specify a package they want to add in the format of packagename@version.
454
+ """
455
+ from dbt.task.deps import DepsTask
456
+
457
+ flags = ctx.obj["flags"]
458
+ if flags.ADD_PACKAGE:
459
+ if not flags.ADD_PACKAGE["version"] and flags.SOURCE != "local":
460
+ raise BadOptionUsage(
461
+ message=f"Version is required in --add-package when a package when source is {flags.SOURCE}",
462
+ option_name="--add-package",
463
+ )
464
+ with DepsTask(flags, ctx.obj["project"]) as task:
465
+ results = task.run()
466
+ success = task.interpret_results(results)
467
+ return results, success
468
+
469
+
470
+ # dbt init
471
+ @cli.command("init")
472
+ @click.pass_context
473
+ @global_flags
474
+ # for backwards compatibility, accept 'project_name' as an optional positional argument
475
+ @click.argument("project_name", required=False)
476
+ @p.profiles_dir_exists_false
477
+ @p.project_dir
478
+ @p.skip_profile_setup
479
+ @p.vars
480
+ @requires.postflight
481
+ @requires.preflight
482
+ def init(ctx, **kwargs):
483
+ """Initialize a new dbt project."""
484
+ from dbt.task.init import InitTask
485
+
486
+ with InitTask(ctx.obj["flags"]) as task:
487
+ results = task.run()
488
+ success = task.interpret_results(results)
489
+ return results, success
490
+
491
+
492
+ # dbt list
493
+ @cli.command("list")
494
+ @click.pass_context
495
+ @global_flags
496
+ @p.exclude
497
+ @p.models
498
+ @p.output
499
+ @p.output_keys
500
+ @p.profiles_dir
501
+ @p.project_dir
502
+ @p.resource_type
503
+ @p.exclude_resource_type
504
+ @p.raw_select
505
+ @p.selector
506
+ @p.target_path
507
+ @p.vars
508
+ @requires.postflight
509
+ @requires.preflight
510
+ @requires.profile
511
+ @requires.project
512
+ @requires.runtime_config
513
+ @requires.manifest
514
+ def list(ctx, **kwargs):
515
+ """List the resources in your project"""
516
+ from dbt.task.list import ListTask
517
+
518
+ task = ListTask(
519
+ ctx.obj["flags"],
520
+ ctx.obj["runtime_config"],
521
+ ctx.obj["manifest"],
522
+ )
523
+
524
+ results = task.run()
525
+ success = task.interpret_results(results)
526
+ return results, success
527
+
528
+
529
+ # Alias "list" to "ls"
530
+ ls = copy(cli.commands["list"])
531
+ ls.hidden = True
532
+ cli.add_command(ls, "ls")
533
+
534
+
535
+ # dbt parse
536
+ @cli.command("parse")
537
+ @click.pass_context
538
+ @global_flags
539
+ @p.profiles_dir
540
+ @p.project_dir
541
+ @p.target_path
542
+ @p.threads
543
+ @p.vars
544
+ @requires.postflight
545
+ @requires.preflight
546
+ @requires.profile
547
+ @requires.project
548
+ @requires.catalogs
549
+ @requires.runtime_config
550
+ @requires.manifest(write_perf_info=True)
551
+ def parse(ctx, **kwargs):
552
+ """Parses the project and provides information on performance"""
553
+ # manifest generation and writing happens in @requires.manifest
554
+ return ctx.obj["manifest"], True
555
+
556
+
557
+ # dbt run
558
+ @cli.command("run")
559
+ @click.pass_context
560
+ @global_flags
561
+ @p.exclude
562
+ @p.full_refresh
563
+ @p.profiles_dir
564
+ @p.project_dir
565
+ @p.empty
566
+ @p.event_time_start
567
+ @p.event_time_end
568
+ @p.sample
569
+ @p.select
570
+ @p.selector
571
+ @p.target_path
572
+ @p.threads
573
+ @p.vars
574
+ @requires.postflight
575
+ @requires.preflight
576
+ @requires.profile
577
+ @requires.project
578
+ @requires.catalogs
579
+ @requires.runtime_config
580
+ @requires.manifest
581
+ def run(ctx, **kwargs):
582
+ """Compile SQL and execute against the current target database."""
583
+ from dbt.task.run import RunTask
584
+
585
+ task = RunTask(
586
+ ctx.obj["flags"],
587
+ ctx.obj["runtime_config"],
588
+ ctx.obj["manifest"],
589
+ )
590
+
591
+ results = task.run()
592
+ success = task.interpret_results(results)
593
+ return results, success
594
+
595
+
596
+ # dbt retry
597
+ @cli.command("retry")
598
+ @click.pass_context
599
+ @global_flags
600
+ @p.project_dir
601
+ @p.profiles_dir
602
+ @p.vars
603
+ @p.target_path
604
+ @p.threads
605
+ @p.full_refresh
606
+ @requires.postflight
607
+ @requires.preflight
608
+ @requires.profile
609
+ @requires.project
610
+ @requires.runtime_config
611
+ def retry(ctx, **kwargs):
612
+ """Retry the nodes that failed in the previous run."""
613
+ from dbt.task.retry import RetryTask
614
+
615
+ # Retry will parse manifest inside the task after we consolidate the flags
616
+ task = RetryTask(
617
+ ctx.obj["flags"],
618
+ ctx.obj["runtime_config"],
619
+ )
620
+
621
+ results = task.run()
622
+ success = task.interpret_results(results)
623
+ return results, success
624
+
625
+
626
+ # dbt clone
627
+ @cli.command("clone")
628
+ @click.pass_context
629
+ @global_flags
630
+ @p.exclude
631
+ @p.full_refresh
632
+ @p.profiles_dir
633
+ @p.project_dir
634
+ @p.resource_type
635
+ @p.exclude_resource_type
636
+ @p.select
637
+ @p.selector
638
+ @p.target_path
639
+ @p.threads
640
+ @p.vars
641
+ @requires.preflight
642
+ @requires.profile
643
+ @requires.project
644
+ @requires.runtime_config
645
+ @requires.manifest
646
+ @requires.postflight
647
+ def clone(ctx, **kwargs):
648
+ """Create clones of selected nodes based on their location in the manifest provided to --state."""
649
+ from dbt.task.clone import CloneTask
650
+
651
+ task = CloneTask(
652
+ ctx.obj["flags"],
653
+ ctx.obj["runtime_config"],
654
+ ctx.obj["manifest"],
655
+ )
656
+
657
+ results = task.run()
658
+ success = task.interpret_results(results)
659
+ return results, success
660
+
661
+
662
+ # dbt run operation
663
+ @cli.command("run-operation")
664
+ @click.pass_context
665
+ @global_flags
666
+ @click.argument("macro")
667
+ @p.args
668
+ @p.profiles_dir
669
+ @p.project_dir
670
+ @p.target_path
671
+ @p.threads
672
+ @p.vars
673
+ @requires.postflight
674
+ @requires.preflight
675
+ @requires.profile
676
+ @requires.project
677
+ @requires.runtime_config
678
+ @requires.manifest
679
+ def run_operation(ctx, **kwargs):
680
+ """Run the named macro with any supplied arguments."""
681
+ from dbt.task.run_operation import RunOperationTask
682
+
683
+ task = RunOperationTask(
684
+ ctx.obj["flags"],
685
+ ctx.obj["runtime_config"],
686
+ ctx.obj["manifest"],
687
+ )
688
+
689
+ results = task.run()
690
+ success = task.interpret_results(results)
691
+ return results, success
692
+
693
+
694
+ # dbt seed
695
+ @cli.command("seed")
696
+ @click.pass_context
697
+ @global_flags
698
+ @p.exclude
699
+ @p.full_refresh
700
+ @p.profiles_dir
701
+ @p.project_dir
702
+ @p.select
703
+ @p.selector
704
+ @p.show
705
+ @p.target_path
706
+ @p.threads
707
+ @p.vars
708
+ @requires.postflight
709
+ @requires.preflight
710
+ @requires.profile
711
+ @requires.project
712
+ @requires.catalogs
713
+ @requires.runtime_config
714
+ @requires.manifest
715
+ def seed(ctx, **kwargs):
716
+ """Load data from csv files into your data warehouse."""
717
+ from dbt.task.seed import SeedTask
718
+
719
+ task = SeedTask(
720
+ ctx.obj["flags"],
721
+ ctx.obj["runtime_config"],
722
+ ctx.obj["manifest"],
723
+ )
724
+ results = task.run()
725
+ success = task.interpret_results(results)
726
+ return results, success
727
+
728
+
729
+ # dbt snapshot
730
+ @cli.command("snapshot")
731
+ @click.pass_context
732
+ @global_flags
733
+ @p.empty
734
+ @p.exclude
735
+ @p.profiles_dir
736
+ @p.project_dir
737
+ @p.select
738
+ @p.selector
739
+ @p.target_path
740
+ @p.threads
741
+ @p.vars
742
+ @requires.postflight
743
+ @requires.preflight
744
+ @requires.profile
745
+ @requires.project
746
+ @requires.catalogs
747
+ @requires.runtime_config
748
+ @requires.manifest
749
+ def snapshot(ctx, **kwargs):
750
+ """Execute snapshots defined in your project"""
751
+ from dbt.task.snapshot import SnapshotTask
752
+
753
+ task = SnapshotTask(
754
+ ctx.obj["flags"],
755
+ ctx.obj["runtime_config"],
756
+ ctx.obj["manifest"],
757
+ )
758
+
759
+ results = task.run()
760
+ success = task.interpret_results(results)
761
+ return results, success
762
+
763
+
764
+ # dbt source
765
+ @cli.group()
766
+ @click.pass_context
767
+ @global_flags
768
+ def source(ctx, **kwargs):
769
+ """Manage your project's sources"""
770
+
771
+
772
+ # dbt source freshness
773
+ @source.command("freshness")
774
+ @click.pass_context
775
+ @global_flags
776
+ @p.exclude
777
+ @p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate?
778
+ @p.profiles_dir
779
+ @p.project_dir
780
+ @p.select
781
+ @p.selector
782
+ @p.target_path
783
+ @p.threads
784
+ @p.vars
785
+ @requires.postflight
786
+ @requires.preflight
787
+ @requires.profile
788
+ @requires.project
789
+ @requires.runtime_config
790
+ @requires.manifest
791
+ def freshness(ctx, **kwargs):
792
+ """check the current freshness of the project's sources"""
793
+ from dbt.task.freshness import FreshnessTask
794
+
795
+ task = FreshnessTask(
796
+ ctx.obj["flags"],
797
+ ctx.obj["runtime_config"],
798
+ ctx.obj["manifest"],
799
+ )
800
+
801
+ results = task.run()
802
+ success = task.interpret_results(results)
803
+ return results, success
804
+
805
+
806
+ # Alias "source freshness" to "snapshot-freshness"
807
+ snapshot_freshness = copy(cli.commands["source"].commands["freshness"]) # type: ignore
808
+ snapshot_freshness.hidden = True
809
+ cli.commands["source"].add_command(snapshot_freshness, "snapshot-freshness") # type: ignore
810
+
811
+
812
+ # dbt test
813
+ @cli.command("test")
814
+ @click.pass_context
815
+ @global_flags
816
+ @p.exclude
817
+ @p.resource_type
818
+ @p.exclude_resource_type
819
+ @p.profiles_dir
820
+ @p.project_dir
821
+ @p.select
822
+ @p.selector
823
+ @p.store_failures
824
+ @p.target_path
825
+ @p.threads
826
+ @p.vars
827
+ @requires.postflight
828
+ @requires.preflight
829
+ @requires.profile
830
+ @requires.project
831
+ @requires.runtime_config
832
+ @requires.manifest
833
+ def test(ctx, **kwargs):
834
+ """Runs tests on data in deployed models. Run this after `dbt run`"""
835
+ from dbt.task.test import TestTask
836
+
837
+ task = TestTask(
838
+ ctx.obj["flags"],
839
+ ctx.obj["runtime_config"],
840
+ ctx.obj["manifest"],
841
+ )
842
+
843
+ results = task.run()
844
+ success = task.interpret_results(results)
845
+ return results, success
846
+
847
+
848
+ # DVT compute commands
849
+ @cli.group()
850
+ @click.pass_context
851
+ @global_flags
852
+ def compute(ctx, **kwargs):
853
+ """Manage DVT compute engines for multi-source federation.
854
+
855
+ Compute engines are configured in ~/.dvt/.data/computes.yml.
856
+ Use 'dvt compute edit' to modify configuration.
857
+
858
+ Commands:
859
+ test Show all compute engines with connection status
860
+ edit Open computes.yml in your editor
861
+ validate Validate computes.yml syntax
862
+ """
863
+
864
+
865
+ @compute.command("list")
866
+ @click.pass_context
867
+ @p.project_dir
868
+ def compute_list(ctx, **kwargs):
869
+ """List all configured compute engines.
870
+
871
+ Shows all compute engines defined in computes.yml with their
872
+ platform type and description.
873
+
874
+ Examples:
875
+ dvt compute list # List all compute engines
876
+ """
877
+ from dbt.task.compute import ComputeTask
878
+
879
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
880
+ success = task.list_computes()
881
+ return None, success
882
+
883
+
884
+ @compute.command("test")
885
+ @click.pass_context
886
+ @click.argument("compute_name", required=True)
887
+ @p.project_dir
888
+ def compute_test(ctx, compute_name, **kwargs):
889
+ """Test a compute engine's connectivity.
890
+
891
+ Tests the specified compute engine and shows its status.
892
+ Use 'dvt compute list' to see all available compute engines.
893
+
894
+ Shows rich status symbols:
895
+ ✅ Connected/Available
896
+ ❌ Error/Not available
897
+ ⚠️ Warning (missing optional dependency)
898
+
899
+ Examples:
900
+ dvt compute test spark-local # Test local Spark
901
+ dvt compute test databricks-prod # Test Databricks connectivity
902
+ dvt compute test spark-docker # Test Docker Spark cluster
903
+ """
904
+ from dbt.task.compute import ComputeTask
905
+
906
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
907
+ success = task.test_single_compute(compute_name)
908
+ return None, success
909
+
910
+
911
+ @compute.command("edit")
912
+ @click.pass_context
913
+ @p.project_dir
914
+ def compute_edit(ctx, **kwargs):
915
+ """Open computes.yml in your editor.
916
+
917
+ Opens the compute configuration file in your preferred editor.
918
+ Uses EDITOR environment variable, or falls back to common editors
919
+ (code, nano, vim, vi, notepad).
920
+
921
+ The file contains comprehensive commented samples for:
922
+ - Local Spark (default)
923
+ - Databricks (SQL Warehouse and Interactive Cluster)
924
+ - AWS EMR
925
+ - GCP Dataproc
926
+ - Standalone Spark clusters
927
+
928
+ After editing, run 'dvt compute validate' to check syntax.
929
+
930
+ Examples:
931
+ dvt compute edit # Open in default editor
932
+ EDITOR=nano dvt compute edit # Use specific editor
933
+ """
934
+ from dbt.task.compute import ComputeTask
935
+
936
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
937
+ success = task.edit_config()
938
+ return None, success
939
+
940
+
941
+ @compute.command("validate")
942
+ @click.pass_context
943
+ @p.project_dir
944
+ def compute_validate(ctx, **kwargs):
945
+ """Validate computes.yml syntax and configuration.
946
+
947
+ Checks the compute configuration file for:
948
+ - Valid YAML syntax
949
+ - Required fields (target_compute, type)
950
+ - Valid compute engine references
951
+ - Platform-specific configuration
952
+
953
+ Examples:
954
+ dvt compute validate # Validate configuration
955
+ """
956
+ from dbt.task.compute import ComputeTask
957
+
958
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
959
+ is_valid = task.validate_config()
960
+ return None, is_valid
961
+
962
+
963
+
964
+
965
+ # DVT migrate command - migrate from .dbt/ to .dvt/
966
+ @cli.command("migrate")
967
+ @click.pass_context
968
+ @p.profiles_dir
969
+ @p.project_dir
970
+ def migrate(ctx, **kwargs):
971
+ """Migrate configuration from .dbt/ to .dvt/ directory"""
972
+ from pathlib import Path
973
+ import shutil
974
+
975
+ home = Path.home()
976
+
977
+ old_dbt_dir = home / ".dbt"
978
+ new_dvt_dir = home / ".dvt"
979
+
980
+ if not old_dbt_dir.exists():
981
+ click.echo("No .dbt/ directory found - nothing to migrate")
982
+ return True, True
983
+
984
+ if new_dvt_dir.exists():
985
+ response = click.confirm(
986
+ f".dvt/ directory already exists at {new_dvt_dir}. Overwrite files?"
987
+ )
988
+ if not response:
989
+ click.echo("Migration cancelled")
990
+ return False, False
991
+
992
+ # Create .dvt directory
993
+ new_dvt_dir.mkdir(parents=True, exist_ok=True)
994
+
995
+ # Copy profiles.yml if it exists
996
+ old_profiles = old_dbt_dir / "profiles.yml"
997
+ new_profiles = new_dvt_dir / "profiles.yml"
998
+
999
+ migrated_files = []
1000
+
1001
+ if old_profiles.exists():
1002
+ shutil.copy2(old_profiles, new_profiles)
1003
+ migrated_files.append("profiles.yml")
1004
+ click.echo(f"✓ Copied {old_profiles} → {new_profiles}")
1005
+
1006
+ if migrated_files:
1007
+ click.echo("")
1008
+ click.echo(f"Migration complete! Migrated {len(migrated_files)} files:")
1009
+ for f in migrated_files:
1010
+ click.echo(f" - {f}")
1011
+ click.echo("")
1012
+ click.echo("Your .dbt/ directory has been preserved.")
1013
+ click.echo("DVT will now use .dvt/ for all configurations.")
1014
+ click.echo("")
1015
+ click.echo("Note: Compute engines are managed via 'dvt compute' commands.")
1016
+ else:
1017
+ click.echo("No files to migrate")
1018
+
1019
+ return True, True
1020
+
1021
+
1022
+ # DVT target (connection) management commands
1023
+ @cli.group()
1024
+ @click.pass_context
1025
+ @global_flags
1026
+ def target(ctx, **kwargs):
1027
+ """Manage connection targets in profiles.yml"""
1028
+
1029
+
1030
+ @target.command("list")
1031
+ @click.option("--profile", help="Profile name to list targets from")
1032
+ @click.pass_context
1033
+ @p.profiles_dir
1034
+ @p.project_dir
1035
+ def target_list(ctx, profile, profiles_dir, project_dir, **kwargs):
1036
+ """List available targets in profiles.yml.
1037
+
1038
+ If executed from within a DVT project directory, automatically detects the profile
1039
+ from dbt_project.yml. Use --profile to override or when outside a project directory.
1040
+
1041
+ Features colored output for improved readability:
1042
+ - Cyan: Profile and target names
1043
+ - Green: Default target indicators
1044
+ - Red: Error messages
1045
+
1046
+ Examples:
1047
+ dvt target list # Auto-detect from project
1048
+ dvt target list --profile my_proj # Specific profile
1049
+ """
1050
+ from dbt.config.profile import read_profile
1051
+ from dbt.config.project_utils import get_project_profile_name
1052
+
1053
+ profiles = read_profile(profiles_dir)
1054
+
1055
+ if not profiles:
1056
+ click.echo(click.style("No profiles found in profiles.yml", fg="red"))
1057
+ ctx.exit(1)
1058
+
1059
+ # If --profile not provided, try to get from dbt_project.yml
1060
+ if not profile:
1061
+ profile = get_project_profile_name(project_dir)
1062
+ if not profile:
1063
+ # No profile specified and none found in project - show all profiles
1064
+ click.echo(click.style("Available profiles:", fg="cyan", bold=True))
1065
+ click.echo("")
1066
+ for profile_name, profile_data in profiles.items():
1067
+ if profile_data is None:
1068
+ profile_data = {}
1069
+ outputs = profile_data.get("outputs", {})
1070
+ target_count = len(outputs)
1071
+ default_target = profile_data.get(
1072
+ "default_target", profile_data.get("target", "unknown")
1073
+ )
1074
+ click.echo(f" {click.style(profile_name, fg='cyan')}")
1075
+ click.echo(
1076
+ f" Default target: {click.style(default_target, fg='green')}"
1077
+ )
1078
+ click.echo(f" Targets: {target_count}")
1079
+ click.echo("")
1080
+ return True, True
1081
+
1082
+ # Show targets for specific profile
1083
+ if profile not in profiles:
1084
+ click.echo(click.style(f"✗ Profile '{profile}' not found", fg="red"))
1085
+ ctx.exit(1)
1086
+
1087
+ profile_data = profiles[profile]
1088
+ if profile_data is None:
1089
+ click.echo(click.style(f"✗ Profile '{profile}' is empty or invalid", fg="red"))
1090
+ ctx.exit(1)
1091
+ outputs = profile_data.get("outputs", {})
1092
+
1093
+ if not outputs:
1094
+ click.echo(click.style(f"No targets found in profile '{profile}'", fg="yellow"))
1095
+ return True, True
1096
+
1097
+ default_target = profile_data.get(
1098
+ "default_target", profile_data.get("target", "unknown")
1099
+ )
1100
+
1101
+ # Always show profile name header for context
1102
+ click.echo(click.style(f"Profile: {profile}", fg="cyan", bold=True))
1103
+ click.echo(f"Default target: {click.style(default_target, fg='green')}")
1104
+ click.echo("")
1105
+ click.echo("Available targets:")
1106
+ for target_name, target_config in outputs.items():
1107
+ default_marker = (
1108
+ click.style(" (default)", fg="green")
1109
+ if target_name == default_target
1110
+ else ""
1111
+ )
1112
+ adapter_type = target_config.get("type", "unknown")
1113
+ click.echo(
1114
+ f" {click.style(target_name, fg='cyan')} ({adapter_type}){default_marker}"
1115
+ )
1116
+
1117
+ return True, True
1118
+
1119
+
1120
+ def _get_test_query(adapter_type: str) -> str:
1121
+ """Get adapter-specific test query for connection validation.
1122
+
1123
+ Args:
1124
+ adapter_type: The adapter type (postgres, snowflake, etc.)
1125
+
1126
+ Returns:
1127
+ SQL query string for testing connectivity
1128
+ """
1129
+ test_queries = {
1130
+ "postgres": "SELECT 1",
1131
+ "snowflake": "SELECT CURRENT_VERSION()",
1132
+ "bigquery": "SELECT 1",
1133
+ "redshift": "SELECT 1",
1134
+ "databricks": "SELECT 1",
1135
+ "mysql": "SELECT 1",
1136
+ "sqlserver": "SELECT 1",
1137
+ "oracle": "SELECT 1 FROM DUAL",
1138
+ "db2": "SELECT 1 FROM SYSIBM.SYSDUMMY1",
1139
+ "teradata": "SELECT 1",
1140
+ }
1141
+ return test_queries.get(adapter_type, "SELECT 1")
1142
+
1143
+
1144
+ def _get_connection_error_hint(exception: Exception, adapter_type: str) -> str:
1145
+ """Provide user-friendly hints for common connection errors.
1146
+
1147
+ Args:
1148
+ exception: The exception that was raised
1149
+ adapter_type: The adapter type being tested
1150
+
1151
+ Returns:
1152
+ A helpful error message with troubleshooting hints
1153
+ """
1154
+ error_str = str(exception).lower()
1155
+
1156
+ # Common error patterns and hints
1157
+ if "timeout" in error_str or "timed out" in error_str:
1158
+ return "Connection timeout - Check network connectivity and firewall rules"
1159
+ elif "could not connect" in error_str or "connection refused" in error_str:
1160
+ return "Connection refused - Verify host and port are correct"
1161
+ elif (
1162
+ "authentication" in error_str or "password" in error_str or "login" in error_str
1163
+ ):
1164
+ return "Authentication failed - Check username and password"
1165
+ elif "database" in error_str and "does not exist" in error_str:
1166
+ return "Database not found - Verify database name"
1167
+ elif "permission" in error_str or "access denied" in error_str:
1168
+ return "Permission denied - Check user privileges"
1169
+ elif "ssl" in error_str or "certificate" in error_str:
1170
+ return "SSL/TLS error - Check SSL configuration"
1171
+ elif "no such host" in error_str or "name resolution" in error_str:
1172
+ return "Host not found - Verify hostname is correct"
1173
+
1174
+ # Adapter-specific hints
1175
+ if adapter_type == "snowflake":
1176
+ if "account" in error_str:
1177
+ return "Invalid Snowflake account - Check account identifier format"
1178
+ elif "warehouse" in error_str:
1179
+ return "Warehouse error - Verify warehouse name and status"
1180
+ elif adapter_type == "databricks":
1181
+ if "token" in error_str:
1182
+ return "Invalid token - Check Databricks access token"
1183
+ elif "cluster" in error_str:
1184
+ return "Cluster error - Verify cluster is running and accessible"
1185
+
1186
+ return "Connection failed - See error details above"
1187
+
1188
+
1189
+ def _test_single_target(profile_data: dict, target_name: str) -> tuple[bool, str]:
1190
+ """Test connection to a single target by executing a query.
1191
+
1192
+ Args:
1193
+ profile_data: The profile dictionary containing outputs
1194
+ target_name: Name of the target to test
1195
+
1196
+ Returns:
1197
+ Tuple of (success: bool, message: str)
1198
+ """
1199
+ outputs = profile_data.get("outputs", {})
1200
+
1201
+ if target_name not in outputs:
1202
+ return False, f"Target '{target_name}' not found"
1203
+
1204
+ target_config = outputs[target_name]
1205
+ adapter_type = target_config.get("type", "unknown")
1206
+
1207
+ # Validate required fields first
1208
+ required_fields = {
1209
+ "postgres": ["host", "user", "database"],
1210
+ "snowflake": ["account", "user", "database", "warehouse"],
1211
+ "bigquery": ["project", "dataset"],
1212
+ "redshift": ["host", "user", "database"],
1213
+ "databricks": ["host", "http_path"],
1214
+ }
1215
+
1216
+ missing_fields = []
1217
+ if adapter_type in required_fields:
1218
+ for field in required_fields[adapter_type]:
1219
+ if field not in target_config:
1220
+ missing_fields.append(field)
1221
+
1222
+ if missing_fields:
1223
+ return False, f"Missing required fields: {', '.join(missing_fields)}"
1224
+
1225
+ # Import adapter and test connection
1226
+ try:
1227
+ # Get test query for this adapter
1228
+ test_query = _get_test_query(adapter_type)
1229
+
1230
+ # Use the adapter's native connection testing approach
1231
+ # Each adapter has different connection methods, so we'll use
1232
+ # a simplified approach that works across adapters
1233
+
1234
+ if adapter_type == "postgres":
1235
+ try:
1236
+ import psycopg2
1237
+
1238
+ # Build connection string
1239
+ conn_params = {
1240
+ "host": target_config.get("host", "localhost"),
1241
+ "port": target_config.get("port", 5432),
1242
+ "database": target_config.get(
1243
+ "database", target_config.get("dbname")
1244
+ ),
1245
+ "user": target_config.get("user"),
1246
+ "password": target_config.get("password"),
1247
+ }
1248
+ # Test connection
1249
+ conn = psycopg2.connect(**conn_params, connect_timeout=10)
1250
+ cursor = conn.cursor()
1251
+ cursor.execute(test_query)
1252
+ cursor.fetchone()
1253
+ cursor.close()
1254
+ conn.close()
1255
+ return True, "Connection successful"
1256
+ except ImportError:
1257
+ return (
1258
+ False,
1259
+ "psycopg2 not installed - Run 'pip install psycopg2-binary' or 'pip install dbt-postgres'",
1260
+ )
1261
+ except Exception as e:
1262
+ hint = _get_connection_error_hint(e, adapter_type)
1263
+ return False, f"Connection failed: {str(e)}\nHint: {hint}"
1264
+
1265
+ elif adapter_type == "snowflake":
1266
+ try:
1267
+ import snowflake.connector
1268
+
1269
+ # Build connection params
1270
+ conn_params = {
1271
+ "account": target_config.get("account"),
1272
+ "user": target_config.get("user"),
1273
+ "password": target_config.get("password"),
1274
+ "database": target_config.get("database"),
1275
+ "warehouse": target_config.get("warehouse"),
1276
+ "schema": target_config.get("schema", "PUBLIC"),
1277
+ }
1278
+ # Test connection
1279
+ conn = snowflake.connector.connect(**conn_params, login_timeout=10)
1280
+ cursor = conn.cursor()
1281
+ cursor.execute(test_query)
1282
+ cursor.fetchone()
1283
+ cursor.close()
1284
+ conn.close()
1285
+ return True, "Connection successful"
1286
+ except ImportError:
1287
+ return (
1288
+ False,
1289
+ "snowflake-connector-python not installed - Run 'pip install snowflake-connector-python' or 'pip install dbt-snowflake'",
1290
+ )
1291
+ except Exception as e:
1292
+ hint = _get_connection_error_hint(e, adapter_type)
1293
+ return False, f"Connection failed: {str(e)}\nHint: {hint}"
1294
+
1295
+ elif adapter_type == "bigquery":
1296
+ try:
1297
+ from google.cloud import bigquery
1298
+
1299
+ # Build client
1300
+ project = target_config.get("project")
1301
+ client = bigquery.Client(project=project)
1302
+ # Test connection with simple query
1303
+ query_job = client.query(test_query)
1304
+ query_job.result(timeout=10)
1305
+ return True, "Connection successful"
1306
+ except ImportError:
1307
+ return (
1308
+ False,
1309
+ "google-cloud-bigquery not installed - Run 'pip install google-cloud-bigquery' or 'pip install dbt-bigquery'",
1310
+ )
1311
+ except Exception as e:
1312
+ hint = _get_connection_error_hint(e, adapter_type)
1313
+ return False, f"Connection failed: {str(e)}\nHint: {hint}"
1314
+
1315
+ elif adapter_type == "redshift":
1316
+ try:
1317
+ import psycopg2
1318
+
1319
+ # Redshift uses postgres protocol
1320
+ conn_params = {
1321
+ "host": target_config.get("host"),
1322
+ "port": target_config.get("port", 5439),
1323
+ "database": target_config.get("database"),
1324
+ "user": target_config.get("user"),
1325
+ "password": target_config.get("password"),
1326
+ }
1327
+ # Test connection
1328
+ conn = psycopg2.connect(**conn_params, connect_timeout=10)
1329
+ cursor = conn.cursor()
1330
+ cursor.execute(test_query)
1331
+ cursor.fetchone()
1332
+ cursor.close()
1333
+ conn.close()
1334
+ return True, "Connection successful"
1335
+ except ImportError:
1336
+ return (
1337
+ False,
1338
+ "psycopg2 not installed - Run 'pip install psycopg2-binary' or 'pip install dbt-redshift'",
1339
+ )
1340
+ except Exception as e:
1341
+ hint = _get_connection_error_hint(e, adapter_type)
1342
+ return False, f"Connection failed: {str(e)}\nHint: {hint}"
1343
+
1344
+ elif adapter_type == "databricks":
1345
+ try:
1346
+ from databricks import sql
1347
+
1348
+ # Build connection params
1349
+ conn_params = {
1350
+ "server_hostname": target_config.get("host", "").replace(
1351
+ "https://", ""
1352
+ ),
1353
+ "http_path": target_config.get("http_path"),
1354
+ "access_token": target_config.get("token"),
1355
+ }
1356
+ # Test connection
1357
+ conn = sql.connect(**conn_params)
1358
+ cursor = conn.cursor()
1359
+ cursor.execute(test_query)
1360
+ cursor.fetchone()
1361
+ cursor.close()
1362
+ conn.close()
1363
+ return True, "Connection successful"
1364
+ except ImportError:
1365
+ return (
1366
+ False,
1367
+ "databricks-sql-connector not installed - Run 'pip install databricks-sql-connector' or 'pip install dbt-databricks'",
1368
+ )
1369
+ except Exception as e:
1370
+ hint = _get_connection_error_hint(e, adapter_type)
1371
+ return False, f"Connection failed: {str(e)}\nHint: {hint}"
1372
+
1373
+ else:
1374
+ # For unsupported adapter types, just return configuration validation
1375
+ return (
1376
+ True,
1377
+ "Configuration valid (network testing not available for this adapter type)",
1378
+ )
1379
+
1380
+ except Exception as e:
1381
+ hint = _get_connection_error_hint(e, adapter_type)
1382
+ return False, f"Unexpected error: {str(e)}\nHint: {hint}"
1383
+
1384
+
1385
+ def _test_target_with_timeout(
1386
+ profile_data: dict, target_name: str, timeout: int = 30
1387
+ ) -> tuple[bool, str]:
1388
+ """Test target connection with timeout protection.
1389
+
1390
+ Args:
1391
+ profile_data: The profile dictionary
1392
+ target_name: Name of the target to test
1393
+ timeout: Timeout in seconds (default 30)
1394
+
1395
+ Returns:
1396
+ Tuple of (success: bool, message: str)
1397
+ """
1398
+ with ThreadPoolExecutor(max_workers=1) as executor:
1399
+ future = executor.submit(_test_single_target, profile_data, target_name)
1400
+ try:
1401
+ success, message = future.result(timeout=timeout)
1402
+ return success, message
1403
+ except FuturesTimeoutError:
1404
+ return False, f"Connection test timed out after {timeout} seconds"
1405
+ except Exception as e:
1406
+ return False, f"Unexpected error during connection test: {str(e)}"
1407
+
1408
+
1409
+ @target.command("test")
1410
+ @click.argument("target_name", required=False, default=None)
1411
+ @click.option("--profile", help="Profile name (defaults to project profile)")
1412
+ @click.option(
1413
+ "--timeout",
1414
+ type=int,
1415
+ default=30,
1416
+ help="Connection timeout in seconds (default: 30)",
1417
+ )
1418
+ @click.pass_context
1419
+ @p.profiles_dir
1420
+ @p.project_dir
1421
+ def target_test(
1422
+ ctx, target_name, profile, timeout, profiles_dir, project_dir, **kwargs
1423
+ ):
1424
+ """Test connection to one or all targets.
1425
+
1426
+ When TARGET_NAME is provided: Tests connection to a specific target
1427
+ When TARGET_NAME is omitted: Tests all targets in the profile
1428
+
1429
+ This command now performs REAL connection testing by executing a simple query
1430
+ against the target database. It validates both configuration AND network connectivity.
1431
+
1432
+ Features colored output and proper exit codes:
1433
+ - Exit code 0: All connections succeeded
1434
+ - Exit code 1: One or more connections failed
1435
+ - Green checkmarks (✓): Success
1436
+ - Red X marks (✗): Errors
1437
+
1438
+ Examples:
1439
+ dvt target test # Test ALL targets (auto-detect profile)
1440
+ dvt target test dev # Test specific target (auto-detect profile)
1441
+ dvt target test prod --profile my_proj # Test specific target (explicit profile)
1442
+ dvt target test --profile my_proj # Test all targets in profile
1443
+ dvt target test dev --timeout 60 # Custom timeout
1444
+
1445
+ Performance:
1446
+ - Tests run with configurable timeout (default 30s)
1447
+ - Provides helpful error hints for common connection issues
1448
+ - Shows detailed connection information on success
1449
+ """
1450
+ from dbt.config.profile import read_profile
1451
+ from dbt.config.project_utils import get_project_profile_name
1452
+
1453
+ profiles = read_profile(profiles_dir)
1454
+
1455
+ # Determine which profile to use
1456
+ if not profile:
1457
+ # Try to get from dbt_project.yml first
1458
+ profile = get_project_profile_name(project_dir)
1459
+
1460
+ if not profile:
1461
+ # If testing single target without profile, search all profiles
1462
+ if target_name:
1463
+ for prof_name, prof_data in profiles.items():
1464
+ outputs = prof_data.get("outputs", {}) if prof_data else {}
1465
+ if target_name in outputs:
1466
+ profile = prof_name
1467
+ break
1468
+
1469
+ if not profile:
1470
+ click.echo(
1471
+ click.style(
1472
+ "✗ Error: Could not determine profile. Use --profile flag.", fg="red"
1473
+ )
1474
+ )
1475
+ ctx.exit(1)
1476
+
1477
+ if profile not in profiles:
1478
+ click.echo(click.style(f"✗ Profile '{profile}' not found", fg="red"))
1479
+ ctx.exit(1)
1480
+
1481
+ profile_data = profiles[profile]
1482
+ if profile_data is None:
1483
+ click.echo(click.style(f"✗ Profile '{profile}' is empty or invalid", fg="red"))
1484
+ ctx.exit(1)
1485
+ outputs = profile_data.get("outputs", {})
1486
+
1487
+ if not outputs:
1488
+ click.echo(click.style(f"No targets found in profile '{profile}'", fg="yellow"))
1489
+ ctx.exit(0)
1490
+
1491
+ # CASE 1: Test specific target
1492
+ if target_name:
1493
+ if target_name not in outputs:
1494
+ click.echo(
1495
+ click.style(
1496
+ f"✗ Target '{target_name}' not found in profile '{profile}'",
1497
+ fg="red",
1498
+ )
1499
+ )
1500
+ ctx.exit(1)
1501
+
1502
+ target_config = outputs[target_name]
1503
+ adapter_type = target_config.get("type", "unknown")
1504
+
1505
+ click.echo(
1506
+ f"Testing connection: {click.style(target_name, fg='cyan')} ({adapter_type})"
1507
+ )
1508
+
1509
+ # Show connection details FIRST (like dbt debug)
1510
+ if "host" in target_config:
1511
+ click.echo(f" host: {target_config['host']}")
1512
+ if "port" in target_config:
1513
+ click.echo(f" port: {target_config['port']}")
1514
+ if "account" in target_config:
1515
+ click.echo(f" account: {target_config['account']}")
1516
+ if "database" in target_config or "dbname" in target_config:
1517
+ db = target_config.get("database") or target_config.get("dbname")
1518
+ click.echo(f" database: {db}")
1519
+ if "warehouse" in target_config:
1520
+ click.echo(f" warehouse: {target_config['warehouse']}")
1521
+ if "schema" in target_config:
1522
+ click.echo(f" schema: {target_config['schema']}")
1523
+ if "project" in target_config:
1524
+ click.echo(f" project: {target_config['project']}")
1525
+
1526
+ # Test connection with timeout
1527
+ success, message = _test_target_with_timeout(profile_data, target_name, timeout)
1528
+
1529
+ # Show test result (like dbt debug)
1530
+ if success:
1531
+ click.echo(
1532
+ f" Connection test: {click.style('[OK connection ok]', fg='green')}"
1533
+ )
1534
+ ctx.exit(0)
1535
+ else:
1536
+ click.echo(f" Connection test: {click.style('[ERROR]', fg='red')}")
1537
+ click.echo(f" {message}")
1538
+ ctx.exit(1)
1539
+
1540
+ # CASE 2: Test all targets in profile
1541
+ else:
1542
+ total_targets = len(outputs)
1543
+ click.echo(
1544
+ f"Testing all connections in profile {click.style(profile, fg='cyan')}...\n"
1545
+ )
1546
+
1547
+ # Test each target with progress indicators
1548
+ passed_count = 0
1549
+ failed_count = 0
1550
+ target_index = 1
1551
+
1552
+ for tgt_name, target_config in outputs.items():
1553
+ adapter_type = target_config.get("type", "unknown")
1554
+
1555
+ # Progress indicator
1556
+ progress = click.style(f"[{target_index}/{total_targets}]", fg="yellow")
1557
+ click.echo(
1558
+ f"{progress} Testing connection: {click.style(tgt_name, fg='cyan')} ({adapter_type})"
1559
+ )
1560
+
1561
+ # Show connection details FIRST (like dbt debug)
1562
+ if "host" in target_config:
1563
+ click.echo(f" host: {target_config['host']}")
1564
+ if "port" in target_config:
1565
+ click.echo(f" port: {target_config['port']}")
1566
+ if "account" in target_config:
1567
+ click.echo(f" account: {target_config['account']}")
1568
+ if "database" in target_config or "dbname" in target_config:
1569
+ db = target_config.get("database") or target_config.get("dbname")
1570
+ click.echo(f" database: {db}")
1571
+ if "warehouse" in target_config:
1572
+ click.echo(f" warehouse: {target_config['warehouse']}")
1573
+ if "schema" in target_config:
1574
+ click.echo(f" schema: {target_config['schema']}")
1575
+ if "project" in target_config:
1576
+ click.echo(f" project: {target_config['project']}")
1577
+
1578
+ # Test connection
1579
+ success, message = _test_target_with_timeout(
1580
+ profile_data, tgt_name, timeout
1581
+ )
1582
+
1583
+ # Show test result (like dbt debug)
1584
+ if success:
1585
+ click.echo(
1586
+ f" Connection test: {click.style('[OK connection ok]', fg='green')}"
1587
+ )
1588
+ passed_count += 1
1589
+ else:
1590
+ click.echo(f" Connection test: {click.style('[ERROR]', fg='red')}")
1591
+ click.echo(f" {message}")
1592
+ failed_count += 1
1593
+
1594
+ click.echo("")
1595
+ target_index += 1
1596
+
1597
+ # Summary line
1598
+ click.echo("─" * 60)
1599
+ if failed_count == 0:
1600
+ summary = click.style(
1601
+ f"✓ All {passed_count} connection tests passed", fg="green", bold=True
1602
+ )
1603
+ click.echo(summary)
1604
+ ctx.exit(0)
1605
+ else:
1606
+ passed_str = click.style(f"{passed_count} passed", fg="green")
1607
+ failed_str = click.style(f"{failed_count} failed", fg="red")
1608
+ summary = f"✗ {passed_str}, {failed_str}"
1609
+ click.echo(summary)
1610
+ ctx.exit(1)
1611
+
1612
+
1613
+ @target.command("add")
1614
+ @click.argument("target_name")
1615
+ @click.option("--profile", required=True, help="Profile name to add target to")
1616
+ @click.option(
1617
+ "--type",
1618
+ "adapter_type",
1619
+ required=True,
1620
+ help="Adapter type (postgres, snowflake, etc)",
1621
+ )
1622
+ @click.option("--host", help="Database host")
1623
+ @click.option("--port", type=int, help="Database port")
1624
+ @click.option("--user", help="Database user")
1625
+ @click.option("--password", help="Database password")
1626
+ @click.option("--database", help="Database name")
1627
+ @click.option("--schema", help="Default schema")
1628
+ @click.option("--threads", type=int, default=4, help="Number of threads")
1629
+ @click.option("--set-default", is_flag=True, help="Set as default target for profile")
1630
+ @click.pass_context
1631
+ @p.profiles_dir
1632
+ def target_add(
1633
+ ctx,
1634
+ target_name,
1635
+ profile,
1636
+ adapter_type,
1637
+ host,
1638
+ port,
1639
+ user,
1640
+ password,
1641
+ database,
1642
+ schema,
1643
+ threads,
1644
+ set_default,
1645
+ profiles_dir,
1646
+ **kwargs,
1647
+ ):
1648
+ """Add a new target to a profile in profiles.yml"""
1649
+ import yaml
1650
+ from pathlib import Path
1651
+
1652
+ profiles_file = Path(profiles_dir) / "profiles.yml"
1653
+
1654
+ if not profiles_file.exists():
1655
+ click.echo(f"✗ profiles.yml not found at {profiles_file}")
1656
+ return False, False
1657
+
1658
+ with open(profiles_file, "r") as f:
1659
+ profiles = yaml.safe_load(f) or {}
1660
+
1661
+ if profile not in profiles:
1662
+ click.echo(f"✗ Profile '{profile}' not found in profiles.yml")
1663
+ return False, False
1664
+
1665
+ profile_data = profiles[profile]
1666
+ if profile_data is None:
1667
+ click.echo(f"✗ Profile '{profile}' is empty or invalid")
1668
+ return False, False
1669
+
1670
+ # Get or create outputs dict (standard dbt format)
1671
+ if "outputs" not in profile_data:
1672
+ profile_data["outputs"] = {}
1673
+
1674
+ outputs = profile_data["outputs"]
1675
+
1676
+ # Check if target already exists
1677
+ if target_name in outputs:
1678
+ if not click.confirm(f"Target '{target_name}' already exists. Overwrite?"):
1679
+ return False, False
1680
+
1681
+ # Build target config
1682
+ target_config = {"type": adapter_type}
1683
+
1684
+ if host:
1685
+ target_config["host"] = host
1686
+ if port:
1687
+ target_config["port"] = port
1688
+ if user:
1689
+ target_config["user"] = user
1690
+ if password:
1691
+ target_config["password"] = password
1692
+ if database:
1693
+ target_config["database"] = database
1694
+ if schema:
1695
+ target_config["schema"] = schema
1696
+ if threads:
1697
+ target_config["threads"] = threads
1698
+
1699
+ # Add target to outputs
1700
+ outputs[target_name] = target_config
1701
+
1702
+ # Set as default if requested
1703
+ if set_default:
1704
+ profile_data["target"] = target_name
1705
+
1706
+ # Write back to profiles.yml
1707
+ with open(profiles_file, "w") as f:
1708
+ yaml.dump(profiles, f, default_flow_style=False, sort_keys=False)
1709
+
1710
+ click.echo(f"✓ Added target '{target_name}' to profile '{profile}'")
1711
+ if set_default:
1712
+ click.echo(f" Set as default target")
1713
+
1714
+ return True, True
1715
+
1716
+
1717
+ @target.command("sync")
1718
+ @click.option("--profile", help="Profile name (defaults to project profile)")
1719
+ @click.option("--clean", is_flag=True, help="Remove adapters not needed by profiles.yml")
1720
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
1721
+ @click.pass_context
1722
+ @p.profiles_dir
1723
+ @p.project_dir
1724
+ def target_sync(ctx, profile, clean, dry_run, profiles_dir, project_dir, **kwargs):
1725
+ """Sync adapters and JDBC JARs based on profiles.yml connections.
1726
+
1727
+ Scans your profiles.yml to find all connection types, then:
1728
+ - Installs required dbt adapters via pip
1729
+ - Updates JDBC JARs for Spark federation
1730
+ - Optionally removes unused adapters (with --clean)
1731
+
1732
+ Examples:
1733
+
1734
+ dvt target sync # Sync for current project
1735
+ dvt target sync --profile my_project # Sync specific profile
1736
+ dvt target sync --dry-run # Show what would happen
1737
+ dvt target sync --clean # Also remove unused adapters
1738
+ """
1739
+ from dbt.task.target_sync import TargetSyncTask
1740
+
1741
+ task = TargetSyncTask(
1742
+ project_dir=project_dir,
1743
+ profiles_dir=profiles_dir,
1744
+ profile_name=profile,
1745
+ )
1746
+ success = task.sync(verbose=True, clean=clean, dry_run=dry_run)
1747
+ return None, success
1748
+
1749
+
1750
+ @target.command("remove")
1751
+ @click.argument("target_name")
1752
+ @click.option("--profile", required=True, help="Profile name to remove target from")
1753
+ @click.pass_context
1754
+ @p.profiles_dir
1755
+ def target_remove(ctx, target_name, profile, profiles_dir, **kwargs):
1756
+ """Remove a target from a profile in profiles.yml"""
1757
+ import yaml
1758
+ from pathlib import Path
1759
+
1760
+ profiles_file = Path(profiles_dir) / "profiles.yml"
1761
+
1762
+ if not profiles_file.exists():
1763
+ click.echo(f"✗ profiles.yml not found at {profiles_file}")
1764
+ return False, False
1765
+
1766
+ with open(profiles_file, "r") as f:
1767
+ profiles = yaml.safe_load(f) or {}
1768
+
1769
+ if profile not in profiles:
1770
+ click.echo(f"✗ Profile '{profile}' not found in profiles.yml")
1771
+ return False, False
1772
+
1773
+ profile_data = profiles[profile]
1774
+ if profile_data is None:
1775
+ click.echo(f"✗ Profile '{profile}' is empty or invalid")
1776
+ return False, False
1777
+ outputs = profile_data.get("outputs", {})
1778
+
1779
+ if target_name not in outputs:
1780
+ click.echo(f"✗ Target '{target_name}' not found in profile '{profile}'")
1781
+ return False, False
1782
+
1783
+ # Remove the target
1784
+ del outputs[target_name]
1785
+
1786
+ # Check if this was the default target
1787
+ default_target = profile_data.get("target")
1788
+ if default_target == target_name:
1789
+ # Set new default to first available target
1790
+ if outputs:
1791
+ new_default = list(outputs.keys())[0]
1792
+ profile_data["target"] = new_default
1793
+ click.echo(
1794
+ f" Note: '{target_name}' was the default target, changed to '{new_default}'"
1795
+ )
1796
+ else:
1797
+ click.echo(f" Warning: No targets remaining in profile '{profile}'")
1798
+
1799
+ # Write back to profiles.yml
1800
+ with open(profiles_file, "w") as f:
1801
+ yaml.dump(profiles, f, default_flow_style=False, sort_keys=False)
1802
+
1803
+ click.echo(f"✓ Removed target '{target_name}' from profile '{profile}'")
1804
+
1805
+ return True, True
1806
+
1807
+
1808
+ # DVT java commands for Java management
1809
+ @cli.group()
1810
+ @click.pass_context
1811
+ @global_flags
1812
+ def java(ctx, **kwargs):
1813
+ """Manage Java installations for PySpark.
1814
+
1815
+ Java is required for Spark compute engines. DVT supports multiple
1816
+ Java versions depending on your PySpark version:
1817
+
1818
+ \b
1819
+ PySpark 4.0.x -> Java 17 or 21
1820
+ PySpark 3.5.x -> Java 8, 11, or 17
1821
+ PySpark 3.4.x -> Java 8, 11, or 17
1822
+ PySpark 3.3.x -> Java 8 or 11
1823
+ PySpark 3.2.x -> Java 8 or 11
1824
+
1825
+ Examples:
1826
+
1827
+ \b
1828
+ dvt java check # Check Java compatibility with PySpark
1829
+ dvt java search # Find all Java installations
1830
+ dvt java set # Select and configure JAVA_HOME
1831
+ dvt java install # Show installation guide
1832
+ """
1833
+
1834
+
1835
+ @java.command("check")
1836
+ @click.pass_context
1837
+ def java_check(ctx, **kwargs):
1838
+ """Check Java installation and PySpark compatibility.
1839
+
1840
+ Shows current Java version, installed PySpark version, and whether
1841
+ they are compatible. Provides guidance if there's a mismatch.
1842
+
1843
+ Exit codes:
1844
+ 0 - Java and PySpark are compatible
1845
+ 1 - Java/PySpark mismatch or not found
1846
+ """
1847
+ from dbt.task.java import JavaTask
1848
+
1849
+ task = JavaTask()
1850
+ is_compatible = task.check()
1851
+ ctx.exit(0 if is_compatible else 1)
1852
+
1853
+
1854
+ @java.command("search")
1855
+ @click.pass_context
1856
+ def java_search(ctx, **kwargs):
1857
+ """Find all Java installations on the system.
1858
+
1859
+ Searches common installation locations for Java on your OS:
1860
+
1861
+ \b
1862
+ macOS: /Library/Java/JavaVirtualMachines, Homebrew, SDKMAN
1863
+ Linux: /usr/lib/jvm, /opt/java, update-alternatives, SDKMAN
1864
+ Windows: Program Files, Registry, Scoop, Chocolatey
1865
+
1866
+ Shows Java version, vendor, and compatibility with installed PySpark.
1867
+ """
1868
+ from dbt.task.java import JavaTask
1869
+
1870
+ task = JavaTask()
1871
+ installations = task.search()
1872
+ ctx.exit(0 if installations else 1)
1873
+
1874
+
1875
+ @java.command("set")
1876
+ @click.pass_context
1877
+ def java_set(ctx, **kwargs):
1878
+ """Interactively select and set JAVA_HOME.
1879
+
1880
+ Shows all found Java installations with compatibility indicators,
1881
+ lets you choose one, and updates your shell configuration file
1882
+ (.zshrc, .bashrc, etc.) to persist JAVA_HOME.
1883
+
1884
+ After setting, restart your terminal or run 'source ~/.zshrc'
1885
+ (or equivalent) for changes to take effect.
1886
+ """
1887
+ from dbt.task.java import JavaTask
1888
+
1889
+ task = JavaTask()
1890
+ success = task.set_java_home()
1891
+ ctx.exit(0 if success else 1)
1892
+
1893
+
1894
+ @java.command("install")
1895
+ @click.pass_context
1896
+ def java_install(ctx, **kwargs):
1897
+ """Show Java installation guide for your platform.
1898
+
1899
+ Provides platform-specific installation instructions based on
1900
+ your installed PySpark version. Includes options for:
1901
+
1902
+ \b
1903
+ macOS: Homebrew, SDKMAN, manual download
1904
+ Linux: apt-get, dnf, pacman, SDKMAN
1905
+ Windows: Winget, Chocolatey, Scoop, manual download
1906
+ """
1907
+ from dbt.task.java import JavaTask
1908
+
1909
+ task = JavaTask()
1910
+ task.install_guide()
1911
+ ctx.exit(0)
1912
+
1913
+
1914
+ # DVT spark commands for Spark/PySpark management
1915
+ @cli.group()
1916
+ @click.pass_context
1917
+ @global_flags
1918
+ def spark(ctx, **kwargs):
1919
+ """Manage PySpark installations and cluster compatibility.
1920
+
1921
+ PySpark is the Python API for Apache Spark, used by DVT for
1922
+ federated query execution across multiple data sources.
1923
+
1924
+ Different PySpark versions require different Java versions.
1925
+ Use these commands to manage versions and ensure compatibility.
1926
+
1927
+ Examples:
1928
+
1929
+ \b
1930
+ dvt spark check # Check PySpark/Java status
1931
+ dvt spark set-version # Install specific PySpark version
1932
+ dvt spark match-cluster <name> # Match PySpark to cluster version
1933
+ dvt spark versions # Show compatibility matrix
1934
+ """
1935
+
1936
+
1937
+ @spark.command("check")
1938
+ @click.pass_context
1939
+ def spark_check(ctx, **kwargs):
1940
+ """Check PySpark installation and Java compatibility.
1941
+
1942
+ Shows:
1943
+ - Installed PySpark version and requirements
1944
+ - Current Java version
1945
+ - Compatibility status
1946
+
1947
+ Exit codes:
1948
+ 0 - PySpark installed and Java compatible
1949
+ 1 - PySpark not installed or Java incompatible
1950
+ """
1951
+ from dbt.task.spark import SparkTask
1952
+
1953
+ task = SparkTask()
1954
+ is_ok = task.check()
1955
+ ctx.exit(0 if is_ok else 1)
1956
+
1957
+
1958
+ @spark.command("set-version")
1959
+ @click.pass_context
1960
+ def spark_set_version(ctx, **kwargs):
1961
+ """Interactively select and install a PySpark version.
1962
+
1963
+ Presents available PySpark versions with their Java requirements.
1964
+ Shows compatibility indicators based on your current Java.
1965
+ Installs the selected version via pip.
1966
+
1967
+ Available versions:
1968
+ \b
1969
+ PySpark 4.0.x - Latest, requires Java 17+
1970
+ PySpark 3.5.x - Stable, Java 8/11/17
1971
+ PySpark 3.4.x - Java 8/11/17
1972
+ PySpark 3.3.x - Java 8/11
1973
+ PySpark 3.2.x - Java 8/11
1974
+
1975
+ After installing, check Java compatibility with 'dvt java check'.
1976
+ """
1977
+ from dbt.task.spark import SparkTask
1978
+
1979
+ task = SparkTask()
1980
+ success = task.set_version()
1981
+ ctx.exit(0 if success else 1)
1982
+
1983
+
1984
+ @spark.command("match-cluster")
1985
+ @click.argument("compute_name")
1986
+ @click.pass_context
1987
+ def spark_match_cluster(ctx, compute_name, **kwargs):
1988
+ """Detect cluster Spark version and check PySpark compatibility.
1989
+
1990
+ Connects to the specified compute engine from computes.yml,
1991
+ detects its Spark version, and compares with locally installed
1992
+ PySpark. Provides recommendations if versions don't match.
1993
+
1994
+ IMPORTANT: PySpark version must match the cluster's Spark version
1995
+ (same major.minor). A mismatch can cause runtime errors.
1996
+
1997
+ Arguments:
1998
+ COMPUTE_NAME: Name of compute engine in computes.yml
1999
+
2000
+ Examples:
2001
+
2002
+ \b
2003
+ dvt spark match-cluster spark-docker
2004
+ dvt spark match-cluster spark-local
2005
+ dvt spark match-cluster databricks-prod
2006
+ """
2007
+ from dbt.task.spark import SparkTask
2008
+
2009
+ task = SparkTask()
2010
+ is_match = task.match_cluster(compute_name)
2011
+ ctx.exit(0 if is_match else 1)
2012
+
2013
+
2014
+ @spark.command("versions")
2015
+ @click.pass_context
2016
+ def spark_versions(ctx, **kwargs):
2017
+ """Display PySpark/Java compatibility matrix.
2018
+
2019
+ Shows all available PySpark versions with their Java requirements,
2020
+ marks the currently installed version, and shows your current
2021
+ Java installation.
2022
+ """
2023
+ from dbt.task.spark import SparkTask
2024
+
2025
+ task = SparkTask()
2026
+ task.show_versions()
2027
+ ctx.exit(0)
2028
+
2029
+
2030
+ # Register DVT command groups with main CLI
2031
+ cli.add_command(compute)
2032
+ cli.add_command(target)
2033
+ cli.add_command(java)
2034
+ cli.add_command(spark)
2035
+
2036
+
2037
+ # Support running as a module (python -m dbt.cli.main)
2038
+ if __name__ == "__main__":
2039
+ cli()