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/utils/utils.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import decimal
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date, datetime, time, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import PosixPath, WindowsPath
|
|
11
|
+
from typing import (
|
|
12
|
+
AbstractSet,
|
|
13
|
+
Any,
|
|
14
|
+
Dict,
|
|
15
|
+
Iterable,
|
|
16
|
+
Iterator,
|
|
17
|
+
List,
|
|
18
|
+
Mapping,
|
|
19
|
+
Optional,
|
|
20
|
+
Sequence,
|
|
21
|
+
Set,
|
|
22
|
+
Tuple,
|
|
23
|
+
Type,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
import jinja2
|
|
27
|
+
from dvt import flags
|
|
28
|
+
from dvt.exceptions import DuplicateAliasError
|
|
29
|
+
|
|
30
|
+
from dbt_common.exceptions import RecursionError
|
|
31
|
+
from dbt_common.helper_types import WarnErrorOptionsV2
|
|
32
|
+
from dbt_common.utils import md5
|
|
33
|
+
|
|
34
|
+
DECIMALS: Tuple[Type[Any], ...]
|
|
35
|
+
try:
|
|
36
|
+
import cdecimal # typing: ignore
|
|
37
|
+
except ImportError:
|
|
38
|
+
DECIMALS = (decimal.Decimal,)
|
|
39
|
+
else:
|
|
40
|
+
DECIMALS = (decimal.Decimal, cdecimal.Decimal)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExitCodes(int, Enum):
|
|
44
|
+
Success = 0
|
|
45
|
+
ModelError = 1
|
|
46
|
+
UnhandledError = 2
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def coalesce(*args):
|
|
50
|
+
for arg in args:
|
|
51
|
+
if arg is not None:
|
|
52
|
+
return arg
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_profile_from_project(project):
|
|
57
|
+
target_name = project.get("target", {})
|
|
58
|
+
profile = project.get("outputs", {}).get(target_name, {})
|
|
59
|
+
return profile
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_model_name_or_none(model):
|
|
63
|
+
if model is None:
|
|
64
|
+
name = "<None>"
|
|
65
|
+
|
|
66
|
+
elif isinstance(model, str):
|
|
67
|
+
name = model
|
|
68
|
+
elif isinstance(model, dict):
|
|
69
|
+
name = model.get("alias", model.get("name"))
|
|
70
|
+
elif hasattr(model, "alias"):
|
|
71
|
+
name = model.alias
|
|
72
|
+
elif hasattr(model, "name"):
|
|
73
|
+
name = model.name
|
|
74
|
+
else:
|
|
75
|
+
name = str(model)
|
|
76
|
+
return name
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def split_path(path):
|
|
80
|
+
return path.split(os.sep)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_pseudo_test_path(node_name, source_path):
|
|
84
|
+
"schema tests all come from schema.yml files. fake a source sql file"
|
|
85
|
+
source_path_parts = split_path(source_path)
|
|
86
|
+
source_path_parts.pop() # ignore filename
|
|
87
|
+
suffix = ["{}.sql".format(node_name)]
|
|
88
|
+
pseudo_path_parts = source_path_parts + suffix
|
|
89
|
+
return os.path.join(*pseudo_path_parts)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_pseudo_hook_path(hook_name):
|
|
93
|
+
path_parts = ["hooks", "{}.sql".format(hook_name)]
|
|
94
|
+
return os.path.join(*path_parts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_hash(model):
|
|
98
|
+
return md5(model.unique_id)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_hashed_contents(model):
|
|
102
|
+
return md5(model.raw_code)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def flatten_nodes(dep_list):
|
|
106
|
+
return list(itertools.chain.from_iterable(dep_list))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class memoized:
|
|
110
|
+
"""Decorator. Caches a function's return value each time it is called. If
|
|
111
|
+
called later with the same arguments, the cached value is returned (not
|
|
112
|
+
reevaluated).
|
|
113
|
+
|
|
114
|
+
Taken from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, func) -> None:
|
|
117
|
+
self.func = func
|
|
118
|
+
self.cache: Dict[Any, Any] = {}
|
|
119
|
+
|
|
120
|
+
def __call__(self, *args):
|
|
121
|
+
if not isinstance(args, collections.abc.Hashable):
|
|
122
|
+
# uncacheable. a list, for instance.
|
|
123
|
+
# better to not cache than blow up.
|
|
124
|
+
return self.func(*args)
|
|
125
|
+
if args in self.cache:
|
|
126
|
+
return self.cache[args]
|
|
127
|
+
value = self.func(*args)
|
|
128
|
+
self.cache[args] = value
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
def __repr__(self):
|
|
132
|
+
"""Return the function's docstring."""
|
|
133
|
+
return self.func.__doc__
|
|
134
|
+
|
|
135
|
+
def __get__(self, obj, objtype):
|
|
136
|
+
"""Support instance methods."""
|
|
137
|
+
return functools.partial(self.__call__, obj)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def add_ephemeral_model_prefix(s: str) -> str:
|
|
141
|
+
return "__dbt__cte__{}".format(s)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def timestring() -> str:
|
|
145
|
+
"""Get the current datetime as an RFC 3339-compliant string"""
|
|
146
|
+
# isoformat doesn't include the mandatory trailing 'Z' for UTC.
|
|
147
|
+
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def humanize_execution_time(execution_time: int) -> str:
|
|
151
|
+
minutes, seconds = divmod(execution_time, 60)
|
|
152
|
+
hours, minutes = divmod(minutes, 60)
|
|
153
|
+
|
|
154
|
+
return f" in {int(hours)} hours {int(minutes)} minutes and {seconds:0.2f} seconds"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class JSONEncoder(json.JSONEncoder):
|
|
158
|
+
"""A 'custom' json encoder that does normal json encoder things, but also
|
|
159
|
+
handles `Decimal`s and `Undefined`s. Decimals can lose precision because
|
|
160
|
+
they get converted to floats. Undefined's are serialized to an empty string
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def default(self, obj):
|
|
164
|
+
if isinstance(obj, DECIMALS):
|
|
165
|
+
return float(obj)
|
|
166
|
+
elif isinstance(obj, (datetime, date, time)):
|
|
167
|
+
return obj.isoformat()
|
|
168
|
+
elif isinstance(obj, jinja2.Undefined):
|
|
169
|
+
return ""
|
|
170
|
+
elif isinstance(obj, Exception):
|
|
171
|
+
return repr(obj)
|
|
172
|
+
elif hasattr(obj, "to_dict"):
|
|
173
|
+
# if we have a to_dict we should try to serialize the result of
|
|
174
|
+
# that!
|
|
175
|
+
return obj.to_dict(omit_none=True)
|
|
176
|
+
else:
|
|
177
|
+
return super().default(obj)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Translator:
|
|
181
|
+
def __init__(self, aliases: Mapping[str, str], recursive: bool = False) -> None:
|
|
182
|
+
self.aliases = aliases
|
|
183
|
+
self.recursive = recursive
|
|
184
|
+
|
|
185
|
+
def translate_mapping(self, kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
|
186
|
+
result: Dict[str, Any] = {}
|
|
187
|
+
|
|
188
|
+
for key, value in kwargs.items():
|
|
189
|
+
canonical_key = self.aliases.get(key, key)
|
|
190
|
+
if canonical_key in result:
|
|
191
|
+
raise DuplicateAliasError(kwargs, self.aliases, canonical_key)
|
|
192
|
+
result[canonical_key] = self.translate_value(value)
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def translate_sequence(self, value: Sequence[Any]) -> List[Any]:
|
|
196
|
+
return [self.translate_value(v) for v in value]
|
|
197
|
+
|
|
198
|
+
def translate_value(self, value: Any) -> Any:
|
|
199
|
+
if self.recursive:
|
|
200
|
+
if isinstance(value, Mapping):
|
|
201
|
+
return self.translate_mapping(value)
|
|
202
|
+
elif isinstance(value, (list, tuple)):
|
|
203
|
+
return self.translate_sequence(value)
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
def translate(self, value: Mapping[str, Any]) -> Dict[str, Any]:
|
|
207
|
+
try:
|
|
208
|
+
return self.translate_mapping(value)
|
|
209
|
+
except RuntimeError as exc:
|
|
210
|
+
if "maximum recursion depth exceeded" in str(exc):
|
|
211
|
+
raise RecursionError("Cycle detected in a value passed to translate!")
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def translate_aliases(
|
|
216
|
+
kwargs: Dict[str, Any],
|
|
217
|
+
aliases: Dict[str, str],
|
|
218
|
+
recurse: bool = False,
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""Given a dict of keyword arguments and a dict mapping aliases to their
|
|
221
|
+
canonical values, canonicalize the keys in the kwargs dict.
|
|
222
|
+
|
|
223
|
+
If recurse is True, perform this operation recursively.
|
|
224
|
+
|
|
225
|
+
:returns: A dict containing all the values in kwargs referenced by their
|
|
226
|
+
canonical key.
|
|
227
|
+
:raises: `AliasError`, if a canonical key is defined more than once.
|
|
228
|
+
"""
|
|
229
|
+
translator = Translator(aliases, recurse)
|
|
230
|
+
return translator.translate(kwargs)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Note that this only affects hologram json validation.
|
|
234
|
+
# It has no effect on mashumaro serialization.
|
|
235
|
+
# Q: Can this be removed?
|
|
236
|
+
def restrict_to(*restrictions):
|
|
237
|
+
"""Create the metadata for a restricted dataclass field"""
|
|
238
|
+
return {"restrict": list(restrictions)}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def coerce_dict_str(value: Any) -> Optional[Dict[str, Any]]:
|
|
242
|
+
"""For annoying mypy reasons, this helper makes dealing with nested dicts
|
|
243
|
+
easier. You get either `None` if it's not a Dict[str, Any], or the
|
|
244
|
+
Dict[str, Any] you expected (to pass it to dbtClassMixin.from_dict(...)).
|
|
245
|
+
"""
|
|
246
|
+
if isinstance(value, dict) and all(isinstance(k, str) for k in value):
|
|
247
|
+
return value
|
|
248
|
+
else:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _coerce_decimal(value):
|
|
253
|
+
if isinstance(value, DECIMALS):
|
|
254
|
+
return float(value)
|
|
255
|
+
return value
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def fqn_search(root: Dict[str, Any], fqn: List[str]) -> Iterator[Dict[str, Any]]:
|
|
259
|
+
"""Iterate into a nested dictionary, looking for keys in the fqn as levels.
|
|
260
|
+
Yield the level config.
|
|
261
|
+
"""
|
|
262
|
+
yield root
|
|
263
|
+
|
|
264
|
+
for level in fqn:
|
|
265
|
+
level_config = root.get(level, None)
|
|
266
|
+
if not isinstance(level_config, dict):
|
|
267
|
+
break
|
|
268
|
+
# This used to do a 'deepcopy',
|
|
269
|
+
# but it didn't seem to be necessary
|
|
270
|
+
yield level_config
|
|
271
|
+
root = level_config
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
StringMap = Mapping[str, Any]
|
|
275
|
+
StringMapList = List[StringMap]
|
|
276
|
+
StringMapIter = Iterable[StringMap]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class MultiDict(Mapping[str, Any]):
|
|
280
|
+
"""Implement the mapping protocol using a list of mappings. The most
|
|
281
|
+
recently added mapping "wins".
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def __init__(self, sources: Optional[StringMapList] = None) -> None:
|
|
285
|
+
super().__init__()
|
|
286
|
+
self.sources: StringMapList
|
|
287
|
+
|
|
288
|
+
if sources is None:
|
|
289
|
+
self.sources = []
|
|
290
|
+
else:
|
|
291
|
+
self.sources = sources
|
|
292
|
+
|
|
293
|
+
def add_from(self, sources: StringMapIter):
|
|
294
|
+
self.sources.extend(sources)
|
|
295
|
+
|
|
296
|
+
def add(self, source: StringMap):
|
|
297
|
+
self.sources.append(source)
|
|
298
|
+
|
|
299
|
+
def _keyset(self) -> AbstractSet[str]:
|
|
300
|
+
# return the set of keys
|
|
301
|
+
keys: Set[str] = set()
|
|
302
|
+
for entry in self._itersource():
|
|
303
|
+
keys.update(entry)
|
|
304
|
+
return keys
|
|
305
|
+
|
|
306
|
+
def _itersource(self) -> StringMapIter:
|
|
307
|
+
return reversed(self.sources)
|
|
308
|
+
|
|
309
|
+
def __iter__(self) -> Iterator[str]:
|
|
310
|
+
# we need to avoid duplicate keys
|
|
311
|
+
return iter(self._keyset())
|
|
312
|
+
|
|
313
|
+
def __len__(self):
|
|
314
|
+
return len(self._keyset())
|
|
315
|
+
|
|
316
|
+
def __getitem__(self, name: str) -> Any:
|
|
317
|
+
for entry in self._itersource():
|
|
318
|
+
if name in entry:
|
|
319
|
+
return entry[name]
|
|
320
|
+
raise KeyError(name)
|
|
321
|
+
|
|
322
|
+
def __contains__(self, name) -> bool:
|
|
323
|
+
return any((name in entry for entry in self._itersource()))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# This is used to serialize the args in the run_results and in the logs.
|
|
327
|
+
# We do this separately because there are a few fields that don't serialize,
|
|
328
|
+
# i.e. PosixPath, WindowsPath, and types. It also includes args from both
|
|
329
|
+
# cli args and flags, which is more complete than just the cli args.
|
|
330
|
+
# If new args are added that are false by default (particularly in the
|
|
331
|
+
# global options) they should be added to the 'default_false_keys' list.
|
|
332
|
+
def args_to_dict(args):
|
|
333
|
+
var_args = vars(args).copy()
|
|
334
|
+
# update the args with the flags, which could also come from environment
|
|
335
|
+
# variables or project_flags
|
|
336
|
+
flag_dict = flags.get_flag_dict()
|
|
337
|
+
var_args.update(flag_dict)
|
|
338
|
+
dict_args = {}
|
|
339
|
+
# remove args keys that clutter up the dictionary
|
|
340
|
+
for key in var_args:
|
|
341
|
+
if key.lower() in var_args and key == key.upper():
|
|
342
|
+
# skip all capped keys being introduced by Flags in dbt.cli.flags
|
|
343
|
+
continue
|
|
344
|
+
if key in ["cls", "mp_context"]:
|
|
345
|
+
continue
|
|
346
|
+
if var_args[key] is None:
|
|
347
|
+
continue
|
|
348
|
+
# TODO: add more default_false_keys
|
|
349
|
+
default_false_keys = (
|
|
350
|
+
"debug",
|
|
351
|
+
"full_refresh",
|
|
352
|
+
"fail_fast",
|
|
353
|
+
"warn_error",
|
|
354
|
+
"single_threaded",
|
|
355
|
+
"log_cache_events",
|
|
356
|
+
"store_failures",
|
|
357
|
+
"use_experimental_parser",
|
|
358
|
+
)
|
|
359
|
+
default_empty_yaml_dict_keys = ("vars", "warn_error_options")
|
|
360
|
+
if key in default_false_keys and var_args[key] is False:
|
|
361
|
+
continue
|
|
362
|
+
if key in default_empty_yaml_dict_keys and var_args[key] == "{}":
|
|
363
|
+
continue
|
|
364
|
+
# this was required for a test case
|
|
365
|
+
if isinstance(var_args[key], PosixPath) or isinstance(var_args[key], WindowsPath):
|
|
366
|
+
var_args[key] = str(var_args[key])
|
|
367
|
+
if isinstance(var_args[key], WarnErrorOptionsV2):
|
|
368
|
+
var_args[key] = var_args[key].to_dict()
|
|
369
|
+
|
|
370
|
+
dict_args[key] = var_args[key]
|
|
371
|
+
return dict_args
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Taken from https://github.com/python/cpython/blob/3.11/Lib/distutils/util.py
|
|
375
|
+
# This is a copy of the function from distutils.util, which was removed in Python 3.12.
|
|
376
|
+
def strtobool(val: str) -> bool:
|
|
377
|
+
"""Convert a string representation of truth to True or False.
|
|
378
|
+
|
|
379
|
+
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
|
|
380
|
+
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
|
381
|
+
'val' is anything else.
|
|
382
|
+
"""
|
|
383
|
+
val = val.lower()
|
|
384
|
+
if val in ("y", "yes", "t", "true", "on", "1"):
|
|
385
|
+
return True
|
|
386
|
+
elif val in ("n", "no", "f", "false", "off", "0"):
|
|
387
|
+
return False
|
|
388
|
+
else:
|
|
389
|
+
raise ValueError("invalid truth value %r" % (val,))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def try_get_max_rss_kb() -> Optional[int]:
|
|
393
|
+
"""Attempts to get the high water mark for this process's memory use via
|
|
394
|
+
the most reliable and accurate mechanism available through the host OS.
|
|
395
|
+
Currently only implemented for Linux."""
|
|
396
|
+
if sys.platform == "linux" and os.path.isfile("/proc/self/status"):
|
|
397
|
+
try:
|
|
398
|
+
# On Linux, the most reliable documented mechanism for getting the RSS
|
|
399
|
+
# high-water-mark comes from the line confusingly labeled VmHWM in the
|
|
400
|
+
# /proc/self/status virtual file.
|
|
401
|
+
with open("/proc/self/status") as f:
|
|
402
|
+
for line in f:
|
|
403
|
+
if line.startswith("VmHWM:"):
|
|
404
|
+
return int(str.split(line)[1])
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
return None
|
dvt/version.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from importlib import metadata as importlib_metadata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterator, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
import dbt_common.semver as semver
|
|
14
|
+
from dbt_common.ui import green, yellow
|
|
15
|
+
|
|
16
|
+
PYPI_VERSION_URL = "https://pypi.org/pypi/dbt-core/json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_version_information() -> str:
|
|
20
|
+
installed = get_installed_version()
|
|
21
|
+
latest = get_latest_version()
|
|
22
|
+
|
|
23
|
+
core_msg_lines, core_info_msg = _get_core_msg_lines(installed, latest)
|
|
24
|
+
core_msg = _format_core_msg(core_msg_lines)
|
|
25
|
+
plugin_version_msg = _get_plugins_msg()
|
|
26
|
+
|
|
27
|
+
msg_lines = [core_msg]
|
|
28
|
+
|
|
29
|
+
if core_info_msg != "":
|
|
30
|
+
msg_lines.append(core_info_msg)
|
|
31
|
+
|
|
32
|
+
msg_lines.append(plugin_version_msg)
|
|
33
|
+
msg_lines.append("")
|
|
34
|
+
|
|
35
|
+
return "\n\n".join(msg_lines)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_installed_version() -> semver.VersionSpecifier:
|
|
39
|
+
return semver.VersionSpecifier.from_version_string(__version__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_latest_version(
|
|
43
|
+
version_url: str = PYPI_VERSION_URL,
|
|
44
|
+
) -> Optional[semver.VersionSpecifier]:
|
|
45
|
+
try:
|
|
46
|
+
resp = requests.get(version_url, timeout=1)
|
|
47
|
+
data = resp.json()
|
|
48
|
+
version_string = data["info"]["version"]
|
|
49
|
+
except (json.JSONDecodeError, KeyError, requests.RequestException):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
return semver.VersionSpecifier.from_version_string(version_string)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_core_msg_lines(
|
|
56
|
+
installed: semver.VersionSpecifier,
|
|
57
|
+
latest: Optional[semver.VersionSpecifier],
|
|
58
|
+
) -> Tuple[List[List[str]], str]:
|
|
59
|
+
installed_s = installed.to_version_string(skip_matcher=True)
|
|
60
|
+
installed_line = ["installed", installed_s, ""]
|
|
61
|
+
update_info = ""
|
|
62
|
+
|
|
63
|
+
if latest is None:
|
|
64
|
+
update_info = (
|
|
65
|
+
" The latest version of dbt-core could not be determined!\n"
|
|
66
|
+
" Make sure that the following URL is accessible:\n"
|
|
67
|
+
f" {PYPI_VERSION_URL}"
|
|
68
|
+
)
|
|
69
|
+
return [installed_line], update_info
|
|
70
|
+
|
|
71
|
+
latest_s = latest.to_version_string(skip_matcher=True)
|
|
72
|
+
latest_line = ["latest", latest_s, green("Up to date!")]
|
|
73
|
+
|
|
74
|
+
if installed > latest:
|
|
75
|
+
latest_line[2] = yellow("Ahead of latest version!")
|
|
76
|
+
elif installed < latest:
|
|
77
|
+
latest_line[2] = yellow("Update available!")
|
|
78
|
+
update_info = (
|
|
79
|
+
" Your version of dbt-core is out of date!\n"
|
|
80
|
+
" You can find instructions for upgrading here:\n"
|
|
81
|
+
" https://docs.getdbt.com/docs/installation"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
installed_line,
|
|
86
|
+
latest_line,
|
|
87
|
+
], update_info
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_core_msg(lines: List[List[str]]) -> str:
|
|
91
|
+
msg = "Core:\n"
|
|
92
|
+
msg_lines = []
|
|
93
|
+
|
|
94
|
+
for name, version, update_msg in _pad_lines(lines, seperator=":"):
|
|
95
|
+
line_msg = f" - {name} {version}"
|
|
96
|
+
if update_msg != "":
|
|
97
|
+
line_msg += f" - {update_msg}"
|
|
98
|
+
msg_lines.append(line_msg)
|
|
99
|
+
|
|
100
|
+
return msg + "\n".join(msg_lines)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_plugins_msg() -> str:
|
|
104
|
+
msg_lines = ["Plugins:"]
|
|
105
|
+
|
|
106
|
+
plugins = []
|
|
107
|
+
display_update_msg = False
|
|
108
|
+
for name, version_s in _get_dbt_plugins_info():
|
|
109
|
+
compatability_msg, needs_update = _get_plugin_msg_info(name, version_s, installed)
|
|
110
|
+
if needs_update:
|
|
111
|
+
display_update_msg = True
|
|
112
|
+
plugins.append([name, version_s, compatability_msg])
|
|
113
|
+
|
|
114
|
+
for plugin in _pad_lines(plugins, seperator=":"):
|
|
115
|
+
msg_lines.append(_format_single_plugin(plugin, ""))
|
|
116
|
+
|
|
117
|
+
if display_update_msg:
|
|
118
|
+
update_msg = (
|
|
119
|
+
" At least one plugin is out of date with dbt-core.\n"
|
|
120
|
+
" You can find instructions for upgrading here:\n"
|
|
121
|
+
" https://docs.getdbt.com/docs/installation"
|
|
122
|
+
)
|
|
123
|
+
msg_lines += ["", update_msg]
|
|
124
|
+
|
|
125
|
+
return "\n".join(msg_lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_plugin_msg_info(
|
|
129
|
+
name: str, version_s: str, core: semver.VersionSpecifier
|
|
130
|
+
) -> Tuple[str, bool]:
|
|
131
|
+
plugin = semver.VersionSpecifier.from_version_string(version_s)
|
|
132
|
+
latest_plugin = get_latest_version(version_url=get_package_pypi_url(name))
|
|
133
|
+
|
|
134
|
+
needs_update = False
|
|
135
|
+
|
|
136
|
+
if not latest_plugin:
|
|
137
|
+
compatibility_msg = yellow("Could not determine latest version")
|
|
138
|
+
return (compatibility_msg, needs_update)
|
|
139
|
+
|
|
140
|
+
if plugin < latest_plugin:
|
|
141
|
+
compatibility_msg = yellow("Update available!")
|
|
142
|
+
needs_update = True
|
|
143
|
+
elif plugin > latest_plugin:
|
|
144
|
+
compatibility_msg = yellow("Ahead of latest version!")
|
|
145
|
+
else:
|
|
146
|
+
compatibility_msg = green("Up to date!")
|
|
147
|
+
|
|
148
|
+
return (compatibility_msg, needs_update)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _format_single_plugin(plugin: List[str], update_msg: str) -> str:
|
|
152
|
+
name, version_s, compatability_msg = plugin
|
|
153
|
+
msg = f" - {name} {version_s} - {compatability_msg}"
|
|
154
|
+
if update_msg != "":
|
|
155
|
+
msg += f"\n{update_msg}\n"
|
|
156
|
+
return msg
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _pad_lines(lines: List[List[str]], seperator: str = "") -> List[List[str]]:
|
|
160
|
+
if len(lines) == 0:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
# count the max line length for each column in the line
|
|
164
|
+
counter = [0] * len(lines[0])
|
|
165
|
+
for line in lines:
|
|
166
|
+
for i, item in enumerate(line):
|
|
167
|
+
counter[i] = max(counter[i], len(item))
|
|
168
|
+
|
|
169
|
+
result: List[List[str]] = []
|
|
170
|
+
for i, line in enumerate(lines):
|
|
171
|
+
# add another list to hold padded strings
|
|
172
|
+
if len(result) == i:
|
|
173
|
+
result.append([""] * len(line))
|
|
174
|
+
|
|
175
|
+
# iterate over columns in the line
|
|
176
|
+
for j, item in enumerate(line):
|
|
177
|
+
# the last column does not need padding
|
|
178
|
+
if j == len(line) - 1:
|
|
179
|
+
result[i][j] = item
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# if the following column has no length
|
|
183
|
+
# the string does not need padding
|
|
184
|
+
if counter[j + 1] == 0:
|
|
185
|
+
result[i][j] = item
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# only add the seperator to the first column
|
|
189
|
+
offset = 0
|
|
190
|
+
if j == 0 and seperator != "":
|
|
191
|
+
item += seperator
|
|
192
|
+
offset = len(seperator)
|
|
193
|
+
|
|
194
|
+
result[i][j] = item.ljust(counter[j] + offset)
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_package_pypi_url(package_name: str) -> str:
|
|
200
|
+
return f"https://pypi.org/pypi/dbt-{package_name}/json"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _get_dbt_plugins_info() -> Iterator[Tuple[str, str]]:
|
|
204
|
+
for plugin_name in _get_adapter_plugin_names():
|
|
205
|
+
if plugin_name == "core":
|
|
206
|
+
continue
|
|
207
|
+
try:
|
|
208
|
+
mod = importlib.import_module(f"dbt.adapters.{plugin_name}.__version__")
|
|
209
|
+
except ImportError:
|
|
210
|
+
# not an adapter
|
|
211
|
+
continue
|
|
212
|
+
yield plugin_name, mod.version
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _get_adapter_plugin_names() -> Iterator[str]:
|
|
216
|
+
spec = importlib.util.find_spec("dbt.adapters")
|
|
217
|
+
# If None, then nothing provides an importable 'dbt.adapters', so we will
|
|
218
|
+
# not be reporting plugin versions today
|
|
219
|
+
if spec is None or spec.submodule_search_locations is None:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
for adapters_path in spec.submodule_search_locations:
|
|
223
|
+
version_glob = os.path.join(adapters_path, "*", "__version__.py")
|
|
224
|
+
for version_path in glob.glob(version_glob):
|
|
225
|
+
# the path is like .../dbt/adapters/{plugin_name}/__version__.py
|
|
226
|
+
# except it could be \\ on windows!
|
|
227
|
+
plugin_root, _ = os.path.split(version_path)
|
|
228
|
+
_, plugin_name = os.path.split(plugin_root)
|
|
229
|
+
yield plugin_name
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _resolve_version() -> str:
|
|
233
|
+
try:
|
|
234
|
+
return importlib_metadata.version("dvt-core")
|
|
235
|
+
except importlib_metadata.PackageNotFoundError:
|
|
236
|
+
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
237
|
+
if not pyproject_path.exists():
|
|
238
|
+
raise RuntimeError("Unable to locate pyproject.toml to determine dvt-core version")
|
|
239
|
+
|
|
240
|
+
text = pyproject_path.read_text(encoding="utf-8")
|
|
241
|
+
match = re.search(r'^version\s*=\s*"(?P<version>[^"]+)"', text, re.MULTILINE)
|
|
242
|
+
if match:
|
|
243
|
+
return match.group("version")
|
|
244
|
+
|
|
245
|
+
raise RuntimeError("Unable to determine dbt-core version from pyproject.toml")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
__version__ = _resolve_version()
|
|
249
|
+
installed = get_installed_version()
|