dvt-core 0.52.2__cp310-cp310-macosx_10_9_x86_64.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.
Files changed (275) hide show
  1. dbt/__init__.py +7 -0
  2. dbt/_pydantic_shim.py +26 -0
  3. dbt/artifacts/__init__.py +0 -0
  4. dbt/artifacts/exceptions/__init__.py +1 -0
  5. dbt/artifacts/exceptions/schemas.py +31 -0
  6. dbt/artifacts/resources/__init__.py +116 -0
  7. dbt/artifacts/resources/base.py +67 -0
  8. dbt/artifacts/resources/types.py +93 -0
  9. dbt/artifacts/resources/v1/analysis.py +10 -0
  10. dbt/artifacts/resources/v1/catalog.py +23 -0
  11. dbt/artifacts/resources/v1/components.py +274 -0
  12. dbt/artifacts/resources/v1/config.py +277 -0
  13. dbt/artifacts/resources/v1/documentation.py +11 -0
  14. dbt/artifacts/resources/v1/exposure.py +51 -0
  15. dbt/artifacts/resources/v1/function.py +52 -0
  16. dbt/artifacts/resources/v1/generic_test.py +31 -0
  17. dbt/artifacts/resources/v1/group.py +21 -0
  18. dbt/artifacts/resources/v1/hook.py +11 -0
  19. dbt/artifacts/resources/v1/macro.py +29 -0
  20. dbt/artifacts/resources/v1/metric.py +172 -0
  21. dbt/artifacts/resources/v1/model.py +145 -0
  22. dbt/artifacts/resources/v1/owner.py +10 -0
  23. dbt/artifacts/resources/v1/saved_query.py +111 -0
  24. dbt/artifacts/resources/v1/seed.py +41 -0
  25. dbt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  26. dbt/artifacts/resources/v1/semantic_model.py +314 -0
  27. dbt/artifacts/resources/v1/singular_test.py +14 -0
  28. dbt/artifacts/resources/v1/snapshot.py +91 -0
  29. dbt/artifacts/resources/v1/source_definition.py +84 -0
  30. dbt/artifacts/resources/v1/sql_operation.py +10 -0
  31. dbt/artifacts/resources/v1/unit_test_definition.py +77 -0
  32. dbt/artifacts/schemas/__init__.py +0 -0
  33. dbt/artifacts/schemas/base.py +191 -0
  34. dbt/artifacts/schemas/batch_results.py +24 -0
  35. dbt/artifacts/schemas/catalog/__init__.py +11 -0
  36. dbt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  37. dbt/artifacts/schemas/catalog/v1/catalog.py +59 -0
  38. dbt/artifacts/schemas/freshness/__init__.py +1 -0
  39. dbt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  40. dbt/artifacts/schemas/freshness/v3/freshness.py +158 -0
  41. dbt/artifacts/schemas/manifest/__init__.py +2 -0
  42. dbt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  43. dbt/artifacts/schemas/manifest/v12/manifest.py +211 -0
  44. dbt/artifacts/schemas/results.py +147 -0
  45. dbt/artifacts/schemas/run/__init__.py +2 -0
  46. dbt/artifacts/schemas/run/v5/__init__.py +0 -0
  47. dbt/artifacts/schemas/run/v5/run.py +184 -0
  48. dbt/artifacts/schemas/upgrades/__init__.py +4 -0
  49. dbt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  50. dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  51. dbt/artifacts/utils/validation.py +153 -0
  52. dbt/cli/__init__.py +1 -0
  53. dbt/cli/context.py +17 -0
  54. dbt/cli/exceptions.py +57 -0
  55. dbt/cli/flags.py +560 -0
  56. dbt/cli/main.py +2039 -0
  57. dbt/cli/option_types.py +121 -0
  58. dbt/cli/options.py +80 -0
  59. dbt/cli/params.py +804 -0
  60. dbt/cli/requires.py +490 -0
  61. dbt/cli/resolvers.py +50 -0
  62. dbt/cli/types.py +40 -0
  63. dbt/clients/__init__.py +0 -0
  64. dbt/clients/checked_load.py +83 -0
  65. dbt/clients/git.py +164 -0
  66. dbt/clients/jinja.py +206 -0
  67. dbt/clients/jinja_static.py +245 -0
  68. dbt/clients/registry.py +192 -0
  69. dbt/clients/yaml_helper.py +68 -0
  70. dbt/compilation.py +876 -0
  71. dbt/compute/__init__.py +14 -0
  72. dbt/compute/engines/__init__.py +12 -0
  73. dbt/compute/engines/spark_engine.py +624 -0
  74. dbt/compute/federated_executor.py +837 -0
  75. dbt/compute/filter_pushdown.cpython-310-darwin.so +0 -0
  76. dbt/compute/filter_pushdown.py +273 -0
  77. dbt/compute/jar_provisioning.cpython-310-darwin.so +0 -0
  78. dbt/compute/jar_provisioning.py +255 -0
  79. dbt/compute/java_compat.cpython-310-darwin.so +0 -0
  80. dbt/compute/java_compat.py +689 -0
  81. dbt/compute/jdbc_utils.cpython-310-darwin.so +0 -0
  82. dbt/compute/jdbc_utils.py +678 -0
  83. dbt/compute/smart_selector.cpython-310-darwin.so +0 -0
  84. dbt/compute/smart_selector.py +311 -0
  85. dbt/compute/strategies/__init__.py +54 -0
  86. dbt/compute/strategies/base.py +165 -0
  87. dbt/compute/strategies/dataproc.py +207 -0
  88. dbt/compute/strategies/emr.py +203 -0
  89. dbt/compute/strategies/local.py +364 -0
  90. dbt/compute/strategies/standalone.py +262 -0
  91. dbt/config/__init__.py +4 -0
  92. dbt/config/catalogs.py +94 -0
  93. dbt/config/compute.cpython-310-darwin.so +0 -0
  94. dbt/config/compute.py +547 -0
  95. dbt/config/dvt_profile.cpython-310-darwin.so +0 -0
  96. dbt/config/dvt_profile.py +342 -0
  97. dbt/config/profile.py +422 -0
  98. dbt/config/project.py +873 -0
  99. dbt/config/project_utils.py +28 -0
  100. dbt/config/renderer.py +231 -0
  101. dbt/config/runtime.py +553 -0
  102. dbt/config/selectors.py +208 -0
  103. dbt/config/utils.py +77 -0
  104. dbt/constants.py +28 -0
  105. dbt/context/__init__.py +0 -0
  106. dbt/context/base.py +745 -0
  107. dbt/context/configured.py +135 -0
  108. dbt/context/context_config.py +382 -0
  109. dbt/context/docs.py +82 -0
  110. dbt/context/exceptions_jinja.py +178 -0
  111. dbt/context/macro_resolver.py +195 -0
  112. dbt/context/macros.py +171 -0
  113. dbt/context/manifest.py +72 -0
  114. dbt/context/providers.py +2249 -0
  115. dbt/context/query_header.py +13 -0
  116. dbt/context/secret.py +58 -0
  117. dbt/context/target.py +74 -0
  118. dbt/contracts/__init__.py +0 -0
  119. dbt/contracts/files.py +413 -0
  120. dbt/contracts/graph/__init__.py +0 -0
  121. dbt/contracts/graph/manifest.py +1904 -0
  122. dbt/contracts/graph/metrics.py +97 -0
  123. dbt/contracts/graph/model_config.py +70 -0
  124. dbt/contracts/graph/node_args.py +42 -0
  125. dbt/contracts/graph/nodes.py +1806 -0
  126. dbt/contracts/graph/semantic_manifest.py +232 -0
  127. dbt/contracts/graph/unparsed.py +811 -0
  128. dbt/contracts/project.py +417 -0
  129. dbt/contracts/results.py +53 -0
  130. dbt/contracts/selection.py +23 -0
  131. dbt/contracts/sql.py +85 -0
  132. dbt/contracts/state.py +68 -0
  133. dbt/contracts/util.py +46 -0
  134. dbt/deprecations.py +346 -0
  135. dbt/deps/__init__.py +0 -0
  136. dbt/deps/base.py +152 -0
  137. dbt/deps/git.py +195 -0
  138. dbt/deps/local.py +79 -0
  139. dbt/deps/registry.py +130 -0
  140. dbt/deps/resolver.py +149 -0
  141. dbt/deps/tarball.py +120 -0
  142. dbt/docs/source/_ext/dbt_click.py +119 -0
  143. dbt/docs/source/conf.py +32 -0
  144. dbt/env_vars.py +64 -0
  145. dbt/event_time/event_time.py +40 -0
  146. dbt/event_time/sample_window.py +60 -0
  147. dbt/events/__init__.py +15 -0
  148. dbt/events/base_types.py +36 -0
  149. dbt/events/core_types_pb2.py +2 -0
  150. dbt/events/logging.py +108 -0
  151. dbt/events/types.py +2516 -0
  152. dbt/exceptions.py +1486 -0
  153. dbt/flags.py +89 -0
  154. dbt/graph/__init__.py +11 -0
  155. dbt/graph/cli.py +247 -0
  156. dbt/graph/graph.py +172 -0
  157. dbt/graph/queue.py +214 -0
  158. dbt/graph/selector.py +374 -0
  159. dbt/graph/selector_methods.py +975 -0
  160. dbt/graph/selector_spec.py +222 -0
  161. dbt/graph/thread_pool.py +18 -0
  162. dbt/hooks.py +21 -0
  163. dbt/include/README.md +49 -0
  164. dbt/include/__init__.py +3 -0
  165. dbt/include/starter_project/.gitignore +4 -0
  166. dbt/include/starter_project/README.md +15 -0
  167. dbt/include/starter_project/__init__.py +3 -0
  168. dbt/include/starter_project/analyses/.gitkeep +0 -0
  169. dbt/include/starter_project/dbt_project.yml +36 -0
  170. dbt/include/starter_project/macros/.gitkeep +0 -0
  171. dbt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
  172. dbt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
  173. dbt/include/starter_project/models/example/schema.yml +21 -0
  174. dbt/include/starter_project/seeds/.gitkeep +0 -0
  175. dbt/include/starter_project/snapshots/.gitkeep +0 -0
  176. dbt/include/starter_project/tests/.gitkeep +0 -0
  177. dbt/internal_deprecations.py +26 -0
  178. dbt/jsonschemas/__init__.py +3 -0
  179. dbt/jsonschemas/jsonschemas.py +309 -0
  180. dbt/jsonschemas/project/0.0.110.json +4717 -0
  181. dbt/jsonschemas/project/0.0.85.json +2015 -0
  182. dbt/jsonschemas/resources/0.0.110.json +2636 -0
  183. dbt/jsonschemas/resources/0.0.85.json +2536 -0
  184. dbt/jsonschemas/resources/latest.json +6773 -0
  185. dbt/links.py +4 -0
  186. dbt/materializations/__init__.py +0 -0
  187. dbt/materializations/incremental/__init__.py +0 -0
  188. dbt/materializations/incremental/microbatch.py +236 -0
  189. dbt/mp_context.py +8 -0
  190. dbt/node_types.py +37 -0
  191. dbt/parser/__init__.py +23 -0
  192. dbt/parser/analysis.py +21 -0
  193. dbt/parser/base.py +548 -0
  194. dbt/parser/common.py +266 -0
  195. dbt/parser/docs.py +52 -0
  196. dbt/parser/fixtures.py +51 -0
  197. dbt/parser/functions.py +30 -0
  198. dbt/parser/generic_test.py +100 -0
  199. dbt/parser/generic_test_builders.py +333 -0
  200. dbt/parser/hooks.py +118 -0
  201. dbt/parser/macros.py +137 -0
  202. dbt/parser/manifest.py +2204 -0
  203. dbt/parser/models.py +573 -0
  204. dbt/parser/partial.py +1178 -0
  205. dbt/parser/read_files.py +445 -0
  206. dbt/parser/schema_generic_tests.py +422 -0
  207. dbt/parser/schema_renderer.py +111 -0
  208. dbt/parser/schema_yaml_readers.py +935 -0
  209. dbt/parser/schemas.py +1466 -0
  210. dbt/parser/search.py +149 -0
  211. dbt/parser/seeds.py +28 -0
  212. dbt/parser/singular_test.py +20 -0
  213. dbt/parser/snapshots.py +44 -0
  214. dbt/parser/sources.py +558 -0
  215. dbt/parser/sql.py +62 -0
  216. dbt/parser/unit_tests.py +621 -0
  217. dbt/plugins/__init__.py +20 -0
  218. dbt/plugins/contracts.py +9 -0
  219. dbt/plugins/exceptions.py +2 -0
  220. dbt/plugins/manager.py +163 -0
  221. dbt/plugins/manifest.py +21 -0
  222. dbt/profiler.py +20 -0
  223. dbt/py.typed +1 -0
  224. dbt/query_analyzer.cpython-310-darwin.so +0 -0
  225. dbt/query_analyzer.py +410 -0
  226. dbt/runners/__init__.py +2 -0
  227. dbt/runners/exposure_runner.py +7 -0
  228. dbt/runners/no_op_runner.py +45 -0
  229. dbt/runners/saved_query_runner.py +7 -0
  230. dbt/selected_resources.py +8 -0
  231. dbt/task/__init__.py +0 -0
  232. dbt/task/base.py +503 -0
  233. dbt/task/build.py +197 -0
  234. dbt/task/clean.py +56 -0
  235. dbt/task/clone.py +161 -0
  236. dbt/task/compile.py +150 -0
  237. dbt/task/compute.py +454 -0
  238. dbt/task/debug.py +505 -0
  239. dbt/task/deps.py +280 -0
  240. dbt/task/docs/__init__.py +3 -0
  241. dbt/task/docs/generate.py +660 -0
  242. dbt/task/docs/index.html +250 -0
  243. dbt/task/docs/serve.py +29 -0
  244. dbt/task/freshness.py +322 -0
  245. dbt/task/function.py +121 -0
  246. dbt/task/group_lookup.py +46 -0
  247. dbt/task/init.py +553 -0
  248. dbt/task/java.py +316 -0
  249. dbt/task/list.py +236 -0
  250. dbt/task/printer.py +175 -0
  251. dbt/task/retry.py +175 -0
  252. dbt/task/run.py +1306 -0
  253. dbt/task/run_operation.py +141 -0
  254. dbt/task/runnable.py +758 -0
  255. dbt/task/seed.py +103 -0
  256. dbt/task/show.py +149 -0
  257. dbt/task/snapshot.py +56 -0
  258. dbt/task/spark.py +414 -0
  259. dbt/task/sql.py +110 -0
  260. dbt/task/target_sync.py +759 -0
  261. dbt/task/test.py +464 -0
  262. dbt/tests/fixtures/__init__.py +1 -0
  263. dbt/tests/fixtures/project.py +620 -0
  264. dbt/tests/util.py +651 -0
  265. dbt/tracking.py +529 -0
  266. dbt/utils/__init__.py +3 -0
  267. dbt/utils/artifact_upload.py +151 -0
  268. dbt/utils/utils.py +408 -0
  269. dbt/version.py +268 -0
  270. dvt_cli/__init__.py +72 -0
  271. dvt_core-0.52.2.dist-info/METADATA +286 -0
  272. dvt_core-0.52.2.dist-info/RECORD +275 -0
  273. dvt_core-0.52.2.dist-info/WHEEL +5 -0
  274. dvt_core-0.52.2.dist-info/entry_points.txt +2 -0
  275. dvt_core-0.52.2.dist-info/top_level.txt +2 -0
@@ -0,0 +1,621 @@
1
+ import csv
2
+ import os
3
+ from copy import deepcopy
4
+ from csv import DictReader
5
+ from io import StringIO
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Set
8
+
9
+ from dbt import utils
10
+ from dbt.artifacts.resources import ModelConfig, UnitTestConfig, UnitTestFormat
11
+ from dbt.config import RuntimeConfig
12
+ from dbt.context.context_config import ContextConfig
13
+ from dbt.context.providers import generate_parse_exposure, get_rendered
14
+ from dbt.contracts.files import FileHash, SchemaSourceFile
15
+ from dbt.contracts.graph.manifest import Manifest
16
+ from dbt.contracts.graph.model_config import UnitTestNodeConfig
17
+ from dbt.contracts.graph.nodes import (
18
+ DependsOn,
19
+ ModelNode,
20
+ UnitTestDefinition,
21
+ UnitTestNode,
22
+ UnitTestSourceDefinition,
23
+ )
24
+ from dbt.contracts.graph.unparsed import UnparsedUnitTest
25
+ from dbt.exceptions import InvalidUnitTestGivenInput, ParsingError
26
+ from dbt.graph import UniqueId
27
+ from dbt.node_types import NodeType
28
+ from dbt.parser.schemas import (
29
+ JSONValidationError,
30
+ ParseResult,
31
+ SchemaParser,
32
+ ValidationError,
33
+ YamlBlock,
34
+ YamlParseDictError,
35
+ YamlReader,
36
+ )
37
+ from dbt.utils import get_pseudo_test_path
38
+ from dbt_common.events.functions import fire_event
39
+ from dbt_common.events.types import SystemStdErr
40
+ from dbt_extractor import ExtractionError, py_extract_from_source # type: ignore
41
+
42
+
43
+ class UnitTestManifestLoader:
44
+ def __init__(self, manifest, root_project, selected) -> None:
45
+ self.manifest: Manifest = manifest
46
+ self.root_project: RuntimeConfig = root_project
47
+ # selected comes from the initial selection against a "regular" manifest
48
+ self.selected: Set[UniqueId] = selected
49
+ self.unit_test_manifest = Manifest(macros=manifest.macros)
50
+
51
+ def load(self) -> Manifest:
52
+ for unique_id in self.selected:
53
+ if unique_id in self.manifest.unit_tests:
54
+ unit_test_case: UnitTestDefinition = self.manifest.unit_tests[unique_id]
55
+ if not unit_test_case.config.enabled:
56
+ continue
57
+ self.parse_unit_test_case(unit_test_case)
58
+ return self.unit_test_manifest
59
+
60
+ def parse_unit_test_case(self, test_case: UnitTestDefinition):
61
+ # Create unit test node based on the node being tested
62
+ # The tested_node has already been resolved and is in depends_on.nodes
63
+ tested_node_unique_id = test_case.depends_on.nodes[0]
64
+ tested_node = self.manifest.nodes[tested_node_unique_id]
65
+ assert isinstance(tested_node, ModelNode)
66
+
67
+ # Create UnitTestNode based on model being tested. Since selection has
68
+ # already been done, we don't have to care about fields that are necessary
69
+ # for selection.
70
+ # Note: no depends_on, that's added later using input nodes
71
+ name = test_case.name
72
+ if tested_node.is_versioned:
73
+ name = name + f"_v{tested_node.version}"
74
+ expected_sql: Optional[str] = None
75
+ if test_case.expect.format == UnitTestFormat.SQL:
76
+ expected_rows: List[Dict[str, Any]] = []
77
+ expected_sql = test_case.expect.rows # type: ignore
78
+ else:
79
+ assert isinstance(test_case.expect.rows, List)
80
+ expected_rows = deepcopy(test_case.expect.rows)
81
+
82
+ assert isinstance(expected_rows, List)
83
+ unit_test_node = UnitTestNode(
84
+ name=name,
85
+ resource_type=NodeType.Unit,
86
+ package_name=test_case.package_name,
87
+ path=get_pseudo_test_path(name, test_case.original_file_path),
88
+ original_file_path=test_case.original_file_path,
89
+ unique_id=test_case.unique_id,
90
+ config=UnitTestNodeConfig(
91
+ materialized="unit", expected_rows=expected_rows, expected_sql=expected_sql
92
+ ),
93
+ raw_code=tested_node.raw_code,
94
+ database=tested_node.database,
95
+ schema=tested_node.schema,
96
+ alias=name,
97
+ fqn=test_case.unique_id.split("."),
98
+ checksum=FileHash.empty(),
99
+ tested_node_unique_id=tested_node.unique_id,
100
+ overrides=test_case.overrides,
101
+ )
102
+
103
+ ctx = generate_parse_exposure(
104
+ unit_test_node, # type: ignore
105
+ self.root_project,
106
+ self.manifest,
107
+ test_case.package_name,
108
+ )
109
+ get_rendered(unit_test_node.raw_code, ctx, unit_test_node, capture_macros=True)
110
+ # unit_test_node now has a populated refs/sources
111
+
112
+ self.unit_test_manifest.nodes[unit_test_node.unique_id] = unit_test_node
113
+ # Now create input_nodes for the test inputs
114
+ """
115
+ given:
116
+ - input: ref('my_model_a')
117
+ rows: []
118
+ - input: ref('my_model_b')
119
+ rows:
120
+ - {id: 1, b: 2}
121
+ - {id: 2, b: 2}
122
+ """
123
+ # Add the model "input" nodes, consisting of all referenced models in the unit test.
124
+ # This creates an ephemeral model for every input in every test, so there may be multiple
125
+ # input models substituting for the same input ref'd model. Note that since these are
126
+ # always "ephemeral" they just wrap the tested_node SQL in additional CTEs. No actual table
127
+ # or view is created.
128
+ for given in test_case.given:
129
+ # extract the original_input_node from the ref in the "input" key of the given list
130
+ original_input_node = self._get_original_input_node(
131
+ given.input, tested_node, test_case.name
132
+ )
133
+ input_name = original_input_node.name
134
+ common_fields = {
135
+ "resource_type": NodeType.Model,
136
+ # root directory for input and output fixtures
137
+ "original_file_path": unit_test_node.original_file_path,
138
+ "config": ModelConfig(materialized="ephemeral"),
139
+ "database": original_input_node.database,
140
+ "alias": original_input_node.identifier,
141
+ "schema": original_input_node.schema,
142
+ "fqn": original_input_node.fqn,
143
+ "checksum": FileHash.empty(),
144
+ "raw_code": self._build_fixture_raw_code(given.rows, None, given.format),
145
+ "package_name": original_input_node.package_name,
146
+ "unique_id": f"model.{original_input_node.package_name}.{input_name}",
147
+ "name": input_name,
148
+ "path": f"{input_name}.sql",
149
+ }
150
+ resource_type = original_input_node.resource_type
151
+
152
+ if resource_type in (
153
+ NodeType.Model,
154
+ NodeType.Seed,
155
+ NodeType.Snapshot,
156
+ ):
157
+
158
+ input_node = ModelNode(
159
+ **common_fields,
160
+ defer_relation=original_input_node.defer_relation,
161
+ )
162
+ if resource_type == NodeType.Model:
163
+ if original_input_node.version:
164
+ input_node.version = original_input_node.version
165
+ if original_input_node.latest_version:
166
+ input_node.latest_version = original_input_node.latest_version
167
+
168
+ elif resource_type == NodeType.Source:
169
+ # We are reusing the database/schema/identifier from the original source,
170
+ # but that shouldn't matter since this acts as an ephemeral model which just
171
+ # wraps a CTE around the unit test node.
172
+ input_node = UnitTestSourceDefinition(
173
+ **common_fields,
174
+ source_name=original_input_node.source_name, # needed for source lookup
175
+ )
176
+ # Sources need to go in the sources dictionary in order to create the right lookup
177
+ self.unit_test_manifest.sources[input_node.unique_id] = input_node # type: ignore
178
+
179
+ # Both ModelNode and UnitTestSourceDefinition need to go in nodes dictionary
180
+ self.unit_test_manifest.nodes[input_node.unique_id] = input_node
181
+
182
+ # Populate this_input_node_unique_id if input fixture represents node being tested
183
+ if original_input_node == tested_node:
184
+ unit_test_node.this_input_node_unique_id = input_node.unique_id
185
+
186
+ # Add unique ids of input_nodes to depends_on
187
+ unit_test_node.depends_on.nodes.append(input_node.unique_id)
188
+
189
+ # Add functions to the manifest and depends_on
190
+ for unique_id in tested_node.depends_on.nodes:
191
+ if unique_id in self.manifest.functions:
192
+ unit_test_node.depends_on.nodes.append(unique_id)
193
+ self.unit_test_manifest.functions[unique_id] = self.manifest.functions[unique_id]
194
+
195
+ def _build_fixture_raw_code(self, rows, column_name_to_data_types, fixture_format) -> str:
196
+ # We're not currently using column_name_to_data_types, but leaving here for
197
+ # possible future use.
198
+ if fixture_format == UnitTestFormat.SQL:
199
+ return rows
200
+ else:
201
+ return ("{{{{ get_fixture_sql({rows}, {column_name_to_data_types}) }}}}").format(
202
+ rows=rows, column_name_to_data_types=column_name_to_data_types
203
+ )
204
+
205
+ def _get_original_input_node(self, input: str, tested_node: ModelNode, test_case_name: str):
206
+ """
207
+ Returns the original input node as defined in the project given an input reference
208
+ and the node being tested.
209
+
210
+ input: str representing how input node is referenced in tested model sql
211
+ * examples:
212
+ - "ref('my_model_a')"
213
+ - "source('my_source_schema', 'my_source_name')"
214
+ - "this"
215
+ tested_node: ModelNode of representing node being tested
216
+ """
217
+ if input.strip() == "this":
218
+ original_input_node = tested_node
219
+ else:
220
+ try:
221
+ statically_parsed = py_extract_from_source(f"{{{{ {input} }}}}")
222
+ except ExtractionError:
223
+ raise InvalidUnitTestGivenInput(input=input)
224
+
225
+ if statically_parsed["refs"]:
226
+ ref = list(statically_parsed["refs"])[0]
227
+ name = ref.get("name")
228
+ package = ref.get("package")
229
+ version = ref.get("version")
230
+ # TODO: disabled lookup, versioned lookup, public models
231
+ original_input_node = self.manifest.ref_lookup.find(
232
+ name, package, version, self.manifest
233
+ )
234
+ elif statically_parsed["sources"]:
235
+ source = list(statically_parsed["sources"])[0]
236
+ input_source_name, input_name = source
237
+ original_input_node = self.manifest.source_lookup.find(
238
+ f"{input_source_name}.{input_name}",
239
+ None,
240
+ self.manifest,
241
+ )
242
+ else:
243
+ raise InvalidUnitTestGivenInput(input=input)
244
+
245
+ if not original_input_node:
246
+ msg = f"Unit test '{test_case_name}' had an input ({input}) which was not found in the manifest."
247
+ raise ParsingError(msg)
248
+
249
+ return original_input_node
250
+
251
+
252
+ class UnitTestParser(YamlReader):
253
+ def __init__(self, schema_parser: SchemaParser, yaml: YamlBlock) -> None:
254
+ super().__init__(schema_parser, yaml, "unit_tests")
255
+ self.schema_parser = schema_parser
256
+ self.yaml = yaml
257
+
258
+ def parse(self) -> ParseResult:
259
+ for data in self.get_key_dicts():
260
+ unit_test: UnparsedUnitTest = self._get_unit_test(data)
261
+ tested_model_node = find_tested_model_node(
262
+ self.manifest, self.project.project_name, unit_test.model
263
+ )
264
+ unit_test_case_unique_id = (
265
+ f"{NodeType.Unit}.{self.project.project_name}.{unit_test.model}.{unit_test.name}"
266
+ )
267
+ unit_test_fqn = self._build_fqn(
268
+ self.project.project_name,
269
+ self.yaml.path.original_file_path,
270
+ unit_test.model,
271
+ unit_test.name,
272
+ )
273
+ unit_test_config = self._build_unit_test_config(unit_test_fqn, unit_test.config)
274
+
275
+ unit_test_definition = UnitTestDefinition(
276
+ name=unit_test.name,
277
+ model=unit_test.model,
278
+ resource_type=NodeType.Unit,
279
+ package_name=self.project.project_name,
280
+ path=self.yaml.path.relative_path,
281
+ original_file_path=self.yaml.path.original_file_path,
282
+ unique_id=unit_test_case_unique_id,
283
+ given=unit_test.given,
284
+ expect=unit_test.expect,
285
+ description=unit_test.description,
286
+ overrides=unit_test.overrides,
287
+ depends_on=DependsOn(),
288
+ fqn=unit_test_fqn,
289
+ config=unit_test_config,
290
+ versions=unit_test.versions,
291
+ )
292
+
293
+ if tested_model_node:
294
+ unit_test_definition.depends_on.nodes.append(tested_model_node.unique_id)
295
+ unit_test_definition.schema = tested_model_node.schema
296
+
297
+ # Check that format and type of rows matches for each given input,
298
+ # convert rows to a list of dictionaries, and add the unique_id of
299
+ # the unit_test_definition to the fixture source_file for partial parsing.
300
+ self._validate_and_normalize_given(unit_test_definition)
301
+ self._validate_and_normalize_expect(unit_test_definition)
302
+
303
+ # for calculating state:modified
304
+ unit_test_definition.build_unit_test_checksum()
305
+ assert isinstance(self.yaml.file, SchemaSourceFile)
306
+ if unit_test_config.enabled:
307
+ self.manifest.add_unit_test(self.yaml.file, unit_test_definition)
308
+ else:
309
+ self.manifest.add_disabled(self.yaml.file, unit_test_definition)
310
+
311
+ return ParseResult()
312
+
313
+ def _get_unit_test(self, data: Dict[str, Any]) -> UnparsedUnitTest:
314
+ try:
315
+ UnparsedUnitTest.validate(data)
316
+ return UnparsedUnitTest.from_dict(data)
317
+ except (ValidationError, JSONValidationError) as exc:
318
+ raise YamlParseDictError(self.yaml.path, self.key, data, exc)
319
+
320
+ def _build_unit_test_config(
321
+ self, unit_test_fqn: List[str], config_dict: Dict[str, Any]
322
+ ) -> UnitTestConfig:
323
+ config = ContextConfig(
324
+ self.schema_parser.root_project,
325
+ unit_test_fqn,
326
+ NodeType.Unit,
327
+ self.schema_parser.project.project_name,
328
+ )
329
+ unit_test_config_dict = config.build_config_dict(patch_config_dict=config_dict)
330
+ unit_test_config_dict = self.render_entry(unit_test_config_dict)
331
+
332
+ return UnitTestConfig.from_dict(unit_test_config_dict)
333
+
334
+ def _build_fqn(self, package_name, original_file_path, model_name, test_name):
335
+ # This code comes from "get_fqn" and "get_fqn_prefix" in the base parser.
336
+ # We need to get the directories underneath the model-path.
337
+ path = Path(original_file_path)
338
+ relative_path = str(path.relative_to(*path.parts[:1]))
339
+ no_ext = os.path.splitext(relative_path)[0]
340
+ fqn = [package_name]
341
+ fqn.extend(utils.split_path(no_ext)[:-1])
342
+ fqn.append(model_name)
343
+ fqn.append(test_name)
344
+ return fqn
345
+
346
+ def _get_fixture(self, fixture_name: str, project_name: str):
347
+ fixture_unique_id = f"{NodeType.Fixture}.{project_name}.{fixture_name}"
348
+ if fixture_unique_id in self.manifest.fixtures:
349
+ fixture = self.manifest.fixtures[fixture_unique_id]
350
+ return fixture
351
+ else:
352
+ raise ParsingError(
353
+ f"File not found for fixture '{fixture_name}' in unit tests in {self.yaml.path.original_file_path}"
354
+ )
355
+
356
+ def _validate_and_normalize_given(self, unit_test_definition):
357
+ for ut_input in unit_test_definition.given:
358
+ self._validate_and_normalize_rows(ut_input, unit_test_definition, "input")
359
+
360
+ def _validate_and_normalize_expect(self, unit_test_definition):
361
+ self._validate_and_normalize_rows(
362
+ unit_test_definition.expect, unit_test_definition, "expected"
363
+ )
364
+
365
+ def _validate_and_normalize_rows(self, ut_fixture, unit_test_definition, fixture_type) -> None:
366
+ if ut_fixture.format == UnitTestFormat.Dict:
367
+ if ut_fixture.rows is None and ut_fixture.fixture is None: # This is a seed
368
+ ut_fixture.rows = self._load_rows_from_seed(ut_fixture.input)
369
+ if not isinstance(ut_fixture.rows, list):
370
+ raise ParsingError(
371
+ f"Unit test {unit_test_definition.name} has {fixture_type} rows "
372
+ f"which do not match format {ut_fixture.format}"
373
+ )
374
+ elif ut_fixture.format == UnitTestFormat.CSV:
375
+ if not (isinstance(ut_fixture.rows, str) or isinstance(ut_fixture.fixture, str)):
376
+ raise ParsingError(
377
+ f"Unit test {unit_test_definition.name} has {fixture_type} rows or fixtures "
378
+ f"which do not match format {ut_fixture.format}. Expected string."
379
+ )
380
+
381
+ if ut_fixture.fixture:
382
+ csv_rows = self.get_fixture_file_rows(
383
+ ut_fixture.fixture, self.project.project_name, unit_test_definition.unique_id
384
+ )
385
+ else:
386
+ csv_rows = self._convert_csv_to_list_of_dicts(ut_fixture.rows)
387
+
388
+ # Empty values (e.g. ,,) in a csv fixture should default to null, not ""
389
+ ut_fixture.rows = [
390
+ {k: (None if v == "" else v) for k, v in row.items()} for row in csv_rows
391
+ ]
392
+
393
+ elif ut_fixture.format == UnitTestFormat.SQL:
394
+ if not (isinstance(ut_fixture.rows, str) or isinstance(ut_fixture.fixture, str)):
395
+ raise ParsingError(
396
+ f"Unit test {unit_test_definition.name} has {fixture_type} rows or fixtures "
397
+ f"which do not match format {ut_fixture.format}. Expected string."
398
+ )
399
+
400
+ if ut_fixture.fixture:
401
+ ut_fixture.rows = self.get_fixture_file_rows(
402
+ ut_fixture.fixture, self.project.project_name, unit_test_definition.unique_id
403
+ )
404
+
405
+ # sanitize order of input
406
+ if ut_fixture.rows and (
407
+ ut_fixture.format == UnitTestFormat.Dict or ut_fixture.format == UnitTestFormat.CSV
408
+ ):
409
+ self._promote_first_non_none_row(ut_fixture)
410
+
411
+ def _promote_first_non_none_row(self, ut_fixture):
412
+ """
413
+ Promote the first row with no None values to the top of the ut_fixture.rows list.
414
+
415
+ This function modifies the ut_fixture object in place.
416
+
417
+ Needed for databases like Redshift which uses the first value in a column to determine
418
+ the column type. If the first value is None, the type is assumed to be VARCHAR(1).
419
+ This leads to obscure type mismatch errors centered on a unit test fixture's `expect`.
420
+ See https://github.com/dbt-labs/dbt-redshift/issues/821 for more info.
421
+ """
422
+ non_none_row_index = None
423
+
424
+ # Iterate through each row and its index
425
+ for index, row in enumerate(ut_fixture.rows):
426
+ # Check if all values in the row are not None
427
+ if all(value is not None for value in row.values()):
428
+ non_none_row_index = index
429
+ break
430
+
431
+ if non_none_row_index is None:
432
+ fire_event(
433
+ SystemStdErr(
434
+ bmsg="Unit Test fixtures benefit from having at least one row free of Null values to ensure consistent column types. Failure to meet this recommendation can result in type mismatch errors between unit test source models and `expected` fixtures."
435
+ )
436
+ )
437
+ else:
438
+ ut_fixture.rows[0], ut_fixture.rows[non_none_row_index] = (
439
+ ut_fixture.rows[non_none_row_index],
440
+ ut_fixture.rows[0],
441
+ )
442
+
443
+ def get_fixture_file_rows(self, fixture_name, project_name, utdef_unique_id):
444
+ # find fixture file object and store unit_test_definition unique_id
445
+ fixture = self._get_fixture(fixture_name, project_name)
446
+ fixture_source_file = self.manifest.files[fixture.file_id]
447
+ fixture_source_file.unit_tests.append(utdef_unique_id)
448
+ return fixture.rows
449
+
450
+ def _convert_csv_to_list_of_dicts(self, csv_string: str) -> List[Dict[str, Any]]:
451
+ dummy_file = StringIO(csv_string)
452
+ reader = csv.DictReader(dummy_file)
453
+ rows = []
454
+ for row in reader:
455
+ rows.append(row)
456
+ return rows
457
+
458
+ def _load_rows_from_seed(self, ref_str: str) -> List[Dict[str, Any]]:
459
+ """Read rows from seed file on disk if not specified in YAML config. If seed file doesn't exist, return empty list."""
460
+ ref = py_extract_from_source("{{ " + ref_str + " }}")["refs"][0]
461
+
462
+ rows: List[Dict[str, Any]] = []
463
+
464
+ seed_name = ref["name"]
465
+ package_name = ref.get("package", self.project.project_name)
466
+
467
+ seed_node = self.manifest.ref_lookup.find(seed_name, package_name, None, self.manifest)
468
+
469
+ if not seed_node or seed_node.resource_type != NodeType.Seed:
470
+ # Seed not found in custom package specified
471
+ if package_name != self.project.project_name:
472
+ raise ParsingError(
473
+ f"Unable to find seed '{package_name}.{seed_name}' for unit tests in '{package_name}' package"
474
+ )
475
+ else:
476
+ raise ParsingError(
477
+ f"Unable to find seed '{package_name}.{seed_name}' for unit tests in directories: {self.project.seed_paths}"
478
+ )
479
+
480
+ seed_path = Path(self.project.project_root) / seed_node.original_file_path
481
+ with open(seed_path, "r") as f:
482
+ for row in DictReader(f):
483
+ rows.append(row)
484
+
485
+ return rows
486
+
487
+
488
+ def find_tested_model_node(
489
+ manifest: Manifest, current_project: str, unit_test_model: str
490
+ ) -> Optional[ModelNode]:
491
+ model_name_split = unit_test_model.split()
492
+ model_name = model_name_split[0]
493
+ model_version = model_name_split[1] if len(model_name_split) == 2 else None
494
+
495
+ tested_node = manifest.ref_lookup.find(model_name, current_project, model_version, manifest)
496
+ return tested_node
497
+
498
+
499
+ # This is called by the ManifestLoader after other processing has been done,
500
+ # so that model versions are available.
501
+ def process_models_for_unit_test(
502
+ manifest: Manifest, current_project: str, unit_test_def: UnitTestDefinition, models_to_versions
503
+ ):
504
+ # If the unit tests doesn't have a depends_on.nodes[0] then we weren't able to resolve
505
+ # the model, either because versions hadn't been processed yet, or it's not a valid model name
506
+ if not unit_test_def.depends_on.nodes:
507
+ tested_node = find_tested_model_node(manifest, current_project, unit_test_def.model)
508
+ if not tested_node:
509
+ raise ParsingError(
510
+ f"Unable to find model '{current_project}.{unit_test_def.model}' for "
511
+ f"unit test '{unit_test_def.name}' in {unit_test_def.original_file_path}"
512
+ )
513
+ unit_test_def.depends_on.nodes.append(tested_node.unique_id)
514
+ unit_test_def.schema = tested_node.schema
515
+
516
+ # The UnitTestDefinition should only have one "depends_on" at this point,
517
+ # the one that's found by the "model" field.
518
+ target_model_id = unit_test_def.depends_on.nodes[0]
519
+ if target_model_id not in manifest.nodes:
520
+ if target_model_id in manifest.disabled:
521
+ # The model is disabled, so we don't need to do anything (#10540)
522
+ return
523
+ else:
524
+ # If we've reached here and the model is not disabled, throw an error
525
+ raise ParsingError(
526
+ f"Unit test '{unit_test_def.name}' references a model that does not exist: {target_model_id}"
527
+ )
528
+
529
+ target_model = manifest.nodes[target_model_id]
530
+ assert isinstance(target_model, ModelNode)
531
+
532
+ target_model_is_incremental = "macro.dbt.is_incremental" in target_model.depends_on.macros
533
+ unit_test_def_has_incremental_override = unit_test_def.overrides and isinstance(
534
+ unit_test_def.overrides.macros.get("is_incremental"), bool
535
+ )
536
+
537
+ if target_model_is_incremental and (not unit_test_def_has_incremental_override):
538
+ raise ParsingError(
539
+ f"Boolean override for 'is_incremental' must be provided for unit test '{unit_test_def.name}' in model '{target_model.name}'"
540
+ )
541
+
542
+ unit_test_def_incremental_override_true = (
543
+ unit_test_def.overrides and unit_test_def.overrides.macros.get("is_incremental")
544
+ )
545
+ unit_test_def_has_this_input = "this" in [i.input for i in unit_test_def.given]
546
+
547
+ if (
548
+ target_model_is_incremental
549
+ and unit_test_def_incremental_override_true
550
+ and (not unit_test_def_has_this_input)
551
+ ):
552
+ raise ParsingError(
553
+ f"Unit test '{unit_test_def.name}' for incremental model '{target_model.name}' must have a 'this' input"
554
+ )
555
+
556
+ # unit_test_versions = unit_test_def.versions
557
+ # We're setting up unit tests for versioned models, so if
558
+ # the model isn't versioned, we don't need to do anything
559
+ if not target_model.is_versioned:
560
+ if unit_test_def.versions and (
561
+ unit_test_def.versions.include or unit_test_def.versions.exclude
562
+ ):
563
+ # If model is not versioned, we should not have an include or exclude
564
+ msg = (
565
+ f"Unit test '{unit_test_def.name}' should not have a versions include or exclude "
566
+ f"when referencing non-versioned model '{target_model.name}'"
567
+ )
568
+ raise ParsingError(msg)
569
+ else:
570
+ return
571
+ versioned_models = []
572
+ if (
573
+ target_model.package_name in models_to_versions
574
+ and target_model.name in models_to_versions[target_model.package_name]
575
+ ):
576
+ versioned_models = models_to_versions[target_model.package_name][target_model.name]
577
+
578
+ versions_to_test = []
579
+ if unit_test_def.versions is None:
580
+ versions_to_test = versioned_models
581
+ elif unit_test_def.versions.exclude:
582
+ for model_unique_id in versioned_models:
583
+ model = manifest.nodes[model_unique_id]
584
+ assert isinstance(model, ModelNode)
585
+ if model.version in unit_test_def.versions.exclude:
586
+ continue
587
+ else:
588
+ versions_to_test.append(model.unique_id)
589
+ elif unit_test_def.versions.include:
590
+ for model_unique_id in versioned_models:
591
+ model = manifest.nodes[model_unique_id]
592
+ assert isinstance(model, ModelNode)
593
+ if model.version in unit_test_def.versions.include:
594
+ versions_to_test.append(model.unique_id)
595
+ else:
596
+ continue
597
+
598
+ if not versions_to_test:
599
+ msg = (
600
+ f"Unit test '{unit_test_def.name}' referenced a version of '{target_model.name}' "
601
+ "which was not found."
602
+ )
603
+ raise ParsingError(msg)
604
+ else:
605
+ # Create unit test definitions that match the model versions
606
+ original_unit_test_def = manifest.unit_tests.pop(unit_test_def.unique_id)
607
+ original_unit_test_dict = original_unit_test_def.to_dict()
608
+ schema_file = manifest.files[original_unit_test_def.file_id]
609
+ assert isinstance(schema_file, SchemaSourceFile)
610
+ schema_file.unit_tests.remove(original_unit_test_def.unique_id)
611
+ for versioned_model_unique_id in versions_to_test:
612
+ versioned_model = manifest.nodes[versioned_model_unique_id]
613
+ assert isinstance(versioned_model, ModelNode)
614
+ versioned_unit_test_unique_id = f"{NodeType.Unit}.{unit_test_def.package_name}.{unit_test_def.model}.{unit_test_def.name}_v{versioned_model.version}"
615
+ new_unit_test_def = UnitTestDefinition.from_dict(original_unit_test_dict)
616
+ new_unit_test_def.unique_id = versioned_unit_test_unique_id
617
+ new_unit_test_def.depends_on.nodes[0] = versioned_model_unique_id
618
+ new_unit_test_def.version = versioned_model.version
619
+ schema_file.unit_tests.append(versioned_unit_test_unique_id)
620
+ # fqn?
621
+ manifest.unit_tests[versioned_unit_test_unique_id] = new_unit_test_def
@@ -0,0 +1,20 @@
1
+ from typing import Optional
2
+
3
+ # these are just exports, they need "noqa" so flake8 will not complain.
4
+ from .manager import PluginManager, dbt_hook, dbtPlugin # noqa
5
+
6
+ PLUGIN_MANAGER: Optional[PluginManager] = None
7
+
8
+
9
+ def set_up_plugin_manager(project_name: str):
10
+ global PLUGIN_MANAGER
11
+ PLUGIN_MANAGER = PluginManager.from_modules(project_name)
12
+
13
+
14
+ def get_plugin_manager(project_name: str) -> PluginManager:
15
+ global PLUGIN_MANAGER
16
+ if not PLUGIN_MANAGER:
17
+ set_up_plugin_manager(project_name)
18
+
19
+ assert PLUGIN_MANAGER
20
+ return PLUGIN_MANAGER
@@ -0,0 +1,9 @@
1
+ from typing import Dict
2
+
3
+ # just exports, they need "noqa" so flake8 will not complain.
4
+ from dbt.artifacts.schemas.base import ArtifactMixin as PluginArtifact # noqa
5
+ from dbt.artifacts.schemas.base import BaseArtifactMetadata # noqa
6
+ from dbt.artifacts.schemas.base import schema_version # noqa
7
+ from dbt_common.dataclass_schema import ExtensibleDbtClassMixin, dbtClassMixin # noqa
8
+
9
+ PluginArtifacts = Dict[str, PluginArtifact]
@@ -0,0 +1,2 @@
1
+ # just exports, they need "noqa" so flake8 will not complain.
2
+ from dbt.exceptions import dbtPluginError # noqa