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.
- dvt/__init__.py +7 -0
- dvt/_pydantic_shim.py +26 -0
- dvt/adapters/__init__.py +16 -0
- dvt/adapters/multi_adapter_manager.py +268 -0
- dvt/artifacts/__init__.py +0 -0
- dvt/artifacts/exceptions/__init__.py +1 -0
- dvt/artifacts/exceptions/schemas.py +31 -0
- dvt/artifacts/resources/__init__.py +116 -0
- dvt/artifacts/resources/base.py +68 -0
- dvt/artifacts/resources/types.py +93 -0
- dvt/artifacts/resources/v1/analysis.py +10 -0
- dvt/artifacts/resources/v1/catalog.py +23 -0
- dvt/artifacts/resources/v1/components.py +275 -0
- dvt/artifacts/resources/v1/config.py +282 -0
- dvt/artifacts/resources/v1/documentation.py +11 -0
- dvt/artifacts/resources/v1/exposure.py +52 -0
- dvt/artifacts/resources/v1/function.py +53 -0
- dvt/artifacts/resources/v1/generic_test.py +32 -0
- dvt/artifacts/resources/v1/group.py +22 -0
- dvt/artifacts/resources/v1/hook.py +11 -0
- dvt/artifacts/resources/v1/macro.py +30 -0
- dvt/artifacts/resources/v1/metric.py +173 -0
- dvt/artifacts/resources/v1/model.py +146 -0
- dvt/artifacts/resources/v1/owner.py +10 -0
- dvt/artifacts/resources/v1/saved_query.py +112 -0
- dvt/artifacts/resources/v1/seed.py +42 -0
- dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
- dvt/artifacts/resources/v1/semantic_model.py +315 -0
- dvt/artifacts/resources/v1/singular_test.py +14 -0
- dvt/artifacts/resources/v1/snapshot.py +92 -0
- dvt/artifacts/resources/v1/source_definition.py +85 -0
- dvt/artifacts/resources/v1/sql_operation.py +10 -0
- dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
- dvt/artifacts/schemas/__init__.py +0 -0
- dvt/artifacts/schemas/base.py +191 -0
- dvt/artifacts/schemas/batch_results.py +24 -0
- dvt/artifacts/schemas/catalog/__init__.py +12 -0
- dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
- dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
- dvt/artifacts/schemas/freshness/__init__.py +1 -0
- dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
- dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
- dvt/artifacts/schemas/manifest/__init__.py +2 -0
- dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
- dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
- dvt/artifacts/schemas/results.py +148 -0
- dvt/artifacts/schemas/run/__init__.py +2 -0
- dvt/artifacts/schemas/run/v5/__init__.py +0 -0
- dvt/artifacts/schemas/run/v5/run.py +184 -0
- dvt/artifacts/schemas/upgrades/__init__.py +4 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
- dvt/artifacts/utils/validation.py +153 -0
- dvt/cli/__init__.py +1 -0
- dvt/cli/context.py +16 -0
- dvt/cli/exceptions.py +56 -0
- dvt/cli/flags.py +558 -0
- dvt/cli/main.py +971 -0
- dvt/cli/option_types.py +121 -0
- dvt/cli/options.py +79 -0
- dvt/cli/params.py +803 -0
- dvt/cli/requires.py +478 -0
- dvt/cli/resolvers.py +32 -0
- dvt/cli/types.py +40 -0
- dvt/clients/__init__.py +0 -0
- dvt/clients/checked_load.py +82 -0
- dvt/clients/git.py +164 -0
- dvt/clients/jinja.py +206 -0
- dvt/clients/jinja_static.py +245 -0
- dvt/clients/registry.py +192 -0
- dvt/clients/yaml_helper.py +68 -0
- dvt/compilation.py +833 -0
- dvt/compute/__init__.py +26 -0
- dvt/compute/base.py +288 -0
- dvt/compute/engines/__init__.py +13 -0
- dvt/compute/engines/duckdb_engine.py +368 -0
- dvt/compute/engines/spark_engine.py +273 -0
- dvt/compute/query_analyzer.py +212 -0
- dvt/compute/router.py +483 -0
- dvt/config/__init__.py +4 -0
- dvt/config/catalogs.py +95 -0
- dvt/config/compute_config.py +406 -0
- dvt/config/profile.py +411 -0
- dvt/config/profiles_v2.py +464 -0
- dvt/config/project.py +893 -0
- dvt/config/renderer.py +232 -0
- dvt/config/runtime.py +491 -0
- dvt/config/selectors.py +209 -0
- dvt/config/utils.py +78 -0
- dvt/connectors/.gitignore +6 -0
- dvt/connectors/README.md +306 -0
- dvt/connectors/catalog.yml +217 -0
- dvt/connectors/download_connectors.py +300 -0
- dvt/constants.py +29 -0
- dvt/context/__init__.py +0 -0
- dvt/context/base.py +746 -0
- dvt/context/configured.py +136 -0
- dvt/context/context_config.py +350 -0
- dvt/context/docs.py +82 -0
- dvt/context/exceptions_jinja.py +179 -0
- dvt/context/macro_resolver.py +195 -0
- dvt/context/macros.py +171 -0
- dvt/context/manifest.py +73 -0
- dvt/context/providers.py +2198 -0
- dvt/context/query_header.py +14 -0
- dvt/context/secret.py +59 -0
- dvt/context/target.py +74 -0
- dvt/contracts/__init__.py +0 -0
- dvt/contracts/files.py +413 -0
- dvt/contracts/graph/__init__.py +0 -0
- dvt/contracts/graph/manifest.py +1904 -0
- dvt/contracts/graph/metrics.py +98 -0
- dvt/contracts/graph/model_config.py +71 -0
- dvt/contracts/graph/node_args.py +42 -0
- dvt/contracts/graph/nodes.py +1806 -0
- dvt/contracts/graph/semantic_manifest.py +233 -0
- dvt/contracts/graph/unparsed.py +812 -0
- dvt/contracts/project.py +417 -0
- dvt/contracts/results.py +53 -0
- dvt/contracts/selection.py +23 -0
- dvt/contracts/sql.py +86 -0
- dvt/contracts/state.py +69 -0
- dvt/contracts/util.py +46 -0
- dvt/deprecations.py +347 -0
- dvt/deps/__init__.py +0 -0
- dvt/deps/base.py +153 -0
- dvt/deps/git.py +196 -0
- dvt/deps/local.py +80 -0
- dvt/deps/registry.py +131 -0
- dvt/deps/resolver.py +149 -0
- dvt/deps/tarball.py +121 -0
- dvt/docs/source/_ext/dbt_click.py +118 -0
- dvt/docs/source/conf.py +32 -0
- dvt/env_vars.py +64 -0
- dvt/event_time/event_time.py +40 -0
- dvt/event_time/sample_window.py +60 -0
- dvt/events/__init__.py +16 -0
- dvt/events/base_types.py +37 -0
- dvt/events/core_types_pb2.py +2 -0
- dvt/events/logging.py +109 -0
- dvt/events/types.py +2534 -0
- dvt/exceptions.py +1487 -0
- dvt/flags.py +89 -0
- dvt/graph/__init__.py +11 -0
- dvt/graph/cli.py +248 -0
- dvt/graph/graph.py +172 -0
- dvt/graph/queue.py +213 -0
- dvt/graph/selector.py +375 -0
- dvt/graph/selector_methods.py +976 -0
- dvt/graph/selector_spec.py +223 -0
- dvt/graph/thread_pool.py +18 -0
- dvt/hooks.py +21 -0
- dvt/include/README.md +49 -0
- dvt/include/__init__.py +3 -0
- dvt/include/global_project.py +4 -0
- dvt/include/starter_project/.gitignore +4 -0
- dvt/include/starter_project/README.md +15 -0
- dvt/include/starter_project/__init__.py +3 -0
- dvt/include/starter_project/analyses/.gitkeep +0 -0
- dvt/include/starter_project/dvt_project.yml +36 -0
- dvt/include/starter_project/macros/.gitkeep +0 -0
- dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
- dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
- dvt/include/starter_project/models/example/schema.yml +21 -0
- dvt/include/starter_project/seeds/.gitkeep +0 -0
- dvt/include/starter_project/snapshots/.gitkeep +0 -0
- dvt/include/starter_project/tests/.gitkeep +0 -0
- dvt/internal_deprecations.py +27 -0
- dvt/jsonschemas/__init__.py +3 -0
- dvt/jsonschemas/jsonschemas.py +309 -0
- dvt/jsonschemas/project/0.0.110.json +4717 -0
- dvt/jsonschemas/project/0.0.85.json +2015 -0
- dvt/jsonschemas/resources/0.0.110.json +2636 -0
- dvt/jsonschemas/resources/0.0.85.json +2536 -0
- dvt/jsonschemas/resources/latest.json +6773 -0
- dvt/links.py +4 -0
- dvt/materializations/__init__.py +0 -0
- dvt/materializations/incremental/__init__.py +0 -0
- dvt/materializations/incremental/microbatch.py +235 -0
- dvt/mp_context.py +8 -0
- dvt/node_types.py +37 -0
- dvt/parser/__init__.py +23 -0
- dvt/parser/analysis.py +21 -0
- dvt/parser/base.py +549 -0
- dvt/parser/common.py +267 -0
- dvt/parser/docs.py +52 -0
- dvt/parser/fixtures.py +51 -0
- dvt/parser/functions.py +30 -0
- dvt/parser/generic_test.py +100 -0
- dvt/parser/generic_test_builders.py +334 -0
- dvt/parser/hooks.py +119 -0
- dvt/parser/macros.py +137 -0
- dvt/parser/manifest.py +2204 -0
- dvt/parser/models.py +574 -0
- dvt/parser/partial.py +1179 -0
- dvt/parser/read_files.py +445 -0
- dvt/parser/schema_generic_tests.py +423 -0
- dvt/parser/schema_renderer.py +111 -0
- dvt/parser/schema_yaml_readers.py +936 -0
- dvt/parser/schemas.py +1467 -0
- dvt/parser/search.py +149 -0
- dvt/parser/seeds.py +28 -0
- dvt/parser/singular_test.py +20 -0
- dvt/parser/snapshots.py +44 -0
- dvt/parser/sources.py +557 -0
- dvt/parser/sql.py +63 -0
- dvt/parser/unit_tests.py +622 -0
- dvt/plugins/__init__.py +20 -0
- dvt/plugins/contracts.py +10 -0
- dvt/plugins/exceptions.py +2 -0
- dvt/plugins/manager.py +164 -0
- dvt/plugins/manifest.py +21 -0
- dvt/profiler.py +20 -0
- dvt/py.typed +1 -0
- dvt/runners/__init__.py +2 -0
- dvt/runners/exposure_runner.py +7 -0
- dvt/runners/no_op_runner.py +46 -0
- dvt/runners/saved_query_runner.py +7 -0
- dvt/selected_resources.py +8 -0
- dvt/task/__init__.py +0 -0
- dvt/task/base.py +504 -0
- dvt/task/build.py +197 -0
- dvt/task/clean.py +57 -0
- dvt/task/clone.py +162 -0
- dvt/task/compile.py +151 -0
- dvt/task/compute.py +366 -0
- dvt/task/debug.py +650 -0
- dvt/task/deps.py +280 -0
- dvt/task/docs/__init__.py +3 -0
- dvt/task/docs/generate.py +408 -0
- dvt/task/docs/index.html +250 -0
- dvt/task/docs/serve.py +28 -0
- dvt/task/freshness.py +323 -0
- dvt/task/function.py +122 -0
- dvt/task/group_lookup.py +46 -0
- dvt/task/init.py +374 -0
- dvt/task/list.py +237 -0
- dvt/task/printer.py +176 -0
- dvt/task/profiles.py +256 -0
- dvt/task/retry.py +175 -0
- dvt/task/run.py +1146 -0
- dvt/task/run_operation.py +142 -0
- dvt/task/runnable.py +802 -0
- dvt/task/seed.py +104 -0
- dvt/task/show.py +150 -0
- dvt/task/snapshot.py +57 -0
- dvt/task/sql.py +111 -0
- dvt/task/test.py +464 -0
- dvt/tests/fixtures/__init__.py +1 -0
- dvt/tests/fixtures/project.py +620 -0
- dvt/tests/util.py +651 -0
- dvt/tracking.py +529 -0
- dvt/utils/__init__.py +3 -0
- dvt/utils/artifact_upload.py +151 -0
- dvt/utils/utils.py +408 -0
- dvt/version.py +249 -0
- dvt_core-1.11.0b4.dist-info/METADATA +252 -0
- dvt_core-1.11.0b4.dist-info/RECORD +261 -0
- dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
- dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
- dvt_core-1.11.0b4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from typing import Any, Dict, Generic, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from dvt import deprecations
|
|
6
|
+
from dvt.artifacts.resources import NodeVersion
|
|
7
|
+
from dvt.clients.jinja import GENERIC_TEST_KWARGS_NAME, get_rendered
|
|
8
|
+
from dvt.contracts.graph.nodes import UnpatchedSourceDefinition
|
|
9
|
+
from dvt.contracts.graph.unparsed import UnparsedModelUpdate, UnparsedNodeUpdate
|
|
10
|
+
from dvt.exceptions import (
|
|
11
|
+
CustomMacroPopulatingConfigValueError,
|
|
12
|
+
SameKeyNestedError,
|
|
13
|
+
TagNotStringError,
|
|
14
|
+
TagsNotListOfStringsError,
|
|
15
|
+
TestArgIncludesModelError,
|
|
16
|
+
TestArgsNotDictError,
|
|
17
|
+
TestDefinitionDictLengthError,
|
|
18
|
+
TestNameNotStringError,
|
|
19
|
+
TestTypeError,
|
|
20
|
+
UnexpectedTestNamePatternError,
|
|
21
|
+
)
|
|
22
|
+
from dvt.flags import get_flags
|
|
23
|
+
from dvt.parser.common import Testable
|
|
24
|
+
from dvt.utils import md5
|
|
25
|
+
|
|
26
|
+
from dbt_common.exceptions.macros import UndefinedMacroError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def synthesize_generic_test_names(
|
|
30
|
+
test_type: str, test_name: str, args: Dict[str, Any]
|
|
31
|
+
) -> Tuple[str, str]:
|
|
32
|
+
# Using the type, name, and arguments to this generic test, synthesize a (hopefully) unique name
|
|
33
|
+
# Will not be unique if multiple tests have same name + arguments, and only configs differ
|
|
34
|
+
# Returns a shorter version (hashed/truncated, for the compiled file)
|
|
35
|
+
# as well as the full name (for the unique_id + FQN)
|
|
36
|
+
flat_args = []
|
|
37
|
+
for arg_name in sorted(args):
|
|
38
|
+
# the model is already embedded in the name, so skip it
|
|
39
|
+
if arg_name == "model":
|
|
40
|
+
continue
|
|
41
|
+
arg_val = args[arg_name]
|
|
42
|
+
|
|
43
|
+
if isinstance(arg_val, dict):
|
|
44
|
+
parts = list(arg_val.values())
|
|
45
|
+
elif isinstance(arg_val, (list, tuple)):
|
|
46
|
+
parts = list(arg_val)
|
|
47
|
+
else:
|
|
48
|
+
parts = [arg_val]
|
|
49
|
+
|
|
50
|
+
flat_args.extend([str(part) for part in parts])
|
|
51
|
+
|
|
52
|
+
clean_flat_args = [re.sub("[^0-9a-zA-Z_]+", "_", arg) for arg in flat_args]
|
|
53
|
+
unique = "__".join(clean_flat_args)
|
|
54
|
+
|
|
55
|
+
# for the file path + alias, the name must be <64 characters
|
|
56
|
+
# if the full name is too long, include the first 30 identifying chars plus
|
|
57
|
+
# a 32-character hash of the full contents
|
|
58
|
+
|
|
59
|
+
test_identifier = "{}_{}".format(test_type, test_name)
|
|
60
|
+
full_name = "{}_{}".format(test_identifier, unique)
|
|
61
|
+
|
|
62
|
+
if len(full_name) >= 64:
|
|
63
|
+
test_trunc_identifier = test_identifier[:30]
|
|
64
|
+
label = md5(full_name)
|
|
65
|
+
short_name = "{}_{}".format(test_trunc_identifier, label)
|
|
66
|
+
else:
|
|
67
|
+
short_name = full_name
|
|
68
|
+
|
|
69
|
+
return short_name, full_name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestBuilder(Generic[Testable]):
|
|
73
|
+
"""An object to hold assorted test settings and perform basic parsing
|
|
74
|
+
|
|
75
|
+
Test names have the following pattern:
|
|
76
|
+
- the test name itself may be namespaced (package.test)
|
|
77
|
+
- or it may not be namespaced (test)
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# The 'test_name' is used to find the 'macro' that implements the test
|
|
82
|
+
TEST_NAME_PATTERN = re.compile(
|
|
83
|
+
r"((?P<test_namespace>([a-zA-Z_][0-9a-zA-Z_]*))\.)?"
|
|
84
|
+
r"(?P<test_name>([a-zA-Z_][0-9a-zA-Z_]*))"
|
|
85
|
+
)
|
|
86
|
+
# args in the test entry representing test configs
|
|
87
|
+
CONFIG_ARGS = (
|
|
88
|
+
"severity",
|
|
89
|
+
"tags",
|
|
90
|
+
"enabled",
|
|
91
|
+
"where",
|
|
92
|
+
"limit",
|
|
93
|
+
"warn_if",
|
|
94
|
+
"error_if",
|
|
95
|
+
"fail_calc",
|
|
96
|
+
"store_failures",
|
|
97
|
+
"store_failures_as",
|
|
98
|
+
"meta",
|
|
99
|
+
"database",
|
|
100
|
+
"schema",
|
|
101
|
+
"alias",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
data_test: Dict[str, Any],
|
|
107
|
+
target: Testable,
|
|
108
|
+
package_name: str,
|
|
109
|
+
render_ctx: Dict[str, Any],
|
|
110
|
+
column_name: Optional[str] = None,
|
|
111
|
+
version: Optional[NodeVersion] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
test_name, test_args = self.extract_test_args(
|
|
114
|
+
data_test, target.original_file_path, target.name, column_name, package_name
|
|
115
|
+
)
|
|
116
|
+
self.args: Dict[str, Any] = test_args
|
|
117
|
+
if "model" in self.args:
|
|
118
|
+
raise TestArgIncludesModelError()
|
|
119
|
+
self.package_name: str = package_name
|
|
120
|
+
self.target: Testable = target
|
|
121
|
+
self.version: Optional[NodeVersion] = version
|
|
122
|
+
self.render_ctx: Dict[str, Any] = render_ctx
|
|
123
|
+
self.column_name: Optional[str] = column_name
|
|
124
|
+
self.args["model"] = self.build_model_str()
|
|
125
|
+
|
|
126
|
+
match = self.TEST_NAME_PATTERN.match(test_name)
|
|
127
|
+
if match is None:
|
|
128
|
+
raise UnexpectedTestNamePatternError(test_name)
|
|
129
|
+
|
|
130
|
+
groups = match.groupdict()
|
|
131
|
+
self.name: str = groups["test_name"]
|
|
132
|
+
self.namespace: str = groups["test_namespace"]
|
|
133
|
+
self.config: Dict[str, Any] = {}
|
|
134
|
+
# Process legacy args
|
|
135
|
+
self.config.update(self._process_legacy_args())
|
|
136
|
+
|
|
137
|
+
# Process config args if present
|
|
138
|
+
if "config" in self.args:
|
|
139
|
+
self.config.update(self._render_values(self.args.pop("config", {})))
|
|
140
|
+
|
|
141
|
+
if self.namespace is not None:
|
|
142
|
+
self.package_name = self.namespace
|
|
143
|
+
|
|
144
|
+
# If the user has provided a description for this generic test, use it
|
|
145
|
+
# Then delete the "description" argument to:
|
|
146
|
+
# 1. Avoid passing it into the test macro
|
|
147
|
+
# 2. Avoid passing it into the test name synthesis
|
|
148
|
+
# Otherwise, use an empty string
|
|
149
|
+
self.description: str = ""
|
|
150
|
+
|
|
151
|
+
if "description" in self.args:
|
|
152
|
+
self.description = self.args["description"]
|
|
153
|
+
del self.args["description"]
|
|
154
|
+
|
|
155
|
+
# If the user has provided a custom name for this generic test, use it
|
|
156
|
+
# Then delete the "name" argument to avoid passing it into the test macro
|
|
157
|
+
# Otherwise, use an auto-generated name synthesized from test inputs
|
|
158
|
+
self.compiled_name: str = ""
|
|
159
|
+
self.fqn_name: str = ""
|
|
160
|
+
|
|
161
|
+
if "name" in self.args:
|
|
162
|
+
# Assign the user-defined name here, which will be checked for uniqueness later
|
|
163
|
+
# we will raise an error if two tests have same name for same model + column combo
|
|
164
|
+
self.compiled_name = self.args["name"]
|
|
165
|
+
self.fqn_name = self.args["name"]
|
|
166
|
+
del self.args["name"]
|
|
167
|
+
else:
|
|
168
|
+
short_name, full_name = self.get_synthetic_test_names()
|
|
169
|
+
self.compiled_name = short_name
|
|
170
|
+
self.fqn_name = full_name
|
|
171
|
+
# use hashed name as alias if full name is too long
|
|
172
|
+
if short_name != full_name and "alias" not in self.config:
|
|
173
|
+
self.config["alias"] = short_name
|
|
174
|
+
|
|
175
|
+
def _process_legacy_args(self):
|
|
176
|
+
config = {}
|
|
177
|
+
for key in self.CONFIG_ARGS:
|
|
178
|
+
value = self.args.pop(key, None)
|
|
179
|
+
if value and "config" in self.args and key in self.args["config"]:
|
|
180
|
+
raise SameKeyNestedError()
|
|
181
|
+
if not value and "config" in self.args:
|
|
182
|
+
value = self.args["config"].pop(key, None)
|
|
183
|
+
config[key] = value
|
|
184
|
+
|
|
185
|
+
return self._render_values(config)
|
|
186
|
+
|
|
187
|
+
def _render_values(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
188
|
+
rendered_config = {}
|
|
189
|
+
for key, value in config.items():
|
|
190
|
+
if isinstance(value, str):
|
|
191
|
+
try:
|
|
192
|
+
value = get_rendered(value, self.render_ctx, native=True)
|
|
193
|
+
except UndefinedMacroError as e:
|
|
194
|
+
raise CustomMacroPopulatingConfigValueError(
|
|
195
|
+
target_name=self.target.name,
|
|
196
|
+
column_name=self.column_name,
|
|
197
|
+
name=self.name,
|
|
198
|
+
key=key,
|
|
199
|
+
err_msg=e.msg,
|
|
200
|
+
)
|
|
201
|
+
if value is not None:
|
|
202
|
+
rendered_config[key] = value
|
|
203
|
+
return rendered_config
|
|
204
|
+
|
|
205
|
+
def _bad_type(self) -> TypeError:
|
|
206
|
+
return TypeError('invalid target type "{}"'.format(type(self.target)))
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def extract_test_args(
|
|
210
|
+
data_test, file_path, resource_name=None, column_name=None, package_name=None
|
|
211
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
212
|
+
if not isinstance(data_test, dict):
|
|
213
|
+
raise TestTypeError(data_test)
|
|
214
|
+
|
|
215
|
+
# If the test is a dictionary with top-level keys, the test name is "test_name"
|
|
216
|
+
# and the rest are arguments
|
|
217
|
+
# {'name': 'my_favorite_test', 'test_name': 'unique', 'config': {'where': '1=1'}}
|
|
218
|
+
if "test_name" in data_test.keys():
|
|
219
|
+
test_name = data_test.pop("test_name")
|
|
220
|
+
test_args = data_test
|
|
221
|
+
# If the test is a nested dictionary with one top-level key, the test name
|
|
222
|
+
# is the dict name, and nested keys are arguments
|
|
223
|
+
# {'unique': {'name': 'my_favorite_test', 'config': {'where': '1=1'}}}
|
|
224
|
+
else:
|
|
225
|
+
data_test = list(data_test.items())
|
|
226
|
+
if len(data_test) != 1:
|
|
227
|
+
raise TestDefinitionDictLengthError(data_test)
|
|
228
|
+
test_name, test_args = data_test[0]
|
|
229
|
+
|
|
230
|
+
if not isinstance(test_args, dict):
|
|
231
|
+
raise TestArgsNotDictError(test_args)
|
|
232
|
+
if not isinstance(test_name, str):
|
|
233
|
+
raise TestNameNotStringError(test_name)
|
|
234
|
+
test_args = deepcopy(test_args)
|
|
235
|
+
if column_name is not None:
|
|
236
|
+
test_args["column_name"] = column_name
|
|
237
|
+
|
|
238
|
+
# Extract kwargs when they are nested under new 'arguments' property separately from 'config' if require_generic_test_arguments_property is enabled
|
|
239
|
+
if get_flags().require_generic_test_arguments_property:
|
|
240
|
+
arguments = test_args.pop("arguments", {})
|
|
241
|
+
if not arguments and any(
|
|
242
|
+
k not in ("config", "column_name", "description", "name") for k in test_args.keys()
|
|
243
|
+
):
|
|
244
|
+
resource = (
|
|
245
|
+
f"'{resource_name}' in package '{package_name}'"
|
|
246
|
+
if package_name
|
|
247
|
+
else f"'{resource_name}'"
|
|
248
|
+
)
|
|
249
|
+
deprecations.warn(
|
|
250
|
+
"missing-arguments-property-in-generic-test-deprecation",
|
|
251
|
+
test_name=f"`{test_name}` defined on {resource} ({file_path})",
|
|
252
|
+
)
|
|
253
|
+
if isinstance(arguments, dict):
|
|
254
|
+
test_args = {**test_args, **arguments}
|
|
255
|
+
elif "arguments" in test_args:
|
|
256
|
+
deprecations.warn(
|
|
257
|
+
"arguments-property-in-generic-test-deprecation",
|
|
258
|
+
test_name=f"`{test_name}` ({test_args['arguments']})",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return test_name, test_args
|
|
262
|
+
|
|
263
|
+
def tags(self) -> List[str]:
|
|
264
|
+
tags = self.config.get("tags", [])
|
|
265
|
+
if isinstance(tags, str):
|
|
266
|
+
tags = [tags]
|
|
267
|
+
if not isinstance(tags, list):
|
|
268
|
+
raise TagsNotListOfStringsError(tags)
|
|
269
|
+
for tag in tags:
|
|
270
|
+
if not isinstance(tag, str):
|
|
271
|
+
raise TagNotStringError(tag)
|
|
272
|
+
return tags[:]
|
|
273
|
+
|
|
274
|
+
def macro_name(self) -> str:
|
|
275
|
+
macro_name = "test_{}".format(self.name)
|
|
276
|
+
if self.namespace is not None:
|
|
277
|
+
macro_name = "{}.{}".format(self.namespace, macro_name)
|
|
278
|
+
return macro_name
|
|
279
|
+
|
|
280
|
+
def get_synthetic_test_names(self) -> Tuple[str, str]:
|
|
281
|
+
# Returns two names: shorter (for the compiled file), full (for the unique_id + FQN)
|
|
282
|
+
target_name = self.target.name
|
|
283
|
+
if isinstance(self.target, UnparsedModelUpdate):
|
|
284
|
+
name = self.name
|
|
285
|
+
if self.version:
|
|
286
|
+
target_name = f"{self.target.name}_v{self.version}"
|
|
287
|
+
elif isinstance(self.target, UnparsedNodeUpdate):
|
|
288
|
+
name = self.name
|
|
289
|
+
elif isinstance(self.target, UnpatchedSourceDefinition):
|
|
290
|
+
name = "source_" + self.name
|
|
291
|
+
else:
|
|
292
|
+
raise self._bad_type()
|
|
293
|
+
if self.namespace is not None:
|
|
294
|
+
name = "{}_{}".format(self.namespace, name)
|
|
295
|
+
return synthesize_generic_test_names(name, target_name, self.args)
|
|
296
|
+
|
|
297
|
+
def construct_config(self) -> str:
|
|
298
|
+
configs = ",".join(
|
|
299
|
+
[
|
|
300
|
+
f"{key}="
|
|
301
|
+
+ (
|
|
302
|
+
('"' + value.replace('"', '\\"') + '"')
|
|
303
|
+
if isinstance(value, str)
|
|
304
|
+
else str(value)
|
|
305
|
+
)
|
|
306
|
+
for key, value in self.config.items()
|
|
307
|
+
]
|
|
308
|
+
)
|
|
309
|
+
if configs:
|
|
310
|
+
return f"{{{{ config({configs}) }}}}"
|
|
311
|
+
else:
|
|
312
|
+
return ""
|
|
313
|
+
|
|
314
|
+
# this is the 'raw_code' that's used in 'render_update' and execution
|
|
315
|
+
# of the test macro
|
|
316
|
+
def build_raw_code(self) -> str:
|
|
317
|
+
return ("{{{{ {macro}(**{kwargs_name}) }}}}{config}").format(
|
|
318
|
+
macro=self.macro_name(),
|
|
319
|
+
config=self.construct_config(),
|
|
320
|
+
kwargs_name=GENERIC_TEST_KWARGS_NAME,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def build_model_str(self):
|
|
324
|
+
targ = self.target
|
|
325
|
+
if isinstance(self.target, UnparsedModelUpdate):
|
|
326
|
+
if self.version:
|
|
327
|
+
target_str = f"ref('{targ.name}', version='{self.version}')"
|
|
328
|
+
else:
|
|
329
|
+
target_str = f"ref('{targ.name}')"
|
|
330
|
+
elif isinstance(self.target, UnparsedNodeUpdate):
|
|
331
|
+
target_str = f"ref('{targ.name}')"
|
|
332
|
+
elif isinstance(self.target, UnpatchedSourceDefinition):
|
|
333
|
+
target_str = f"source('{targ.source.name}', '{targ.table.name}')"
|
|
334
|
+
return f"{{{{ get_where_subquery({target_str}) }}}}"
|
dvt/parser/hooks.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Iterable, Iterator, List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from dvt.context.context_config import ContextConfig
|
|
5
|
+
from dvt.contracts.files import FilePath
|
|
6
|
+
from dvt.contracts.graph.nodes import HookNode
|
|
7
|
+
from dvt.node_types import NodeType, RunHookType
|
|
8
|
+
from dvt.parser.base import SimpleParser
|
|
9
|
+
from dvt.parser.search import FileBlock
|
|
10
|
+
from dvt.utils import get_pseudo_hook_path
|
|
11
|
+
|
|
12
|
+
from dbt_common.exceptions import DbtInternalError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class HookBlock(FileBlock):
|
|
17
|
+
project: str
|
|
18
|
+
value: str
|
|
19
|
+
index: int
|
|
20
|
+
hook_type: RunHookType
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def contents(self):
|
|
24
|
+
return self.value
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self):
|
|
28
|
+
return "{}-{!s}-{!s}".format(self.project, self.hook_type, self.index)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HookSearcher(Iterable[HookBlock]):
|
|
32
|
+
def __init__(self, project, source_file, hook_type) -> None:
|
|
33
|
+
self.project = project
|
|
34
|
+
self.source_file = source_file
|
|
35
|
+
self.hook_type = hook_type
|
|
36
|
+
|
|
37
|
+
def _hook_list(self, hooks: Union[str, List[str], Tuple[str, ...]]) -> List[str]:
|
|
38
|
+
if isinstance(hooks, tuple):
|
|
39
|
+
hooks = list(hooks)
|
|
40
|
+
elif not isinstance(hooks, list):
|
|
41
|
+
hooks = [hooks]
|
|
42
|
+
return hooks
|
|
43
|
+
|
|
44
|
+
def get_hook_defs(self) -> List[str]:
|
|
45
|
+
if self.hook_type == RunHookType.Start:
|
|
46
|
+
hooks = self.project.on_run_start
|
|
47
|
+
elif self.hook_type == RunHookType.End:
|
|
48
|
+
hooks = self.project.on_run_end
|
|
49
|
+
else:
|
|
50
|
+
raise DbtInternalError(
|
|
51
|
+
'hook_type must be one of "{}" or "{}" (got {})'.format(
|
|
52
|
+
RunHookType.Start, RunHookType.End, self.hook_type
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return self._hook_list(hooks)
|
|
56
|
+
|
|
57
|
+
def __iter__(self) -> Iterator[HookBlock]:
|
|
58
|
+
hooks = self.get_hook_defs()
|
|
59
|
+
for index, hook in enumerate(hooks):
|
|
60
|
+
yield HookBlock(
|
|
61
|
+
file=self.source_file,
|
|
62
|
+
project=self.project.project_name,
|
|
63
|
+
value=hook,
|
|
64
|
+
index=index,
|
|
65
|
+
hook_type=self.hook_type,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class HookParser(SimpleParser[HookBlock, HookNode]):
|
|
70
|
+
|
|
71
|
+
# Hooks are only in the dbt_project.yml file for the project
|
|
72
|
+
def get_path(self) -> FilePath:
|
|
73
|
+
# There ought to be an existing file object for this, but
|
|
74
|
+
# until that is implemented use a dummy modification time
|
|
75
|
+
path = FilePath(
|
|
76
|
+
project_root=self.project.project_root,
|
|
77
|
+
searched_path=".",
|
|
78
|
+
relative_path="dbt_project.yml",
|
|
79
|
+
modification_time=0.0,
|
|
80
|
+
)
|
|
81
|
+
return path
|
|
82
|
+
|
|
83
|
+
def parse_from_dict(self, dct, validate=True) -> HookNode:
|
|
84
|
+
if validate:
|
|
85
|
+
HookNode.validate(dct)
|
|
86
|
+
return HookNode.from_dict(dct)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_compiled_path(cls, block: HookBlock):
|
|
90
|
+
return get_pseudo_hook_path(block.name)
|
|
91
|
+
|
|
92
|
+
def _create_parsetime_node(
|
|
93
|
+
self,
|
|
94
|
+
block: HookBlock,
|
|
95
|
+
path: str,
|
|
96
|
+
config: ContextConfig,
|
|
97
|
+
fqn: List[str],
|
|
98
|
+
name=None,
|
|
99
|
+
**kwargs,
|
|
100
|
+
) -> HookNode:
|
|
101
|
+
|
|
102
|
+
return super()._create_parsetime_node(
|
|
103
|
+
block=block,
|
|
104
|
+
path=path,
|
|
105
|
+
config=config,
|
|
106
|
+
fqn=fqn,
|
|
107
|
+
index=block.index,
|
|
108
|
+
name=name,
|
|
109
|
+
tags=[str(block.hook_type)],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def resource_type(self) -> NodeType:
|
|
114
|
+
return NodeType.Operation
|
|
115
|
+
|
|
116
|
+
def parse_file(self, block: FileBlock) -> None:
|
|
117
|
+
for hook_type in RunHookType:
|
|
118
|
+
for hook in HookSearcher(self.project, block.file, hook_type):
|
|
119
|
+
self.parse_node(hook)
|
dvt/parser/macros.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from typing import Iterable, List
|
|
2
|
+
|
|
3
|
+
import jinja2
|
|
4
|
+
from dvt.artifacts.resources import MacroArgument
|
|
5
|
+
from dvt.clients.jinja import get_supported_languages
|
|
6
|
+
from dvt.contracts.files import FilePath, SourceFile
|
|
7
|
+
from dvt.contracts.graph.nodes import Macro
|
|
8
|
+
from dvt.contracts.graph.unparsed import UnparsedMacro
|
|
9
|
+
from dvt.exceptions import ParsingError
|
|
10
|
+
from dvt.flags import get_flags
|
|
11
|
+
from dvt.node_types import NodeType
|
|
12
|
+
from dvt.parser.base import BaseParser
|
|
13
|
+
from dvt.parser.search import FileBlock, filesystem_search
|
|
14
|
+
|
|
15
|
+
from dbt_common.clients import jinja
|
|
16
|
+
from dbt_common.clients._jinja_blocks import ExtractWarning
|
|
17
|
+
from dbt_common.utils import MACRO_PREFIX
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MacroParser(BaseParser[Macro]):
|
|
21
|
+
# This is only used when creating a MacroManifest separate
|
|
22
|
+
# from the normal parsing flow.
|
|
23
|
+
def get_paths(self) -> List[FilePath]:
|
|
24
|
+
return filesystem_search(
|
|
25
|
+
project=self.project, relative_dirs=self.project.macro_paths, extension=".sql"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def resource_type(self) -> NodeType:
|
|
30
|
+
return NodeType.Macro
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_compiled_path(cls, block: FileBlock):
|
|
34
|
+
return block.path.relative_path
|
|
35
|
+
|
|
36
|
+
def parse_macro(self, block: jinja.BlockTag, base_node: UnparsedMacro, name: str) -> Macro:
|
|
37
|
+
unique_id = self.generate_unique_id(name)
|
|
38
|
+
macro_sql = block.full_block or ""
|
|
39
|
+
|
|
40
|
+
return Macro(
|
|
41
|
+
path=base_node.path,
|
|
42
|
+
macro_sql=macro_sql,
|
|
43
|
+
original_file_path=base_node.original_file_path,
|
|
44
|
+
package_name=base_node.package_name,
|
|
45
|
+
resource_type=base_node.resource_type,
|
|
46
|
+
name=name,
|
|
47
|
+
unique_id=unique_id,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def parse_unparsed_macros(self, base_node: UnparsedMacro) -> Iterable[Macro]:
|
|
51
|
+
# This is a bit of a hack to get the file path to the deprecation
|
|
52
|
+
def wrap_handle_extract_warning(warning: ExtractWarning) -> None:
|
|
53
|
+
self._handle_extract_warning(warning=warning, file=base_node.original_file_path)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
blocks: List[jinja.BlockTag] = [
|
|
57
|
+
t
|
|
58
|
+
for t in jinja.extract_toplevel_blocks(
|
|
59
|
+
base_node.raw_code,
|
|
60
|
+
allowed_blocks={"macro", "materialization", "test", "data_test"},
|
|
61
|
+
collect_raw_data=False,
|
|
62
|
+
warning_callback=wrap_handle_extract_warning,
|
|
63
|
+
)
|
|
64
|
+
if isinstance(t, jinja.BlockTag)
|
|
65
|
+
]
|
|
66
|
+
except ParsingError as exc:
|
|
67
|
+
exc.add_node(base_node)
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
for block in blocks:
|
|
71
|
+
try:
|
|
72
|
+
ast = jinja.parse(block.full_block)
|
|
73
|
+
except ParsingError as e:
|
|
74
|
+
e.add_node(base_node)
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
isinstance(ast, jinja2.nodes.Template)
|
|
79
|
+
and hasattr(ast, "body")
|
|
80
|
+
and len(ast.body) == 1
|
|
81
|
+
and isinstance(ast.body[0], jinja2.nodes.Macro)
|
|
82
|
+
):
|
|
83
|
+
# If the top level node in the Template is a Macro, things look
|
|
84
|
+
# good and this is much faster than traversing the full ast, as
|
|
85
|
+
# in the following else clause. It's not clear if that traversal
|
|
86
|
+
# is ever really needed.
|
|
87
|
+
macro = ast.body[0]
|
|
88
|
+
else:
|
|
89
|
+
macro_nodes = list(ast.find_all(jinja2.nodes.Macro))
|
|
90
|
+
|
|
91
|
+
if len(macro_nodes) != 1:
|
|
92
|
+
# things have gone disastrously wrong, we thought we only
|
|
93
|
+
# parsed one block!
|
|
94
|
+
raise ParsingError(
|
|
95
|
+
f"Found multiple macros in {block.full_block}, expected 1", node=base_node
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
macro = macro_nodes[0]
|
|
99
|
+
|
|
100
|
+
if not macro.name.startswith(MACRO_PREFIX):
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
name: str = macro.name.replace(MACRO_PREFIX, "")
|
|
104
|
+
node = self.parse_macro(block, base_node, name)
|
|
105
|
+
|
|
106
|
+
if getattr(get_flags(), "validate_macro_args", False):
|
|
107
|
+
node.arguments = self._extract_args(macro)
|
|
108
|
+
|
|
109
|
+
# get supported_languages for materialization macro
|
|
110
|
+
if block.block_type_name == "materialization":
|
|
111
|
+
node.supported_languages = get_supported_languages(macro)
|
|
112
|
+
yield node
|
|
113
|
+
|
|
114
|
+
def _extract_args(self, macro) -> List[MacroArgument]:
|
|
115
|
+
try:
|
|
116
|
+
return list([MacroArgument(name=arg.name) for arg in macro.args])
|
|
117
|
+
except Exception:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
def parse_file(self, block: FileBlock):
|
|
121
|
+
assert isinstance(block.file, SourceFile)
|
|
122
|
+
source_file = block.file
|
|
123
|
+
assert isinstance(source_file.contents, str)
|
|
124
|
+
original_file_path = source_file.path.original_file_path
|
|
125
|
+
|
|
126
|
+
# this is really only used for error messages
|
|
127
|
+
base_node = UnparsedMacro(
|
|
128
|
+
path=original_file_path,
|
|
129
|
+
original_file_path=original_file_path,
|
|
130
|
+
package_name=self.project.project_name,
|
|
131
|
+
raw_code=source_file.contents,
|
|
132
|
+
resource_type=NodeType.Macro,
|
|
133
|
+
language="sql",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for node in self.parse_unparsed_macros(base_node):
|
|
137
|
+
self.manifest.add_macro(block.file, node)
|