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
dvt/flags.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Do not import the os package because we expose this package in jinja
|
|
2
|
+
from argparse import Namespace
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# this roughly follows the patten of EVENT_MANAGER in dbt/common/events/functions.py
|
|
6
|
+
# During de-globlization, we'll need to handle both similarly
|
|
7
|
+
# Match USE_COLORS default with default in dbt.cli.params.use_colors for use in --version
|
|
8
|
+
GLOBAL_FLAGS = Namespace(USE_COLORS=True) # type: ignore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_flags(flags):
|
|
12
|
+
global GLOBAL_FLAGS
|
|
13
|
+
GLOBAL_FLAGS = flags
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_flags():
|
|
17
|
+
return GLOBAL_FLAGS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_from_args(args: Namespace, project_flags):
|
|
21
|
+
global GLOBAL_FLAGS
|
|
22
|
+
from dvt.cli.flags import Flags, convert_config
|
|
23
|
+
from dvt.cli.main import cli
|
|
24
|
+
|
|
25
|
+
# we set attributes of args after initialize the flags, but project_flags
|
|
26
|
+
# is being read in the Flags constructor, so we need to read it here and pass in
|
|
27
|
+
# to make sure we use the correct project_flags
|
|
28
|
+
profiles_dir = getattr(args, "PROFILES_DIR", None) or getattr(args, "profiles_dir", None)
|
|
29
|
+
project_dir = getattr(args, "PROJECT_DIR", None) or getattr(args, "project_dir", None)
|
|
30
|
+
if profiles_dir and project_dir:
|
|
31
|
+
from dvt.config.project import read_project_flags
|
|
32
|
+
|
|
33
|
+
project_flags = read_project_flags(project_dir, profiles_dir)
|
|
34
|
+
|
|
35
|
+
# make a dummy context to get the flags, totally arbitrary
|
|
36
|
+
ctx = cli.make_context("run", ["run"])
|
|
37
|
+
flags = Flags(ctx, project_flags)
|
|
38
|
+
for arg_name, args_param_value in vars(args).items():
|
|
39
|
+
args_param_value = convert_config(arg_name, args_param_value)
|
|
40
|
+
object.__setattr__(flags, arg_name.upper(), args_param_value)
|
|
41
|
+
object.__setattr__(flags, arg_name.lower(), args_param_value)
|
|
42
|
+
flags.set_common_global_flags()
|
|
43
|
+
GLOBAL_FLAGS = flags # type: ignore
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_flag_dict():
|
|
47
|
+
flag_attr = {
|
|
48
|
+
"use_experimental_parser",
|
|
49
|
+
"static_parser",
|
|
50
|
+
"warn_error",
|
|
51
|
+
"warn_error_options",
|
|
52
|
+
"write_json",
|
|
53
|
+
"partial_parse",
|
|
54
|
+
"use_colors",
|
|
55
|
+
"profiles_dir",
|
|
56
|
+
"debug",
|
|
57
|
+
"log_format",
|
|
58
|
+
"version_check",
|
|
59
|
+
"fail_fast",
|
|
60
|
+
"send_anonymous_usage_stats",
|
|
61
|
+
"printer_width",
|
|
62
|
+
"indirect_selection",
|
|
63
|
+
"log_cache_events",
|
|
64
|
+
"quiet",
|
|
65
|
+
"no_print",
|
|
66
|
+
"cache_selected_only",
|
|
67
|
+
"introspect",
|
|
68
|
+
"target_path",
|
|
69
|
+
"log_path",
|
|
70
|
+
"invocation_command",
|
|
71
|
+
"empty",
|
|
72
|
+
}
|
|
73
|
+
return {key: getattr(GLOBAL_FLAGS, key.upper(), None) for key in flag_attr}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# This is used by core/dbt/context/base.py to return a flag object
|
|
77
|
+
# in Jinja.
|
|
78
|
+
def get_flag_obj():
|
|
79
|
+
new_flags = Namespace()
|
|
80
|
+
for key, val in get_flag_dict().items():
|
|
81
|
+
if isinstance(val, Path):
|
|
82
|
+
val = str(val)
|
|
83
|
+
setattr(new_flags, key.upper(), val)
|
|
84
|
+
# The following 3 are CLI arguments only so they're not full-fledged flags,
|
|
85
|
+
# but we put in flags for users.
|
|
86
|
+
setattr(new_flags, "FULL_REFRESH", getattr(GLOBAL_FLAGS, "FULL_REFRESH", None))
|
|
87
|
+
setattr(new_flags, "STORE_FAILURES", getattr(GLOBAL_FLAGS, "STORE_FAILURES", None))
|
|
88
|
+
setattr(new_flags, "WHICH", getattr(GLOBAL_FLAGS, "WHICH", None))
|
|
89
|
+
return new_flags
|
dvt/graph/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .cli import parse_difference, parse_from_selectors_definition # noqa: F401
|
|
2
|
+
from .graph import Graph, UniqueId # noqa: F401
|
|
3
|
+
from .queue import GraphQueue # noqa: F401
|
|
4
|
+
from .selector import NodeSelector, ResourceTypeSelector # noqa: F401
|
|
5
|
+
from .selector_spec import ( # noqa: F401
|
|
6
|
+
SelectionCriteria,
|
|
7
|
+
SelectionDifference,
|
|
8
|
+
SelectionIntersection,
|
|
9
|
+
SelectionSpec,
|
|
10
|
+
SelectionUnion,
|
|
11
|
+
)
|
dvt/graph/cli.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# special support for CLI argument parsing.
|
|
2
|
+
# TODO: Remove as part of https://github.com/dbt-labs/dbt-core/issues/6701
|
|
3
|
+
import itertools
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from dvt.clients.yaml_helper import Dumper, Loader, yaml # noqa: F401
|
|
8
|
+
from dvt.contracts.selection import SelectorDefinition, SelectorFile
|
|
9
|
+
from dvt.flags import get_flags
|
|
10
|
+
|
|
11
|
+
from dbt_common.exceptions import DbtInternalError, DbtValidationError
|
|
12
|
+
|
|
13
|
+
from .selector_spec import (
|
|
14
|
+
IndirectSelection,
|
|
15
|
+
SelectionCriteria,
|
|
16
|
+
SelectionDifference,
|
|
17
|
+
SelectionIntersection,
|
|
18
|
+
SelectionSpec,
|
|
19
|
+
SelectionUnion,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
INTERSECTION_DELIMITER = ","
|
|
23
|
+
|
|
24
|
+
DEFAULT_INCLUDES: List[str] = ["fqn:*", "source:*", "exposure:*", "metric:*", "semantic_model:*"]
|
|
25
|
+
DEFAULT_EXCLUDES: List[str] = []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_union(
|
|
29
|
+
components: List[str],
|
|
30
|
+
expect_exists: bool,
|
|
31
|
+
) -> SelectionUnion:
|
|
32
|
+
# turn ['a b', 'c'] -> ['a', 'b', 'c']
|
|
33
|
+
raw_specs = itertools.chain.from_iterable(r.split(" ") for r in components)
|
|
34
|
+
union_components: List[SelectionSpec] = []
|
|
35
|
+
flags = get_flags()
|
|
36
|
+
# ['a', 'b', 'c,d'] -> union('a', 'b', intersection('c', 'd'))
|
|
37
|
+
for raw_spec in raw_specs:
|
|
38
|
+
intersection_components: List[SelectionSpec] = [
|
|
39
|
+
SelectionCriteria.from_single_spec(part)
|
|
40
|
+
for part in raw_spec.split(INTERSECTION_DELIMITER)
|
|
41
|
+
]
|
|
42
|
+
union_components.append(
|
|
43
|
+
SelectionIntersection(
|
|
44
|
+
components=intersection_components,
|
|
45
|
+
expect_exists=expect_exists,
|
|
46
|
+
raw=raw_spec,
|
|
47
|
+
indirect_selection=IndirectSelection(flags.INDIRECT_SELECTION),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return SelectionUnion(
|
|
51
|
+
components=union_components,
|
|
52
|
+
expect_exists=False,
|
|
53
|
+
raw=components,
|
|
54
|
+
indirect_selection=IndirectSelection(flags.INDIRECT_SELECTION),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_union_from_default(raw: Optional[List[str]], default: List[str]) -> SelectionUnion:
|
|
59
|
+
components: List[str]
|
|
60
|
+
expect_exists: bool
|
|
61
|
+
if raw is None:
|
|
62
|
+
return parse_union(components=default, expect_exists=False)
|
|
63
|
+
else:
|
|
64
|
+
return parse_union(components=raw, expect_exists=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_difference(
|
|
68
|
+
include: Optional[List[str]], exclude: Optional[List[str]]
|
|
69
|
+
) -> SelectionDifference:
|
|
70
|
+
|
|
71
|
+
if include == ():
|
|
72
|
+
include = None
|
|
73
|
+
|
|
74
|
+
included = parse_union_from_default(include, DEFAULT_INCLUDES)
|
|
75
|
+
excluded = parse_union_from_default(exclude, DEFAULT_EXCLUDES)
|
|
76
|
+
return SelectionDifference(components=[included, excluded])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
RawDefinition = Union[str, Dict[str, Any]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_list_dicts(dct: Dict[str, Any], key: str) -> List[RawDefinition]:
|
|
83
|
+
result: List[RawDefinition] = []
|
|
84
|
+
if key not in dct:
|
|
85
|
+
raise DbtInternalError(f"Expected to find key {key} in dict, only found {list(dct)}")
|
|
86
|
+
values = dct[key]
|
|
87
|
+
if not isinstance(values, list):
|
|
88
|
+
raise DbtValidationError(f'Invalid value for key "{key}". Expected a list.')
|
|
89
|
+
for value in values:
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
for value_key in value:
|
|
92
|
+
if not isinstance(value_key, str):
|
|
93
|
+
raise DbtValidationError(
|
|
94
|
+
f'Expected all keys to "{key}" dict to be strings, '
|
|
95
|
+
f'but "{value_key}" is a "{type(value_key)}"'
|
|
96
|
+
)
|
|
97
|
+
result.append(value)
|
|
98
|
+
elif isinstance(value, str):
|
|
99
|
+
result.append(value)
|
|
100
|
+
else:
|
|
101
|
+
raise DbtValidationError(
|
|
102
|
+
f'Invalid value type {type(value)} in key "{key}", expected '
|
|
103
|
+
f"dict or str (value: {value})."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_exclusions(definition, result={}) -> Optional[SelectionSpec]:
|
|
110
|
+
exclusions = _get_list_dicts(definition, "exclude")
|
|
111
|
+
parsed_exclusions = [parse_from_definition(excl, result=result) for excl in exclusions]
|
|
112
|
+
if len(parsed_exclusions) == 1:
|
|
113
|
+
return parsed_exclusions[0]
|
|
114
|
+
elif len(parsed_exclusions) > 1:
|
|
115
|
+
return SelectionUnion(components=parsed_exclusions, raw=exclusions)
|
|
116
|
+
else:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_include_exclude_subdefs(
|
|
121
|
+
definitions: List[RawDefinition], result={}
|
|
122
|
+
) -> Tuple[List[SelectionSpec], Optional[SelectionSpec]]:
|
|
123
|
+
include_parts: List[SelectionSpec] = []
|
|
124
|
+
diff_arg: Optional[SelectionSpec] = None
|
|
125
|
+
|
|
126
|
+
for definition in definitions:
|
|
127
|
+
if isinstance(definition, dict) and "exclude" in definition:
|
|
128
|
+
# do not allow multiple exclude: defs at the same level
|
|
129
|
+
if diff_arg is not None:
|
|
130
|
+
yaml_sel_cfg = yaml.dump(definition)
|
|
131
|
+
raise DbtValidationError(
|
|
132
|
+
f"You cannot provide multiple exclude arguments to the "
|
|
133
|
+
f"same selector set operator:\n{yaml_sel_cfg}"
|
|
134
|
+
)
|
|
135
|
+
diff_arg = _parse_exclusions(definition, result=result)
|
|
136
|
+
else:
|
|
137
|
+
include_parts.append(parse_from_definition(definition, result=result))
|
|
138
|
+
|
|
139
|
+
return (include_parts, diff_arg)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_union_definition(definition: Dict[str, Any], result={}) -> SelectionSpec:
|
|
143
|
+
union_def_parts = _get_list_dicts(definition, "union")
|
|
144
|
+
include, exclude = _parse_include_exclude_subdefs(union_def_parts, result=result)
|
|
145
|
+
|
|
146
|
+
union = SelectionUnion(components=include)
|
|
147
|
+
|
|
148
|
+
if exclude is None:
|
|
149
|
+
union.raw = definition
|
|
150
|
+
return union
|
|
151
|
+
else:
|
|
152
|
+
return SelectionDifference(components=[union, exclude], raw=definition)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_intersection_definition(definition: Dict[str, Any], result={}) -> SelectionSpec:
|
|
156
|
+
intersection_def_parts = _get_list_dicts(definition, "intersection")
|
|
157
|
+
include, exclude = _parse_include_exclude_subdefs(intersection_def_parts, result=result)
|
|
158
|
+
intersection = SelectionIntersection(components=include)
|
|
159
|
+
|
|
160
|
+
if exclude is None:
|
|
161
|
+
intersection.raw = definition
|
|
162
|
+
return intersection
|
|
163
|
+
else:
|
|
164
|
+
return SelectionDifference(components=[intersection, exclude], raw=definition)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def parse_dict_definition(definition: Dict[str, Any], result={}) -> SelectionSpec:
|
|
168
|
+
diff_arg: Optional[SelectionSpec] = None
|
|
169
|
+
if len(definition) == 1:
|
|
170
|
+
key = list(definition)[0]
|
|
171
|
+
value = definition[key]
|
|
172
|
+
if not isinstance(key, str):
|
|
173
|
+
raise DbtValidationError(
|
|
174
|
+
f'Expected definition key to be a "str", got one of type ' f'"{type(key)}" ({key})'
|
|
175
|
+
)
|
|
176
|
+
dct = {
|
|
177
|
+
"method": key,
|
|
178
|
+
"value": value,
|
|
179
|
+
}
|
|
180
|
+
elif definition.get("method") == "selector":
|
|
181
|
+
sel_def = definition.get("value")
|
|
182
|
+
if sel_def not in result:
|
|
183
|
+
raise DbtValidationError(f"Existing selector definition for {sel_def} not found.")
|
|
184
|
+
return result[definition["value"]]["definition"]
|
|
185
|
+
elif "method" in definition and "value" in definition:
|
|
186
|
+
dct = definition
|
|
187
|
+
if "exclude" in definition:
|
|
188
|
+
diff_arg = _parse_exclusions(definition, result=result)
|
|
189
|
+
dct = {k: v for k, v in dct.items() if k != "exclude"}
|
|
190
|
+
else:
|
|
191
|
+
raise DbtValidationError(
|
|
192
|
+
f'Expected either 1 key or else "method" '
|
|
193
|
+
f'and "value" keys, but got {list(definition)}'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# if key isn't a valid method name, this will raise
|
|
197
|
+
base = SelectionCriteria.selection_criteria_from_dict(definition, dct)
|
|
198
|
+
if diff_arg is None:
|
|
199
|
+
return base
|
|
200
|
+
else:
|
|
201
|
+
return SelectionDifference(components=[base, diff_arg])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_from_definition(
|
|
205
|
+
definition: RawDefinition,
|
|
206
|
+
rootlevel=False,
|
|
207
|
+
result: Dict[str, Dict[str, Union[SelectionSpec, bool]]] = {},
|
|
208
|
+
) -> SelectionSpec:
|
|
209
|
+
|
|
210
|
+
if (
|
|
211
|
+
isinstance(definition, dict)
|
|
212
|
+
and ("union" in definition or "intersection" in definition)
|
|
213
|
+
and rootlevel
|
|
214
|
+
and len(definition) > 1
|
|
215
|
+
):
|
|
216
|
+
keys = ",".join(definition.keys())
|
|
217
|
+
raise DbtValidationError(
|
|
218
|
+
f"Only a single 'union' or 'intersection' key is allowed "
|
|
219
|
+
f"in a root level selector definition; found {keys}."
|
|
220
|
+
)
|
|
221
|
+
if isinstance(definition, str):
|
|
222
|
+
return SelectionCriteria.from_single_spec(definition)
|
|
223
|
+
elif "union" in definition:
|
|
224
|
+
return parse_union_definition(definition, result=result)
|
|
225
|
+
elif "intersection" in definition:
|
|
226
|
+
return parse_intersection_definition(definition, result=result)
|
|
227
|
+
elif isinstance(definition, dict):
|
|
228
|
+
return parse_dict_definition(definition, result=result)
|
|
229
|
+
else:
|
|
230
|
+
raise DbtValidationError(
|
|
231
|
+
f"Expected to find union, intersection, str or dict, instead "
|
|
232
|
+
f"found {type(definition)}: {definition}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def parse_from_selectors_definition(
|
|
237
|
+
source: SelectorFile,
|
|
238
|
+
) -> Dict[str, Dict[str, Union[SelectionSpec, bool]]]:
|
|
239
|
+
result: Dict[str, Dict[str, Union[SelectionSpec, bool]]] = {}
|
|
240
|
+
selector: SelectorDefinition
|
|
241
|
+
for selector in source.selectors:
|
|
242
|
+
result[selector.name] = {
|
|
243
|
+
"default": selector.default,
|
|
244
|
+
"definition": parse_from_definition(
|
|
245
|
+
selector.definition, rootlevel=True, result=deepcopy(result)
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
return result
|
dvt/graph/graph.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from itertools import product
|
|
3
|
+
from typing import Iterable, Iterator, NewType, Optional, Set
|
|
4
|
+
|
|
5
|
+
import networkx as nx # type: ignore
|
|
6
|
+
|
|
7
|
+
from dbt_common.exceptions import DbtInternalError
|
|
8
|
+
|
|
9
|
+
UniqueId = NewType("UniqueId", str)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Graph:
|
|
13
|
+
"""A wrapper around the networkx graph that understands SelectionCriteria
|
|
14
|
+
and how they interact with the graph.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, graph) -> None:
|
|
18
|
+
self.graph: nx.DiGraph = graph
|
|
19
|
+
|
|
20
|
+
def nodes(self) -> Set[UniqueId]:
|
|
21
|
+
return set(self.graph.nodes())
|
|
22
|
+
|
|
23
|
+
def edges(self):
|
|
24
|
+
return self.graph.edges()
|
|
25
|
+
|
|
26
|
+
def __iter__(self) -> Iterator[UniqueId]:
|
|
27
|
+
return iter(self.graph.nodes())
|
|
28
|
+
|
|
29
|
+
def ancestors(self, node: UniqueId, max_depth: Optional[int] = None) -> Set[UniqueId]:
|
|
30
|
+
"""Returns all nodes having a path to `node` in `graph`"""
|
|
31
|
+
if not self.graph.has_node(node):
|
|
32
|
+
raise DbtInternalError(f"Node {node} not found in the graph!")
|
|
33
|
+
filtered_graph = self.exclude_edge_type("parent_test")
|
|
34
|
+
return {
|
|
35
|
+
child
|
|
36
|
+
for _, child in nx.bfs_edges(filtered_graph, node, reverse=True, depth_limit=max_depth)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def descendants(self, node: UniqueId, max_depth: Optional[int] = None) -> Set[UniqueId]:
|
|
40
|
+
"""Returns all nodes reachable from `node` in `graph`"""
|
|
41
|
+
if not self.graph.has_node(node):
|
|
42
|
+
raise DbtInternalError(f"Node {node} not found in the graph!")
|
|
43
|
+
filtered_graph = self.exclude_edge_type("parent_test")
|
|
44
|
+
return {child for _, child in nx.bfs_edges(filtered_graph, node, depth_limit=max_depth)}
|
|
45
|
+
|
|
46
|
+
def exclude_edge_type(self, edge_type_to_exclude):
|
|
47
|
+
return nx.subgraph_view(
|
|
48
|
+
self.graph,
|
|
49
|
+
filter_edge=partial(self.filter_edges_by_type, edge_type=edge_type_to_exclude),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def filter_edges_by_type(self, first_node, second_node, edge_type):
|
|
53
|
+
return self.graph.get_edge_data(first_node, second_node).get("edge_type") != edge_type
|
|
54
|
+
|
|
55
|
+
def select_childrens_parents(self, selected: Set[UniqueId]) -> Set[UniqueId]:
|
|
56
|
+
ancestors_for = self.select_children(selected) | selected
|
|
57
|
+
return self.select_parents(ancestors_for) | ancestors_for
|
|
58
|
+
|
|
59
|
+
def select_children(
|
|
60
|
+
self, selected: Set[UniqueId], max_depth: Optional[int] = None
|
|
61
|
+
) -> Set[UniqueId]:
|
|
62
|
+
"""Returns all nodes which are descendants of the 'selected' set.
|
|
63
|
+
Nodes in the 'selected' set are counted as children only if
|
|
64
|
+
they are descendants of other nodes in the 'selected' set."""
|
|
65
|
+
children: Set[UniqueId] = set()
|
|
66
|
+
i = 0
|
|
67
|
+
while len(selected) > 0 and (max_depth is None or i < max_depth):
|
|
68
|
+
next_layer: Set[UniqueId] = set()
|
|
69
|
+
for node in selected:
|
|
70
|
+
next_layer.update(
|
|
71
|
+
iter(
|
|
72
|
+
e[1]
|
|
73
|
+
for e in self.graph.out_edges(node)
|
|
74
|
+
if e[1] not in children
|
|
75
|
+
and self.filter_edges_by_type(e[0], e[1], "parent_test")
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
children.update(next_layer)
|
|
79
|
+
selected = next_layer
|
|
80
|
+
i += 1
|
|
81
|
+
|
|
82
|
+
return children
|
|
83
|
+
|
|
84
|
+
def select_parents(
|
|
85
|
+
self, selected: Set[UniqueId], max_depth: Optional[int] = None
|
|
86
|
+
) -> Set[UniqueId]:
|
|
87
|
+
"""Returns all nodes which are ancestors of the 'selected' set.
|
|
88
|
+
Nodes in the 'selected' set are counted as parents only if
|
|
89
|
+
they are ancestors of other nodes in the 'selected' set."""
|
|
90
|
+
parents: Set[UniqueId] = set()
|
|
91
|
+
i = 0
|
|
92
|
+
while len(selected) > 0 and (max_depth is None or i < max_depth):
|
|
93
|
+
next_layer: Set[UniqueId] = set()
|
|
94
|
+
for node in selected:
|
|
95
|
+
next_layer.update(
|
|
96
|
+
iter(
|
|
97
|
+
e[0]
|
|
98
|
+
for e in self.graph.in_edges(node)
|
|
99
|
+
if e[0] not in parents
|
|
100
|
+
and self.filter_edges_by_type(e[0], e[1], "parent_test")
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
parents.update(next_layer)
|
|
104
|
+
selected = next_layer
|
|
105
|
+
i += 1
|
|
106
|
+
|
|
107
|
+
return parents
|
|
108
|
+
|
|
109
|
+
def select_successors(self, selected: Set[UniqueId]) -> Set[UniqueId]:
|
|
110
|
+
successors: Set[UniqueId] = set()
|
|
111
|
+
for node in selected:
|
|
112
|
+
successors.update(self.graph.successors(node))
|
|
113
|
+
return successors
|
|
114
|
+
|
|
115
|
+
def get_subset_graph(self, selected: Iterable[UniqueId]) -> "Graph":
|
|
116
|
+
"""Create and return a new graph that is a shallow copy of the graph,
|
|
117
|
+
but with only the nodes in include_nodes. Transitive edges across
|
|
118
|
+
removed nodes are preserved as explicit new edges.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
new_graph: nx.DiGraph = self.graph.copy()
|
|
122
|
+
include_nodes: Set[UniqueId] = set(selected)
|
|
123
|
+
|
|
124
|
+
still_removing: bool = True
|
|
125
|
+
while still_removing:
|
|
126
|
+
nodes_to_remove = list(
|
|
127
|
+
node
|
|
128
|
+
for node in new_graph
|
|
129
|
+
if node not in include_nodes
|
|
130
|
+
and (new_graph.in_degree(node) * new_graph.out_degree(node)) == 0
|
|
131
|
+
)
|
|
132
|
+
if len(nodes_to_remove) == 0:
|
|
133
|
+
still_removing = False
|
|
134
|
+
else:
|
|
135
|
+
new_graph.remove_nodes_from(nodes_to_remove)
|
|
136
|
+
|
|
137
|
+
# sort remaining nodes by degree
|
|
138
|
+
remaining_nodes = list(new_graph.nodes())
|
|
139
|
+
remaining_nodes.sort(
|
|
140
|
+
key=lambda node: new_graph.in_degree(node) * new_graph.out_degree(node)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for node in remaining_nodes:
|
|
144
|
+
if node not in include_nodes:
|
|
145
|
+
source_nodes = [x for x, _ in new_graph.in_edges(node)]
|
|
146
|
+
target_nodes = [x for _, x in new_graph.out_edges(node)]
|
|
147
|
+
|
|
148
|
+
new_edges = product(source_nodes, target_nodes)
|
|
149
|
+
non_cyclic_new_edges = [
|
|
150
|
+
(source, target)
|
|
151
|
+
for source, target in new_edges
|
|
152
|
+
if source != target and not new_graph.has_edge(source, target)
|
|
153
|
+
] # removes cyclic refs and edges already existing in new graph
|
|
154
|
+
|
|
155
|
+
new_graph.add_edges_from(non_cyclic_new_edges)
|
|
156
|
+
new_graph.remove_node(node)
|
|
157
|
+
|
|
158
|
+
for node in include_nodes:
|
|
159
|
+
if node not in new_graph:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"Couldn't find model '{}' -- does it exist or is it disabled?".format(node)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return Graph(new_graph)
|
|
165
|
+
|
|
166
|
+
def subgraph(self, nodes: Iterable[UniqueId]) -> "Graph":
|
|
167
|
+
# Take the original networkx graph and return a subgraph containing only
|
|
168
|
+
# the selected unique_id nodes.
|
|
169
|
+
return Graph(self.graph.subgraph(nodes))
|
|
170
|
+
|
|
171
|
+
def get_dependent_nodes(self, node: UniqueId):
|
|
172
|
+
return nx.descendants(self.graph, node)
|