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/clients/git.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from dvt.events.types import (
|
|
5
|
+
GitNothingToDo,
|
|
6
|
+
GitProgressCheckedOutAt,
|
|
7
|
+
GitProgressCheckoutRevision,
|
|
8
|
+
GitProgressPullingNewDependency,
|
|
9
|
+
GitProgressUpdatedCheckoutRange,
|
|
10
|
+
GitProgressUpdatingExistingDependency,
|
|
11
|
+
GitSparseCheckoutSubdirectory,
|
|
12
|
+
)
|
|
13
|
+
from dvt.exceptions import (
|
|
14
|
+
CommandResultError,
|
|
15
|
+
DbtRuntimeError,
|
|
16
|
+
GitCheckoutError,
|
|
17
|
+
GitCloningError,
|
|
18
|
+
UnknownGitCloningProblemError,
|
|
19
|
+
)
|
|
20
|
+
from packaging import version
|
|
21
|
+
|
|
22
|
+
from dbt_common.clients.system import rmdir, run_cmd
|
|
23
|
+
from dbt_common.events.functions import fire_event
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_commit(revision: str) -> bool:
|
|
27
|
+
# match SHA-1 git commit
|
|
28
|
+
return bool(re.match(r"\b[0-9a-f]{40}\b", revision))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None):
|
|
32
|
+
has_revision = revision is not None
|
|
33
|
+
is_commit = _is_commit(revision or "")
|
|
34
|
+
|
|
35
|
+
clone_cmd = ["git", "clone", "--depth", "1"]
|
|
36
|
+
if subdirectory:
|
|
37
|
+
fire_event(GitSparseCheckoutSubdirectory(subdir=subdirectory))
|
|
38
|
+
out, _ = run_cmd(cwd, ["git", "--version"], env={"LC_ALL": "C"})
|
|
39
|
+
git_version = version.parse(re.search(r"\d+\.\d+\.\d+", out.decode("utf-8")).group(0))
|
|
40
|
+
if not git_version >= version.parse("2.25.0"):
|
|
41
|
+
# 2.25.0 introduces --sparse
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"Please update your git version to pull a dbt package "
|
|
44
|
+
"from a subdirectory: your version is {}, >= 2.25.0 needed".format(git_version)
|
|
45
|
+
)
|
|
46
|
+
clone_cmd.extend(["--filter=blob:none", "--sparse"])
|
|
47
|
+
|
|
48
|
+
if has_revision and not is_commit:
|
|
49
|
+
clone_cmd.extend(["--branch", revision])
|
|
50
|
+
|
|
51
|
+
clone_cmd.append(repo)
|
|
52
|
+
|
|
53
|
+
if dirname is not None:
|
|
54
|
+
clone_cmd.append(dirname)
|
|
55
|
+
try:
|
|
56
|
+
result = run_cmd(cwd, clone_cmd, env={"LC_ALL": "C"})
|
|
57
|
+
except CommandResultError as exc:
|
|
58
|
+
raise GitCloningError(repo, revision, exc)
|
|
59
|
+
|
|
60
|
+
if subdirectory:
|
|
61
|
+
cwd_subdir = os.path.join(cwd, dirname or "")
|
|
62
|
+
clone_cmd_subdir = ["git", "sparse-checkout", "set", subdirectory]
|
|
63
|
+
try:
|
|
64
|
+
run_cmd(cwd_subdir, clone_cmd_subdir)
|
|
65
|
+
except CommandResultError as exc:
|
|
66
|
+
raise GitCloningError(repo, revision, exc)
|
|
67
|
+
|
|
68
|
+
if remove_git_dir:
|
|
69
|
+
rmdir(os.path.join(dirname, ".git"))
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_tags(cwd):
|
|
75
|
+
out, err = run_cmd(cwd, ["git", "tag", "--list"], env={"LC_ALL": "C"})
|
|
76
|
+
tags = out.decode("utf-8").strip().split("\n")
|
|
77
|
+
return tags
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _checkout(cwd, repo, revision):
|
|
81
|
+
fire_event(GitProgressCheckoutRevision(revision=revision))
|
|
82
|
+
|
|
83
|
+
fetch_cmd = ["git", "fetch", "origin", "--depth", "1"]
|
|
84
|
+
|
|
85
|
+
if _is_commit(revision):
|
|
86
|
+
run_cmd(cwd, fetch_cmd + [revision])
|
|
87
|
+
else:
|
|
88
|
+
run_cmd(cwd, ["git", "remote", "set-branches", "origin", revision])
|
|
89
|
+
run_cmd(cwd, fetch_cmd + ["--tags", revision])
|
|
90
|
+
|
|
91
|
+
if _is_commit(revision):
|
|
92
|
+
spec = revision
|
|
93
|
+
# Prefer tags to branches if one exists
|
|
94
|
+
elif revision in list_tags(cwd):
|
|
95
|
+
spec = "tags/{}".format(revision)
|
|
96
|
+
else:
|
|
97
|
+
spec = "origin/{}".format(revision)
|
|
98
|
+
|
|
99
|
+
out, err = run_cmd(cwd, ["git", "reset", "--hard", spec], env={"LC_ALL": "C"})
|
|
100
|
+
return out, err
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def checkout(cwd, repo, revision=None):
|
|
104
|
+
if revision is None:
|
|
105
|
+
revision = "HEAD"
|
|
106
|
+
try:
|
|
107
|
+
return _checkout(cwd, repo, revision)
|
|
108
|
+
except CommandResultError as exc:
|
|
109
|
+
raise GitCheckoutError(repo=repo, revision=revision, error=exc)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_current_sha(cwd):
|
|
113
|
+
out, err = run_cmd(cwd, ["git", "rev-parse", "HEAD"], env={"LC_ALL": "C"})
|
|
114
|
+
|
|
115
|
+
return out.decode("utf-8").strip()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def remove_remote(cwd):
|
|
119
|
+
return run_cmd(cwd, ["git", "remote", "rm", "origin"], env={"LC_ALL": "C"})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clone_and_checkout(
|
|
123
|
+
repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None
|
|
124
|
+
):
|
|
125
|
+
exists = None
|
|
126
|
+
try:
|
|
127
|
+
_, err = clone(
|
|
128
|
+
repo,
|
|
129
|
+
cwd,
|
|
130
|
+
dirname=dirname,
|
|
131
|
+
remove_git_dir=remove_git_dir,
|
|
132
|
+
subdirectory=subdirectory,
|
|
133
|
+
)
|
|
134
|
+
except CommandResultError as exc:
|
|
135
|
+
err = exc.stderr
|
|
136
|
+
exists = re.match("fatal: destination path '(.+)' already exists", err)
|
|
137
|
+
if not exists:
|
|
138
|
+
raise UnknownGitCloningProblemError(repo)
|
|
139
|
+
|
|
140
|
+
directory = None
|
|
141
|
+
start_sha = None
|
|
142
|
+
if exists:
|
|
143
|
+
directory = exists.group(1)
|
|
144
|
+
fire_event(GitProgressUpdatingExistingDependency(dir=directory))
|
|
145
|
+
else:
|
|
146
|
+
matches = re.match("Cloning into '(.+)'", err.decode("utf-8"))
|
|
147
|
+
if matches is None:
|
|
148
|
+
raise DbtRuntimeError(f'Error cloning {repo} - never saw "Cloning into ..." from git')
|
|
149
|
+
directory = matches.group(1)
|
|
150
|
+
fire_event(GitProgressPullingNewDependency(dir=directory))
|
|
151
|
+
full_path = os.path.join(cwd, directory)
|
|
152
|
+
start_sha = get_current_sha(full_path)
|
|
153
|
+
checkout(full_path, repo, revision)
|
|
154
|
+
end_sha = get_current_sha(full_path)
|
|
155
|
+
if exists:
|
|
156
|
+
if start_sha == end_sha:
|
|
157
|
+
fire_event(GitNothingToDo(sha=start_sha[:7]))
|
|
158
|
+
else:
|
|
159
|
+
fire_event(
|
|
160
|
+
GitProgressUpdatedCheckoutRange(start_sha=start_sha[:7], end_sha=end_sha[:7])
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
fire_event(GitProgressCheckedOutAt(end_sha=end_sha[:7]))
|
|
164
|
+
return os.path.join(directory, subdirectory or "")
|
dvt/clients/jinja.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import threading
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import jinja2
|
|
7
|
+
import jinja2.ext
|
|
8
|
+
import jinja2.nativetypes # type: ignore
|
|
9
|
+
import jinja2.nodes
|
|
10
|
+
import jinja2.parser
|
|
11
|
+
import jinja2.sandbox
|
|
12
|
+
from dvt.contracts.graph.nodes import GenericTestNode
|
|
13
|
+
from dvt.exceptions import (
|
|
14
|
+
DbtInternalError,
|
|
15
|
+
MaterializtionMacroNotUsedError,
|
|
16
|
+
NoSupportedLanguagesFoundError,
|
|
17
|
+
)
|
|
18
|
+
from dvt.node_types import ModelLanguage
|
|
19
|
+
|
|
20
|
+
from dbt_common.clients.jinja import (
|
|
21
|
+
CallableMacroGenerator,
|
|
22
|
+
MacroProtocol,
|
|
23
|
+
get_template,
|
|
24
|
+
render_template,
|
|
25
|
+
)
|
|
26
|
+
from dbt_common.utils import deep_map_render
|
|
27
|
+
|
|
28
|
+
SUPPORTED_LANG_ARG = jinja2.nodes.Name("supported_languages", "param")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MacroStack(threading.local):
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.call_stack = []
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def depth(self) -> int:
|
|
38
|
+
return len(self.call_stack)
|
|
39
|
+
|
|
40
|
+
def push(self, name):
|
|
41
|
+
self.call_stack.append(name)
|
|
42
|
+
|
|
43
|
+
def pop(self, name):
|
|
44
|
+
got = self.call_stack.pop()
|
|
45
|
+
if got != name:
|
|
46
|
+
raise DbtInternalError(f"popped {got}, expected {name}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MacroGenerator(CallableMacroGenerator):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
macro: MacroProtocol,
|
|
53
|
+
context: Optional[Dict[str, Any]] = None,
|
|
54
|
+
node: Optional[Any] = None,
|
|
55
|
+
stack: Optional[MacroStack] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__(macro, context)
|
|
58
|
+
self.node = node
|
|
59
|
+
self.stack = stack
|
|
60
|
+
|
|
61
|
+
# This adds the macro's unique id to the node's 'depends_on'
|
|
62
|
+
@contextmanager
|
|
63
|
+
def track_call(self):
|
|
64
|
+
# This is only called from __call__
|
|
65
|
+
if self.stack is None:
|
|
66
|
+
yield
|
|
67
|
+
else:
|
|
68
|
+
unique_id = self.macro.unique_id
|
|
69
|
+
depth = self.stack.depth
|
|
70
|
+
# only mark depth=0 as a dependency, when creating this dependency we don't pass in stack
|
|
71
|
+
if depth == 0 and self.node:
|
|
72
|
+
self.node.depends_on.add_macro(unique_id)
|
|
73
|
+
self.stack.push(unique_id)
|
|
74
|
+
try:
|
|
75
|
+
yield
|
|
76
|
+
finally:
|
|
77
|
+
self.stack.pop(unique_id)
|
|
78
|
+
|
|
79
|
+
# this makes MacroGenerator objects callable like functions
|
|
80
|
+
def __call__(self, *args, **kwargs):
|
|
81
|
+
with self.track_call():
|
|
82
|
+
return self.call_macro(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class UnitTestMacroGenerator(MacroGenerator):
|
|
86
|
+
# this makes UnitTestMacroGenerator objects callable like functions
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
macro_generator: MacroGenerator,
|
|
90
|
+
call_return_value: Any,
|
|
91
|
+
) -> None:
|
|
92
|
+
super().__init__(
|
|
93
|
+
macro_generator.macro,
|
|
94
|
+
macro_generator.context,
|
|
95
|
+
macro_generator.node,
|
|
96
|
+
macro_generator.stack,
|
|
97
|
+
)
|
|
98
|
+
self.call_return_value = call_return_value
|
|
99
|
+
|
|
100
|
+
def __call__(self, *args, **kwargs):
|
|
101
|
+
with self.track_call():
|
|
102
|
+
return self.call_return_value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# performance note: Local benmcharking (so take it with a big grain of salt!)
|
|
106
|
+
# on this indicates that it is is on average slightly slower than
|
|
107
|
+
# checking two separate patterns, but the standard deviation is smaller with
|
|
108
|
+
# one pattern. The time difference between the two was ~2 std deviations, which
|
|
109
|
+
# is small enough that I've just chosen the more readable option.
|
|
110
|
+
_HAS_RENDER_CHARS_PAT = re.compile(r"({[{%#]|[#}%]})")
|
|
111
|
+
|
|
112
|
+
_render_cache: Dict[str, Any] = dict()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_rendered(
|
|
116
|
+
string: str,
|
|
117
|
+
ctx: Dict[str, Any],
|
|
118
|
+
node=None,
|
|
119
|
+
capture_macros: bool = False,
|
|
120
|
+
native: bool = False,
|
|
121
|
+
) -> Any:
|
|
122
|
+
# performance optimization: if there are no jinja control characters in the
|
|
123
|
+
# string, we can just return the input. Fall back to jinja if the type is
|
|
124
|
+
# not a string or if native rendering is enabled (so '1' -> 1, etc...)
|
|
125
|
+
# If this is desirable in the native env as well, we could handle the
|
|
126
|
+
# native=True case by passing the input string to ast.literal_eval, like
|
|
127
|
+
# the native renderer does.
|
|
128
|
+
has_render_chars = not isinstance(string, str) or _HAS_RENDER_CHARS_PAT.search(string)
|
|
129
|
+
|
|
130
|
+
if not has_render_chars:
|
|
131
|
+
if not native:
|
|
132
|
+
return string
|
|
133
|
+
elif string in _render_cache:
|
|
134
|
+
return _render_cache[string]
|
|
135
|
+
|
|
136
|
+
template = get_template(
|
|
137
|
+
string,
|
|
138
|
+
ctx,
|
|
139
|
+
node,
|
|
140
|
+
capture_macros=capture_macros,
|
|
141
|
+
native=native,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
rendered = render_template(template, ctx, node)
|
|
145
|
+
|
|
146
|
+
if not has_render_chars and native:
|
|
147
|
+
_render_cache[string] = rendered
|
|
148
|
+
|
|
149
|
+
return rendered
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def undefined_error(msg) -> NoReturn:
|
|
153
|
+
raise jinja2.exceptions.UndefinedError(msg)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
GENERIC_TEST_KWARGS_NAME = "_dbt_generic_test_kwargs"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def add_rendered_test_kwargs(
|
|
160
|
+
context: Dict[str, Any],
|
|
161
|
+
node: GenericTestNode,
|
|
162
|
+
capture_macros: bool = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Render each of the test kwargs in the given context using the native
|
|
165
|
+
renderer, then insert that value into the given context as the special test
|
|
166
|
+
keyword arguments member.
|
|
167
|
+
"""
|
|
168
|
+
looks_like_func = r"^\s*(env_var|ref|var|source|doc)\s*\(.+\)\s*$"
|
|
169
|
+
|
|
170
|
+
def _convert_function(value: Any, keypath: Tuple[Union[str, int], ...]) -> Any:
|
|
171
|
+
if isinstance(value, str):
|
|
172
|
+
if keypath == ("column_name",):
|
|
173
|
+
# special case: Don't render column names as native, make them
|
|
174
|
+
# be strings
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
if re.match(looks_like_func, value) is not None:
|
|
178
|
+
# curly braces to make rendering happy
|
|
179
|
+
value = f"{{{{ {value} }}}}"
|
|
180
|
+
|
|
181
|
+
value = get_rendered(value, context, node, capture_macros=capture_macros, native=True)
|
|
182
|
+
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
# The test_metadata.kwargs come from the test builder, and were set
|
|
186
|
+
# when the test node was created in _parse_generic_test.
|
|
187
|
+
kwargs = deep_map_render(_convert_function, node.test_metadata.kwargs)
|
|
188
|
+
context[GENERIC_TEST_KWARGS_NAME] = kwargs
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_supported_languages(node: jinja2.nodes.Macro) -> List[ModelLanguage]:
|
|
192
|
+
if "materialization" not in node.name:
|
|
193
|
+
raise MaterializtionMacroNotUsedError(node=node)
|
|
194
|
+
|
|
195
|
+
no_kwargs = not node.defaults
|
|
196
|
+
no_langs_found = SUPPORTED_LANG_ARG not in node.args
|
|
197
|
+
|
|
198
|
+
if no_kwargs or no_langs_found:
|
|
199
|
+
raise NoSupportedLanguagesFoundError(node=node)
|
|
200
|
+
|
|
201
|
+
lang_idx = node.args.index(SUPPORTED_LANG_ARG)
|
|
202
|
+
# indexing defaults from the end
|
|
203
|
+
# since supported_languages is a kwarg, and kwargs are at always after args
|
|
204
|
+
return [
|
|
205
|
+
ModelLanguage[item.value] for item in node.defaults[-(len(node.args) - lang_idx)].items
|
|
206
|
+
]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from typing import Any, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
import jinja2
|
|
5
|
+
from dvt.artifacts.resources import RefArgs
|
|
6
|
+
from dvt.exceptions import MacroNamespaceNotStringError, ParsingError
|
|
7
|
+
|
|
8
|
+
from dbt_common.clients.jinja import get_environment
|
|
9
|
+
from dbt_common.exceptions.macros import MacroNameNotStringError
|
|
10
|
+
from dbt_common.tests import test_caching_enabled
|
|
11
|
+
from dbt_extractor import ExtractionError, py_extract_from_source # type: ignore
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from dvt.context.providers import ParseDatabaseWrapper
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_TESTING_MACRO_CACHE: Dict[str, Any] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def statically_extract_has_name_this(source: str) -> bool:
|
|
21
|
+
"""Checks whether the raw jinja has any references to `this`"""
|
|
22
|
+
env = get_environment(None, capture_macros=True)
|
|
23
|
+
parsed = env.parse(source)
|
|
24
|
+
names = tuple(parsed.find_all(jinja2.nodes.Name))
|
|
25
|
+
|
|
26
|
+
for name in names:
|
|
27
|
+
if hasattr(name, "name") and name.name == "this":
|
|
28
|
+
return True
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def statically_extract_macro_calls(
|
|
33
|
+
source: str, ctx: Dict[str, Any], db_wrapper: Optional["ParseDatabaseWrapper"] = None
|
|
34
|
+
) -> List[str]:
|
|
35
|
+
# set 'capture_macros' to capture undefined
|
|
36
|
+
env = get_environment(None, capture_macros=True)
|
|
37
|
+
|
|
38
|
+
global _TESTING_MACRO_CACHE
|
|
39
|
+
if test_caching_enabled() and source in _TESTING_MACRO_CACHE:
|
|
40
|
+
parsed = _TESTING_MACRO_CACHE.get(source, None)
|
|
41
|
+
func_calls = getattr(parsed, "_dbt_cached_calls")
|
|
42
|
+
else:
|
|
43
|
+
parsed = env.parse(source)
|
|
44
|
+
func_calls = tuple(parsed.find_all(jinja2.nodes.Call))
|
|
45
|
+
|
|
46
|
+
if test_caching_enabled():
|
|
47
|
+
_TESTING_MACRO_CACHE[source] = parsed
|
|
48
|
+
setattr(parsed, "_dbt_cached_calls", func_calls)
|
|
49
|
+
|
|
50
|
+
standard_calls = ["source", "ref", "config"]
|
|
51
|
+
possible_macro_calls = []
|
|
52
|
+
for func_call in func_calls:
|
|
53
|
+
func_name = None
|
|
54
|
+
if hasattr(func_call, "node") and hasattr(func_call.node, "name"):
|
|
55
|
+
func_name = func_call.node.name
|
|
56
|
+
else:
|
|
57
|
+
if (
|
|
58
|
+
hasattr(func_call, "node")
|
|
59
|
+
and hasattr(func_call.node, "node")
|
|
60
|
+
and type(func_call.node.node).__name__ == "Name"
|
|
61
|
+
and hasattr(func_call.node, "attr")
|
|
62
|
+
):
|
|
63
|
+
package_name = func_call.node.node.name
|
|
64
|
+
macro_name = func_call.node.attr
|
|
65
|
+
if package_name == "adapter":
|
|
66
|
+
if macro_name == "dispatch":
|
|
67
|
+
ad_macro_calls = statically_parse_adapter_dispatch(
|
|
68
|
+
func_call, ctx, db_wrapper
|
|
69
|
+
)
|
|
70
|
+
possible_macro_calls.extend(ad_macro_calls)
|
|
71
|
+
else:
|
|
72
|
+
# This skips calls such as adapter.parse_index
|
|
73
|
+
continue
|
|
74
|
+
else:
|
|
75
|
+
func_name = f"{package_name}.{macro_name}"
|
|
76
|
+
else:
|
|
77
|
+
continue
|
|
78
|
+
if not func_name:
|
|
79
|
+
continue
|
|
80
|
+
if func_name in standard_calls:
|
|
81
|
+
continue
|
|
82
|
+
elif ctx.get(func_name):
|
|
83
|
+
continue
|
|
84
|
+
else:
|
|
85
|
+
if func_name not in possible_macro_calls:
|
|
86
|
+
possible_macro_calls.append(func_name)
|
|
87
|
+
|
|
88
|
+
return possible_macro_calls
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def statically_parse_adapter_dispatch(
|
|
92
|
+
func_call, ctx: Dict[str, Any], db_wrapper: Optional["ParseDatabaseWrapper"]
|
|
93
|
+
) -> List[str]:
|
|
94
|
+
possible_macro_calls = []
|
|
95
|
+
# This captures an adapter.dispatch('<macro_name>') call.
|
|
96
|
+
|
|
97
|
+
func_name = None
|
|
98
|
+
# macro_name positional argument
|
|
99
|
+
if len(func_call.args) > 0:
|
|
100
|
+
func_name = func_call.args[0].value
|
|
101
|
+
if func_name:
|
|
102
|
+
possible_macro_calls.append(func_name)
|
|
103
|
+
|
|
104
|
+
# packages positional argument
|
|
105
|
+
macro_namespace = None
|
|
106
|
+
packages_arg = None
|
|
107
|
+
packages_arg_type = None
|
|
108
|
+
|
|
109
|
+
if len(func_call.args) > 1:
|
|
110
|
+
packages_arg = func_call.args[1]
|
|
111
|
+
# This can be a List or a Call
|
|
112
|
+
packages_arg_type = type(func_call.args[1]).__name__
|
|
113
|
+
|
|
114
|
+
# keyword arguments
|
|
115
|
+
if func_call.kwargs:
|
|
116
|
+
for kwarg in func_call.kwargs:
|
|
117
|
+
if kwarg.key == "macro_name":
|
|
118
|
+
# This will remain to enable static resolution
|
|
119
|
+
if type(kwarg.value).__name__ == "Const":
|
|
120
|
+
func_name = kwarg.value.value
|
|
121
|
+
possible_macro_calls.append(func_name)
|
|
122
|
+
else:
|
|
123
|
+
raise MacroNameNotStringError(kwarg_value=kwarg.value.value)
|
|
124
|
+
elif kwarg.key == "macro_namespace":
|
|
125
|
+
# This will remain to enable static resolution
|
|
126
|
+
kwarg_type = type(kwarg.value).__name__
|
|
127
|
+
if kwarg_type == "Const":
|
|
128
|
+
macro_namespace = kwarg.value.value
|
|
129
|
+
else:
|
|
130
|
+
raise MacroNamespaceNotStringError(kwarg_type)
|
|
131
|
+
|
|
132
|
+
# positional arguments
|
|
133
|
+
if packages_arg:
|
|
134
|
+
if packages_arg_type == "List":
|
|
135
|
+
# This will remain to enable static resolution
|
|
136
|
+
packages = []
|
|
137
|
+
for item in packages_arg.items:
|
|
138
|
+
packages.append(item.value)
|
|
139
|
+
elif packages_arg_type == "Const":
|
|
140
|
+
# This will remain to enable static resolution
|
|
141
|
+
macro_namespace = packages_arg.value
|
|
142
|
+
|
|
143
|
+
if db_wrapper:
|
|
144
|
+
macro = db_wrapper.dispatch(func_name, macro_namespace=macro_namespace).macro
|
|
145
|
+
func_name = f"{macro.package_name}.{macro.name}" # type: ignore[attr-defined]
|
|
146
|
+
possible_macro_calls.append(func_name)
|
|
147
|
+
else: # this is only for tests/unit/test_macro_calls.py
|
|
148
|
+
if macro_namespace:
|
|
149
|
+
packages = [macro_namespace]
|
|
150
|
+
else:
|
|
151
|
+
packages = []
|
|
152
|
+
for package_name in packages:
|
|
153
|
+
possible_macro_calls.append(f"{package_name}.{func_name}")
|
|
154
|
+
|
|
155
|
+
return possible_macro_calls
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def statically_parse_ref_or_source(expression: str) -> Union[RefArgs, List[str]]:
|
|
159
|
+
"""
|
|
160
|
+
Returns a RefArgs or List[str] object, corresponding to ref or source respectively, given an input jinja expression.
|
|
161
|
+
|
|
162
|
+
input: str representing how input node is referenced in tested model sql
|
|
163
|
+
* examples:
|
|
164
|
+
- "ref('my_model_a')"
|
|
165
|
+
- "ref('my_model_a', version=3)"
|
|
166
|
+
- "ref('package', 'my_model_a', version=3)"
|
|
167
|
+
- "source('my_source_schema', 'my_source_name')"
|
|
168
|
+
|
|
169
|
+
If input is not a well-formed jinja ref or source expression, a ParsingError is raised.
|
|
170
|
+
"""
|
|
171
|
+
ref_or_source: Union[RefArgs, List[str]]
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
statically_parsed = py_extract_from_source(f"{{{{ {expression} }}}}")
|
|
175
|
+
except ExtractionError:
|
|
176
|
+
raise ParsingError(f"Invalid jinja expression: {expression}")
|
|
177
|
+
|
|
178
|
+
if statically_parsed.get("refs"):
|
|
179
|
+
raw_ref = list(statically_parsed["refs"])[0]
|
|
180
|
+
ref_or_source = RefArgs(
|
|
181
|
+
package=raw_ref.get("package"),
|
|
182
|
+
name=raw_ref.get("name"),
|
|
183
|
+
version=raw_ref.get("version"),
|
|
184
|
+
)
|
|
185
|
+
elif statically_parsed.get("sources"):
|
|
186
|
+
source_name, source_table_name = list(statically_parsed["sources"])[0]
|
|
187
|
+
ref_or_source = [source_name, source_table_name]
|
|
188
|
+
else:
|
|
189
|
+
raise ParsingError(f"Invalid ref or source expression: {expression}")
|
|
190
|
+
|
|
191
|
+
return ref_or_source
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def statically_parse_unrendered_config(string: str) -> Optional[Dict[str, Any]]:
|
|
195
|
+
"""
|
|
196
|
+
Given a string with jinja, extract an unrendered config call.
|
|
197
|
+
If no config call is present, returns None.
|
|
198
|
+
|
|
199
|
+
For example, given:
|
|
200
|
+
"{{ config(materialized=env_var('DBT_TEST_STATE_MODIFIED')) }}\nselect 1 as id"
|
|
201
|
+
returns: {'materialized': "Keyword(key='materialized', value=Call(node=Name(name='env_var', ctx='load'), args=[Const(value='DBT_TEST_STATE_MODIFIED')], kwargs=[], dyn_args=None, dyn_kwargs=None))"}
|
|
202
|
+
|
|
203
|
+
No config call:
|
|
204
|
+
"select 1 as id"
|
|
205
|
+
returns: None
|
|
206
|
+
"""
|
|
207
|
+
# Return early to avoid creating jinja environemt if no config call in input string
|
|
208
|
+
if "config(" not in string:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
# set 'capture_macros' to capture undefined
|
|
212
|
+
env = get_environment(None, capture_macros=True)
|
|
213
|
+
|
|
214
|
+
global _TESTING_MACRO_CACHE
|
|
215
|
+
if test_caching_enabled() and _TESTING_MACRO_CACHE and string in _TESTING_MACRO_CACHE:
|
|
216
|
+
parsed = _TESTING_MACRO_CACHE.get(string, None)
|
|
217
|
+
func_calls = getattr(parsed, "_dbt_cached_calls")
|
|
218
|
+
else:
|
|
219
|
+
parsed = env.parse(string)
|
|
220
|
+
func_calls = tuple(parsed.find_all(jinja2.nodes.Call))
|
|
221
|
+
|
|
222
|
+
config_func_calls = list(
|
|
223
|
+
filter(
|
|
224
|
+
lambda f: hasattr(f, "node") and hasattr(f.node, "name") and f.node.name == "config",
|
|
225
|
+
func_calls,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
# There should only be one {{ config(...) }} call per input
|
|
229
|
+
config_func_call = config_func_calls[0] if config_func_calls else None
|
|
230
|
+
|
|
231
|
+
if not config_func_call:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
unrendered_config = {}
|
|
235
|
+
for kwarg in config_func_call.kwargs:
|
|
236
|
+
unrendered_config[kwarg.key] = construct_static_kwarg_value(kwarg)
|
|
237
|
+
|
|
238
|
+
return unrendered_config
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def construct_static_kwarg_value(kwarg) -> str:
|
|
242
|
+
# Instead of trying to re-assemble complex kwarg value, simply stringify the value.
|
|
243
|
+
# This is still useful to be able to detect changes in unrendered configs, even if it is
|
|
244
|
+
# not an exact representation of the user input.
|
|
245
|
+
return str(kwarg)
|