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
@@ -0,0 +1,334 @@
1
+ import re
2
+ from copy import deepcopy
3
+ from typing import Any, Dict, Generic, List, Optional, Tuple
4
+
5
+ from dvt import deprecations
6
+ from dvt.artifacts.resources import NodeVersion
7
+ from dvt.clients.jinja import GENERIC_TEST_KWARGS_NAME, get_rendered
8
+ from dvt.contracts.graph.nodes import UnpatchedSourceDefinition
9
+ from dvt.contracts.graph.unparsed import UnparsedModelUpdate, UnparsedNodeUpdate
10
+ from dvt.exceptions import (
11
+ CustomMacroPopulatingConfigValueError,
12
+ SameKeyNestedError,
13
+ TagNotStringError,
14
+ TagsNotListOfStringsError,
15
+ TestArgIncludesModelError,
16
+ TestArgsNotDictError,
17
+ TestDefinitionDictLengthError,
18
+ TestNameNotStringError,
19
+ TestTypeError,
20
+ UnexpectedTestNamePatternError,
21
+ )
22
+ from dvt.flags import get_flags
23
+ from dvt.parser.common import Testable
24
+ from dvt.utils import md5
25
+
26
+ from dbt_common.exceptions.macros import UndefinedMacroError
27
+
28
+
29
+ def synthesize_generic_test_names(
30
+ test_type: str, test_name: str, args: Dict[str, Any]
31
+ ) -> Tuple[str, str]:
32
+ # Using the type, name, and arguments to this generic test, synthesize a (hopefully) unique name
33
+ # Will not be unique if multiple tests have same name + arguments, and only configs differ
34
+ # Returns a shorter version (hashed/truncated, for the compiled file)
35
+ # as well as the full name (for the unique_id + FQN)
36
+ flat_args = []
37
+ for arg_name in sorted(args):
38
+ # the model is already embedded in the name, so skip it
39
+ if arg_name == "model":
40
+ continue
41
+ arg_val = args[arg_name]
42
+
43
+ if isinstance(arg_val, dict):
44
+ parts = list(arg_val.values())
45
+ elif isinstance(arg_val, (list, tuple)):
46
+ parts = list(arg_val)
47
+ else:
48
+ parts = [arg_val]
49
+
50
+ flat_args.extend([str(part) for part in parts])
51
+
52
+ clean_flat_args = [re.sub("[^0-9a-zA-Z_]+", "_", arg) for arg in flat_args]
53
+ unique = "__".join(clean_flat_args)
54
+
55
+ # for the file path + alias, the name must be <64 characters
56
+ # if the full name is too long, include the first 30 identifying chars plus
57
+ # a 32-character hash of the full contents
58
+
59
+ test_identifier = "{}_{}".format(test_type, test_name)
60
+ full_name = "{}_{}".format(test_identifier, unique)
61
+
62
+ if len(full_name) >= 64:
63
+ test_trunc_identifier = test_identifier[:30]
64
+ label = md5(full_name)
65
+ short_name = "{}_{}".format(test_trunc_identifier, label)
66
+ else:
67
+ short_name = full_name
68
+
69
+ return short_name, full_name
70
+
71
+
72
+ class TestBuilder(Generic[Testable]):
73
+ """An object to hold assorted test settings and perform basic parsing
74
+
75
+ Test names have the following pattern:
76
+ - the test name itself may be namespaced (package.test)
77
+ - or it may not be namespaced (test)
78
+
79
+ """
80
+
81
+ # The 'test_name' is used to find the 'macro' that implements the test
82
+ TEST_NAME_PATTERN = re.compile(
83
+ r"((?P<test_namespace>([a-zA-Z_][0-9a-zA-Z_]*))\.)?"
84
+ r"(?P<test_name>([a-zA-Z_][0-9a-zA-Z_]*))"
85
+ )
86
+ # args in the test entry representing test configs
87
+ CONFIG_ARGS = (
88
+ "severity",
89
+ "tags",
90
+ "enabled",
91
+ "where",
92
+ "limit",
93
+ "warn_if",
94
+ "error_if",
95
+ "fail_calc",
96
+ "store_failures",
97
+ "store_failures_as",
98
+ "meta",
99
+ "database",
100
+ "schema",
101
+ "alias",
102
+ )
103
+
104
+ def __init__(
105
+ self,
106
+ data_test: Dict[str, Any],
107
+ target: Testable,
108
+ package_name: str,
109
+ render_ctx: Dict[str, Any],
110
+ column_name: Optional[str] = None,
111
+ version: Optional[NodeVersion] = None,
112
+ ) -> None:
113
+ test_name, test_args = self.extract_test_args(
114
+ data_test, target.original_file_path, target.name, column_name, package_name
115
+ )
116
+ self.args: Dict[str, Any] = test_args
117
+ if "model" in self.args:
118
+ raise TestArgIncludesModelError()
119
+ self.package_name: str = package_name
120
+ self.target: Testable = target
121
+ self.version: Optional[NodeVersion] = version
122
+ self.render_ctx: Dict[str, Any] = render_ctx
123
+ self.column_name: Optional[str] = column_name
124
+ self.args["model"] = self.build_model_str()
125
+
126
+ match = self.TEST_NAME_PATTERN.match(test_name)
127
+ if match is None:
128
+ raise UnexpectedTestNamePatternError(test_name)
129
+
130
+ groups = match.groupdict()
131
+ self.name: str = groups["test_name"]
132
+ self.namespace: str = groups["test_namespace"]
133
+ self.config: Dict[str, Any] = {}
134
+ # Process legacy args
135
+ self.config.update(self._process_legacy_args())
136
+
137
+ # Process config args if present
138
+ if "config" in self.args:
139
+ self.config.update(self._render_values(self.args.pop("config", {})))
140
+
141
+ if self.namespace is not None:
142
+ self.package_name = self.namespace
143
+
144
+ # If the user has provided a description for this generic test, use it
145
+ # Then delete the "description" argument to:
146
+ # 1. Avoid passing it into the test macro
147
+ # 2. Avoid passing it into the test name synthesis
148
+ # Otherwise, use an empty string
149
+ self.description: str = ""
150
+
151
+ if "description" in self.args:
152
+ self.description = self.args["description"]
153
+ del self.args["description"]
154
+
155
+ # If the user has provided a custom name for this generic test, use it
156
+ # Then delete the "name" argument to avoid passing it into the test macro
157
+ # Otherwise, use an auto-generated name synthesized from test inputs
158
+ self.compiled_name: str = ""
159
+ self.fqn_name: str = ""
160
+
161
+ if "name" in self.args:
162
+ # Assign the user-defined name here, which will be checked for uniqueness later
163
+ # we will raise an error if two tests have same name for same model + column combo
164
+ self.compiled_name = self.args["name"]
165
+ self.fqn_name = self.args["name"]
166
+ del self.args["name"]
167
+ else:
168
+ short_name, full_name = self.get_synthetic_test_names()
169
+ self.compiled_name = short_name
170
+ self.fqn_name = full_name
171
+ # use hashed name as alias if full name is too long
172
+ if short_name != full_name and "alias" not in self.config:
173
+ self.config["alias"] = short_name
174
+
175
+ def _process_legacy_args(self):
176
+ config = {}
177
+ for key in self.CONFIG_ARGS:
178
+ value = self.args.pop(key, None)
179
+ if value and "config" in self.args and key in self.args["config"]:
180
+ raise SameKeyNestedError()
181
+ if not value and "config" in self.args:
182
+ value = self.args["config"].pop(key, None)
183
+ config[key] = value
184
+
185
+ return self._render_values(config)
186
+
187
+ def _render_values(self, config: Dict[str, Any]) -> Dict[str, Any]:
188
+ rendered_config = {}
189
+ for key, value in config.items():
190
+ if isinstance(value, str):
191
+ try:
192
+ value = get_rendered(value, self.render_ctx, native=True)
193
+ except UndefinedMacroError as e:
194
+ raise CustomMacroPopulatingConfigValueError(
195
+ target_name=self.target.name,
196
+ column_name=self.column_name,
197
+ name=self.name,
198
+ key=key,
199
+ err_msg=e.msg,
200
+ )
201
+ if value is not None:
202
+ rendered_config[key] = value
203
+ return rendered_config
204
+
205
+ def _bad_type(self) -> TypeError:
206
+ return TypeError('invalid target type "{}"'.format(type(self.target)))
207
+
208
+ @staticmethod
209
+ def extract_test_args(
210
+ data_test, file_path, resource_name=None, column_name=None, package_name=None
211
+ ) -> Tuple[str, Dict[str, Any]]:
212
+ if not isinstance(data_test, dict):
213
+ raise TestTypeError(data_test)
214
+
215
+ # If the test is a dictionary with top-level keys, the test name is "test_name"
216
+ # and the rest are arguments
217
+ # {'name': 'my_favorite_test', 'test_name': 'unique', 'config': {'where': '1=1'}}
218
+ if "test_name" in data_test.keys():
219
+ test_name = data_test.pop("test_name")
220
+ test_args = data_test
221
+ # If the test is a nested dictionary with one top-level key, the test name
222
+ # is the dict name, and nested keys are arguments
223
+ # {'unique': {'name': 'my_favorite_test', 'config': {'where': '1=1'}}}
224
+ else:
225
+ data_test = list(data_test.items())
226
+ if len(data_test) != 1:
227
+ raise TestDefinitionDictLengthError(data_test)
228
+ test_name, test_args = data_test[0]
229
+
230
+ if not isinstance(test_args, dict):
231
+ raise TestArgsNotDictError(test_args)
232
+ if not isinstance(test_name, str):
233
+ raise TestNameNotStringError(test_name)
234
+ test_args = deepcopy(test_args)
235
+ if column_name is not None:
236
+ test_args["column_name"] = column_name
237
+
238
+ # Extract kwargs when they are nested under new 'arguments' property separately from 'config' if require_generic_test_arguments_property is enabled
239
+ if get_flags().require_generic_test_arguments_property:
240
+ arguments = test_args.pop("arguments", {})
241
+ if not arguments and any(
242
+ k not in ("config", "column_name", "description", "name") for k in test_args.keys()
243
+ ):
244
+ resource = (
245
+ f"'{resource_name}' in package '{package_name}'"
246
+ if package_name
247
+ else f"'{resource_name}'"
248
+ )
249
+ deprecations.warn(
250
+ "missing-arguments-property-in-generic-test-deprecation",
251
+ test_name=f"`{test_name}` defined on {resource} ({file_path})",
252
+ )
253
+ if isinstance(arguments, dict):
254
+ test_args = {**test_args, **arguments}
255
+ elif "arguments" in test_args:
256
+ deprecations.warn(
257
+ "arguments-property-in-generic-test-deprecation",
258
+ test_name=f"`{test_name}` ({test_args['arguments']})",
259
+ )
260
+
261
+ return test_name, test_args
262
+
263
+ def tags(self) -> List[str]:
264
+ tags = self.config.get("tags", [])
265
+ if isinstance(tags, str):
266
+ tags = [tags]
267
+ if not isinstance(tags, list):
268
+ raise TagsNotListOfStringsError(tags)
269
+ for tag in tags:
270
+ if not isinstance(tag, str):
271
+ raise TagNotStringError(tag)
272
+ return tags[:]
273
+
274
+ def macro_name(self) -> str:
275
+ macro_name = "test_{}".format(self.name)
276
+ if self.namespace is not None:
277
+ macro_name = "{}.{}".format(self.namespace, macro_name)
278
+ return macro_name
279
+
280
+ def get_synthetic_test_names(self) -> Tuple[str, str]:
281
+ # Returns two names: shorter (for the compiled file), full (for the unique_id + FQN)
282
+ target_name = self.target.name
283
+ if isinstance(self.target, UnparsedModelUpdate):
284
+ name = self.name
285
+ if self.version:
286
+ target_name = f"{self.target.name}_v{self.version}"
287
+ elif isinstance(self.target, UnparsedNodeUpdate):
288
+ name = self.name
289
+ elif isinstance(self.target, UnpatchedSourceDefinition):
290
+ name = "source_" + self.name
291
+ else:
292
+ raise self._bad_type()
293
+ if self.namespace is not None:
294
+ name = "{}_{}".format(self.namespace, name)
295
+ return synthesize_generic_test_names(name, target_name, self.args)
296
+
297
+ def construct_config(self) -> str:
298
+ configs = ",".join(
299
+ [
300
+ f"{key}="
301
+ + (
302
+ ('"' + value.replace('"', '\\"') + '"')
303
+ if isinstance(value, str)
304
+ else str(value)
305
+ )
306
+ for key, value in self.config.items()
307
+ ]
308
+ )
309
+ if configs:
310
+ return f"{{{{ config({configs}) }}}}"
311
+ else:
312
+ return ""
313
+
314
+ # this is the 'raw_code' that's used in 'render_update' and execution
315
+ # of the test macro
316
+ def build_raw_code(self) -> str:
317
+ return ("{{{{ {macro}(**{kwargs_name}) }}}}{config}").format(
318
+ macro=self.macro_name(),
319
+ config=self.construct_config(),
320
+ kwargs_name=GENERIC_TEST_KWARGS_NAME,
321
+ )
322
+
323
+ def build_model_str(self):
324
+ targ = self.target
325
+ if isinstance(self.target, UnparsedModelUpdate):
326
+ if self.version:
327
+ target_str = f"ref('{targ.name}', version='{self.version}')"
328
+ else:
329
+ target_str = f"ref('{targ.name}')"
330
+ elif isinstance(self.target, UnparsedNodeUpdate):
331
+ target_str = f"ref('{targ.name}')"
332
+ elif isinstance(self.target, UnpatchedSourceDefinition):
333
+ target_str = f"source('{targ.source.name}', '{targ.table.name}')"
334
+ return f"{{{{ get_where_subquery({target_str}) }}}}"
dvt/parser/hooks.py ADDED
@@ -0,0 +1,119 @@
1
+ from dataclasses import dataclass
2
+ from typing import Iterable, Iterator, List, Tuple, Union
3
+
4
+ from dvt.context.context_config import ContextConfig
5
+ from dvt.contracts.files import FilePath
6
+ from dvt.contracts.graph.nodes import HookNode
7
+ from dvt.node_types import NodeType, RunHookType
8
+ from dvt.parser.base import SimpleParser
9
+ from dvt.parser.search import FileBlock
10
+ from dvt.utils import get_pseudo_hook_path
11
+
12
+ from dbt_common.exceptions import DbtInternalError
13
+
14
+
15
+ @dataclass
16
+ class HookBlock(FileBlock):
17
+ project: str
18
+ value: str
19
+ index: int
20
+ hook_type: RunHookType
21
+
22
+ @property
23
+ def contents(self):
24
+ return self.value
25
+
26
+ @property
27
+ def name(self):
28
+ return "{}-{!s}-{!s}".format(self.project, self.hook_type, self.index)
29
+
30
+
31
+ class HookSearcher(Iterable[HookBlock]):
32
+ def __init__(self, project, source_file, hook_type) -> None:
33
+ self.project = project
34
+ self.source_file = source_file
35
+ self.hook_type = hook_type
36
+
37
+ def _hook_list(self, hooks: Union[str, List[str], Tuple[str, ...]]) -> List[str]:
38
+ if isinstance(hooks, tuple):
39
+ hooks = list(hooks)
40
+ elif not isinstance(hooks, list):
41
+ hooks = [hooks]
42
+ return hooks
43
+
44
+ def get_hook_defs(self) -> List[str]:
45
+ if self.hook_type == RunHookType.Start:
46
+ hooks = self.project.on_run_start
47
+ elif self.hook_type == RunHookType.End:
48
+ hooks = self.project.on_run_end
49
+ else:
50
+ raise DbtInternalError(
51
+ 'hook_type must be one of "{}" or "{}" (got {})'.format(
52
+ RunHookType.Start, RunHookType.End, self.hook_type
53
+ )
54
+ )
55
+ return self._hook_list(hooks)
56
+
57
+ def __iter__(self) -> Iterator[HookBlock]:
58
+ hooks = self.get_hook_defs()
59
+ for index, hook in enumerate(hooks):
60
+ yield HookBlock(
61
+ file=self.source_file,
62
+ project=self.project.project_name,
63
+ value=hook,
64
+ index=index,
65
+ hook_type=self.hook_type,
66
+ )
67
+
68
+
69
+ class HookParser(SimpleParser[HookBlock, HookNode]):
70
+
71
+ # Hooks are only in the dbt_project.yml file for the project
72
+ def get_path(self) -> FilePath:
73
+ # There ought to be an existing file object for this, but
74
+ # until that is implemented use a dummy modification time
75
+ path = FilePath(
76
+ project_root=self.project.project_root,
77
+ searched_path=".",
78
+ relative_path="dbt_project.yml",
79
+ modification_time=0.0,
80
+ )
81
+ return path
82
+
83
+ def parse_from_dict(self, dct, validate=True) -> HookNode:
84
+ if validate:
85
+ HookNode.validate(dct)
86
+ return HookNode.from_dict(dct)
87
+
88
+ @classmethod
89
+ def get_compiled_path(cls, block: HookBlock):
90
+ return get_pseudo_hook_path(block.name)
91
+
92
+ def _create_parsetime_node(
93
+ self,
94
+ block: HookBlock,
95
+ path: str,
96
+ config: ContextConfig,
97
+ fqn: List[str],
98
+ name=None,
99
+ **kwargs,
100
+ ) -> HookNode:
101
+
102
+ return super()._create_parsetime_node(
103
+ block=block,
104
+ path=path,
105
+ config=config,
106
+ fqn=fqn,
107
+ index=block.index,
108
+ name=name,
109
+ tags=[str(block.hook_type)],
110
+ )
111
+
112
+ @property
113
+ def resource_type(self) -> NodeType:
114
+ return NodeType.Operation
115
+
116
+ def parse_file(self, block: FileBlock) -> None:
117
+ for hook_type in RunHookType:
118
+ for hook in HookSearcher(self.project, block.file, hook_type):
119
+ self.parse_node(hook)
dvt/parser/macros.py ADDED
@@ -0,0 +1,137 @@
1
+ from typing import Iterable, List
2
+
3
+ import jinja2
4
+ from dvt.artifacts.resources import MacroArgument
5
+ from dvt.clients.jinja import get_supported_languages
6
+ from dvt.contracts.files import FilePath, SourceFile
7
+ from dvt.contracts.graph.nodes import Macro
8
+ from dvt.contracts.graph.unparsed import UnparsedMacro
9
+ from dvt.exceptions import ParsingError
10
+ from dvt.flags import get_flags
11
+ from dvt.node_types import NodeType
12
+ from dvt.parser.base import BaseParser
13
+ from dvt.parser.search import FileBlock, filesystem_search
14
+
15
+ from dbt_common.clients import jinja
16
+ from dbt_common.clients._jinja_blocks import ExtractWarning
17
+ from dbt_common.utils import MACRO_PREFIX
18
+
19
+
20
+ class MacroParser(BaseParser[Macro]):
21
+ # This is only used when creating a MacroManifest separate
22
+ # from the normal parsing flow.
23
+ def get_paths(self) -> List[FilePath]:
24
+ return filesystem_search(
25
+ project=self.project, relative_dirs=self.project.macro_paths, extension=".sql"
26
+ )
27
+
28
+ @property
29
+ def resource_type(self) -> NodeType:
30
+ return NodeType.Macro
31
+
32
+ @classmethod
33
+ def get_compiled_path(cls, block: FileBlock):
34
+ return block.path.relative_path
35
+
36
+ def parse_macro(self, block: jinja.BlockTag, base_node: UnparsedMacro, name: str) -> Macro:
37
+ unique_id = self.generate_unique_id(name)
38
+ macro_sql = block.full_block or ""
39
+
40
+ return Macro(
41
+ path=base_node.path,
42
+ macro_sql=macro_sql,
43
+ original_file_path=base_node.original_file_path,
44
+ package_name=base_node.package_name,
45
+ resource_type=base_node.resource_type,
46
+ name=name,
47
+ unique_id=unique_id,
48
+ )
49
+
50
+ def parse_unparsed_macros(self, base_node: UnparsedMacro) -> Iterable[Macro]:
51
+ # This is a bit of a hack to get the file path to the deprecation
52
+ def wrap_handle_extract_warning(warning: ExtractWarning) -> None:
53
+ self._handle_extract_warning(warning=warning, file=base_node.original_file_path)
54
+
55
+ try:
56
+ blocks: List[jinja.BlockTag] = [
57
+ t
58
+ for t in jinja.extract_toplevel_blocks(
59
+ base_node.raw_code,
60
+ allowed_blocks={"macro", "materialization", "test", "data_test"},
61
+ collect_raw_data=False,
62
+ warning_callback=wrap_handle_extract_warning,
63
+ )
64
+ if isinstance(t, jinja.BlockTag)
65
+ ]
66
+ except ParsingError as exc:
67
+ exc.add_node(base_node)
68
+ raise
69
+
70
+ for block in blocks:
71
+ try:
72
+ ast = jinja.parse(block.full_block)
73
+ except ParsingError as e:
74
+ e.add_node(base_node)
75
+ raise
76
+
77
+ if (
78
+ isinstance(ast, jinja2.nodes.Template)
79
+ and hasattr(ast, "body")
80
+ and len(ast.body) == 1
81
+ and isinstance(ast.body[0], jinja2.nodes.Macro)
82
+ ):
83
+ # If the top level node in the Template is a Macro, things look
84
+ # good and this is much faster than traversing the full ast, as
85
+ # in the following else clause. It's not clear if that traversal
86
+ # is ever really needed.
87
+ macro = ast.body[0]
88
+ else:
89
+ macro_nodes = list(ast.find_all(jinja2.nodes.Macro))
90
+
91
+ if len(macro_nodes) != 1:
92
+ # things have gone disastrously wrong, we thought we only
93
+ # parsed one block!
94
+ raise ParsingError(
95
+ f"Found multiple macros in {block.full_block}, expected 1", node=base_node
96
+ )
97
+
98
+ macro = macro_nodes[0]
99
+
100
+ if not macro.name.startswith(MACRO_PREFIX):
101
+ continue
102
+
103
+ name: str = macro.name.replace(MACRO_PREFIX, "")
104
+ node = self.parse_macro(block, base_node, name)
105
+
106
+ if getattr(get_flags(), "validate_macro_args", False):
107
+ node.arguments = self._extract_args(macro)
108
+
109
+ # get supported_languages for materialization macro
110
+ if block.block_type_name == "materialization":
111
+ node.supported_languages = get_supported_languages(macro)
112
+ yield node
113
+
114
+ def _extract_args(self, macro) -> List[MacroArgument]:
115
+ try:
116
+ return list([MacroArgument(name=arg.name) for arg in macro.args])
117
+ except Exception:
118
+ return []
119
+
120
+ def parse_file(self, block: FileBlock):
121
+ assert isinstance(block.file, SourceFile)
122
+ source_file = block.file
123
+ assert isinstance(source_file.contents, str)
124
+ original_file_path = source_file.path.original_file_path
125
+
126
+ # this is really only used for error messages
127
+ base_node = UnparsedMacro(
128
+ path=original_file_path,
129
+ original_file_path=original_file_path,
130
+ package_name=self.project.project_name,
131
+ raw_code=source_file.contents,
132
+ resource_type=NodeType.Macro,
133
+ language="sql",
134
+ )
135
+
136
+ for node in self.parse_unparsed_macros(base_node):
137
+ self.manifest.add_macro(block.file, node)