dvt-core 1.11.0b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dvt-core might be problematic. Click here for more details.

Files changed (261) hide show
  1. dvt/__init__.py +7 -0
  2. dvt/_pydantic_shim.py +26 -0
  3. dvt/adapters/__init__.py +16 -0
  4. dvt/adapters/multi_adapter_manager.py +268 -0
  5. dvt/artifacts/__init__.py +0 -0
  6. dvt/artifacts/exceptions/__init__.py +1 -0
  7. dvt/artifacts/exceptions/schemas.py +31 -0
  8. dvt/artifacts/resources/__init__.py +116 -0
  9. dvt/artifacts/resources/base.py +68 -0
  10. dvt/artifacts/resources/types.py +93 -0
  11. dvt/artifacts/resources/v1/analysis.py +10 -0
  12. dvt/artifacts/resources/v1/catalog.py +23 -0
  13. dvt/artifacts/resources/v1/components.py +275 -0
  14. dvt/artifacts/resources/v1/config.py +282 -0
  15. dvt/artifacts/resources/v1/documentation.py +11 -0
  16. dvt/artifacts/resources/v1/exposure.py +52 -0
  17. dvt/artifacts/resources/v1/function.py +53 -0
  18. dvt/artifacts/resources/v1/generic_test.py +32 -0
  19. dvt/artifacts/resources/v1/group.py +22 -0
  20. dvt/artifacts/resources/v1/hook.py +11 -0
  21. dvt/artifacts/resources/v1/macro.py +30 -0
  22. dvt/artifacts/resources/v1/metric.py +173 -0
  23. dvt/artifacts/resources/v1/model.py +146 -0
  24. dvt/artifacts/resources/v1/owner.py +10 -0
  25. dvt/artifacts/resources/v1/saved_query.py +112 -0
  26. dvt/artifacts/resources/v1/seed.py +42 -0
  27. dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  28. dvt/artifacts/resources/v1/semantic_model.py +315 -0
  29. dvt/artifacts/resources/v1/singular_test.py +14 -0
  30. dvt/artifacts/resources/v1/snapshot.py +92 -0
  31. dvt/artifacts/resources/v1/source_definition.py +85 -0
  32. dvt/artifacts/resources/v1/sql_operation.py +10 -0
  33. dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
  34. dvt/artifacts/schemas/__init__.py +0 -0
  35. dvt/artifacts/schemas/base.py +191 -0
  36. dvt/artifacts/schemas/batch_results.py +24 -0
  37. dvt/artifacts/schemas/catalog/__init__.py +12 -0
  38. dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  39. dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
  40. dvt/artifacts/schemas/freshness/__init__.py +1 -0
  41. dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  42. dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
  43. dvt/artifacts/schemas/manifest/__init__.py +2 -0
  44. dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  45. dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
  46. dvt/artifacts/schemas/results.py +148 -0
  47. dvt/artifacts/schemas/run/__init__.py +2 -0
  48. dvt/artifacts/schemas/run/v5/__init__.py +0 -0
  49. dvt/artifacts/schemas/run/v5/run.py +184 -0
  50. dvt/artifacts/schemas/upgrades/__init__.py +4 -0
  51. dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  52. dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  53. dvt/artifacts/utils/validation.py +153 -0
  54. dvt/cli/__init__.py +1 -0
  55. dvt/cli/context.py +16 -0
  56. dvt/cli/exceptions.py +56 -0
  57. dvt/cli/flags.py +558 -0
  58. dvt/cli/main.py +971 -0
  59. dvt/cli/option_types.py +121 -0
  60. dvt/cli/options.py +79 -0
  61. dvt/cli/params.py +803 -0
  62. dvt/cli/requires.py +478 -0
  63. dvt/cli/resolvers.py +32 -0
  64. dvt/cli/types.py +40 -0
  65. dvt/clients/__init__.py +0 -0
  66. dvt/clients/checked_load.py +82 -0
  67. dvt/clients/git.py +164 -0
  68. dvt/clients/jinja.py +206 -0
  69. dvt/clients/jinja_static.py +245 -0
  70. dvt/clients/registry.py +192 -0
  71. dvt/clients/yaml_helper.py +68 -0
  72. dvt/compilation.py +833 -0
  73. dvt/compute/__init__.py +26 -0
  74. dvt/compute/base.py +288 -0
  75. dvt/compute/engines/__init__.py +13 -0
  76. dvt/compute/engines/duckdb_engine.py +368 -0
  77. dvt/compute/engines/spark_engine.py +273 -0
  78. dvt/compute/query_analyzer.py +212 -0
  79. dvt/compute/router.py +483 -0
  80. dvt/config/__init__.py +4 -0
  81. dvt/config/catalogs.py +95 -0
  82. dvt/config/compute_config.py +406 -0
  83. dvt/config/profile.py +411 -0
  84. dvt/config/profiles_v2.py +464 -0
  85. dvt/config/project.py +893 -0
  86. dvt/config/renderer.py +232 -0
  87. dvt/config/runtime.py +491 -0
  88. dvt/config/selectors.py +209 -0
  89. dvt/config/utils.py +78 -0
  90. dvt/connectors/.gitignore +6 -0
  91. dvt/connectors/README.md +306 -0
  92. dvt/connectors/catalog.yml +217 -0
  93. dvt/connectors/download_connectors.py +300 -0
  94. dvt/constants.py +29 -0
  95. dvt/context/__init__.py +0 -0
  96. dvt/context/base.py +746 -0
  97. dvt/context/configured.py +136 -0
  98. dvt/context/context_config.py +350 -0
  99. dvt/context/docs.py +82 -0
  100. dvt/context/exceptions_jinja.py +179 -0
  101. dvt/context/macro_resolver.py +195 -0
  102. dvt/context/macros.py +171 -0
  103. dvt/context/manifest.py +73 -0
  104. dvt/context/providers.py +2198 -0
  105. dvt/context/query_header.py +14 -0
  106. dvt/context/secret.py +59 -0
  107. dvt/context/target.py +74 -0
  108. dvt/contracts/__init__.py +0 -0
  109. dvt/contracts/files.py +413 -0
  110. dvt/contracts/graph/__init__.py +0 -0
  111. dvt/contracts/graph/manifest.py +1904 -0
  112. dvt/contracts/graph/metrics.py +98 -0
  113. dvt/contracts/graph/model_config.py +71 -0
  114. dvt/contracts/graph/node_args.py +42 -0
  115. dvt/contracts/graph/nodes.py +1806 -0
  116. dvt/contracts/graph/semantic_manifest.py +233 -0
  117. dvt/contracts/graph/unparsed.py +812 -0
  118. dvt/contracts/project.py +417 -0
  119. dvt/contracts/results.py +53 -0
  120. dvt/contracts/selection.py +23 -0
  121. dvt/contracts/sql.py +86 -0
  122. dvt/contracts/state.py +69 -0
  123. dvt/contracts/util.py +46 -0
  124. dvt/deprecations.py +347 -0
  125. dvt/deps/__init__.py +0 -0
  126. dvt/deps/base.py +153 -0
  127. dvt/deps/git.py +196 -0
  128. dvt/deps/local.py +80 -0
  129. dvt/deps/registry.py +131 -0
  130. dvt/deps/resolver.py +149 -0
  131. dvt/deps/tarball.py +121 -0
  132. dvt/docs/source/_ext/dbt_click.py +118 -0
  133. dvt/docs/source/conf.py +32 -0
  134. dvt/env_vars.py +64 -0
  135. dvt/event_time/event_time.py +40 -0
  136. dvt/event_time/sample_window.py +60 -0
  137. dvt/events/__init__.py +16 -0
  138. dvt/events/base_types.py +37 -0
  139. dvt/events/core_types_pb2.py +2 -0
  140. dvt/events/logging.py +109 -0
  141. dvt/events/types.py +2534 -0
  142. dvt/exceptions.py +1487 -0
  143. dvt/flags.py +89 -0
  144. dvt/graph/__init__.py +11 -0
  145. dvt/graph/cli.py +248 -0
  146. dvt/graph/graph.py +172 -0
  147. dvt/graph/queue.py +213 -0
  148. dvt/graph/selector.py +375 -0
  149. dvt/graph/selector_methods.py +976 -0
  150. dvt/graph/selector_spec.py +223 -0
  151. dvt/graph/thread_pool.py +18 -0
  152. dvt/hooks.py +21 -0
  153. dvt/include/README.md +49 -0
  154. dvt/include/__init__.py +3 -0
  155. dvt/include/global_project.py +4 -0
  156. dvt/include/starter_project/.gitignore +4 -0
  157. dvt/include/starter_project/README.md +15 -0
  158. dvt/include/starter_project/__init__.py +3 -0
  159. dvt/include/starter_project/analyses/.gitkeep +0 -0
  160. dvt/include/starter_project/dvt_project.yml +36 -0
  161. dvt/include/starter_project/macros/.gitkeep +0 -0
  162. dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
  163. dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
  164. dvt/include/starter_project/models/example/schema.yml +21 -0
  165. dvt/include/starter_project/seeds/.gitkeep +0 -0
  166. dvt/include/starter_project/snapshots/.gitkeep +0 -0
  167. dvt/include/starter_project/tests/.gitkeep +0 -0
  168. dvt/internal_deprecations.py +27 -0
  169. dvt/jsonschemas/__init__.py +3 -0
  170. dvt/jsonschemas/jsonschemas.py +309 -0
  171. dvt/jsonschemas/project/0.0.110.json +4717 -0
  172. dvt/jsonschemas/project/0.0.85.json +2015 -0
  173. dvt/jsonschemas/resources/0.0.110.json +2636 -0
  174. dvt/jsonschemas/resources/0.0.85.json +2536 -0
  175. dvt/jsonschemas/resources/latest.json +6773 -0
  176. dvt/links.py +4 -0
  177. dvt/materializations/__init__.py +0 -0
  178. dvt/materializations/incremental/__init__.py +0 -0
  179. dvt/materializations/incremental/microbatch.py +235 -0
  180. dvt/mp_context.py +8 -0
  181. dvt/node_types.py +37 -0
  182. dvt/parser/__init__.py +23 -0
  183. dvt/parser/analysis.py +21 -0
  184. dvt/parser/base.py +549 -0
  185. dvt/parser/common.py +267 -0
  186. dvt/parser/docs.py +52 -0
  187. dvt/parser/fixtures.py +51 -0
  188. dvt/parser/functions.py +30 -0
  189. dvt/parser/generic_test.py +100 -0
  190. dvt/parser/generic_test_builders.py +334 -0
  191. dvt/parser/hooks.py +119 -0
  192. dvt/parser/macros.py +137 -0
  193. dvt/parser/manifest.py +2204 -0
  194. dvt/parser/models.py +574 -0
  195. dvt/parser/partial.py +1179 -0
  196. dvt/parser/read_files.py +445 -0
  197. dvt/parser/schema_generic_tests.py +423 -0
  198. dvt/parser/schema_renderer.py +111 -0
  199. dvt/parser/schema_yaml_readers.py +936 -0
  200. dvt/parser/schemas.py +1467 -0
  201. dvt/parser/search.py +149 -0
  202. dvt/parser/seeds.py +28 -0
  203. dvt/parser/singular_test.py +20 -0
  204. dvt/parser/snapshots.py +44 -0
  205. dvt/parser/sources.py +557 -0
  206. dvt/parser/sql.py +63 -0
  207. dvt/parser/unit_tests.py +622 -0
  208. dvt/plugins/__init__.py +20 -0
  209. dvt/plugins/contracts.py +10 -0
  210. dvt/plugins/exceptions.py +2 -0
  211. dvt/plugins/manager.py +164 -0
  212. dvt/plugins/manifest.py +21 -0
  213. dvt/profiler.py +20 -0
  214. dvt/py.typed +1 -0
  215. dvt/runners/__init__.py +2 -0
  216. dvt/runners/exposure_runner.py +7 -0
  217. dvt/runners/no_op_runner.py +46 -0
  218. dvt/runners/saved_query_runner.py +7 -0
  219. dvt/selected_resources.py +8 -0
  220. dvt/task/__init__.py +0 -0
  221. dvt/task/base.py +504 -0
  222. dvt/task/build.py +197 -0
  223. dvt/task/clean.py +57 -0
  224. dvt/task/clone.py +162 -0
  225. dvt/task/compile.py +151 -0
  226. dvt/task/compute.py +366 -0
  227. dvt/task/debug.py +650 -0
  228. dvt/task/deps.py +280 -0
  229. dvt/task/docs/__init__.py +3 -0
  230. dvt/task/docs/generate.py +408 -0
  231. dvt/task/docs/index.html +250 -0
  232. dvt/task/docs/serve.py +28 -0
  233. dvt/task/freshness.py +323 -0
  234. dvt/task/function.py +122 -0
  235. dvt/task/group_lookup.py +46 -0
  236. dvt/task/init.py +374 -0
  237. dvt/task/list.py +237 -0
  238. dvt/task/printer.py +176 -0
  239. dvt/task/profiles.py +256 -0
  240. dvt/task/retry.py +175 -0
  241. dvt/task/run.py +1146 -0
  242. dvt/task/run_operation.py +142 -0
  243. dvt/task/runnable.py +802 -0
  244. dvt/task/seed.py +104 -0
  245. dvt/task/show.py +150 -0
  246. dvt/task/snapshot.py +57 -0
  247. dvt/task/sql.py +111 -0
  248. dvt/task/test.py +464 -0
  249. dvt/tests/fixtures/__init__.py +1 -0
  250. dvt/tests/fixtures/project.py +620 -0
  251. dvt/tests/util.py +651 -0
  252. dvt/tracking.py +529 -0
  253. dvt/utils/__init__.py +3 -0
  254. dvt/utils/artifact_upload.py +151 -0
  255. dvt/utils/utils.py +408 -0
  256. dvt/version.py +249 -0
  257. dvt_core-1.11.0b4.dist-info/METADATA +252 -0
  258. dvt_core-1.11.0b4.dist-info/RECORD +261 -0
  259. dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
  260. dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
  261. dvt_core-1.11.0b4.dist-info/top_level.txt +1 -0
dvt/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()