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/task/test.py ADDED
@@ -0,0 +1,464 @@
1
+ import io
2
+ import json
3
+ import re
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Collection,
10
+ Dict,
11
+ List,
12
+ Optional,
13
+ Tuple,
14
+ Type,
15
+ Union,
16
+ )
17
+
18
+ import daff
19
+ from dvt.artifacts.schemas.catalog import PrimitiveDict
20
+ from dvt.artifacts.schemas.results import TestStatus
21
+ from dvt.artifacts.schemas.run import RunResult
22
+ from dvt.clients.jinja import MacroGenerator
23
+ from dvt.context.providers import generate_runtime_model_context
24
+ from dvt.contracts.graph.manifest import Manifest
25
+ from dvt.contracts.graph.nodes import (
26
+ GenericTestNode,
27
+ SingularTestNode,
28
+ TestNode,
29
+ UnitTestDefinition,
30
+ UnitTestNode,
31
+ )
32
+ from dvt.events.types import LogStartLine, LogTestResult
33
+ from dvt.exceptions import BooleanError, DbtInternalError
34
+ from dvt.flags import get_flags
35
+ from dvt.graph import ResourceTypeSelector
36
+ from dvt.node_types import TEST_NODE_TYPES, NodeType
37
+ from dvt.parser.unit_tests import UnitTestManifestLoader
38
+ from dvt.task import group_lookup
39
+ from dvt.task.base import BaseRunner, resource_types_from_args
40
+ from dvt.task.compile import CompileRunner
41
+ from dvt.task.run import RunTask
42
+ from dvt.utils import _coerce_decimal, strtobool
43
+
44
+ from dbt.adapters.exceptions import MissingMaterializationError
45
+ from dbt_common.dataclass_schema import dbtClassMixin
46
+ from dbt_common.events.format import pluralize
47
+ from dbt_common.events.functions import fire_event
48
+ from dbt_common.exceptions import DbtBaseException, DbtRuntimeError
49
+ from dbt_common.ui import green, red
50
+
51
+ if TYPE_CHECKING:
52
+ import agate
53
+
54
+
55
+ @dataclass
56
+ class UnitTestDiff(dbtClassMixin):
57
+ actual: List[Dict[str, Any]]
58
+ expected: List[Dict[str, Any]]
59
+ rendered: str
60
+
61
+
62
+ @dataclass
63
+ class TestResultData(dbtClassMixin):
64
+ failures: int
65
+ should_warn: bool
66
+ should_error: bool
67
+ adapter_response: Dict[str, Any]
68
+
69
+ @classmethod
70
+ def validate(cls, data):
71
+ data["should_warn"] = cls.convert_bool_type(data["should_warn"])
72
+ data["should_error"] = cls.convert_bool_type(data["should_error"])
73
+ super().validate(data)
74
+
75
+ def convert_bool_type(field) -> bool:
76
+ # if it's type string let python decide if it's a valid value to convert to bool
77
+ if isinstance(field, str):
78
+ try:
79
+ return bool(strtobool(field)) # type: ignore
80
+ except ValueError:
81
+ raise BooleanError(field, "get_test_sql")
82
+
83
+ # need this so we catch both true bools and 0/1
84
+ return bool(field)
85
+
86
+
87
+ @dataclass
88
+ class UnitTestResultData(dbtClassMixin):
89
+ should_error: bool
90
+ adapter_response: Dict[str, Any]
91
+ diff: Optional[UnitTestDiff] = None
92
+
93
+
94
+ class TestRunner(CompileRunner):
95
+ _ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
96
+
97
+ def describe_node_name(self) -> str:
98
+ if self.node.resource_type == NodeType.Unit:
99
+ name = f"{self.node.model}::{self.node.versioned_name}"
100
+ return name
101
+ else:
102
+ return self.node.name
103
+
104
+ def describe_node(self) -> str:
105
+ return f"{self.node.resource_type} {self.describe_node_name()}"
106
+
107
+ def print_result_line(self, result):
108
+ model = result.node
109
+ group = group_lookup.get(model.unique_id)
110
+ attached_node = (
111
+ result.node.attached_node if isinstance(result.node, GenericTestNode) else None
112
+ )
113
+
114
+ fire_event(
115
+ LogTestResult(
116
+ name=self.describe_node_name(),
117
+ status=str(result.status),
118
+ index=self.node_index,
119
+ num_models=self.num_nodes,
120
+ execution_time=result.execution_time,
121
+ node_info=model.node_info,
122
+ num_failures=result.failures,
123
+ group=group,
124
+ attached_node=attached_node,
125
+ ),
126
+ level=LogTestResult.status_to_level(str(result.status)),
127
+ )
128
+
129
+ def print_start_line(self):
130
+ fire_event(
131
+ LogStartLine(
132
+ description=self.describe_node(),
133
+ index=self.node_index,
134
+ total=self.num_nodes,
135
+ node_info=self.node.node_info,
136
+ )
137
+ )
138
+
139
+ def before_execute(self) -> None:
140
+ self.print_start_line()
141
+
142
+ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResultData:
143
+ context = generate_runtime_model_context(data_test, self.config, manifest)
144
+
145
+ hook_ctx = self.adapter.pre_model_hook(context["config"])
146
+
147
+ materialization_macro = manifest.find_materialization_macro_by_name(
148
+ self.config.project_name, data_test.get_materialization(), self.adapter.type()
149
+ )
150
+
151
+ if materialization_macro is None:
152
+ raise MissingMaterializationError(
153
+ materialization=data_test.get_materialization(), adapter_type=self.adapter.type()
154
+ )
155
+
156
+ if "config" not in context:
157
+ raise DbtInternalError(
158
+ "Invalid materialization context generated, missing config: {}".format(context)
159
+ )
160
+
161
+ # generate materialization macro
162
+ macro_func = MacroGenerator(materialization_macro, context)
163
+ try:
164
+ # execute materialization macro
165
+ macro_func()
166
+ finally:
167
+ self.adapter.post_model_hook(context, hook_ctx)
168
+
169
+ # load results from context
170
+ # could eventually be returned directly by materialization
171
+ result = context["load_result"]("main")
172
+ table = result["table"]
173
+ num_rows = len(table.rows)
174
+ if num_rows != 1:
175
+ raise DbtInternalError(
176
+ f"dbt internally failed to execute {data_test.unique_id}: "
177
+ f"Returned {num_rows} rows, but expected "
178
+ f"1 row"
179
+ )
180
+ num_cols = len(table.columns)
181
+ if num_cols != 3:
182
+ raise DbtInternalError(
183
+ f"dbt internally failed to execute {data_test.unique_id}: "
184
+ f"Returned {num_cols} columns, but expected "
185
+ f"3 columns"
186
+ )
187
+
188
+ test_result_dct: PrimitiveDict = dict(
189
+ zip(
190
+ [column_name.lower() for column_name in table.column_names],
191
+ map(_coerce_decimal, table.rows[0]),
192
+ )
193
+ )
194
+ test_result_dct["adapter_response"] = result["response"].to_dict(omit_none=True)
195
+ TestResultData.validate(test_result_dct)
196
+ return TestResultData.from_dict(test_result_dct)
197
+
198
+ def build_unit_test_manifest_from_test(
199
+ self, unit_test_def: UnitTestDefinition, manifest: Manifest
200
+ ) -> Manifest:
201
+ # build a unit test manifest with only the test from this UnitTestDefinition
202
+ loader = UnitTestManifestLoader(manifest, self.config, {unit_test_def.unique_id})
203
+ return loader.load()
204
+
205
+ def execute_unit_test(
206
+ self, unit_test_def: UnitTestDefinition, manifest: Manifest
207
+ ) -> Tuple[UnitTestNode, UnitTestResultData]:
208
+
209
+ unit_test_manifest = self.build_unit_test_manifest_from_test(unit_test_def, manifest)
210
+
211
+ # The unit test node and definition have the same unique_id
212
+ unit_test_node = unit_test_manifest.nodes[unit_test_def.unique_id]
213
+ assert isinstance(unit_test_node, UnitTestNode)
214
+
215
+ # Compile the node
216
+ unit_test_node = self.compiler.compile_node(unit_test_node, unit_test_manifest, {})
217
+ assert isinstance(unit_test_node, UnitTestNode)
218
+
219
+ # generate_runtime_unit_test_context not strictly needed - this is to run the 'unit'
220
+ # materialization, not compile the node.compiled_code
221
+ context = generate_runtime_model_context(unit_test_node, self.config, unit_test_manifest)
222
+
223
+ hook_ctx = self.adapter.pre_model_hook(context["config"])
224
+
225
+ materialization_macro = unit_test_manifest.find_materialization_macro_by_name(
226
+ self.config.project_name, unit_test_node.get_materialization(), self.adapter.type()
227
+ )
228
+
229
+ if materialization_macro is None:
230
+ raise MissingMaterializationError(
231
+ materialization=unit_test_node.get_materialization(),
232
+ adapter_type=self.adapter.type(),
233
+ )
234
+
235
+ if "config" not in context:
236
+ raise DbtInternalError(
237
+ "Invalid materialization context generated, missing config: {}".format(context)
238
+ )
239
+
240
+ # generate materialization macro
241
+ macro_func = MacroGenerator(materialization_macro, context)
242
+ try:
243
+ # execute materialization macro
244
+ macro_func()
245
+ except DbtBaseException as e:
246
+ raise DbtRuntimeError(
247
+ f"An error occurred during execution of unit test '{unit_test_def.name}'. "
248
+ f"There may be an error in the unit test definition: check the data types.\n {e}"
249
+ )
250
+ finally:
251
+ self.adapter.post_model_hook(context, hook_ctx)
252
+
253
+ # load results from context
254
+ # could eventually be returned directly by materialization
255
+ result = context["load_result"]("main")
256
+ adapter_response = result["response"].to_dict(omit_none=True)
257
+ table = result["table"]
258
+ actual = self._get_unit_test_agate_table(table, "actual")
259
+ expected = self._get_unit_test_agate_table(table, "expected")
260
+
261
+ # generate diff, if exists
262
+ should_error, diff = False, None
263
+ daff_diff = self._get_daff_diff(expected, actual)
264
+ if daff_diff.hasDifference():
265
+ should_error = True
266
+ rendered = self._render_daff_diff(daff_diff)
267
+ rendered = f"\n\n{green('actual')} differs from {red('expected')}:\n\n{rendered}\n"
268
+
269
+ diff = UnitTestDiff(
270
+ actual=json_rows_from_table(actual),
271
+ expected=json_rows_from_table(expected),
272
+ rendered=rendered,
273
+ )
274
+
275
+ unit_test_result_data = UnitTestResultData(
276
+ diff=diff,
277
+ should_error=should_error,
278
+ adapter_response=adapter_response,
279
+ )
280
+
281
+ return unit_test_node, unit_test_result_data
282
+
283
+ def execute(self, test: Union[TestNode, UnitTestNode], manifest: Manifest):
284
+ if isinstance(test, UnitTestDefinition):
285
+ unit_test_node, unit_test_result = self.execute_unit_test(test, manifest)
286
+ return self.build_unit_test_run_result(unit_test_node, unit_test_result)
287
+ else:
288
+ # Note: manifest here is a normal manifest
289
+ assert isinstance(test, (SingularTestNode, GenericTestNode))
290
+ test_result = self.execute_data_test(test, manifest)
291
+ return self.build_test_run_result(test, test_result)
292
+
293
+ def build_test_run_result(self, test: TestNode, result: TestResultData) -> RunResult:
294
+ severity = test.config.severity.upper()
295
+ thread_id = threading.current_thread().name
296
+ num_errors = pluralize(result.failures, "result")
297
+ status = None
298
+ message = None
299
+ failures = 0
300
+ if severity == "ERROR" and result.should_error:
301
+ status = TestStatus.Fail
302
+ message = f"Got {num_errors}, configured to fail if {test.config.error_if}"
303
+ failures = result.failures
304
+ elif result.should_warn:
305
+ if get_flags().WARN_ERROR or get_flags().WARN_ERROR_OPTIONS.includes(
306
+ LogTestResult.__name__
307
+ ):
308
+ status = TestStatus.Fail
309
+ message = f"Got {num_errors}, configured to fail if {test.config.warn_if}"
310
+ else:
311
+ status = TestStatus.Warn
312
+ message = f"Got {num_errors}, configured to warn if {test.config.warn_if}"
313
+ failures = result.failures
314
+ else:
315
+ status = TestStatus.Pass
316
+
317
+ run_result = RunResult(
318
+ node=test,
319
+ status=status,
320
+ timing=[],
321
+ thread_id=thread_id,
322
+ execution_time=0,
323
+ message=message,
324
+ adapter_response=result.adapter_response,
325
+ failures=failures,
326
+ batch_results=None,
327
+ )
328
+ return run_result
329
+
330
+ def build_unit_test_run_result(
331
+ self, test: UnitTestNode, result: UnitTestResultData
332
+ ) -> RunResult:
333
+ thread_id = threading.current_thread().name
334
+
335
+ status = TestStatus.Pass
336
+ message = None
337
+ failures = 0
338
+ if result.should_error:
339
+ status = TestStatus.Fail
340
+ message = result.diff.rendered if result.diff else None
341
+ failures = 1
342
+
343
+ return RunResult(
344
+ node=test,
345
+ status=status,
346
+ timing=[],
347
+ thread_id=thread_id,
348
+ execution_time=0,
349
+ message=message,
350
+ adapter_response=result.adapter_response,
351
+ failures=failures,
352
+ batch_results=None,
353
+ )
354
+
355
+ def after_execute(self, result) -> None:
356
+ self.print_result_line(result)
357
+
358
+ def _get_unit_test_agate_table(self, result_table, actual_or_expected: str):
359
+ # lower case the column names as platforms like snowflake can sometimes return columns in uppercase
360
+ result_table = result_table.rename([col.lower() for col in result_table.column_names])
361
+ unit_test_table = result_table.where(
362
+ lambda row: row["actual_or_expected"] == actual_or_expected
363
+ )
364
+ columns = list(unit_test_table.columns.keys())
365
+ columns.remove("actual_or_expected")
366
+ return unit_test_table.select(columns)
367
+
368
+ def _get_daff_diff(
369
+ self, expected: "agate.Table", actual: "agate.Table", ordered: bool = False
370
+ ) -> daff.TableDiff:
371
+ # Sort expected and actual inputs prior to creating daff diff to ensure order insensitivity
372
+ # https://github.com/paulfitz/daff/issues/200
373
+ expected_daff_table = daff.PythonTableView(list_rows_from_table(expected, sort=True))
374
+ actual_daff_table = daff.PythonTableView(list_rows_from_table(actual, sort=True))
375
+
376
+ flags = daff.CompareFlags()
377
+ flags.ordered = ordered
378
+
379
+ alignment = daff.Coopy.compareTables(expected_daff_table, actual_daff_table, flags).align()
380
+ result = daff.PythonTableView([])
381
+
382
+ diff = daff.TableDiff(alignment, flags)
383
+ diff.hilite(result)
384
+ return diff
385
+
386
+ def _render_daff_diff(self, daff_diff: daff.TableDiff) -> str:
387
+ result = daff.PythonTableView([])
388
+ daff_diff.hilite(result)
389
+ rendered = daff.TerminalDiffRender().render(result)
390
+ # strip colors if necessary
391
+ if not self.config.args.use_colors:
392
+ rendered = self._ANSI_ESCAPE.sub("", rendered)
393
+
394
+ return rendered
395
+
396
+
397
+ class TestTask(RunTask):
398
+ """
399
+ Testing:
400
+ Read schema files + custom data tests and validate that
401
+ constraints are satisfied.
402
+ """
403
+
404
+ __test__ = False
405
+
406
+ def raise_on_first_error(self) -> bool:
407
+ return False
408
+
409
+ @property
410
+ def resource_types(self) -> List[NodeType]:
411
+ resource_types: Collection[NodeType] = resource_types_from_args(
412
+ self.args, set(TEST_NODE_TYPES), set(TEST_NODE_TYPES)
413
+ )
414
+
415
+ # filter out any non-test node types
416
+ resource_types = [rt for rt in resource_types if rt in TEST_NODE_TYPES]
417
+ return list(resource_types)
418
+
419
+ def get_node_selector(self) -> ResourceTypeSelector:
420
+ if self.manifest is None or self.graph is None:
421
+ raise DbtInternalError("manifest and graph must be set to get perform node selection")
422
+ return ResourceTypeSelector(
423
+ graph=self.graph,
424
+ manifest=self.manifest,
425
+ previous_state=self.previous_state,
426
+ resource_types=self.resource_types,
427
+ )
428
+
429
+ def get_runner_type(self, _) -> Optional[Type[BaseRunner]]:
430
+ return TestRunner
431
+
432
+
433
+ # This was originally in agate_helper, but that was moved out into dbt_common
434
+ def json_rows_from_table(table: "agate.Table") -> List[Dict[str, Any]]:
435
+ "Convert a table to a list of row dict objects"
436
+ output = io.StringIO()
437
+ table.to_json(path=output) # type: ignore
438
+
439
+ return json.loads(output.getvalue())
440
+
441
+
442
+ # This was originally in agate_helper, but that was moved out into dbt_common
443
+ def list_rows_from_table(table: "agate.Table", sort: bool = False) -> List[Any]:
444
+ """
445
+ Convert given table to a list of lists, where the first element represents the header
446
+
447
+ By default, sort is False and no sort order is applied to the non-header rows of the given table.
448
+
449
+ If sort is True, sort the non-header rows hierarchically, treating None values as lower in order.
450
+ Examples:
451
+ * [['a','b','c'],[4,5,6],[1,2,3]] -> [['a','b','c'],[1,2,3],[4,5,6]]
452
+ * [['a','b','c'],[4,5,6],[1,null,3]] -> [['a','b','c'],[1,null,3],[4,5,6]]
453
+ * [['a','b','c'],[4,5,6],[null,2,3]] -> [['a','b','c'],[4,5,6],[null,2,3]]
454
+ """
455
+ header = [col.name for col in table.columns]
456
+
457
+ rows = []
458
+ for row in table.rows:
459
+ rows.append(list(row.values()))
460
+
461
+ if sort:
462
+ rows = sorted(rows, key=lambda x: [(elem is None, elem) for elem in x])
463
+
464
+ return [header] + rows
@@ -0,0 +1 @@
1
+ # dbt.tests.fixtures directory