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.

Files changed (261) hide show
  1. dvt/__init__.py +7 -0
  2. dvt/_pydantic_shim.py +26 -0
  3. dvt/adapters/__init__.py +16 -0
  4. dvt/adapters/multi_adapter_manager.py +268 -0
  5. dvt/artifacts/__init__.py +0 -0
  6. dvt/artifacts/exceptions/__init__.py +1 -0
  7. dvt/artifacts/exceptions/schemas.py +31 -0
  8. dvt/artifacts/resources/__init__.py +116 -0
  9. dvt/artifacts/resources/base.py +68 -0
  10. dvt/artifacts/resources/types.py +93 -0
  11. dvt/artifacts/resources/v1/analysis.py +10 -0
  12. dvt/artifacts/resources/v1/catalog.py +23 -0
  13. dvt/artifacts/resources/v1/components.py +275 -0
  14. dvt/artifacts/resources/v1/config.py +282 -0
  15. dvt/artifacts/resources/v1/documentation.py +11 -0
  16. dvt/artifacts/resources/v1/exposure.py +52 -0
  17. dvt/artifacts/resources/v1/function.py +53 -0
  18. dvt/artifacts/resources/v1/generic_test.py +32 -0
  19. dvt/artifacts/resources/v1/group.py +22 -0
  20. dvt/artifacts/resources/v1/hook.py +11 -0
  21. dvt/artifacts/resources/v1/macro.py +30 -0
  22. dvt/artifacts/resources/v1/metric.py +173 -0
  23. dvt/artifacts/resources/v1/model.py +146 -0
  24. dvt/artifacts/resources/v1/owner.py +10 -0
  25. dvt/artifacts/resources/v1/saved_query.py +112 -0
  26. dvt/artifacts/resources/v1/seed.py +42 -0
  27. dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  28. dvt/artifacts/resources/v1/semantic_model.py +315 -0
  29. dvt/artifacts/resources/v1/singular_test.py +14 -0
  30. dvt/artifacts/resources/v1/snapshot.py +92 -0
  31. dvt/artifacts/resources/v1/source_definition.py +85 -0
  32. dvt/artifacts/resources/v1/sql_operation.py +10 -0
  33. dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
  34. dvt/artifacts/schemas/__init__.py +0 -0
  35. dvt/artifacts/schemas/base.py +191 -0
  36. dvt/artifacts/schemas/batch_results.py +24 -0
  37. dvt/artifacts/schemas/catalog/__init__.py +12 -0
  38. dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  39. dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
  40. dvt/artifacts/schemas/freshness/__init__.py +1 -0
  41. dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  42. dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
  43. dvt/artifacts/schemas/manifest/__init__.py +2 -0
  44. dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  45. dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
  46. dvt/artifacts/schemas/results.py +148 -0
  47. dvt/artifacts/schemas/run/__init__.py +2 -0
  48. dvt/artifacts/schemas/run/v5/__init__.py +0 -0
  49. dvt/artifacts/schemas/run/v5/run.py +184 -0
  50. dvt/artifacts/schemas/upgrades/__init__.py +4 -0
  51. dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  52. dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  53. dvt/artifacts/utils/validation.py +153 -0
  54. dvt/cli/__init__.py +1 -0
  55. dvt/cli/context.py +16 -0
  56. dvt/cli/exceptions.py +56 -0
  57. dvt/cli/flags.py +558 -0
  58. dvt/cli/main.py +971 -0
  59. dvt/cli/option_types.py +121 -0
  60. dvt/cli/options.py +79 -0
  61. dvt/cli/params.py +803 -0
  62. dvt/cli/requires.py +478 -0
  63. dvt/cli/resolvers.py +32 -0
  64. dvt/cli/types.py +40 -0
  65. dvt/clients/__init__.py +0 -0
  66. dvt/clients/checked_load.py +82 -0
  67. dvt/clients/git.py +164 -0
  68. dvt/clients/jinja.py +206 -0
  69. dvt/clients/jinja_static.py +245 -0
  70. dvt/clients/registry.py +192 -0
  71. dvt/clients/yaml_helper.py +68 -0
  72. dvt/compilation.py +833 -0
  73. dvt/compute/__init__.py +26 -0
  74. dvt/compute/base.py +288 -0
  75. dvt/compute/engines/__init__.py +13 -0
  76. dvt/compute/engines/duckdb_engine.py +368 -0
  77. dvt/compute/engines/spark_engine.py +273 -0
  78. dvt/compute/query_analyzer.py +212 -0
  79. dvt/compute/router.py +483 -0
  80. dvt/config/__init__.py +4 -0
  81. dvt/config/catalogs.py +95 -0
  82. dvt/config/compute_config.py +406 -0
  83. dvt/config/profile.py +411 -0
  84. dvt/config/profiles_v2.py +464 -0
  85. dvt/config/project.py +893 -0
  86. dvt/config/renderer.py +232 -0
  87. dvt/config/runtime.py +491 -0
  88. dvt/config/selectors.py +209 -0
  89. dvt/config/utils.py +78 -0
  90. dvt/connectors/.gitignore +6 -0
  91. dvt/connectors/README.md +306 -0
  92. dvt/connectors/catalog.yml +217 -0
  93. dvt/connectors/download_connectors.py +300 -0
  94. dvt/constants.py +29 -0
  95. dvt/context/__init__.py +0 -0
  96. dvt/context/base.py +746 -0
  97. dvt/context/configured.py +136 -0
  98. dvt/context/context_config.py +350 -0
  99. dvt/context/docs.py +82 -0
  100. dvt/context/exceptions_jinja.py +179 -0
  101. dvt/context/macro_resolver.py +195 -0
  102. dvt/context/macros.py +171 -0
  103. dvt/context/manifest.py +73 -0
  104. dvt/context/providers.py +2198 -0
  105. dvt/context/query_header.py +14 -0
  106. dvt/context/secret.py +59 -0
  107. dvt/context/target.py +74 -0
  108. dvt/contracts/__init__.py +0 -0
  109. dvt/contracts/files.py +413 -0
  110. dvt/contracts/graph/__init__.py +0 -0
  111. dvt/contracts/graph/manifest.py +1904 -0
  112. dvt/contracts/graph/metrics.py +98 -0
  113. dvt/contracts/graph/model_config.py +71 -0
  114. dvt/contracts/graph/node_args.py +42 -0
  115. dvt/contracts/graph/nodes.py +1806 -0
  116. dvt/contracts/graph/semantic_manifest.py +233 -0
  117. dvt/contracts/graph/unparsed.py +812 -0
  118. dvt/contracts/project.py +417 -0
  119. dvt/contracts/results.py +53 -0
  120. dvt/contracts/selection.py +23 -0
  121. dvt/contracts/sql.py +86 -0
  122. dvt/contracts/state.py +69 -0
  123. dvt/contracts/util.py +46 -0
  124. dvt/deprecations.py +347 -0
  125. dvt/deps/__init__.py +0 -0
  126. dvt/deps/base.py +153 -0
  127. dvt/deps/git.py +196 -0
  128. dvt/deps/local.py +80 -0
  129. dvt/deps/registry.py +131 -0
  130. dvt/deps/resolver.py +149 -0
  131. dvt/deps/tarball.py +121 -0
  132. dvt/docs/source/_ext/dbt_click.py +118 -0
  133. dvt/docs/source/conf.py +32 -0
  134. dvt/env_vars.py +64 -0
  135. dvt/event_time/event_time.py +40 -0
  136. dvt/event_time/sample_window.py +60 -0
  137. dvt/events/__init__.py +16 -0
  138. dvt/events/base_types.py +37 -0
  139. dvt/events/core_types_pb2.py +2 -0
  140. dvt/events/logging.py +109 -0
  141. dvt/events/types.py +2534 -0
  142. dvt/exceptions.py +1487 -0
  143. dvt/flags.py +89 -0
  144. dvt/graph/__init__.py +11 -0
  145. dvt/graph/cli.py +248 -0
  146. dvt/graph/graph.py +172 -0
  147. dvt/graph/queue.py +213 -0
  148. dvt/graph/selector.py +375 -0
  149. dvt/graph/selector_methods.py +976 -0
  150. dvt/graph/selector_spec.py +223 -0
  151. dvt/graph/thread_pool.py +18 -0
  152. dvt/hooks.py +21 -0
  153. dvt/include/README.md +49 -0
  154. dvt/include/__init__.py +3 -0
  155. dvt/include/global_project.py +4 -0
  156. dvt/include/starter_project/.gitignore +4 -0
  157. dvt/include/starter_project/README.md +15 -0
  158. dvt/include/starter_project/__init__.py +3 -0
  159. dvt/include/starter_project/analyses/.gitkeep +0 -0
  160. dvt/include/starter_project/dvt_project.yml +36 -0
  161. dvt/include/starter_project/macros/.gitkeep +0 -0
  162. dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
  163. dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
  164. dvt/include/starter_project/models/example/schema.yml +21 -0
  165. dvt/include/starter_project/seeds/.gitkeep +0 -0
  166. dvt/include/starter_project/snapshots/.gitkeep +0 -0
  167. dvt/include/starter_project/tests/.gitkeep +0 -0
  168. dvt/internal_deprecations.py +27 -0
  169. dvt/jsonschemas/__init__.py +3 -0
  170. dvt/jsonschemas/jsonschemas.py +309 -0
  171. dvt/jsonschemas/project/0.0.110.json +4717 -0
  172. dvt/jsonschemas/project/0.0.85.json +2015 -0
  173. dvt/jsonschemas/resources/0.0.110.json +2636 -0
  174. dvt/jsonschemas/resources/0.0.85.json +2536 -0
  175. dvt/jsonschemas/resources/latest.json +6773 -0
  176. dvt/links.py +4 -0
  177. dvt/materializations/__init__.py +0 -0
  178. dvt/materializations/incremental/__init__.py +0 -0
  179. dvt/materializations/incremental/microbatch.py +235 -0
  180. dvt/mp_context.py +8 -0
  181. dvt/node_types.py +37 -0
  182. dvt/parser/__init__.py +23 -0
  183. dvt/parser/analysis.py +21 -0
  184. dvt/parser/base.py +549 -0
  185. dvt/parser/common.py +267 -0
  186. dvt/parser/docs.py +52 -0
  187. dvt/parser/fixtures.py +51 -0
  188. dvt/parser/functions.py +30 -0
  189. dvt/parser/generic_test.py +100 -0
  190. dvt/parser/generic_test_builders.py +334 -0
  191. dvt/parser/hooks.py +119 -0
  192. dvt/parser/macros.py +137 -0
  193. dvt/parser/manifest.py +2204 -0
  194. dvt/parser/models.py +574 -0
  195. dvt/parser/partial.py +1179 -0
  196. dvt/parser/read_files.py +445 -0
  197. dvt/parser/schema_generic_tests.py +423 -0
  198. dvt/parser/schema_renderer.py +111 -0
  199. dvt/parser/schema_yaml_readers.py +936 -0
  200. dvt/parser/schemas.py +1467 -0
  201. dvt/parser/search.py +149 -0
  202. dvt/parser/seeds.py +28 -0
  203. dvt/parser/singular_test.py +20 -0
  204. dvt/parser/snapshots.py +44 -0
  205. dvt/parser/sources.py +557 -0
  206. dvt/parser/sql.py +63 -0
  207. dvt/parser/unit_tests.py +622 -0
  208. dvt/plugins/__init__.py +20 -0
  209. dvt/plugins/contracts.py +10 -0
  210. dvt/plugins/exceptions.py +2 -0
  211. dvt/plugins/manager.py +164 -0
  212. dvt/plugins/manifest.py +21 -0
  213. dvt/profiler.py +20 -0
  214. dvt/py.typed +1 -0
  215. dvt/runners/__init__.py +2 -0
  216. dvt/runners/exposure_runner.py +7 -0
  217. dvt/runners/no_op_runner.py +46 -0
  218. dvt/runners/saved_query_runner.py +7 -0
  219. dvt/selected_resources.py +8 -0
  220. dvt/task/__init__.py +0 -0
  221. dvt/task/base.py +504 -0
  222. dvt/task/build.py +197 -0
  223. dvt/task/clean.py +57 -0
  224. dvt/task/clone.py +162 -0
  225. dvt/task/compile.py +151 -0
  226. dvt/task/compute.py +366 -0
  227. dvt/task/debug.py +650 -0
  228. dvt/task/deps.py +280 -0
  229. dvt/task/docs/__init__.py +3 -0
  230. dvt/task/docs/generate.py +408 -0
  231. dvt/task/docs/index.html +250 -0
  232. dvt/task/docs/serve.py +28 -0
  233. dvt/task/freshness.py +323 -0
  234. dvt/task/function.py +122 -0
  235. dvt/task/group_lookup.py +46 -0
  236. dvt/task/init.py +374 -0
  237. dvt/task/list.py +237 -0
  238. dvt/task/printer.py +176 -0
  239. dvt/task/profiles.py +256 -0
  240. dvt/task/retry.py +175 -0
  241. dvt/task/run.py +1146 -0
  242. dvt/task/run_operation.py +142 -0
  243. dvt/task/runnable.py +802 -0
  244. dvt/task/seed.py +104 -0
  245. dvt/task/show.py +150 -0
  246. dvt/task/snapshot.py +57 -0
  247. dvt/task/sql.py +111 -0
  248. dvt/task/test.py +464 -0
  249. dvt/tests/fixtures/__init__.py +1 -0
  250. dvt/tests/fixtures/project.py +620 -0
  251. dvt/tests/util.py +651 -0
  252. dvt/tracking.py +529 -0
  253. dvt/utils/__init__.py +3 -0
  254. dvt/utils/artifact_upload.py +151 -0
  255. dvt/utils/utils.py +408 -0
  256. dvt/version.py +249 -0
  257. dvt_core-1.11.0b4.dist-info/METADATA +252 -0
  258. dvt_core-1.11.0b4.dist-info/RECORD +261 -0
  259. dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
  260. dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
  261. 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)