dvt-core 1.11.0b4__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.
Potentially problematic release.
This version of dvt-core might be problematic. Click here for more details.
- dvt/__init__.py +7 -0
- dvt/_pydantic_shim.py +26 -0
- dvt/adapters/__init__.py +16 -0
- dvt/adapters/multi_adapter_manager.py +268 -0
- dvt/artifacts/__init__.py +0 -0
- dvt/artifacts/exceptions/__init__.py +1 -0
- dvt/artifacts/exceptions/schemas.py +31 -0
- dvt/artifacts/resources/__init__.py +116 -0
- dvt/artifacts/resources/base.py +68 -0
- dvt/artifacts/resources/types.py +93 -0
- dvt/artifacts/resources/v1/analysis.py +10 -0
- dvt/artifacts/resources/v1/catalog.py +23 -0
- dvt/artifacts/resources/v1/components.py +275 -0
- dvt/artifacts/resources/v1/config.py +282 -0
- dvt/artifacts/resources/v1/documentation.py +11 -0
- dvt/artifacts/resources/v1/exposure.py +52 -0
- dvt/artifacts/resources/v1/function.py +53 -0
- dvt/artifacts/resources/v1/generic_test.py +32 -0
- dvt/artifacts/resources/v1/group.py +22 -0
- dvt/artifacts/resources/v1/hook.py +11 -0
- dvt/artifacts/resources/v1/macro.py +30 -0
- dvt/artifacts/resources/v1/metric.py +173 -0
- dvt/artifacts/resources/v1/model.py +146 -0
- dvt/artifacts/resources/v1/owner.py +10 -0
- dvt/artifacts/resources/v1/saved_query.py +112 -0
- dvt/artifacts/resources/v1/seed.py +42 -0
- dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
- dvt/artifacts/resources/v1/semantic_model.py +315 -0
- dvt/artifacts/resources/v1/singular_test.py +14 -0
- dvt/artifacts/resources/v1/snapshot.py +92 -0
- dvt/artifacts/resources/v1/source_definition.py +85 -0
- dvt/artifacts/resources/v1/sql_operation.py +10 -0
- dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
- dvt/artifacts/schemas/__init__.py +0 -0
- dvt/artifacts/schemas/base.py +191 -0
- dvt/artifacts/schemas/batch_results.py +24 -0
- dvt/artifacts/schemas/catalog/__init__.py +12 -0
- dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
- dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
- dvt/artifacts/schemas/freshness/__init__.py +1 -0
- dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
- dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
- dvt/artifacts/schemas/manifest/__init__.py +2 -0
- dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
- dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
- dvt/artifacts/schemas/results.py +148 -0
- dvt/artifacts/schemas/run/__init__.py +2 -0
- dvt/artifacts/schemas/run/v5/__init__.py +0 -0
- dvt/artifacts/schemas/run/v5/run.py +184 -0
- dvt/artifacts/schemas/upgrades/__init__.py +4 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
- dvt/artifacts/utils/validation.py +153 -0
- dvt/cli/__init__.py +1 -0
- dvt/cli/context.py +16 -0
- dvt/cli/exceptions.py +56 -0
- dvt/cli/flags.py +558 -0
- dvt/cli/main.py +971 -0
- dvt/cli/option_types.py +121 -0
- dvt/cli/options.py +79 -0
- dvt/cli/params.py +803 -0
- dvt/cli/requires.py +478 -0
- dvt/cli/resolvers.py +32 -0
- dvt/cli/types.py +40 -0
- dvt/clients/__init__.py +0 -0
- dvt/clients/checked_load.py +82 -0
- dvt/clients/git.py +164 -0
- dvt/clients/jinja.py +206 -0
- dvt/clients/jinja_static.py +245 -0
- dvt/clients/registry.py +192 -0
- dvt/clients/yaml_helper.py +68 -0
- dvt/compilation.py +833 -0
- dvt/compute/__init__.py +26 -0
- dvt/compute/base.py +288 -0
- dvt/compute/engines/__init__.py +13 -0
- dvt/compute/engines/duckdb_engine.py +368 -0
- dvt/compute/engines/spark_engine.py +273 -0
- dvt/compute/query_analyzer.py +212 -0
- dvt/compute/router.py +483 -0
- dvt/config/__init__.py +4 -0
- dvt/config/catalogs.py +95 -0
- dvt/config/compute_config.py +406 -0
- dvt/config/profile.py +411 -0
- dvt/config/profiles_v2.py +464 -0
- dvt/config/project.py +893 -0
- dvt/config/renderer.py +232 -0
- dvt/config/runtime.py +491 -0
- dvt/config/selectors.py +209 -0
- dvt/config/utils.py +78 -0
- dvt/connectors/.gitignore +6 -0
- dvt/connectors/README.md +306 -0
- dvt/connectors/catalog.yml +217 -0
- dvt/connectors/download_connectors.py +300 -0
- dvt/constants.py +29 -0
- dvt/context/__init__.py +0 -0
- dvt/context/base.py +746 -0
- dvt/context/configured.py +136 -0
- dvt/context/context_config.py +350 -0
- dvt/context/docs.py +82 -0
- dvt/context/exceptions_jinja.py +179 -0
- dvt/context/macro_resolver.py +195 -0
- dvt/context/macros.py +171 -0
- dvt/context/manifest.py +73 -0
- dvt/context/providers.py +2198 -0
- dvt/context/query_header.py +14 -0
- dvt/context/secret.py +59 -0
- dvt/context/target.py +74 -0
- dvt/contracts/__init__.py +0 -0
- dvt/contracts/files.py +413 -0
- dvt/contracts/graph/__init__.py +0 -0
- dvt/contracts/graph/manifest.py +1904 -0
- dvt/contracts/graph/metrics.py +98 -0
- dvt/contracts/graph/model_config.py +71 -0
- dvt/contracts/graph/node_args.py +42 -0
- dvt/contracts/graph/nodes.py +1806 -0
- dvt/contracts/graph/semantic_manifest.py +233 -0
- dvt/contracts/graph/unparsed.py +812 -0
- dvt/contracts/project.py +417 -0
- dvt/contracts/results.py +53 -0
- dvt/contracts/selection.py +23 -0
- dvt/contracts/sql.py +86 -0
- dvt/contracts/state.py +69 -0
- dvt/contracts/util.py +46 -0
- dvt/deprecations.py +347 -0
- dvt/deps/__init__.py +0 -0
- dvt/deps/base.py +153 -0
- dvt/deps/git.py +196 -0
- dvt/deps/local.py +80 -0
- dvt/deps/registry.py +131 -0
- dvt/deps/resolver.py +149 -0
- dvt/deps/tarball.py +121 -0
- dvt/docs/source/_ext/dbt_click.py +118 -0
- dvt/docs/source/conf.py +32 -0
- dvt/env_vars.py +64 -0
- dvt/event_time/event_time.py +40 -0
- dvt/event_time/sample_window.py +60 -0
- dvt/events/__init__.py +16 -0
- dvt/events/base_types.py +37 -0
- dvt/events/core_types_pb2.py +2 -0
- dvt/events/logging.py +109 -0
- dvt/events/types.py +2534 -0
- dvt/exceptions.py +1487 -0
- dvt/flags.py +89 -0
- dvt/graph/__init__.py +11 -0
- dvt/graph/cli.py +248 -0
- dvt/graph/graph.py +172 -0
- dvt/graph/queue.py +213 -0
- dvt/graph/selector.py +375 -0
- dvt/graph/selector_methods.py +976 -0
- dvt/graph/selector_spec.py +223 -0
- dvt/graph/thread_pool.py +18 -0
- dvt/hooks.py +21 -0
- dvt/include/README.md +49 -0
- dvt/include/__init__.py +3 -0
- dvt/include/global_project.py +4 -0
- dvt/include/starter_project/.gitignore +4 -0
- dvt/include/starter_project/README.md +15 -0
- dvt/include/starter_project/__init__.py +3 -0
- dvt/include/starter_project/analyses/.gitkeep +0 -0
- dvt/include/starter_project/dvt_project.yml +36 -0
- dvt/include/starter_project/macros/.gitkeep +0 -0
- dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
- dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
- dvt/include/starter_project/models/example/schema.yml +21 -0
- dvt/include/starter_project/seeds/.gitkeep +0 -0
- dvt/include/starter_project/snapshots/.gitkeep +0 -0
- dvt/include/starter_project/tests/.gitkeep +0 -0
- dvt/internal_deprecations.py +27 -0
- dvt/jsonschemas/__init__.py +3 -0
- dvt/jsonschemas/jsonschemas.py +309 -0
- dvt/jsonschemas/project/0.0.110.json +4717 -0
- dvt/jsonschemas/project/0.0.85.json +2015 -0
- dvt/jsonschemas/resources/0.0.110.json +2636 -0
- dvt/jsonschemas/resources/0.0.85.json +2536 -0
- dvt/jsonschemas/resources/latest.json +6773 -0
- dvt/links.py +4 -0
- dvt/materializations/__init__.py +0 -0
- dvt/materializations/incremental/__init__.py +0 -0
- dvt/materializations/incremental/microbatch.py +235 -0
- dvt/mp_context.py +8 -0
- dvt/node_types.py +37 -0
- dvt/parser/__init__.py +23 -0
- dvt/parser/analysis.py +21 -0
- dvt/parser/base.py +549 -0
- dvt/parser/common.py +267 -0
- dvt/parser/docs.py +52 -0
- dvt/parser/fixtures.py +51 -0
- dvt/parser/functions.py +30 -0
- dvt/parser/generic_test.py +100 -0
- dvt/parser/generic_test_builders.py +334 -0
- dvt/parser/hooks.py +119 -0
- dvt/parser/macros.py +137 -0
- dvt/parser/manifest.py +2204 -0
- dvt/parser/models.py +574 -0
- dvt/parser/partial.py +1179 -0
- dvt/parser/read_files.py +445 -0
- dvt/parser/schema_generic_tests.py +423 -0
- dvt/parser/schema_renderer.py +111 -0
- dvt/parser/schema_yaml_readers.py +936 -0
- dvt/parser/schemas.py +1467 -0
- dvt/parser/search.py +149 -0
- dvt/parser/seeds.py +28 -0
- dvt/parser/singular_test.py +20 -0
- dvt/parser/snapshots.py +44 -0
- dvt/parser/sources.py +557 -0
- dvt/parser/sql.py +63 -0
- dvt/parser/unit_tests.py +622 -0
- dvt/plugins/__init__.py +20 -0
- dvt/plugins/contracts.py +10 -0
- dvt/plugins/exceptions.py +2 -0
- dvt/plugins/manager.py +164 -0
- dvt/plugins/manifest.py +21 -0
- dvt/profiler.py +20 -0
- dvt/py.typed +1 -0
- dvt/runners/__init__.py +2 -0
- dvt/runners/exposure_runner.py +7 -0
- dvt/runners/no_op_runner.py +46 -0
- dvt/runners/saved_query_runner.py +7 -0
- dvt/selected_resources.py +8 -0
- dvt/task/__init__.py +0 -0
- dvt/task/base.py +504 -0
- dvt/task/build.py +197 -0
- dvt/task/clean.py +57 -0
- dvt/task/clone.py +162 -0
- dvt/task/compile.py +151 -0
- dvt/task/compute.py +366 -0
- dvt/task/debug.py +650 -0
- dvt/task/deps.py +280 -0
- dvt/task/docs/__init__.py +3 -0
- dvt/task/docs/generate.py +408 -0
- dvt/task/docs/index.html +250 -0
- dvt/task/docs/serve.py +28 -0
- dvt/task/freshness.py +323 -0
- dvt/task/function.py +122 -0
- dvt/task/group_lookup.py +46 -0
- dvt/task/init.py +374 -0
- dvt/task/list.py +237 -0
- dvt/task/printer.py +176 -0
- dvt/task/profiles.py +256 -0
- dvt/task/retry.py +175 -0
- dvt/task/run.py +1146 -0
- dvt/task/run_operation.py +142 -0
- dvt/task/runnable.py +802 -0
- dvt/task/seed.py +104 -0
- dvt/task/show.py +150 -0
- dvt/task/snapshot.py +57 -0
- dvt/task/sql.py +111 -0
- dvt/task/test.py +464 -0
- dvt/tests/fixtures/__init__.py +1 -0
- dvt/tests/fixtures/project.py +620 -0
- dvt/tests/util.py +651 -0
- dvt/tracking.py +529 -0
- dvt/utils/__init__.py +3 -0
- dvt/utils/artifact_upload.py +151 -0
- dvt/utils/utils.py +408 -0
- dvt/version.py +249 -0
- dvt_core-1.11.0b4.dist-info/METADATA +252 -0
- dvt_core-1.11.0b4.dist-info/RECORD +261 -0
- dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
- dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
- dvt_core-1.11.0b4.dist-info/top_level.txt +1 -0
dvt/task/init.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
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 dvt.config
|
|
10
|
+
import yaml
|
|
11
|
+
from dvt.config.profile import read_profile
|
|
12
|
+
from dvt.constants import DBT_PROJECT_FILE_NAME, DVT_PROJECT_FILE_NAME
|
|
13
|
+
from dvt.contracts.util import Identifier as ProjectName
|
|
14
|
+
from dvt.events.types import (
|
|
15
|
+
ConfigFolderDirectory,
|
|
16
|
+
InvalidProfileTemplateYAML,
|
|
17
|
+
NoSampleProfileFound,
|
|
18
|
+
ProfileWrittenWithProjectTemplateYAML,
|
|
19
|
+
ProfileWrittenWithSample,
|
|
20
|
+
ProfileWrittenWithTargetTemplateYAML,
|
|
21
|
+
ProjectCreated,
|
|
22
|
+
ProjectNameAlreadyExists,
|
|
23
|
+
SettingUpProfile,
|
|
24
|
+
StarterProjectPath,
|
|
25
|
+
)
|
|
26
|
+
from dvt.flags import get_flags
|
|
27
|
+
from dvt.task.base import BaseTask, move_to_nearest_project_dir
|
|
28
|
+
from dvt.version import _get_adapter_plugin_names
|
|
29
|
+
|
|
30
|
+
import dbt_common.clients.system
|
|
31
|
+
from dbt.adapters.factory import get_include_paths, load_plugin
|
|
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
|
+
# This file is not needed for the starter project but exists for finding the resource path
|
|
39
|
+
IGNORE_FILES = ["__init__.py", "__pycache__"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# https://click.palletsprojects.com/en/8.0.x/api/#types
|
|
43
|
+
# click v7.0 has UNPROCESSED, STRING, INT, FLOAT, BOOL, and UUID available.
|
|
44
|
+
click_type_mapping = {
|
|
45
|
+
"string": click.STRING,
|
|
46
|
+
"int": click.INT,
|
|
47
|
+
"float": click.FLOAT,
|
|
48
|
+
"bool": click.BOOL,
|
|
49
|
+
None: None,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class InitTask(BaseTask):
|
|
54
|
+
def copy_starter_repo(self, project_name: str) -> None:
|
|
55
|
+
# Lazy import to avoid ModuleNotFoundError
|
|
56
|
+
from dvt.include.starter_project import (
|
|
57
|
+
PACKAGE_PATH as starter_project_directory,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
fire_event(StarterProjectPath(dir=starter_project_directory))
|
|
61
|
+
shutil.copytree(
|
|
62
|
+
starter_project_directory, project_name, ignore=shutil.ignore_patterns(*IGNORE_FILES)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def create_profiles_dir(self, profiles_dir: str) -> bool:
|
|
66
|
+
"""Create the user's profiles directory if it doesn't already exist."""
|
|
67
|
+
profiles_path = Path(profiles_dir)
|
|
68
|
+
if not profiles_path.exists():
|
|
69
|
+
fire_event(ConfigFolderDirectory(dir=str(profiles_dir)))
|
|
70
|
+
dbt_common.clients.system.make_directory(profiles_dir)
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def create_profile_from_sample(self, adapter: str, profile_name: str):
|
|
75
|
+
"""Create a profile entry using the adapter's sample_profiles.yml
|
|
76
|
+
|
|
77
|
+
Renames the profile in sample_profiles.yml to match that of the project."""
|
|
78
|
+
# Line below raises an exception if the specified adapter is not found
|
|
79
|
+
load_plugin(adapter)
|
|
80
|
+
adapter_path = get_include_paths(adapter)[0]
|
|
81
|
+
sample_profiles_path = adapter_path / "sample_profiles.yml"
|
|
82
|
+
|
|
83
|
+
if not sample_profiles_path.exists():
|
|
84
|
+
fire_event(NoSampleProfileFound(adapter=adapter))
|
|
85
|
+
else:
|
|
86
|
+
with open(sample_profiles_path, "r") as f:
|
|
87
|
+
sample_profile = f.read()
|
|
88
|
+
sample_profile_name = list(yaml.safe_load(sample_profile).keys())[0]
|
|
89
|
+
# Use a regex to replace the name of the sample_profile with
|
|
90
|
+
# that of the project without losing any comments from the sample
|
|
91
|
+
sample_profile = re.sub(f"^{sample_profile_name}:", f"{profile_name}:", sample_profile)
|
|
92
|
+
profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
|
|
93
|
+
if profiles_filepath.exists():
|
|
94
|
+
with open(profiles_filepath, "a") as f:
|
|
95
|
+
f.write("\n" + sample_profile)
|
|
96
|
+
else:
|
|
97
|
+
with open(profiles_filepath, "w") as f:
|
|
98
|
+
f.write(sample_profile)
|
|
99
|
+
fire_event(
|
|
100
|
+
ProfileWrittenWithSample(name=profile_name, path=str(profiles_filepath))
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def generate_target_from_input(self, profile_template: dict, target: dict = {}) -> dict:
|
|
104
|
+
"""Generate a target configuration from profile_template and user input."""
|
|
105
|
+
profile_template_local = copy.deepcopy(profile_template)
|
|
106
|
+
for key, value in profile_template_local.items():
|
|
107
|
+
if key.startswith("_choose"):
|
|
108
|
+
choice_type = key[8:].replace("_", " ")
|
|
109
|
+
option_list = list(value.keys())
|
|
110
|
+
prompt_msg = (
|
|
111
|
+
"\n".join([f"[{n+1}] {v}" for n, v in enumerate(option_list)])
|
|
112
|
+
+ f"\nDesired {choice_type} option (enter a number)"
|
|
113
|
+
)
|
|
114
|
+
numeric_choice = click.prompt(prompt_msg, type=click.INT)
|
|
115
|
+
choice = option_list[numeric_choice - 1]
|
|
116
|
+
# Complete the chosen option's values in a recursive call
|
|
117
|
+
target = self.generate_target_from_input(
|
|
118
|
+
profile_template_local[key][choice], target
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
if key.startswith("_fixed"):
|
|
122
|
+
# _fixed prefixed keys are not presented to the user
|
|
123
|
+
target[key[7:]] = value
|
|
124
|
+
else:
|
|
125
|
+
hide_input = value.get("hide_input", False)
|
|
126
|
+
default = value.get("default", None)
|
|
127
|
+
hint = value.get("hint", None)
|
|
128
|
+
type = click_type_mapping[value.get("type", None)]
|
|
129
|
+
text = key + (f" ({hint})" if hint else "")
|
|
130
|
+
target[key] = click.prompt(
|
|
131
|
+
text, default=default, hide_input=hide_input, type=type
|
|
132
|
+
)
|
|
133
|
+
return target
|
|
134
|
+
|
|
135
|
+
def get_profile_name_from_current_project(self) -> str:
|
|
136
|
+
"""Reads dvt_project.yml (or dbt_project.yml for backward compatibility)
|
|
137
|
+
in the current directory to retrieve the profile name.
|
|
138
|
+
"""
|
|
139
|
+
from dbt_common.clients.system import path_exists
|
|
140
|
+
|
|
141
|
+
# Try new filename first
|
|
142
|
+
if path_exists(DVT_PROJECT_FILE_NAME):
|
|
143
|
+
project_file = DVT_PROJECT_FILE_NAME
|
|
144
|
+
else:
|
|
145
|
+
project_file = DBT_PROJECT_FILE_NAME
|
|
146
|
+
|
|
147
|
+
with open(project_file) as f:
|
|
148
|
+
project_dict = yaml.safe_load(f)
|
|
149
|
+
return project_dict["profile"]
|
|
150
|
+
|
|
151
|
+
def write_profile(self, profile: dict, profile_name: str):
|
|
152
|
+
"""Given a profile, write it to the current project's profiles.yml.
|
|
153
|
+
This will overwrite any profile with a matching name."""
|
|
154
|
+
# Create the profile directory if it doesn't exist
|
|
155
|
+
profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
|
|
156
|
+
|
|
157
|
+
profiles = {profile_name: profile}
|
|
158
|
+
|
|
159
|
+
if profiles_filepath.exists():
|
|
160
|
+
with open(profiles_filepath, "r") as f:
|
|
161
|
+
profiles = yaml.safe_load(f) or {}
|
|
162
|
+
profiles[profile_name] = profile
|
|
163
|
+
|
|
164
|
+
# Write the profiles dictionary to a brand-new or pre-existing file
|
|
165
|
+
with open(profiles_filepath, "w") as f:
|
|
166
|
+
yaml.dump(profiles, f)
|
|
167
|
+
|
|
168
|
+
def create_profile_from_profile_template(self, profile_template: dict, profile_name: str):
|
|
169
|
+
"""Create and write a profile using the supplied profile_template."""
|
|
170
|
+
initial_target = profile_template.get("fixed", {})
|
|
171
|
+
prompts = profile_template.get("prompts", {})
|
|
172
|
+
target = self.generate_target_from_input(prompts, initial_target)
|
|
173
|
+
target_name = target.pop("target", "dev")
|
|
174
|
+
profile = {"outputs": {target_name: target}, "target": target_name}
|
|
175
|
+
self.write_profile(profile, profile_name)
|
|
176
|
+
|
|
177
|
+
def create_profile_from_target(self, adapter: str, profile_name: str):
|
|
178
|
+
"""Create a profile without defaults using target's profile_template.yml if available, or
|
|
179
|
+
sample_profiles.yml as a fallback."""
|
|
180
|
+
# Line below raises an exception if the specified adapter is not found
|
|
181
|
+
load_plugin(adapter)
|
|
182
|
+
adapter_path = get_include_paths(adapter)[0]
|
|
183
|
+
profile_template_path = adapter_path / "profile_template.yml"
|
|
184
|
+
|
|
185
|
+
if profile_template_path.exists():
|
|
186
|
+
with open(profile_template_path) as f:
|
|
187
|
+
profile_template = yaml.safe_load(f)
|
|
188
|
+
self.create_profile_from_profile_template(profile_template, profile_name)
|
|
189
|
+
profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
|
|
190
|
+
fire_event(
|
|
191
|
+
ProfileWrittenWithTargetTemplateYAML(
|
|
192
|
+
name=profile_name, path=str(profiles_filepath)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
# For adapters without a profile_template.yml defined, fallback on
|
|
197
|
+
# sample_profiles.yml
|
|
198
|
+
self.create_profile_from_sample(adapter, profile_name)
|
|
199
|
+
|
|
200
|
+
def check_if_profile_exists(self, profile_name: str) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Validate that the specified profile exists. Can't use the regular profile validation
|
|
203
|
+
routine because it assumes the project file exists
|
|
204
|
+
"""
|
|
205
|
+
profiles_dir = get_flags().PROFILES_DIR
|
|
206
|
+
raw_profiles = read_profile(profiles_dir)
|
|
207
|
+
return profile_name in raw_profiles
|
|
208
|
+
|
|
209
|
+
def check_if_can_write_profile(self, profile_name: Optional[str] = None) -> bool:
|
|
210
|
+
"""Using either a provided profile name or that specified in project file,
|
|
211
|
+
check if the profile already exists in profiles.yml, and if so ask the
|
|
212
|
+
user whether to proceed and overwrite it."""
|
|
213
|
+
profiles_file = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
|
|
214
|
+
if not profiles_file.exists():
|
|
215
|
+
return True
|
|
216
|
+
profile_name = profile_name or self.get_profile_name_from_current_project()
|
|
217
|
+
with open(profiles_file, "r") as f:
|
|
218
|
+
profiles = yaml.safe_load(f) or {}
|
|
219
|
+
if profile_name in profiles.keys():
|
|
220
|
+
response = click.confirm(
|
|
221
|
+
f"The profile {profile_name} already exists in "
|
|
222
|
+
f"{profiles_file}. Continue and overwrite it?"
|
|
223
|
+
)
|
|
224
|
+
return response
|
|
225
|
+
else:
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def create_profile_using_project_profile_template(self, profile_name):
|
|
229
|
+
"""Create a profile using the project's profile_template.yml"""
|
|
230
|
+
with open("profile_template.yml") as f:
|
|
231
|
+
profile_template = yaml.safe_load(f)
|
|
232
|
+
self.create_profile_from_profile_template(profile_template, profile_name)
|
|
233
|
+
profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
|
|
234
|
+
fire_event(
|
|
235
|
+
ProfileWrittenWithProjectTemplateYAML(name=profile_name, path=str(profiles_filepath))
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def ask_for_adapter_choice(self) -> str:
|
|
239
|
+
"""Ask the user which adapter (database) they'd like to use."""
|
|
240
|
+
available_adapters = list(_get_adapter_plugin_names())
|
|
241
|
+
|
|
242
|
+
if not available_adapters:
|
|
243
|
+
raise dbt.exceptions.NoAdaptersAvailableError()
|
|
244
|
+
|
|
245
|
+
prompt_msg = (
|
|
246
|
+
"Which database would you like to use?\n"
|
|
247
|
+
+ "\n".join([f"[{n+1}] {v}" for n, v in enumerate(available_adapters)])
|
|
248
|
+
+ "\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)"
|
|
249
|
+
+ "\n\nEnter a number"
|
|
250
|
+
)
|
|
251
|
+
numeric_choice = click.prompt(prompt_msg, type=click.INT)
|
|
252
|
+
return available_adapters[numeric_choice - 1]
|
|
253
|
+
|
|
254
|
+
def setup_profile(self, profile_name: str) -> None:
|
|
255
|
+
"""Set up a new profile for a project"""
|
|
256
|
+
fire_event(SettingUpProfile())
|
|
257
|
+
if not self.check_if_can_write_profile(profile_name=profile_name):
|
|
258
|
+
return
|
|
259
|
+
# If a profile_template.yml exists in the project root, that effectively
|
|
260
|
+
# overrides the profile_template.yml for the given target.
|
|
261
|
+
profile_template_path = Path("profile_template.yml")
|
|
262
|
+
if profile_template_path.exists():
|
|
263
|
+
try:
|
|
264
|
+
# This relies on a valid profile_template.yml from the user,
|
|
265
|
+
# so use a try: except to fall back to the default on failure
|
|
266
|
+
self.create_profile_using_project_profile_template(profile_name)
|
|
267
|
+
return
|
|
268
|
+
except Exception:
|
|
269
|
+
fire_event(InvalidProfileTemplateYAML())
|
|
270
|
+
adapter = self.ask_for_adapter_choice()
|
|
271
|
+
self.create_profile_from_target(adapter, profile_name=profile_name)
|
|
272
|
+
|
|
273
|
+
def get_valid_project_name(self) -> str:
|
|
274
|
+
"""Returns a valid project name, either from CLI arg or user prompt."""
|
|
275
|
+
|
|
276
|
+
# Lazy import to avoid ModuleNotFoundError
|
|
277
|
+
from dvt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
|
|
278
|
+
|
|
279
|
+
name = self.args.project_name
|
|
280
|
+
internal_package_names = {GLOBAL_PROJECT_NAME}
|
|
281
|
+
available_adapters = list(_get_adapter_plugin_names())
|
|
282
|
+
for adapter_name in available_adapters:
|
|
283
|
+
internal_package_names.update(f"dbt_{adapter_name}")
|
|
284
|
+
while not ProjectName.is_valid(name) or name in internal_package_names:
|
|
285
|
+
if name:
|
|
286
|
+
click.echo(name + " is not a valid project name.")
|
|
287
|
+
name = click.prompt("Enter a name for your project (letters, digits, underscore)")
|
|
288
|
+
|
|
289
|
+
return name
|
|
290
|
+
|
|
291
|
+
def create_new_project(self, project_name: str, profile_name: str):
|
|
292
|
+
from dbt_common.clients.system import path_exists
|
|
293
|
+
|
|
294
|
+
self.copy_starter_repo(project_name)
|
|
295
|
+
os.chdir(project_name)
|
|
296
|
+
|
|
297
|
+
# Rename dbt_project.yml to dvt_project.yml if it exists (from starter project)
|
|
298
|
+
if path_exists(DBT_PROJECT_FILE_NAME):
|
|
299
|
+
with open(DBT_PROJECT_FILE_NAME, "r") as f:
|
|
300
|
+
content = f"{f.read()}".format(
|
|
301
|
+
project_name=project_name, profile_name=profile_name
|
|
302
|
+
)
|
|
303
|
+
# Write to new filename
|
|
304
|
+
with open(DVT_PROJECT_FILE_NAME, "w") as f:
|
|
305
|
+
f.write(content)
|
|
306
|
+
# Remove old filename
|
|
307
|
+
os.remove(DBT_PROJECT_FILE_NAME)
|
|
308
|
+
else:
|
|
309
|
+
# Fallback if starter project already has dvt_project.yml
|
|
310
|
+
with open(DVT_PROJECT_FILE_NAME, "r") as f:
|
|
311
|
+
content = f"{f.read()}".format(
|
|
312
|
+
project_name=project_name, profile_name=profile_name
|
|
313
|
+
)
|
|
314
|
+
with open(DVT_PROJECT_FILE_NAME, "w") as f:
|
|
315
|
+
f.write(content)
|
|
316
|
+
|
|
317
|
+
fire_event(
|
|
318
|
+
ProjectCreated(
|
|
319
|
+
project_name=project_name,
|
|
320
|
+
docs_url=DOCS_URL,
|
|
321
|
+
slack_url=SLACK_URL,
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def run(self):
|
|
326
|
+
"""Entry point for the init task."""
|
|
327
|
+
profiles_dir = get_flags().PROFILES_DIR
|
|
328
|
+
self.create_profiles_dir(profiles_dir)
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
move_to_nearest_project_dir(self.args.project_dir)
|
|
332
|
+
in_project = True
|
|
333
|
+
except dbt_common.exceptions.DbtRuntimeError:
|
|
334
|
+
in_project = False
|
|
335
|
+
|
|
336
|
+
if in_project:
|
|
337
|
+
# If --profile was specified, it means use an existing profile, which is not
|
|
338
|
+
# applicable to this case
|
|
339
|
+
if self.args.profile:
|
|
340
|
+
raise DbtRuntimeError(
|
|
341
|
+
msg="Can not init existing project with specified profile, edit project file instead"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# When dbt init is run inside an existing project,
|
|
345
|
+
# just setup the user's profile.
|
|
346
|
+
if not self.args.skip_profile_setup:
|
|
347
|
+
profile_name = self.get_profile_name_from_current_project()
|
|
348
|
+
self.setup_profile(profile_name)
|
|
349
|
+
else:
|
|
350
|
+
# When dbt init is run outside of an existing project,
|
|
351
|
+
# create a new project and set up the user's profile.
|
|
352
|
+
project_name = self.get_valid_project_name()
|
|
353
|
+
project_path = Path(project_name)
|
|
354
|
+
if project_path.exists():
|
|
355
|
+
fire_event(ProjectNameAlreadyExists(name=project_name))
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
# If the user specified an existing profile to use, use it instead of generating a new one
|
|
359
|
+
user_profile_name = self.args.profile
|
|
360
|
+
if user_profile_name:
|
|
361
|
+
if not self.check_if_profile_exists(user_profile_name):
|
|
362
|
+
raise DbtRuntimeError(
|
|
363
|
+
msg="Could not find profile named '{}'".format(user_profile_name)
|
|
364
|
+
)
|
|
365
|
+
self.create_new_project(project_name, user_profile_name)
|
|
366
|
+
else:
|
|
367
|
+
profile_name = project_name
|
|
368
|
+
# Create the profile after creating the project to avoid leaving a random profile
|
|
369
|
+
# if the former fails.
|
|
370
|
+
self.create_new_project(project_name, profile_name)
|
|
371
|
+
|
|
372
|
+
# Ask for adapter only if skip_profile_setup flag is not provided
|
|
373
|
+
if not self.args.skip_profile_setup:
|
|
374
|
+
self.setup_profile(profile_name)
|
dvt/task/list.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Iterator, List
|
|
3
|
+
|
|
4
|
+
from dvt.cli.flags import Flags
|
|
5
|
+
from dvt.config.runtime import RuntimeConfig
|
|
6
|
+
from dvt.contracts.graph.manifest import Manifest
|
|
7
|
+
from dvt.contracts.graph.nodes import (
|
|
8
|
+
Exposure,
|
|
9
|
+
Metric,
|
|
10
|
+
SavedQuery,
|
|
11
|
+
SemanticModel,
|
|
12
|
+
SourceDefinition,
|
|
13
|
+
UnitTestDefinition,
|
|
14
|
+
)
|
|
15
|
+
from dvt.events.types import NoNodesSelected
|
|
16
|
+
from dvt.graph import ResourceTypeSelector
|
|
17
|
+
from dvt.node_types import NodeType
|
|
18
|
+
from dvt.task.base import resource_types_from_args
|
|
19
|
+
from dvt.task.runnable import GraphRunnableTask
|
|
20
|
+
from dvt.utils import JSONEncoder
|
|
21
|
+
|
|
22
|
+
from dbt_common.events.contextvars import task_contextvars
|
|
23
|
+
from dbt_common.events.functions import fire_event, warn_or_error
|
|
24
|
+
from dbt_common.events.types import PrintEvent
|
|
25
|
+
from dbt_common.exceptions import DbtInternalError, DbtRuntimeError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ListTask(GraphRunnableTask):
|
|
29
|
+
DEFAULT_RESOURCE_VALUES = frozenset(
|
|
30
|
+
(
|
|
31
|
+
NodeType.Model,
|
|
32
|
+
NodeType.Snapshot,
|
|
33
|
+
NodeType.Seed,
|
|
34
|
+
NodeType.Test,
|
|
35
|
+
NodeType.Source,
|
|
36
|
+
NodeType.Exposure,
|
|
37
|
+
NodeType.Metric,
|
|
38
|
+
NodeType.SavedQuery,
|
|
39
|
+
NodeType.SemanticModel,
|
|
40
|
+
NodeType.Unit,
|
|
41
|
+
NodeType.Function,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
ALL_RESOURCE_VALUES = DEFAULT_RESOURCE_VALUES | frozenset((NodeType.Analysis,))
|
|
45
|
+
ALLOWED_KEYS = frozenset(
|
|
46
|
+
(
|
|
47
|
+
"alias",
|
|
48
|
+
"name",
|
|
49
|
+
"package_name",
|
|
50
|
+
"depends_on",
|
|
51
|
+
"tags",
|
|
52
|
+
"config",
|
|
53
|
+
"resource_type",
|
|
54
|
+
"source_name",
|
|
55
|
+
"original_file_path",
|
|
56
|
+
"unique_id",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __init__(self, args: Flags, config: RuntimeConfig, manifest: Manifest) -> None:
|
|
61
|
+
super().__init__(args, config, manifest)
|
|
62
|
+
if self.args.models:
|
|
63
|
+
if self.args.select:
|
|
64
|
+
raise DbtRuntimeError('"models" and "select" are mutually exclusive arguments')
|
|
65
|
+
if self.args.resource_types:
|
|
66
|
+
raise DbtRuntimeError(
|
|
67
|
+
'"models" and "resource_type" are mutually exclusive ' "arguments"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _iterate_selected_nodes(self):
|
|
71
|
+
selector = self.get_node_selector()
|
|
72
|
+
spec = self.get_selection_spec()
|
|
73
|
+
unique_ids = sorted(selector.get_selected(spec))
|
|
74
|
+
if not unique_ids:
|
|
75
|
+
warn_or_error(NoNodesSelected())
|
|
76
|
+
return
|
|
77
|
+
if self.manifest is None:
|
|
78
|
+
raise DbtInternalError("manifest is None in _iterate_selected_nodes")
|
|
79
|
+
for unique_id in unique_ids:
|
|
80
|
+
if unique_id in self.manifest.nodes:
|
|
81
|
+
yield self.manifest.nodes[unique_id]
|
|
82
|
+
elif unique_id in self.manifest.sources:
|
|
83
|
+
yield self.manifest.sources[unique_id]
|
|
84
|
+
elif unique_id in self.manifest.exposures:
|
|
85
|
+
yield self.manifest.exposures[unique_id]
|
|
86
|
+
elif unique_id in self.manifest.metrics:
|
|
87
|
+
yield self.manifest.metrics[unique_id]
|
|
88
|
+
elif unique_id in self.manifest.semantic_models:
|
|
89
|
+
yield self.manifest.semantic_models[unique_id]
|
|
90
|
+
elif unique_id in self.manifest.unit_tests:
|
|
91
|
+
yield self.manifest.unit_tests[unique_id]
|
|
92
|
+
elif unique_id in self.manifest.saved_queries:
|
|
93
|
+
yield self.manifest.saved_queries[unique_id]
|
|
94
|
+
elif unique_id in self.manifest.functions:
|
|
95
|
+
yield self.manifest.functions[unique_id]
|
|
96
|
+
else:
|
|
97
|
+
raise DbtRuntimeError(
|
|
98
|
+
f'Got an unexpected result from node selection: "{unique_id}"'
|
|
99
|
+
f"Listing this node type is not yet supported!"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def generate_selectors(self):
|
|
103
|
+
for node in self._iterate_selected_nodes():
|
|
104
|
+
if node.resource_type == NodeType.Source:
|
|
105
|
+
assert isinstance(node, SourceDefinition)
|
|
106
|
+
# sources are searched for by pkg.source_name.table_name
|
|
107
|
+
source_selector = ".".join([node.package_name, node.source_name, node.name])
|
|
108
|
+
yield f"source:{source_selector}"
|
|
109
|
+
elif node.resource_type == NodeType.Exposure:
|
|
110
|
+
assert isinstance(node, Exposure)
|
|
111
|
+
# exposures are searched for by pkg.exposure_name
|
|
112
|
+
exposure_selector = ".".join([node.package_name, node.name])
|
|
113
|
+
yield f"exposure:{exposure_selector}"
|
|
114
|
+
elif node.resource_type == NodeType.Metric:
|
|
115
|
+
assert isinstance(node, Metric)
|
|
116
|
+
# metrics are searched for by pkg.metric_name
|
|
117
|
+
metric_selector = ".".join([node.package_name, node.name])
|
|
118
|
+
yield f"metric:{metric_selector}"
|
|
119
|
+
elif node.resource_type == NodeType.SavedQuery:
|
|
120
|
+
assert isinstance(node, SavedQuery)
|
|
121
|
+
saved_query_selector = ".".join([node.package_name, node.name])
|
|
122
|
+
yield f"saved_query:{saved_query_selector}"
|
|
123
|
+
elif node.resource_type == NodeType.SemanticModel:
|
|
124
|
+
assert isinstance(node, SemanticModel)
|
|
125
|
+
semantic_model_selector = ".".join([node.package_name, node.name])
|
|
126
|
+
yield f"semantic_model:{semantic_model_selector}"
|
|
127
|
+
elif node.resource_type == NodeType.Unit:
|
|
128
|
+
assert isinstance(node, UnitTestDefinition)
|
|
129
|
+
unit_test_selector = ".".join([node.package_name, node.versioned_name])
|
|
130
|
+
yield f"unit_test:{unit_test_selector}"
|
|
131
|
+
else:
|
|
132
|
+
# everything else is from `fqn`
|
|
133
|
+
yield ".".join(node.fqn)
|
|
134
|
+
|
|
135
|
+
def generate_names(self):
|
|
136
|
+
for node in self._iterate_selected_nodes():
|
|
137
|
+
yield node.search_name
|
|
138
|
+
|
|
139
|
+
def _get_nested_value(self, data, key_path):
|
|
140
|
+
"""Get nested value using dot notation (e.g., 'config.materialized')"""
|
|
141
|
+
keys = key_path.split(".")
|
|
142
|
+
current = data
|
|
143
|
+
for key in keys:
|
|
144
|
+
if isinstance(current, dict) and key in current:
|
|
145
|
+
current = current[key]
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
148
|
+
return current
|
|
149
|
+
|
|
150
|
+
def generate_json(self):
|
|
151
|
+
for node in self._iterate_selected_nodes():
|
|
152
|
+
node_dict = node.to_dict(omit_none=False)
|
|
153
|
+
|
|
154
|
+
if self.args.output_keys:
|
|
155
|
+
# Handle both nested and regular keys
|
|
156
|
+
result = {}
|
|
157
|
+
for key in self.args.output_keys:
|
|
158
|
+
if "." in key:
|
|
159
|
+
# Handle nested key (e.g., 'config.materialized')
|
|
160
|
+
value = self._get_nested_value(node_dict, key)
|
|
161
|
+
if value is not None:
|
|
162
|
+
result[key] = value
|
|
163
|
+
else:
|
|
164
|
+
# Handle regular key
|
|
165
|
+
if key in node_dict:
|
|
166
|
+
result[key] = node_dict[key]
|
|
167
|
+
else:
|
|
168
|
+
# Use default allowed keys
|
|
169
|
+
result = {k: v for k, v in node_dict.items() if k in self.ALLOWED_KEYS}
|
|
170
|
+
|
|
171
|
+
yield json.dumps(result, cls=JSONEncoder)
|
|
172
|
+
|
|
173
|
+
def generate_paths(self) -> Iterator[str]:
|
|
174
|
+
for node in self._iterate_selected_nodes():
|
|
175
|
+
yield node.original_file_path
|
|
176
|
+
|
|
177
|
+
def run(self):
|
|
178
|
+
# We set up a context manager here with "task_contextvars" because we
|
|
179
|
+
# we need the project_root in compile_manifest.
|
|
180
|
+
with task_contextvars(project_root=self.config.project_root):
|
|
181
|
+
self.compile_manifest()
|
|
182
|
+
output = self.args.output
|
|
183
|
+
if output == "selector":
|
|
184
|
+
generator = self.generate_selectors
|
|
185
|
+
elif output == "name":
|
|
186
|
+
generator = self.generate_names
|
|
187
|
+
elif output == "json":
|
|
188
|
+
generator = self.generate_json
|
|
189
|
+
elif output == "path":
|
|
190
|
+
generator = self.generate_paths
|
|
191
|
+
else:
|
|
192
|
+
raise DbtInternalError("Invalid output {}".format(output))
|
|
193
|
+
|
|
194
|
+
return self.output_results(generator())
|
|
195
|
+
|
|
196
|
+
def output_results(self, results):
|
|
197
|
+
"""Log, or output a plain, newline-delimited, and ready-to-pipe list of nodes found."""
|
|
198
|
+
for result in results:
|
|
199
|
+
self.node_results.append(result)
|
|
200
|
+
# No formatting, still get to stdout when --quiet is used
|
|
201
|
+
fire_event(PrintEvent(msg=result))
|
|
202
|
+
return self.node_results
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def resource_types(self) -> List[NodeType]:
|
|
206
|
+
if self.args.models:
|
|
207
|
+
return [NodeType.Model]
|
|
208
|
+
|
|
209
|
+
resource_types = resource_types_from_args(
|
|
210
|
+
self.args, set(self.ALL_RESOURCE_VALUES), set(self.DEFAULT_RESOURCE_VALUES)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return list(resource_types)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def selection_arg(self):
|
|
217
|
+
# for backwards compatibility, list accepts both --models and --select,
|
|
218
|
+
# with slightly different behavior: --models implies --resource-type model
|
|
219
|
+
if self.args.models:
|
|
220
|
+
return self.args.models
|
|
221
|
+
else:
|
|
222
|
+
return self.args.select
|
|
223
|
+
|
|
224
|
+
def get_node_selector(self) -> ResourceTypeSelector:
|
|
225
|
+
if self.manifest is None or self.graph is None:
|
|
226
|
+
raise DbtInternalError("manifest and graph must be set to get perform node selection")
|
|
227
|
+
return ResourceTypeSelector(
|
|
228
|
+
graph=self.graph,
|
|
229
|
+
manifest=self.manifest,
|
|
230
|
+
previous_state=self.previous_state,
|
|
231
|
+
resource_types=self.resource_types,
|
|
232
|
+
include_empty_nodes=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def interpret_results(self, results):
|
|
236
|
+
# list command should always return 0 as exit code
|
|
237
|
+
return True
|