dvt-core 0.59.0a51__py3-none-any.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.
Files changed (299) 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 +2660 -0
  57. dbt/cli/option_types.py +121 -0
  58. dbt/cli/options.py +80 -0
  59. dbt/cli/params.py +844 -0
  60. dbt/cli/requires.py +490 -0
  61. dbt/cli/resolvers.py +60 -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 +642 -0
  74. dbt/compute/federated_executor.py +1080 -0
  75. dbt/compute/filter_pushdown.py +273 -0
  76. dbt/compute/jar_provisioning.py +273 -0
  77. dbt/compute/java_compat.py +689 -0
  78. dbt/compute/jdbc_utils.py +1252 -0
  79. dbt/compute/metadata/__init__.py +63 -0
  80. dbt/compute/metadata/adapters_registry.py +370 -0
  81. dbt/compute/metadata/catalog_store.py +1036 -0
  82. dbt/compute/metadata/registry.py +674 -0
  83. dbt/compute/metadata/store.py +1020 -0
  84. dbt/compute/smart_selector.py +377 -0
  85. dbt/compute/spark_logger.py +272 -0
  86. dbt/compute/strategies/__init__.py +55 -0
  87. dbt/compute/strategies/base.py +165 -0
  88. dbt/compute/strategies/dataproc.py +207 -0
  89. dbt/compute/strategies/emr.py +203 -0
  90. dbt/compute/strategies/local.py +472 -0
  91. dbt/compute/strategies/standalone.py +262 -0
  92. dbt/config/__init__.py +4 -0
  93. dbt/config/catalogs.py +94 -0
  94. dbt/config/compute.py +513 -0
  95. dbt/config/dvt_profile.py +408 -0
  96. dbt/config/profile.py +422 -0
  97. dbt/config/project.py +888 -0
  98. dbt/config/project_utils.py +48 -0
  99. dbt/config/renderer.py +231 -0
  100. dbt/config/runtime.py +564 -0
  101. dbt/config/selectors.py +208 -0
  102. dbt/config/utils.py +77 -0
  103. dbt/constants.py +28 -0
  104. dbt/context/__init__.py +0 -0
  105. dbt/context/base.py +745 -0
  106. dbt/context/configured.py +135 -0
  107. dbt/context/context_config.py +382 -0
  108. dbt/context/docs.py +82 -0
  109. dbt/context/exceptions_jinja.py +178 -0
  110. dbt/context/macro_resolver.py +195 -0
  111. dbt/context/macros.py +171 -0
  112. dbt/context/manifest.py +72 -0
  113. dbt/context/providers.py +2249 -0
  114. dbt/context/query_header.py +13 -0
  115. dbt/context/secret.py +58 -0
  116. dbt/context/target.py +74 -0
  117. dbt/contracts/__init__.py +0 -0
  118. dbt/contracts/files.py +413 -0
  119. dbt/contracts/graph/__init__.py +0 -0
  120. dbt/contracts/graph/manifest.py +1904 -0
  121. dbt/contracts/graph/metrics.py +97 -0
  122. dbt/contracts/graph/model_config.py +70 -0
  123. dbt/contracts/graph/node_args.py +42 -0
  124. dbt/contracts/graph/nodes.py +1806 -0
  125. dbt/contracts/graph/semantic_manifest.py +232 -0
  126. dbt/contracts/graph/unparsed.py +811 -0
  127. dbt/contracts/project.py +419 -0
  128. dbt/contracts/results.py +53 -0
  129. dbt/contracts/selection.py +23 -0
  130. dbt/contracts/sql.py +85 -0
  131. dbt/contracts/state.py +68 -0
  132. dbt/contracts/util.py +46 -0
  133. dbt/deprecations.py +348 -0
  134. dbt/deps/__init__.py +0 -0
  135. dbt/deps/base.py +152 -0
  136. dbt/deps/git.py +195 -0
  137. dbt/deps/local.py +79 -0
  138. dbt/deps/registry.py +130 -0
  139. dbt/deps/resolver.py +149 -0
  140. dbt/deps/tarball.py +120 -0
  141. dbt/docs/source/_ext/dbt_click.py +119 -0
  142. dbt/docs/source/conf.py +32 -0
  143. dbt/env_vars.py +64 -0
  144. dbt/event_time/event_time.py +40 -0
  145. dbt/event_time/sample_window.py +60 -0
  146. dbt/events/__init__.py +15 -0
  147. dbt/events/base_types.py +36 -0
  148. dbt/events/core_types_pb2.py +2 -0
  149. dbt/events/logging.py +108 -0
  150. dbt/events/types.py +2516 -0
  151. dbt/exceptions.py +1486 -0
  152. dbt/flags.py +89 -0
  153. dbt/graph/__init__.py +11 -0
  154. dbt/graph/cli.py +249 -0
  155. dbt/graph/graph.py +172 -0
  156. dbt/graph/queue.py +214 -0
  157. dbt/graph/selector.py +374 -0
  158. dbt/graph/selector_methods.py +975 -0
  159. dbt/graph/selector_spec.py +222 -0
  160. dbt/graph/thread_pool.py +18 -0
  161. dbt/hooks.py +21 -0
  162. dbt/include/README.md +49 -0
  163. dbt/include/__init__.py +3 -0
  164. dbt/include/data/adapters_registry.duckdb +0 -0
  165. dbt/include/data/build_comprehensive_registry.py +1254 -0
  166. dbt/include/data/build_registry.py +242 -0
  167. dbt/include/data/csv/adapter_queries.csv +33 -0
  168. dbt/include/data/csv/syntax_rules.csv +9 -0
  169. dbt/include/data/csv/type_mappings_bigquery.csv +28 -0
  170. dbt/include/data/csv/type_mappings_databricks.csv +30 -0
  171. dbt/include/data/csv/type_mappings_mysql.csv +40 -0
  172. dbt/include/data/csv/type_mappings_oracle.csv +30 -0
  173. dbt/include/data/csv/type_mappings_postgres.csv +56 -0
  174. dbt/include/data/csv/type_mappings_redshift.csv +33 -0
  175. dbt/include/data/csv/type_mappings_snowflake.csv +38 -0
  176. dbt/include/data/csv/type_mappings_sqlserver.csv +35 -0
  177. dbt/include/dvt_starter_project/README.md +15 -0
  178. dbt/include/dvt_starter_project/__init__.py +3 -0
  179. dbt/include/dvt_starter_project/analyses/PLACEHOLDER +0 -0
  180. dbt/include/dvt_starter_project/dvt_project.yml +39 -0
  181. dbt/include/dvt_starter_project/logs/PLACEHOLDER +0 -0
  182. dbt/include/dvt_starter_project/macros/PLACEHOLDER +0 -0
  183. dbt/include/dvt_starter_project/models/example/my_first_dbt_model.sql +27 -0
  184. dbt/include/dvt_starter_project/models/example/my_second_dbt_model.sql +6 -0
  185. dbt/include/dvt_starter_project/models/example/schema.yml +21 -0
  186. dbt/include/dvt_starter_project/seeds/PLACEHOLDER +0 -0
  187. dbt/include/dvt_starter_project/snapshots/PLACEHOLDER +0 -0
  188. dbt/include/dvt_starter_project/tests/PLACEHOLDER +0 -0
  189. dbt/internal_deprecations.py +26 -0
  190. dbt/jsonschemas/__init__.py +3 -0
  191. dbt/jsonschemas/jsonschemas.py +309 -0
  192. dbt/jsonschemas/project/0.0.110.json +4717 -0
  193. dbt/jsonschemas/project/0.0.85.json +2015 -0
  194. dbt/jsonschemas/resources/0.0.110.json +2636 -0
  195. dbt/jsonschemas/resources/0.0.85.json +2536 -0
  196. dbt/jsonschemas/resources/latest.json +6773 -0
  197. dbt/links.py +4 -0
  198. dbt/materializations/__init__.py +0 -0
  199. dbt/materializations/incremental/__init__.py +0 -0
  200. dbt/materializations/incremental/microbatch.py +236 -0
  201. dbt/mp_context.py +8 -0
  202. dbt/node_types.py +37 -0
  203. dbt/parser/__init__.py +23 -0
  204. dbt/parser/analysis.py +21 -0
  205. dbt/parser/base.py +548 -0
  206. dbt/parser/common.py +266 -0
  207. dbt/parser/docs.py +52 -0
  208. dbt/parser/fixtures.py +51 -0
  209. dbt/parser/functions.py +30 -0
  210. dbt/parser/generic_test.py +100 -0
  211. dbt/parser/generic_test_builders.py +333 -0
  212. dbt/parser/hooks.py +122 -0
  213. dbt/parser/macros.py +137 -0
  214. dbt/parser/manifest.py +2208 -0
  215. dbt/parser/models.py +573 -0
  216. dbt/parser/partial.py +1178 -0
  217. dbt/parser/read_files.py +445 -0
  218. dbt/parser/schema_generic_tests.py +422 -0
  219. dbt/parser/schema_renderer.py +111 -0
  220. dbt/parser/schema_yaml_readers.py +935 -0
  221. dbt/parser/schemas.py +1466 -0
  222. dbt/parser/search.py +149 -0
  223. dbt/parser/seeds.py +28 -0
  224. dbt/parser/singular_test.py +20 -0
  225. dbt/parser/snapshots.py +44 -0
  226. dbt/parser/sources.py +558 -0
  227. dbt/parser/sql.py +62 -0
  228. dbt/parser/unit_tests.py +621 -0
  229. dbt/plugins/__init__.py +20 -0
  230. dbt/plugins/contracts.py +9 -0
  231. dbt/plugins/exceptions.py +2 -0
  232. dbt/plugins/manager.py +163 -0
  233. dbt/plugins/manifest.py +21 -0
  234. dbt/profiler.py +20 -0
  235. dbt/py.typed +1 -0
  236. dbt/query_analyzer.py +410 -0
  237. dbt/runners/__init__.py +2 -0
  238. dbt/runners/exposure_runner.py +7 -0
  239. dbt/runners/no_op_runner.py +45 -0
  240. dbt/runners/saved_query_runner.py +7 -0
  241. dbt/selected_resources.py +8 -0
  242. dbt/task/__init__.py +0 -0
  243. dbt/task/base.py +506 -0
  244. dbt/task/build.py +197 -0
  245. dbt/task/clean.py +56 -0
  246. dbt/task/clone.py +161 -0
  247. dbt/task/compile.py +150 -0
  248. dbt/task/compute.py +458 -0
  249. dbt/task/debug.py +513 -0
  250. dbt/task/deps.py +280 -0
  251. dbt/task/docs/__init__.py +3 -0
  252. dbt/task/docs/api/__init__.py +23 -0
  253. dbt/task/docs/api/catalog.py +204 -0
  254. dbt/task/docs/api/lineage.py +234 -0
  255. dbt/task/docs/api/profile.py +204 -0
  256. dbt/task/docs/api/spark.py +186 -0
  257. dbt/task/docs/generate.py +1002 -0
  258. dbt/task/docs/index.html +250 -0
  259. dbt/task/docs/serve.py +174 -0
  260. dbt/task/dvt_output.py +509 -0
  261. dbt/task/dvt_run.py +282 -0
  262. dbt/task/dvt_seed.py +806 -0
  263. dbt/task/freshness.py +322 -0
  264. dbt/task/function.py +121 -0
  265. dbt/task/group_lookup.py +46 -0
  266. dbt/task/init.py +1022 -0
  267. dbt/task/java.py +316 -0
  268. dbt/task/list.py +236 -0
  269. dbt/task/metadata.py +804 -0
  270. dbt/task/migrate.py +714 -0
  271. dbt/task/printer.py +175 -0
  272. dbt/task/profile.py +1489 -0
  273. dbt/task/profile_serve.py +662 -0
  274. dbt/task/retract.py +441 -0
  275. dbt/task/retry.py +175 -0
  276. dbt/task/run.py +1647 -0
  277. dbt/task/run_operation.py +141 -0
  278. dbt/task/runnable.py +758 -0
  279. dbt/task/seed.py +103 -0
  280. dbt/task/show.py +149 -0
  281. dbt/task/snapshot.py +56 -0
  282. dbt/task/spark.py +414 -0
  283. dbt/task/sql.py +110 -0
  284. dbt/task/target_sync.py +814 -0
  285. dbt/task/test.py +464 -0
  286. dbt/tests/fixtures/__init__.py +1 -0
  287. dbt/tests/fixtures/project.py +620 -0
  288. dbt/tests/util.py +651 -0
  289. dbt/tracking.py +529 -0
  290. dbt/utils/__init__.py +3 -0
  291. dbt/utils/artifact_upload.py +151 -0
  292. dbt/utils/utils.py +408 -0
  293. dbt/version.py +271 -0
  294. dvt_cli/__init__.py +158 -0
  295. dvt_core-0.59.0a51.dist-info/METADATA +288 -0
  296. dvt_core-0.59.0a51.dist-info/RECORD +299 -0
  297. dvt_core-0.59.0a51.dist-info/WHEEL +5 -0
  298. dvt_core-0.59.0a51.dist-info/entry_points.txt +2 -0
  299. dvt_core-0.59.0a51.dist-info/top_level.txt +2 -0
dbt/task/init.py ADDED
@@ -0,0 +1,1022 @@
1
+ import copy
2
+ import os
3
+ import re
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import click
9
+ import yaml
10
+
11
+ import dbt.config
12
+ import dbt_common.clients.system
13
+ from dbt.adapters.factory import get_include_paths, load_plugin
14
+ from dbt.compute.metadata import ProjectMetadataStore, CatalogStore
15
+ from dbt.config.profile import read_profile
16
+ from dbt.contracts.util import Identifier as ProjectName
17
+ from dbt.events.types import (
18
+ ConfigFolderDirectory,
19
+ InvalidProfileTemplateYAML,
20
+ NoSampleProfileFound,
21
+ ProfileWrittenWithProjectTemplateYAML,
22
+ ProfileWrittenWithSample,
23
+ ProfileWrittenWithTargetTemplateYAML,
24
+ ProjectCreated,
25
+ ProjectNameAlreadyExists,
26
+ SettingUpProfile,
27
+ StarterProjectPath,
28
+ )
29
+ from dbt.flags import get_flags
30
+ from dbt.task.base import BaseTask, move_to_nearest_project_dir
31
+ from dbt.version import _get_adapter_plugin_names
32
+ from dbt_common.events.functions import fire_event
33
+ from dbt_common.exceptions import DbtRuntimeError
34
+
35
+ DOCS_URL = "https://docs.getdbt.com/docs/configure-your-profile"
36
+ SLACK_URL = "https://community.getdbt.com/"
37
+
38
+ # These files are not needed for the starter project but exist for finding the resource path
39
+ # PLACEHOLDER files are used instead of .gitkeep because setuptools doesn't include hidden files
40
+ # gitignore.txt is renamed to .gitignore after copying (setuptools doesn't include hidden files)
41
+ IGNORE_FILES = ["__init__.py", "__pycache__", "PLACEHOLDER", "gitignore.txt"]
42
+
43
+ # Content for .gitignore file (created programmatically because setuptools skips hidden files)
44
+ GITIGNORE_CONTENT = """target/
45
+ dbt_packages/
46
+ logs/
47
+ flatfiles/
48
+ """
49
+
50
+
51
+ # https://click.palletsprojects.com/en/8.0.x/api/#types
52
+ # click v7.0 has UNPROCESSED, STRING, INT, FLOAT, BOOL, and UUID available.
53
+ click_type_mapping = {
54
+ "string": click.STRING,
55
+ "int": click.INT,
56
+ "float": click.FLOAT,
57
+ "bool": click.BOOL,
58
+ None: None,
59
+ }
60
+
61
+
62
+ class InitTask(BaseTask):
63
+ def copy_starter_repo(self, project_name: str) -> None:
64
+ # Lazy import to avoid ModuleNotFoundError
65
+ from dbt.include.dvt_starter_project import (
66
+ PACKAGE_PATH as starter_project_directory,
67
+ )
68
+
69
+ fire_event(StarterProjectPath(dir=starter_project_directory))
70
+ shutil.copytree(
71
+ starter_project_directory, project_name, ignore=shutil.ignore_patterns(*IGNORE_FILES)
72
+ )
73
+
74
+ # Create .gitignore file (setuptools doesn't include hidden files)
75
+ gitignore_path = Path(project_name) / ".gitignore"
76
+ with open(gitignore_path, "w") as f:
77
+ f.write(GITIGNORE_CONTENT)
78
+
79
+ def create_profiles_dir(self, profiles_dir: str) -> bool:
80
+ """Create the user's profiles directory if it doesn't already exist."""
81
+ profiles_path = Path(profiles_dir)
82
+ if not profiles_path.exists():
83
+ fire_event(ConfigFolderDirectory(dir=str(profiles_dir)))
84
+ dbt_common.clients.system.make_directory(profiles_dir)
85
+ return True
86
+ return False
87
+
88
+ def create_profile_from_sample(self, adapter: str, profile_name: str):
89
+ """Create a profile entry using the adapter's sample_profiles.yml
90
+
91
+ Renames the profile in sample_profiles.yml to match that of the project."""
92
+ # Line below raises an exception if the specified adapter is not found
93
+ load_plugin(adapter)
94
+ adapter_path = get_include_paths(adapter)[0]
95
+ sample_profiles_path = adapter_path / "sample_profiles.yml"
96
+
97
+ if not sample_profiles_path.exists():
98
+ fire_event(NoSampleProfileFound(adapter=adapter))
99
+ else:
100
+ with open(sample_profiles_path, "r") as f:
101
+ sample_profile = f.read()
102
+ sample_profile_name = list(yaml.safe_load(sample_profile).keys())[0]
103
+ # Use a regex to replace the name of the sample_profile with
104
+ # that of the project without losing any comments from the sample
105
+ sample_profile = re.sub(f"^{sample_profile_name}:", f"{profile_name}:", sample_profile)
106
+ profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
107
+ if profiles_filepath.exists():
108
+ with open(profiles_filepath, "a") as f:
109
+ f.write("\n" + sample_profile)
110
+ else:
111
+ with open(profiles_filepath, "w") as f:
112
+ f.write(sample_profile)
113
+ fire_event(
114
+ ProfileWrittenWithSample(name=profile_name, path=str(profiles_filepath))
115
+ )
116
+
117
+ def generate_target_from_input(self, profile_template: dict, target: dict = {}) -> dict:
118
+ """Generate a target configuration from profile_template and user input."""
119
+ profile_template_local = copy.deepcopy(profile_template)
120
+ for key, value in profile_template_local.items():
121
+ if key.startswith("_choose"):
122
+ choice_type = key[8:].replace("_", " ")
123
+ option_list = list(value.keys())
124
+ prompt_msg = (
125
+ "\n".join([f"[{n + 1}] {v}" for n, v in enumerate(option_list)])
126
+ + f"\nDesired {choice_type} option (enter a number)"
127
+ )
128
+ numeric_choice = click.prompt(prompt_msg, type=click.INT)
129
+ choice = option_list[numeric_choice - 1]
130
+ # Complete the chosen option's values in a recursive call
131
+ target = self.generate_target_from_input(
132
+ profile_template_local[key][choice], target
133
+ )
134
+ else:
135
+ if key.startswith("_fixed"):
136
+ # _fixed prefixed keys are not presented to the user
137
+ target[key[7:]] = value
138
+ else:
139
+ hide_input = value.get("hide_input", False)
140
+ default = value.get("default", None)
141
+ hint = value.get("hint", None)
142
+ type = click_type_mapping[value.get("type", None)]
143
+ text = key + (f" ({hint})" if hint else "")
144
+ target[key] = click.prompt(
145
+ text, default=default, hide_input=hide_input, type=type
146
+ )
147
+ return target
148
+
149
+ def get_profile_name_from_current_project(self) -> str:
150
+ """Reads dvt_project.yml in the current directory to retrieve the
151
+ profile name.
152
+ """
153
+ with open("dvt_project.yml") as f:
154
+ dvt_project = yaml.safe_load(f)
155
+ return dvt_project["profile"]
156
+
157
+ def write_profile(self, profile: dict, profile_name: str):
158
+ """Given a profile, write it to the current project's profiles.yml.
159
+ This will overwrite any profile with a matching name."""
160
+ # Create the profile directory if it doesn't exist
161
+ profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
162
+
163
+ profiles = {profile_name: profile}
164
+
165
+ if profiles_filepath.exists():
166
+ with open(profiles_filepath, "r") as f:
167
+ profiles = yaml.safe_load(f) or {}
168
+ profiles[profile_name] = profile
169
+
170
+ # Write the profiles dictionary to a brand-new or pre-existing file
171
+ with open(profiles_filepath, "w") as f:
172
+ yaml.dump(profiles, f)
173
+
174
+ def create_profile_from_profile_template(self, profile_template: dict, profile_name: str):
175
+ """Create and write a profile using the supplied profile_template."""
176
+ initial_target = profile_template.get("fixed", {})
177
+ prompts = profile_template.get("prompts", {})
178
+ target = self.generate_target_from_input(prompts, initial_target)
179
+ target_name = target.pop("target", "dev")
180
+ profile = {"outputs": {target_name: target}, "target": target_name}
181
+ self.write_profile(profile, profile_name)
182
+
183
+ def create_profile_from_target(self, adapter: str, profile_name: str):
184
+ """Create a profile without defaults using target's profile_template.yml if available, or
185
+ sample_profiles.yml as a fallback."""
186
+ # Line below raises an exception if the specified adapter is not found
187
+ load_plugin(adapter)
188
+ adapter_path = get_include_paths(adapter)[0]
189
+ profile_template_path = adapter_path / "profile_template.yml"
190
+
191
+ if profile_template_path.exists():
192
+ with open(profile_template_path) as f:
193
+ profile_template = yaml.safe_load(f)
194
+ self.create_profile_from_profile_template(profile_template, profile_name)
195
+ profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
196
+ fire_event(
197
+ ProfileWrittenWithTargetTemplateYAML(
198
+ name=profile_name, path=str(profiles_filepath)
199
+ )
200
+ )
201
+ else:
202
+ # For adapters without a profile_template.yml defined, fallback on
203
+ # sample_profiles.yml
204
+ self.create_profile_from_sample(adapter, profile_name)
205
+
206
+ def check_if_profile_exists(self, profile_name: str) -> bool:
207
+ """
208
+ Validate that the specified profile exists. Can't use the regular profile validation
209
+ routine because it assumes the project file exists
210
+ """
211
+ profiles_dir = get_flags().PROFILES_DIR
212
+ raw_profiles = read_profile(profiles_dir)
213
+ return profile_name in raw_profiles
214
+
215
+ def check_if_can_write_profile(self, profile_name: Optional[str] = None) -> bool:
216
+ """Using either a provided profile name or that specified in dbt_project.yml,
217
+ check if the profile already exists in profiles.yml, and if so ask the
218
+ user whether to proceed and overwrite it."""
219
+ profiles_file = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
220
+ if not profiles_file.exists():
221
+ return True
222
+ profile_name = profile_name or self.get_profile_name_from_current_project()
223
+ with open(profiles_file, "r") as f:
224
+ profiles = yaml.safe_load(f) or {}
225
+ if profile_name in profiles.keys():
226
+ # Profile already exists, just skip profile setup
227
+ click.echo(f"Profile '{profile_name}' already exists in {profiles_file}, skipping profile setup.")
228
+ return False
229
+ else:
230
+ return True
231
+
232
+ def create_profile_using_project_profile_template(self, profile_name):
233
+ """Create a profile using the project's profile_template.yml"""
234
+ with open("profile_template.yml") as f:
235
+ profile_template = yaml.safe_load(f)
236
+ self.create_profile_from_profile_template(profile_template, profile_name)
237
+ profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
238
+ fire_event(
239
+ ProfileWrittenWithProjectTemplateYAML(name=profile_name, path=str(profiles_filepath))
240
+ )
241
+
242
+ def ask_for_adapter_choice(self) -> str:
243
+ """Ask the user which adapter (database) they'd like to use."""
244
+ available_adapters = list(_get_adapter_plugin_names())
245
+
246
+ if not available_adapters:
247
+ raise dbt.exceptions.NoAdaptersAvailableError()
248
+
249
+ prompt_msg = (
250
+ "Which database would you like to use?\n"
251
+ + "\n".join([f"[{n + 1}] {v}" for n, v in enumerate(available_adapters)])
252
+ + "\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)"
253
+ + "\n\nEnter a number"
254
+ )
255
+ numeric_choice = click.prompt(prompt_msg, type=click.INT)
256
+ return available_adapters[numeric_choice - 1]
257
+
258
+ def setup_profile(self, profile_name: str) -> None:
259
+ """Set up a new profile for a project"""
260
+ fire_event(SettingUpProfile())
261
+ if not self.check_if_can_write_profile(profile_name=profile_name):
262
+ return
263
+ # If a profile_template.yml exists in the project root, that effectively
264
+ # overrides the profile_template.yml for the given target.
265
+ profile_template_path = Path("profile_template.yml")
266
+ if profile_template_path.exists():
267
+ try:
268
+ # This relies on a valid profile_template.yml from the user,
269
+ # so use a try: except to fall back to the default on failure
270
+ self.create_profile_using_project_profile_template(profile_name)
271
+ return
272
+ except Exception:
273
+ fire_event(InvalidProfileTemplateYAML())
274
+ adapter = self.ask_for_adapter_choice()
275
+ self.create_profile_from_target(adapter, profile_name=profile_name)
276
+
277
+ def get_valid_project_name(self) -> str:
278
+ """Returns a valid project name, either from CLI arg or user prompt."""
279
+
280
+ # Lazy import to avoid ModuleNotFoundError
281
+ from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
282
+
283
+ name = self.args.project_name
284
+ internal_package_names = {GLOBAL_PROJECT_NAME}
285
+ available_adapters = list(_get_adapter_plugin_names())
286
+ for adapter_name in available_adapters:
287
+ internal_package_names.update(f"dbt_{adapter_name}")
288
+ while not ProjectName.is_valid(name) or name in internal_package_names:
289
+ if name:
290
+ click.echo(name + " is not a valid project name.")
291
+ name = click.prompt("Enter a name for your project (letters, digits, underscore)")
292
+
293
+ return name
294
+
295
+ def create_new_project(self, project_name: str, profile_name: str):
296
+ self.copy_starter_repo(project_name)
297
+ os.chdir(project_name)
298
+ with open("dvt_project.yml", "r") as f:
299
+ content = f"{f.read()}".format(project_name=project_name, profile_name=profile_name)
300
+ with open("dvt_project.yml", "w") as f:
301
+ f.write(content)
302
+
303
+ # v0.59.0: Create project-level .dvt/ structure
304
+ # 1. Create .dvt/ directory
305
+ project_dvt_dir = Path(".") / ".dvt"
306
+ project_dvt_dir.mkdir(parents=True, exist_ok=True)
307
+
308
+ # 2. Create .dvt/jdbc_jars/ directory
309
+ from dbt.config.compute import ComputeRegistry
310
+ ComputeRegistry.ensure_jdbc_jars_dir(".")
311
+
312
+ # 3. Create .dvt/computes.yml with defaults
313
+ registry = ComputeRegistry(project_dir=".")
314
+ registry.ensure_config_exists()
315
+ click.echo(" ✓ Compute config initialized (.dvt/computes.yml)")
316
+
317
+ # 4. Initialize project metadata store (.dvt/metadata_store.duckdb)
318
+ self._initialize_metadata_store(Path("."))
319
+
320
+ # 5. Create default DuckDB database for starter profile
321
+ self._create_default_duckdb(Path("."))
322
+
323
+ # 6. v0.59.0: Create flatfiles/ directory
324
+ self._create_flatfiles_dir()
325
+
326
+ fire_event(
327
+ ProjectCreated(
328
+ project_name=project_name,
329
+ docs_url=DOCS_URL,
330
+ slack_url=SLACK_URL,
331
+ )
332
+ )
333
+
334
+ def _create_project_in_place(self, project_name: str, profile_name: str) -> None:
335
+ """
336
+ Create DVT project in the current directory (no folder creation).
337
+
338
+ v0.59.0a5: When user runs `dvt init` without a project name in an empty
339
+ folder, we initialize the project in place using the folder name.
340
+
341
+ This copies starter project contents to current directory and sets up
342
+ all DVT infrastructure (.dvt/, flatfiles/, etc.).
343
+ """
344
+ from dbt.include.dvt_starter_project import (
345
+ PACKAGE_PATH as starter_project_directory,
346
+ )
347
+ from dbt.config.compute import ComputeRegistry
348
+
349
+ # Track if dvt_project.yml already existed (don't modify if so)
350
+ dvt_project_existed = Path("dvt_project.yml").exists()
351
+
352
+ # Copy starter project contents to current directory (not a subfolder)
353
+ for item in Path(starter_project_directory).iterdir():
354
+ if item.name in IGNORE_FILES or item.name.startswith('__'):
355
+ continue
356
+ dest = Path(".") / item.name
357
+ if item.is_dir():
358
+ if not dest.exists():
359
+ shutil.copytree(item, dest, ignore=shutil.ignore_patterns(*IGNORE_FILES))
360
+ else:
361
+ if not dest.exists():
362
+ shutil.copy2(item, dest)
363
+
364
+ # Create .gitignore file (setuptools doesn't include hidden files)
365
+ gitignore_path = Path(".") / ".gitignore"
366
+ if not gitignore_path.exists():
367
+ with open(gitignore_path, "w") as f:
368
+ f.write(GITIGNORE_CONTENT)
369
+
370
+ # Update dvt_project.yml with project name ONLY if it was just created
371
+ # (existing files may have Jinja templates that conflict with .format())
372
+ if not dvt_project_existed:
373
+ with open("dvt_project.yml", "r") as f:
374
+ content = f.read().replace("{project_name}", project_name).replace("{profile_name}", profile_name)
375
+ with open("dvt_project.yml", "w") as f:
376
+ f.write(content)
377
+
378
+ # Create .dvt/ directory
379
+ project_dvt_dir = Path(".") / ".dvt"
380
+ project_dvt_dir.mkdir(parents=True, exist_ok=True)
381
+
382
+ # Create .dvt/jdbc_jars/ directory
383
+ ComputeRegistry.ensure_jdbc_jars_dir(".")
384
+
385
+ # Create .dvt/computes.yml with defaults
386
+ registry = ComputeRegistry(project_dir=".")
387
+ registry.ensure_config_exists()
388
+ click.echo(" ✓ Compute config initialized (.dvt/computes.yml)")
389
+
390
+ # Initialize project metadata store
391
+ self._initialize_metadata_store(Path("."))
392
+
393
+ # Create default DuckDB database for starter profile
394
+ self._create_default_duckdb(Path("."))
395
+
396
+ # Create flatfiles/ directory
397
+ self._create_flatfiles_dir()
398
+
399
+ fire_event(
400
+ ProjectCreated(
401
+ project_name=project_name,
402
+ docs_url=DOCS_URL,
403
+ slack_url=SLACK_URL,
404
+ )
405
+ )
406
+
407
+ def _initialize_metadata_store(self, project_root: Path) -> None:
408
+ """
409
+ Initialize DVT metadata stores in .dvt/ directory.
410
+
411
+ v0.59.0: Creates TWO project-level DuckDB databases:
412
+
413
+ 1. metastore.duckdb - Runtime/operational data:
414
+ - column_metadata: Schema info from federated runs
415
+ - row_counts: Cached row counts from dvt snap
416
+ - profile_results: Data profiling from dvt profile
417
+
418
+ 2. catalog.duckdb - Project catalog (federation-aware):
419
+ - targets: Available connections from profiles.yml
420
+ - source_definitions: Sources with connections from manifest
421
+ - model_definitions: Models with targets from manifest
422
+ - catalog_nodes: Enriched catalog for docs visualization
423
+ - lineage_edges: DAG lineage for visualization
424
+
425
+ This separation ensures catalog operations don't interfere with
426
+ runtime operations like profiling, run, build, etc.
427
+
428
+ NOTE: Static registry data (type mappings, syntax rules) comes from
429
+ the shipped adapters_registry.duckdb in ~/.dvt/.data/mdm.duckdb.
430
+
431
+ Both databases are overridden with empty schemas on each dvt init.
432
+ """
433
+ try:
434
+ # 1. Initialize metastore.duckdb (runtime data)
435
+ metastore = ProjectMetadataStore(project_root)
436
+ metastore.initialize()
437
+ metastore.close()
438
+ click.echo(" ✓ Metastore initialized (.dvt/metastore.duckdb)")
439
+
440
+ # 2. Initialize catalog.duckdb (project catalog)
441
+ catalog = CatalogStore(project_root)
442
+ catalog.initialize()
443
+ catalog.close()
444
+ click.echo(" ✓ Catalog store initialized (.dvt/catalog.duckdb)")
445
+
446
+ except ImportError:
447
+ # DuckDB not installed - skip metadata stores (optional feature)
448
+ click.echo(" ⚠ DuckDB not installed - skipping metadata stores")
449
+ except Exception as e:
450
+ # Don't fail init on metadata store errors
451
+ click.echo(f" ⚠ Could not initialize metadata stores: {e}")
452
+
453
+ def _create_flatfiles_dir(self) -> None:
454
+ """
455
+ Create empty flatfiles/ directory at project root (gitignored).
456
+
457
+ v0.59.0: For local file staging and data ingestion.
458
+ """
459
+ flatfiles_dir = Path(".") / "flatfiles"
460
+ flatfiles_dir.mkdir(parents=True, exist_ok=True)
461
+ click.echo(" ✓ flatfiles/ directory created")
462
+
463
+ def _create_default_duckdb(self, project_root: Path) -> None:
464
+ """
465
+ Create empty default.duckdb file for starter profile.
466
+
467
+ v0.59.0a14: Creates .dvt/default.duckdb so the DuckDB starter
468
+ profile works immediately without needing dbt-duckdb adapter.
469
+ The file is an empty DuckDB database.
470
+ """
471
+ try:
472
+ import duckdb
473
+ duckdb_path = project_root / ".dvt" / "default.duckdb"
474
+ if not duckdb_path.exists():
475
+ # Create empty DuckDB database
476
+ conn = duckdb.connect(str(duckdb_path))
477
+ conn.close()
478
+ click.echo(" ✓ Default DuckDB database created (.dvt/default.duckdb)")
479
+ except ImportError:
480
+ # DuckDB not available - skip (user will need to install dbt-duckdb)
481
+ pass
482
+ except Exception as e:
483
+ click.echo(f" ⚠ Could not create default DuckDB: {e}")
484
+
485
+ def _add_project_profile(self, project_name: str, project_path: Optional[Path] = None) -> None:
486
+ """
487
+ Add a project profile with a DuckDB starter adapter to ~/.dvt/profiles.yml.
488
+
489
+ v0.59.0a11: Uses ABSOLUTE path to project's .dvt/default.duckdb
490
+ v0.59.0a24: Ensures profiles directory is created if it doesn't exist.
491
+ Surgically adds 'dev' output if missing from existing profile.
492
+
493
+ Args:
494
+ project_name: Name of the project to add profile for
495
+ project_path: Optional path to project directory (defaults to cwd)
496
+ """
497
+ profiles_dir = Path(get_flags().PROFILES_DIR)
498
+ profiles_path = profiles_dir / "profiles.yml"
499
+
500
+ # v0.59.0a24: Ensure profiles directory exists
501
+ profiles_dir.mkdir(parents=True, exist_ok=True)
502
+
503
+ # Get absolute project path for DuckDB file
504
+ # Use provided path or fall back to current directory
505
+ project_dir = (project_path or Path.cwd()).resolve()
506
+ duckdb_path = str(project_dir / ".dvt" / "default.duckdb")
507
+
508
+ # DuckDB starter configuration with ABSOLUTE path
509
+ # v0.59.0a39: Path resolution added in profile loading to handle moved projects
510
+ duckdb_output = {
511
+ "type": "duckdb",
512
+ "path": duckdb_path,
513
+ "threads": 4,
514
+ }
515
+
516
+ if profiles_path.exists():
517
+ # Read existing profiles
518
+ with open(profiles_path, "r") as f:
519
+ existing_profiles = yaml.safe_load(f) or {}
520
+
521
+ action = None
522
+
523
+ if project_name in existing_profiles:
524
+ profile_data = existing_profiles[project_name]
525
+
526
+ if profile_data and isinstance(profile_data, dict):
527
+ outputs = profile_data.get("outputs", {})
528
+
529
+ if outputs and isinstance(outputs, dict):
530
+ # Check if 'dev' output exists
531
+ if "dev" in outputs:
532
+ # Profile has 'dev' output - check if it's DuckDB type
533
+ click.echo(f" Profile '{project_name}' already configured in profiles.yml")
534
+ return
535
+ else:
536
+ # Profile has outputs but missing 'dev' - surgically add it
537
+ outputs["dev"] = duckdb_output
538
+ profile_data["outputs"] = outputs
539
+ existing_profiles[project_name] = profile_data
540
+ action = "add_dev"
541
+ else:
542
+ # Profile has no outputs - add dev
543
+ profile_data["outputs"] = {"dev": duckdb_output}
544
+ if "target" not in profile_data:
545
+ profile_data["target"] = "dev"
546
+ existing_profiles[project_name] = profile_data
547
+ action = "fix_outputs"
548
+ else:
549
+ # Profile is None or invalid - create fresh
550
+ existing_profiles[project_name] = {
551
+ "target": "dev",
552
+ "outputs": {"dev": duckdb_output}
553
+ }
554
+ action = "fix_invalid"
555
+ else:
556
+ # Profile doesn't exist - add it
557
+ existing_profiles[project_name] = {
558
+ "target": "dev",
559
+ "outputs": {"dev": duckdb_output}
560
+ }
561
+ action = "add"
562
+
563
+ # Write profiles back with header
564
+ with open(profiles_path, "w") as f:
565
+ f.write("# DVT Profiles Configuration\n")
566
+ f.write("# =============================================================================\n")
567
+ f.write("# Configure your database connections below. For documentation, see:\n")
568
+ f.write("# https://docs.getdbt.com/docs/configure-your-profile\n")
569
+ f.write("#\n")
570
+ f.write("# After modifying connections, run: dvt target sync\n")
571
+ f.write("# =============================================================================\n\n")
572
+ yaml.dump(existing_profiles, f, default_flow_style=False, sort_keys=False)
573
+
574
+ # Print appropriate message
575
+ if action == "add":
576
+ click.echo(f" ✓ Profile '{project_name}' added to profiles.yml")
577
+ elif action == "add_dev":
578
+ click.echo(f" ✓ Added 'dev' (DuckDB) output to profile '{project_name}'")
579
+ elif action == "fix_outputs":
580
+ click.echo(f" ✓ Profile '{project_name}' updated with DuckDB starter output")
581
+ elif action == "fix_invalid":
582
+ click.echo(f" ✓ Profile '{project_name}' fixed with DuckDB starter configuration")
583
+ else:
584
+ # Create new profiles.yml with header and profile
585
+ new_profile = {
586
+ project_name: {
587
+ "target": "dev",
588
+ "outputs": {"dev": duckdb_output}
589
+ }
590
+ }
591
+ with open(profiles_path, "w") as f:
592
+ f.write("# DVT Profiles Configuration\n")
593
+ f.write("# =============================================================================\n")
594
+ f.write("# Configure your database connections below. For documentation, see:\n")
595
+ f.write("# https://docs.getdbt.com/docs/configure-your-profile\n")
596
+ f.write("#\n")
597
+ f.write("# After modifying connections, run: dvt target sync\n")
598
+ f.write("# =============================================================================\n\n")
599
+ yaml.dump(new_profile, f, default_flow_style=False, sort_keys=False)
600
+
601
+ click.echo(f" ✓ profiles.yml created with '{project_name}' profile")
602
+
603
+ def _convert_dbt_to_dvt(self) -> None:
604
+ """
605
+ Convert dbt project to DVT project.
606
+
607
+ v0.59.0a2: When `dvt init` is issued inside a dbt project (has dbt_project.yml
608
+ but no dvt_project.yml), this method:
609
+
610
+ 1. Backs up dbt_project.yml → dbt_project.yml.bak
611
+ 2. Gets project name and profile from dbt_project.yml
612
+ 3. Creates fresh dvt_project.yml from standard dbt init template + DVT additions
613
+ 4. Creates flatfiles/ directory
614
+ 5. Migrates project-specific profile from ~/.dbt/profiles.yml to ~/.dvt/profiles.yml
615
+
616
+ Note: Fresh template is used for consistency. User can copy project-specific
617
+ configs (like models: overrides) from the backup if needed.
618
+ """
619
+ import shutil
620
+
621
+ dbt_project_file = Path("dbt_project.yml")
622
+ backup_file = Path("dbt_project.yml.bak")
623
+
624
+ # 1. Backup dbt_project.yml
625
+ if dbt_project_file.exists():
626
+ if backup_file.exists():
627
+ # Add timestamp if backup already exists
628
+ import time
629
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
630
+ backup_file = Path(f"dbt_project.yml.{timestamp}.bak")
631
+ shutil.copy2(dbt_project_file, backup_file)
632
+ click.echo(f" ✓ Backed up dbt_project.yml → {backup_file.name}")
633
+
634
+ # 2. Get project name and profile from dbt_project.yml
635
+ with open(dbt_project_file, "r") as f:
636
+ dbt_content = f.read()
637
+
638
+ project_name = Path.cwd().name
639
+ profile_name = project_name # Default to project name
640
+ try:
641
+ config = yaml.safe_load(dbt_content)
642
+ if config:
643
+ if "name" in config:
644
+ project_name = config["name"]
645
+ if "profile" in config:
646
+ profile_name = config["profile"]
647
+ except Exception:
648
+ pass
649
+
650
+ # 3. Create fresh dvt_project.yml from standard dbt init template + DVT additions
651
+ # This matches what dbt init creates, plus DVT-specific flatfile-paths
652
+ dvt_project_content = f"""name: '{project_name}'
653
+ version: '1.0.0'
654
+
655
+ # This setting configures which "profile" dbt uses for this project.
656
+ profile: '{profile_name}'
657
+
658
+ # These configurations specify where dbt should look for different types of files.
659
+ # The `model-paths` config, for example, states that models in this project can be
660
+ # found in the "models/" directory. You probably won't need to change these!
661
+ model-paths: ["models"]
662
+ analysis-paths: ["analyses"]
663
+ test-paths: ["tests"]
664
+ seed-paths: ["seeds"]
665
+ macro-paths: ["macros"]
666
+ snapshot-paths: ["snapshots"]
667
+
668
+ # DVT-specific: Path for flat files (CSV, Parquet, etc.) for local data ingestion
669
+ flatfile-paths: ["flatfiles"]
670
+
671
+ clean-targets:
672
+ - "target"
673
+ - "dbt_packages"
674
+
675
+ # Configuring models
676
+ # Full documentation: https://docs.getdbt.com/docs/configuring-models
677
+
678
+ # In this example config, we tell dbt to build all models in the example/
679
+ # directory as views. These settings can be overridden in the individual model
680
+ # files using the `{{ config(...) }}` macro.
681
+ models:
682
+ {project_name}:
683
+ # Config indicated by + and applies to all files under models/example/
684
+ +materialized: view
685
+ """
686
+ with open("dvt_project.yml", "w") as f:
687
+ f.write(dvt_project_content)
688
+ click.echo(f" ✓ Created dvt_project.yml")
689
+
690
+ # 5. Create flatfiles/ directory
691
+ flatfiles_dir = Path("flatfiles")
692
+ flatfiles_dir.mkdir(exist_ok=True)
693
+ # Add .gitignore to flatfiles/
694
+ gitignore_path = flatfiles_dir / ".gitignore"
695
+ if not gitignore_path.exists():
696
+ with open(gitignore_path, "w") as f:
697
+ f.write("# Ignore all flat files (CSV, Parquet, etc.)\n*\n!.gitignore\n")
698
+ click.echo(" ✓ Created flatfiles/ directory")
699
+
700
+ # 6. Migrate profiles from ~/.dbt/ to ~/.dvt/
701
+ from dbt.task.migrate import MigrateTask
702
+ task = MigrateTask(profiles_only=True)
703
+ task._migrate_profiles()
704
+
705
+ # Get project name from the content for display
706
+ project_name = Path.cwd().name
707
+ try:
708
+ config = yaml.safe_load(dbt_content)
709
+ if config and "name" in config:
710
+ project_name = config["name"]
711
+ except Exception:
712
+ pass
713
+
714
+ click.echo("")
715
+ click.echo("=" * 60)
716
+ click.echo("DVT project initialized from dbt project!")
717
+ click.echo("=" * 60)
718
+ click.echo(f" Project: {project_name}")
719
+ click.echo(f" Config: dvt_project.yml")
720
+ click.echo(f" Backup: {backup_file.name}")
721
+ click.echo("")
722
+ click.echo("Next steps:")
723
+ click.echo(" 1. Review dvt_project.yml")
724
+ click.echo(" 2. Run 'dvt target list' to verify connections")
725
+ click.echo(" 3. Run 'dvt run' to build your models")
726
+ click.echo("=" * 60)
727
+
728
+ def _initialize_user_metadata_db(self) -> None:
729
+ """
730
+ Initialize the user-level metadata database at ~/.dvt/.data/mdm.duckdb.
731
+
732
+ v0.59.0: Copies data from the packaged adapters_registry.duckdb to the
733
+ user-level database. This ensures users have access to type mappings,
734
+ syntax rules, and adapter queries even if the package is not accessible.
735
+
736
+ ALWAYS recreates the database on init for easy testing and updates.
737
+ """
738
+ try:
739
+ import duckdb
740
+ from dbt.compute.metadata import AdaptersRegistry
741
+
742
+ # Get paths
743
+ dvt_home = Path.home() / ".dvt"
744
+ data_dir = dvt_home / ".data"
745
+ user_db_path = data_dir / "mdm.duckdb"
746
+
747
+ # Create directories
748
+ data_dir.mkdir(parents=True, exist_ok=True)
749
+
750
+ # ALWAYS recreate for testing (remove if exists)
751
+ if user_db_path.exists():
752
+ user_db_path.unlink()
753
+
754
+ # Get packaged registry path
755
+ registry = AdaptersRegistry()
756
+ source_db_path = registry.get_registry_path()
757
+
758
+ # Connect to source (read-only) and destination
759
+ source_conn = duckdb.connect(str(source_db_path), read_only=True)
760
+ dest_conn = duckdb.connect(str(user_db_path))
761
+
762
+ # Get data from source and copy to destination
763
+ # (DuckDB doesn't support cross-database queries, so we fetch and insert)
764
+
765
+ # Copy datatype_mappings table
766
+ mappings = source_conn.execute("SELECT * FROM datatype_mappings").fetchall()
767
+ dest_conn.execute("DROP TABLE IF EXISTS datatype_mappings")
768
+ dest_conn.execute("""
769
+ CREATE TABLE datatype_mappings (
770
+ adapter_name VARCHAR,
771
+ adapter_type VARCHAR,
772
+ spark_type VARCHAR,
773
+ spark_version VARCHAR,
774
+ is_complex BOOLEAN,
775
+ cast_expression VARCHAR,
776
+ notes VARCHAR
777
+ )
778
+ """)
779
+
780
+ # Insert data
781
+ if mappings:
782
+ dest_conn.executemany(
783
+ "INSERT INTO datatype_mappings VALUES (?, ?, ?, ?, ?, ?, ?)",
784
+ mappings
785
+ )
786
+
787
+ # Copy adapter_queries table
788
+ queries = source_conn.execute("SELECT * FROM adapter_queries").fetchall()
789
+ dest_conn.execute("DROP TABLE IF EXISTS adapter_queries")
790
+ dest_conn.execute("""
791
+ CREATE TABLE adapter_queries (
792
+ adapter_name VARCHAR,
793
+ query_type VARCHAR,
794
+ query_template VARCHAR,
795
+ notes VARCHAR
796
+ )
797
+ """)
798
+ if queries:
799
+ dest_conn.executemany(
800
+ "INSERT INTO adapter_queries VALUES (?, ?, ?, ?)",
801
+ queries
802
+ )
803
+
804
+ # Copy syntax_registry table
805
+ syntax = source_conn.execute("SELECT * FROM syntax_registry").fetchall()
806
+ dest_conn.execute("DROP TABLE IF EXISTS syntax_registry")
807
+ dest_conn.execute("""
808
+ CREATE TABLE syntax_registry (
809
+ adapter_name VARCHAR,
810
+ quote_start VARCHAR,
811
+ quote_end VARCHAR,
812
+ case_sensitivity VARCHAR,
813
+ reserved_keywords VARCHAR
814
+ )
815
+ """)
816
+ if syntax:
817
+ dest_conn.executemany(
818
+ "INSERT INTO syntax_registry VALUES (?, ?, ?, ?, ?)",
819
+ syntax
820
+ )
821
+
822
+ # Copy value_transformations table (v0.59.0a32: seed pattern transformations)
823
+ transforms = []
824
+ try:
825
+ transforms = source_conn.execute("SELECT * FROM value_transformations").fetchall()
826
+ dest_conn.execute("DROP TABLE IF EXISTS value_transformations")
827
+ dest_conn.execute("""
828
+ CREATE TABLE value_transformations (
829
+ pattern VARCHAR NOT NULL,
830
+ target_type VARCHAR NOT NULL,
831
+ transform_expr VARCHAR NOT NULL,
832
+ priority INTEGER DEFAULT 50,
833
+ description VARCHAR,
834
+ PRIMARY KEY (pattern)
835
+ )
836
+ """)
837
+ if transforms:
838
+ dest_conn.executemany(
839
+ "INSERT INTO value_transformations VALUES (?, ?, ?, ?, ?)",
840
+ transforms
841
+ )
842
+ except Exception:
843
+ # Table might not exist in older registries
844
+ pass
845
+
846
+ source_conn.close()
847
+ dest_conn.close()
848
+
849
+ mappings_count = len(mappings) if mappings else 0
850
+ queries_count = len(queries) if queries else 0
851
+ syntax_count = len(syntax) if syntax else 0
852
+ transforms_count = len(transforms) if transforms else 0
853
+
854
+ click.echo(f" ✓ User metadata database initialized (~/.dvt/.data/mdm.duckdb)")
855
+ click.echo(f" - {mappings_count} type mappings (all adapters x all types x all Spark versions)")
856
+ click.echo(f" - {queries_count} adapter queries")
857
+ click.echo(f" - {syntax_count} syntax rules")
858
+ if transforms_count > 0:
859
+ click.echo(f" - {transforms_count} value transformation patterns (for dvt seed)")
860
+
861
+ except ImportError:
862
+ # DuckDB not installed - skip
863
+ click.echo(" ⚠ DuckDB not installed - skipping user metadata database")
864
+ except Exception as e:
865
+ # Don't fail init on metadata errors
866
+ click.echo(f" ⚠ Could not initialize user metadata database: {e}")
867
+
868
+ def _show_init_success(self, project_name: str) -> None:
869
+ """
870
+ Display success message and next steps after project initialization.
871
+
872
+ v0.59.0: Non-interactive flow - a DuckDB starter adapter is created,
873
+ user can modify it to use their actual database connections.
874
+ """
875
+ click.echo("")
876
+ click.echo("=" * 60)
877
+ click.echo("DVT project initialized successfully!")
878
+ click.echo("=" * 60)
879
+ click.echo(f"\nProject: {project_name}")
880
+ click.echo(f"Profile: ~/.dvt/profiles.yml")
881
+ click.echo("")
882
+ click.echo("A DuckDB starter adapter has been configured.")
883
+ click.echo("Modify ~/.dvt/profiles.yml to add your database connections.")
884
+ click.echo("")
885
+ click.echo("Next Steps:")
886
+ click.echo(f" 1. cd {project_name}")
887
+ click.echo(" 2. dvt run # Test with DuckDB starter")
888
+ click.echo(" 3. Edit ~/.dvt/profiles.yml to add real connections")
889
+ click.echo(" 4. dvt target list # Verify connections")
890
+ click.echo("")
891
+ click.echo("Docs: https://docs.getdbt.com/docs/configure-your-profile")
892
+ click.echo("=" * 60)
893
+
894
+ def run(self):
895
+ """Entry point for the init task."""
896
+ profiles_dir = get_flags().PROFILES_DIR
897
+ # Ensure profiles_dir is a string (may be PosixPath from default_profiles_dir())
898
+ if hasattr(profiles_dir, '__fspath__'):
899
+ profiles_dir = str(profiles_dir)
900
+ self.create_profiles_dir(profiles_dir)
901
+
902
+ # v0.58.8: Initialize user-level metadata database with packaged registry data
903
+ self._initialize_user_metadata_db()
904
+
905
+ try:
906
+ move_to_nearest_project_dir(self.args.project_dir)
907
+ in_project = True
908
+ except dbt_common.exceptions.DbtRuntimeError:
909
+ in_project = False
910
+
911
+ # v0.59.0: Check for dbt project that needs migration
912
+ dbt_project_file = Path("dbt_project.yml")
913
+ dvt_project_file = Path("dvt_project.yml")
914
+ in_dbt_project = dbt_project_file.exists() and not dvt_project_file.exists()
915
+
916
+ if in_dbt_project:
917
+ # Detected dbt project - convert to DVT project
918
+ click.echo("Detected existing dbt project (dbt_project.yml).")
919
+ click.echo("Converting to DVT project...")
920
+ click.echo("")
921
+ self._convert_dbt_to_dvt()
922
+ in_project = True # Now we have a DVT project
923
+
924
+ if in_project:
925
+ # If --profile was specified, it means use an existing profile, which is not
926
+ # applicable to this case
927
+ if self.args.profile:
928
+ raise DbtRuntimeError(
929
+ msg="Can not init existing project with specified profile, edit dvt_project.yml instead"
930
+ )
931
+
932
+ # v0.55.0: Ensure project-level .dvt/ structure exists
933
+ from dbt.config.compute import ComputeRegistry
934
+
935
+ # Create .dvt/ directory and jdbc_jars/
936
+ ComputeRegistry.ensure_jdbc_jars_dir(".")
937
+
938
+ # Ensure computes.yml exists at project level
939
+ registry = ComputeRegistry(project_dir=".")
940
+ registry.ensure_config_exists()
941
+
942
+ # Initialize metadata store if not already present
943
+ self._initialize_metadata_store(Path("."))
944
+
945
+ # Create default DuckDB database for starter profile
946
+ self._create_default_duckdb(Path("."))
947
+
948
+ # v0.59.0a11: Ensure profile has 'dev' DuckDB output
949
+ # Get project name from dvt_project.yml
950
+ try:
951
+ with open("dvt_project.yml", "r") as f:
952
+ project_config = yaml.safe_load(f)
953
+ project_name = project_config.get("name", Path.cwd().name)
954
+ except (FileNotFoundError, yaml.YAMLError):
955
+ # If we can't read project name, use folder name
956
+ project_name = Path.cwd().name
957
+
958
+ # v0.59.0a24: Always add profile (don't let exceptions be swallowed)
959
+ try:
960
+ self._add_project_profile(project_name)
961
+ except Exception as e:
962
+ click.echo(f" ⚠ Could not add profile: {e}")
963
+ else:
964
+ # When dvt init is run outside of an existing project,
965
+ # create a new project.
966
+ #
967
+ # v0.59.0a5: Two modes:
968
+ # 1. dvt init <project_name> → Create new folder with that name
969
+ # 2. dvt init (no args, in empty folder) → Init in place using folder name
970
+
971
+ user_profile_name = self.args.profile
972
+
973
+ if self.args.project_name:
974
+ # Mode 1: User specified project name → create new folder
975
+ project_name = self.args.project_name
976
+ if not ProjectName.is_valid(project_name):
977
+ click.echo(f"'{project_name}' is not a valid project name.")
978
+ click.echo("Project names must contain only letters, digits, and underscores.")
979
+ return
980
+
981
+ project_path = Path(project_name)
982
+ if project_path.exists():
983
+ fire_event(ProjectNameAlreadyExists(name=project_name))
984
+ return
985
+
986
+ if user_profile_name:
987
+ if not self.check_if_profile_exists(user_profile_name):
988
+ raise DbtRuntimeError(
989
+ msg="Could not find profile named '{}'".format(user_profile_name)
990
+ )
991
+ self.create_new_project(project_name, user_profile_name)
992
+ else:
993
+ self.create_new_project(project_name, project_name)
994
+
995
+ # create_new_project changes CWD into the project directory
996
+ # and already sets up .dvt/ structure, so just add profile
997
+ self._add_project_profile(project_name) # Uses CWD which is now inside project
998
+ self._show_init_success(project_name)
999
+ else:
1000
+ # Mode 2: No project name → init in current folder using folder name
1001
+ project_name = Path.cwd().name
1002
+
1003
+ # Validate folder name as project name
1004
+ if not ProjectName.is_valid(project_name):
1005
+ click.echo(f"Current folder '{project_name}' is not a valid project name.")
1006
+ click.echo("Project names must contain only letters, digits, and underscores.")
1007
+ click.echo("Use: dvt init <valid_project_name>")
1008
+ return
1009
+
1010
+ click.echo(f"Initializing DVT project '{project_name}' in current folder...")
1011
+ click.echo("")
1012
+
1013
+ profile_name = user_profile_name if user_profile_name else project_name
1014
+ if user_profile_name and not self.check_if_profile_exists(user_profile_name):
1015
+ raise DbtRuntimeError(
1016
+ msg="Could not find profile named '{}'".format(user_profile_name)
1017
+ )
1018
+
1019
+ # Init in place (no folder creation)
1020
+ self._create_project_in_place(project_name, profile_name)
1021
+ self._add_project_profile(project_name)
1022
+ self._show_init_success(project_name)