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,423 @@
1
+ import itertools
2
+ import os
3
+ import pathlib
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from dvt.artifacts.resources import NodeVersion, RefArgs
7
+ from dvt.clients.jinja import add_rendered_test_kwargs, get_rendered
8
+ from dvt.context.configured import SchemaYamlVars, generate_schema_yml_context
9
+ from dvt.context.context_config import ContextConfig
10
+ from dvt.context.macro_resolver import MacroResolver
11
+ from dvt.context.providers import generate_test_context
12
+ from dvt.contracts.files import FileHash
13
+ from dvt.contracts.graph.nodes import (
14
+ GenericTestNode,
15
+ GraphMemberNode,
16
+ ManifestNode,
17
+ UnpatchedSourceDefinition,
18
+ )
19
+ from dvt.contracts.graph.unparsed import UnparsedColumn, UnparsedNodeUpdate
20
+ from dvt.exceptions import (
21
+ CompilationError,
22
+ ParsingError,
23
+ SchemaConfigError,
24
+ TestConfigError,
25
+ )
26
+ from dvt.node_types import NodeType
27
+ from dvt.parser.base import SimpleParser
28
+ from dvt.parser.common import (
29
+ GenericTestBlock,
30
+ Testable,
31
+ TestBlock,
32
+ TestDef,
33
+ VersionedTestBlock,
34
+ trimmed,
35
+ )
36
+ from dvt.parser.generic_test_builders import TestBuilder
37
+ from dvt.parser.search import FileBlock
38
+ from dvt.utils import get_pseudo_test_path, md5
39
+
40
+ from dbt.adapters.factory import get_adapter, get_adapter_package_names
41
+ from dbt_common.dataclass_schema import ValidationError
42
+
43
+
44
+ # This parser handles the tests that are defined in "schema" (yaml) files, on models,
45
+ # sources, etc. The base generic test is handled by the GenericTestParser
46
+ class SchemaGenericTestParser(SimpleParser):
47
+ def __init__(
48
+ self,
49
+ project,
50
+ manifest,
51
+ root_project,
52
+ ) -> None:
53
+ super().__init__(project, manifest, root_project)
54
+ self.schema_yaml_vars = SchemaYamlVars()
55
+ self.render_ctx = generate_schema_yml_context(
56
+ self.root_project, self.project.project_name, self.schema_yaml_vars
57
+ )
58
+ internal_package_names = get_adapter_package_names(self.root_project.credentials.type)
59
+ self.macro_resolver = MacroResolver(
60
+ self.manifest.macros, self.root_project.project_name, internal_package_names
61
+ )
62
+
63
+ @property
64
+ def resource_type(self) -> NodeType:
65
+ return NodeType.Test
66
+
67
+ @classmethod
68
+ def get_compiled_path(cls, block: FileBlock) -> str:
69
+ return block.path.relative_path
70
+
71
+ def parse_file(self, block: FileBlock, dct: Optional[Dict] = None) -> None:
72
+ pass
73
+
74
+ def parse_from_dict(self, dct, validate=True) -> GenericTestNode:
75
+ if validate:
76
+ GenericTestNode.validate(dct)
77
+ return GenericTestNode.from_dict(dct)
78
+
79
+ def parse_column_tests(
80
+ self, block: TestBlock, column: UnparsedColumn, version: Optional[NodeVersion]
81
+ ) -> None:
82
+ if not column.data_tests:
83
+ return
84
+
85
+ for data_test in column.data_tests:
86
+ self.parse_test(block, data_test, column, version)
87
+
88
+ def create_test_node(
89
+ self,
90
+ target: Union[UnpatchedSourceDefinition, UnparsedNodeUpdate],
91
+ path: str,
92
+ config: ContextConfig,
93
+ tags: List[str],
94
+ fqn: List[str],
95
+ name: str,
96
+ raw_code: str,
97
+ test_metadata: Dict[str, Any],
98
+ file_key_name: str,
99
+ column_name: Optional[str],
100
+ description: str,
101
+ ) -> GenericTestNode:
102
+
103
+ HASH_LENGTH = 10
104
+
105
+ # N.B: This function builds a hashable string from any given test_metadata dict.
106
+ # it's a bit fragile for general use (only supports str, int, float, List, Dict)
107
+ # but it gets the job done here without the overhead of complete ser(de).
108
+ def get_hashable_md(data: Union[str, int, float, List, Dict]) -> Union[str, List, Dict]:
109
+ if type(data) == dict:
110
+ return {k: get_hashable_md(data[k]) for k in sorted(data.keys())} # type: ignore
111
+ elif type(data) == list:
112
+ return [get_hashable_md(val) for val in data] # type: ignore
113
+ else:
114
+ return str(data)
115
+
116
+ hashable_metadata = repr(get_hashable_md(test_metadata))
117
+ hash_string = "".join([name, hashable_metadata])
118
+ test_hash = md5(hash_string)[-HASH_LENGTH:]
119
+
120
+ dct = {
121
+ "alias": name,
122
+ "schema": self.default_schema,
123
+ "database": self.default_database,
124
+ "fqn": fqn,
125
+ "name": name,
126
+ "resource_type": self.resource_type,
127
+ "tags": tags,
128
+ "path": path,
129
+ "original_file_path": target.original_file_path,
130
+ "package_name": self.project.project_name,
131
+ "raw_code": raw_code,
132
+ "language": "sql",
133
+ "unique_id": self.generate_unique_id(name, test_hash),
134
+ "config": self.config_dict(config),
135
+ "test_metadata": test_metadata,
136
+ "column_name": column_name,
137
+ "checksum": FileHash.empty().to_dict(omit_none=True),
138
+ "file_key_name": file_key_name,
139
+ "description": description,
140
+ }
141
+ try:
142
+ GenericTestNode.validate(dct)
143
+ return GenericTestNode.from_dict(dct)
144
+ except ValidationError as exc:
145
+ # this is a bit silly, but build an UnparsedNode just for error
146
+ # message reasons
147
+ node = self._create_error_node(
148
+ name=target.name,
149
+ path=path,
150
+ original_file_path=target.original_file_path,
151
+ raw_code=raw_code,
152
+ )
153
+ raise TestConfigError(exc, node)
154
+
155
+ # This is called directly in the SourcePatcher and by the "parse_node"
156
+ # command which is called by the SchemaParser.
157
+ def parse_generic_test(
158
+ self,
159
+ target: Testable,
160
+ data_test: Dict[str, Any],
161
+ tags: List[str],
162
+ column_name: Optional[str],
163
+ schema_file_id: str,
164
+ version: Optional[NodeVersion],
165
+ ) -> GenericTestNode:
166
+ try:
167
+ builder = TestBuilder(
168
+ data_test=data_test,
169
+ target=target,
170
+ column_name=column_name,
171
+ version=version,
172
+ package_name=target.package_name,
173
+ render_ctx=self.render_ctx,
174
+ )
175
+ if self.schema_yaml_vars.env_vars:
176
+ self.store_env_vars(target, schema_file_id, self.schema_yaml_vars.env_vars)
177
+ self.schema_yaml_vars.env_vars = {}
178
+
179
+ except ParsingError as exc:
180
+ context = trimmed(str(target))
181
+ msg = "Invalid test config given in {}:\n\t{}\n\t@: {}".format(
182
+ target.original_file_path, exc.msg, context
183
+ )
184
+ raise ParsingError(msg) from exc
185
+
186
+ except CompilationError as exc:
187
+ context = trimmed(str(target))
188
+ msg = (
189
+ "Invalid generic test configuration given in "
190
+ f"{target.original_file_path}: \n{exc.msg}\n\t@: {context}"
191
+ )
192
+ raise CompilationError(msg) from exc
193
+
194
+ original_name = os.path.basename(target.original_file_path)
195
+ compiled_path = get_pseudo_test_path(builder.compiled_name, original_name)
196
+
197
+ # fqn is the relative path of the yaml file where this generic test is defined,
198
+ # minus the project-level directory and the file name itself
199
+ # TODO pass a consistent path object from both UnparsedNode and UnpatchedSourceDefinition
200
+ path = pathlib.Path(target.original_file_path)
201
+ relative_path = str(path.relative_to(*path.parts[:1]))
202
+ fqn = self.get_fqn(relative_path, builder.fqn_name)
203
+
204
+ # this is the ContextConfig that is used in render_update
205
+ config: ContextConfig = self.initial_config(fqn)
206
+ # Adding the builder's config to the ContextConfig
207
+ # is needed to ensure the config makes it to the pre_model hook which dbt-snowflake needs
208
+ config.add_config_call(builder.config)
209
+ # builder.args contains keyword args for the test macro,
210
+ # not configs which have been separated out in the builder.
211
+ # The keyword args are not completely rendered until compilation.
212
+ metadata = {
213
+ "namespace": builder.namespace,
214
+ "name": builder.name,
215
+ "kwargs": builder.args,
216
+ }
217
+ tags = sorted(set(itertools.chain(tags, builder.tags())))
218
+
219
+ if isinstance(target, UnpatchedSourceDefinition):
220
+ file_key_name = f"{target.source.yaml_key}.{target.source.name}"
221
+ else:
222
+ file_key_name = f"{target.yaml_key}.{target.name}"
223
+
224
+ node = self.create_test_node(
225
+ target=target,
226
+ path=compiled_path,
227
+ config=config,
228
+ fqn=fqn,
229
+ tags=tags,
230
+ name=builder.fqn_name,
231
+ raw_code=builder.build_raw_code(),
232
+ column_name=column_name,
233
+ test_metadata=metadata,
234
+ file_key_name=file_key_name,
235
+ description=builder.description,
236
+ )
237
+ self.render_test_update(node, config, builder, schema_file_id)
238
+
239
+ return node
240
+
241
+ def _lookup_attached_node(
242
+ self, target: Testable, version: Optional[NodeVersion]
243
+ ) -> Optional[Union[ManifestNode, GraphMemberNode]]:
244
+ """Look up attached node for Testable target nodes other than sources. Can be None if generic test attached to SQL node with no corresponding .sql file."""
245
+ attached_node = None # type: Optional[Union[ManifestNode, GraphMemberNode]]
246
+ if not isinstance(target, UnpatchedSourceDefinition):
247
+ attached_node_unique_id = self.manifest.ref_lookup.get_unique_id(
248
+ target.name, target.package_name, version
249
+ )
250
+ if attached_node_unique_id:
251
+ attached_node = self.manifest.nodes[attached_node_unique_id]
252
+ else:
253
+ disabled_node = self.manifest.disabled_lookup.find(
254
+ target.name, None
255
+ ) or self.manifest.disabled_lookup.find(target.name.upper(), None)
256
+ if disabled_node:
257
+ attached_node = self.manifest.disabled[disabled_node[0].unique_id][0]
258
+ return attached_node
259
+
260
+ def store_env_vars(self, target, schema_file_id, env_vars):
261
+ self.manifest.env_vars.update(env_vars)
262
+ if schema_file_id in self.manifest.files:
263
+ schema_file = self.manifest.files[schema_file_id]
264
+ if isinstance(target, UnpatchedSourceDefinition):
265
+ search_name = target.source.name
266
+ yaml_key = target.source.yaml_key
267
+ if "." in search_name: # source file definitions
268
+ (search_name, _) = search_name.split(".")
269
+ else:
270
+ search_name = target.name
271
+ yaml_key = target.yaml_key
272
+ for var in env_vars.keys():
273
+ schema_file.add_env_var(var, yaml_key, search_name)
274
+
275
+ # This does special shortcut processing for the two
276
+ # most common internal macros, not_null and unique,
277
+ # which avoids the jinja rendering to resolve config
278
+ # and variables, etc, which might be in the macro.
279
+ # In the future we will look at generalizing this
280
+ # more to handle additional macros or to use static
281
+ # parsing to avoid jinja overhead.
282
+ def render_test_update(self, node, config, builder, schema_file_id):
283
+ macro_unique_id = self.macro_resolver.get_macro_id(
284
+ node.package_name, "test_" + builder.name
285
+ )
286
+ # Add the depends_on here so we can limit the macros added
287
+ # to the context in rendering processing
288
+ node.depends_on.add_macro(macro_unique_id)
289
+ if macro_unique_id in ["macro.dbt.test_not_null", "macro.dbt.test_unique"]:
290
+ config_call_dict = builder.config
291
+ config._config_call_dict = config_call_dict
292
+ # This sets the config from dbt_project
293
+ self.update_parsed_node_config(node, config)
294
+ # source node tests are processed at patch_source time
295
+ if isinstance(builder.target, UnpatchedSourceDefinition):
296
+ sources = [builder.target.fqn[-2], builder.target.fqn[-1]]
297
+ node.sources.append(sources)
298
+ else: # all other nodes
299
+ node.refs.append(RefArgs(name=builder.target.name, version=builder.version))
300
+ else:
301
+ try:
302
+ # make a base context that doesn't have the magic kwargs field
303
+ context = generate_test_context(
304
+ node,
305
+ self.root_project,
306
+ self.manifest,
307
+ config,
308
+ self.macro_resolver,
309
+ )
310
+ # update with rendered test kwargs (which collects any refs)
311
+ # Note: This does not actually update the kwargs with the rendered
312
+ # values. That happens in compilation.
313
+ add_rendered_test_kwargs(context, node, capture_macros=True)
314
+ # the parsed node is not rendered in the native context.
315
+ get_rendered(node.raw_code, context, node, capture_macros=True)
316
+ self.update_parsed_node_config(node, config)
317
+ # env_vars should have been updated in the context env_var method
318
+ except ValidationError as exc:
319
+ # we got a ValidationError - probably bad types in config()
320
+ raise SchemaConfigError(exc, node=node) from exc
321
+
322
+ # Set attached_node for generic test nodes, if available.
323
+ # Generic test node inherits attached node's group config value.
324
+ attached_node = self._lookup_attached_node(builder.target, builder.version)
325
+ if attached_node:
326
+ node.attached_node = attached_node.unique_id
327
+ node.group, node.group = attached_node.group, attached_node.group
328
+
329
+ def parse_node(self, block: GenericTestBlock) -> GenericTestNode:
330
+ """In schema parsing, we rewrite most of the part of parse_node that
331
+ builds the initial node to be parsed, but rendering is basically the
332
+ same
333
+ """
334
+ node = self.parse_generic_test(
335
+ target=block.target,
336
+ data_test=block.data_test,
337
+ tags=block.tags,
338
+ column_name=block.column_name,
339
+ schema_file_id=block.file.file_id,
340
+ version=block.version,
341
+ )
342
+ self.add_test_node(block, node)
343
+ return node
344
+
345
+ def add_test_node(self, block: GenericTestBlock, node: GenericTestNode):
346
+ test_from = {"key": block.target.yaml_key, "name": block.target.name}
347
+ if node.config.enabled:
348
+ self.manifest.add_node(block.file, node, test_from)
349
+ else:
350
+ self.manifest.add_disabled(block.file, node, test_from)
351
+
352
+ def render_with_context(
353
+ self,
354
+ node: GenericTestNode,
355
+ config: ContextConfig,
356
+ ) -> None:
357
+ """Given the parsed node and a ContextConfig to use during
358
+ parsing, collect all the refs that might be squirreled away in the test
359
+ arguments. This includes the implicit "model" argument.
360
+ """
361
+ # make a base context that doesn't have the magic kwargs field
362
+ context = self._context_for(node, config)
363
+ # update it with the rendered test kwargs (which collects any refs)
364
+ add_rendered_test_kwargs(context, node, capture_macros=True)
365
+
366
+ # the parsed node is not rendered in the native context.
367
+ get_rendered(node.raw_code, context, node, capture_macros=True)
368
+
369
+ def parse_test(
370
+ self,
371
+ target_block: TestBlock,
372
+ data_test: TestDef,
373
+ column: Optional[UnparsedColumn],
374
+ version: Optional[NodeVersion],
375
+ ) -> None:
376
+ if isinstance(data_test, str):
377
+ data_test = {data_test: {}}
378
+
379
+ if column is None:
380
+ column_name: Optional[str] = None
381
+ column_tags: List[str] = []
382
+ else:
383
+ column_name = column.name
384
+ should_quote = column.quote or (column.quote is None and target_block.quote_columns)
385
+ if should_quote:
386
+ column_name = get_adapter(self.root_project).quote(column_name)
387
+
388
+ column_config_tags = column.config.get("tags", [])
389
+ if isinstance(column_config_tags, str):
390
+ column_config_tags = [column_config_tags]
391
+ column_tags = list(set(column.tags + column_config_tags))
392
+
393
+ block = GenericTestBlock.from_test_block(
394
+ src=target_block,
395
+ data_test=data_test,
396
+ column_name=column_name,
397
+ tags=column_tags,
398
+ version=version,
399
+ )
400
+ self.parse_node(block)
401
+
402
+ def parse_tests(self, block: TestBlock) -> None:
403
+ for column in block.columns:
404
+ self.parse_column_tests(block, column, None)
405
+
406
+ for data_test in block.data_tests:
407
+ self.parse_test(block, data_test, None, None)
408
+
409
+ def parse_versioned_tests(self, block: VersionedTestBlock) -> None:
410
+ if not block.target.versions:
411
+ self.parse_tests(block)
412
+ else:
413
+ for version in block.target.versions:
414
+ for column in block.target.get_columns_for_version(version.v):
415
+ self.parse_column_tests(block, column, version.v)
416
+
417
+ for test in block.target.get_tests_for_version(version.v):
418
+ self.parse_test(block, test, None, version.v)
419
+
420
+ def generate_unique_id(self, resource_name: str, hash: Optional[str] = None) -> str:
421
+ return ".".join(
422
+ filter(None, [self.resource_type, self.project.project_name, resource_name, hash])
423
+ )
@@ -0,0 +1,111 @@
1
+ from typing import Any, Dict
2
+
3
+ from dvt.config.renderer import BaseRenderer, Keypath
4
+
5
+
6
+ # This class renders dictionaries derived from "schema" yaml files.
7
+ # It calls Jinja on strings (in deep_map_render), except for certain
8
+ # keys which are skipped because they need to be rendered later
9
+ # (tests and description). Test configs are rendered in the
10
+ # generic test builder code, but skips the keyword args. The test
11
+ # keyword args are rendered to capture refs in render_test_update.
12
+ # Keyword args are finally rendered at compilation time.
13
+ # Descriptions are not rendered until 'process_docs'.
14
+ # Pre- and post-hooks in configs are late-rendered.
15
+ class SchemaYamlRenderer(BaseRenderer):
16
+ def __init__(self, context: Dict[str, Any], key: str) -> None:
17
+ super().__init__(context)
18
+ self.key = key
19
+
20
+ @property
21
+ def name(self):
22
+ return "Rendering yaml"
23
+
24
+ def _is_norender_key(self, keypath: Keypath) -> bool:
25
+ """
26
+ models:
27
+ - name: blah
28
+ description: blah
29
+ data_tests: ...
30
+ columns:
31
+ - name:
32
+ description: blah
33
+ data_tests: ...
34
+
35
+ Return True if it's tests, data_tests or description - those aren't rendered now
36
+ because they're rendered later in parse_generic_tests or process_docs.
37
+ "tests" and "data_tests" are both currently supported but "tests" has been deprecated
38
+ """
39
+ # top level descriptions and data_tests
40
+ if len(keypath) >= 1 and keypath[0] in (
41
+ "tests",
42
+ "data_tests",
43
+ "description",
44
+ "loaded_at_query",
45
+ ):
46
+ return True
47
+
48
+ # columns descriptions and data_tests
49
+ if len(keypath) == 2 and keypath[1] in (
50
+ "tests",
51
+ "data_tests",
52
+ "description",
53
+ "loaded_at_query",
54
+ ):
55
+ return True
56
+
57
+ # config: pre- and post-hooks, and loaded_at_query
58
+ if (
59
+ len(keypath) >= 2
60
+ and keypath[0] == "config"
61
+ and keypath[1] in ("pre_hook", "post_hook", "loaded_at_query")
62
+ ):
63
+ return True
64
+
65
+ # versions
66
+ if len(keypath) == 5 and keypath[4] == "description":
67
+ return True
68
+
69
+ if (
70
+ len(keypath) >= 3
71
+ and keypath[0] in ("columns", "dimensions", "measures", "entities")
72
+ and keypath[2] in ("tests", "data_tests", "description")
73
+ ):
74
+ return True
75
+
76
+ return False
77
+
78
+ # don't render descriptions or test keyword arguments
79
+ def should_render_keypath(self, keypath: Keypath) -> bool:
80
+ if len(keypath) < 1:
81
+ return True
82
+ if self.key == "sources":
83
+ if keypath[0] in ("description", "loaded_at_query"):
84
+ return False
85
+ if len(keypath) >= 2 and keypath[0] == "config" and keypath[1] == "loaded_at_query":
86
+ return False
87
+ if keypath[0] == "tables":
88
+ if self._is_norender_key(keypath[2:]):
89
+ return False
90
+ elif self.key == "macros":
91
+ if keypath[0] == "arguments":
92
+ if self._is_norender_key(keypath[1:]):
93
+ return False
94
+ elif self._is_norender_key(keypath[0:]):
95
+ return False
96
+ elif self.key == "metrics":
97
+ # This ensures that metric filters are skipped
98
+ if keypath[-1] == "filter" or len(keypath) > 1 and keypath[-2] == "filter":
99
+ return False
100
+ elif self._is_norender_key(keypath[0:]):
101
+ return False
102
+ elif self.key == "saved_queries":
103
+ # This ensures that saved query filters are skipped
104
+ if keypath[0] == "query_params" and len(keypath) > 1 and keypath[1] == "where":
105
+ return False
106
+ elif self._is_norender_key(keypath[0:]):
107
+ return False
108
+ else: # models, seeds, snapshots, analyses
109
+ if self._is_norender_key(keypath[0:]):
110
+ return False
111
+ return True