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/task/docs/serve.py ADDED
@@ -0,0 +1,28 @@
1
+ import os
2
+ import shutil
3
+ import socketserver
4
+ import webbrowser
5
+ from http.server import SimpleHTTPRequestHandler
6
+
7
+ import click
8
+ from dvt.task.base import ConfiguredTask
9
+ from dvt.task.docs import DOCS_INDEX_FILE_PATH
10
+
11
+
12
+ class ServeTask(ConfiguredTask):
13
+ def run(self):
14
+ os.chdir(self.config.project_target_path)
15
+ shutil.copyfile(DOCS_INDEX_FILE_PATH, "index.html")
16
+
17
+ port = self.args.port
18
+ host = self.args.host
19
+
20
+ if self.args.browser:
21
+ webbrowser.open_new_tab(f"http://localhost:{port}")
22
+
23
+ with socketserver.TCPServer((host, port), SimpleHTTPRequestHandler) as httpd:
24
+ click.echo(f"Serving docs at {port}")
25
+ click.echo(f"To access from your browser, navigate to: http://localhost:{port}")
26
+ click.echo("\n\n")
27
+ click.echo("Press Ctrl+C to exit.")
28
+ httpd.serve_forever()
dvt/task/freshness.py ADDED
@@ -0,0 +1,323 @@
1
+ import os
2
+ import threading
3
+ import time
4
+ from typing import AbstractSet, Dict, List, Optional, Type
5
+
6
+ from dvt import deprecations
7
+ from dvt.artifacts.schemas.freshness import (
8
+ FreshnessResult,
9
+ FreshnessStatus,
10
+ PartialSourceFreshnessResult,
11
+ SourceFreshnessResult,
12
+ )
13
+ from dvt.clients import jinja
14
+ from dvt.constants import SOURCE_RESULT_FILE_NAME
15
+ from dvt.context.providers import RuntimeProvider, SourceContext
16
+ from dvt.contracts.graph.manifest import Manifest
17
+ from dvt.contracts.graph.nodes import HookNode, SourceDefinition
18
+ from dvt.contracts.results import RunStatus
19
+ from dvt.events.types import FreshnessCheckComplete, LogFreshnessResult, LogStartLine
20
+ from dvt.graph import ResourceTypeSelector
21
+ from dvt.node_types import NodeType, RunHookType
22
+
23
+ from dbt.adapters.base import BaseAdapter
24
+ from dbt.adapters.base.impl import FreshnessResponse
25
+ from dbt.adapters.base.relation import BaseRelation
26
+ from dbt.adapters.capability import Capability
27
+ from dbt.adapters.contracts.connection import AdapterResponse
28
+ from dbt_common.events.base_types import EventLevel
29
+ from dbt_common.events.functions import fire_event
30
+ from dbt_common.events.types import Note
31
+ from dbt_common.exceptions import DbtInternalError, DbtRuntimeError
32
+
33
+ from .base import BaseRunner
34
+ from .printer import print_run_result_error
35
+ from .run import RunTask
36
+
37
+
38
+ class FreshnessRunner(BaseRunner):
39
+ def __init__(self, config, adapter, node, node_index, num_nodes) -> None:
40
+ super().__init__(config, adapter, node, node_index, num_nodes)
41
+ self._metadata_freshness_cache: Dict[BaseRelation, FreshnessResult] = {}
42
+
43
+ def set_metadata_freshness_cache(
44
+ self, metadata_freshness_cache: Dict[BaseRelation, FreshnessResult]
45
+ ) -> None:
46
+ self._metadata_freshness_cache = metadata_freshness_cache
47
+
48
+ def on_skip(self):
49
+ raise DbtRuntimeError("Freshness: nodes cannot be skipped!")
50
+
51
+ def before_execute(self) -> None:
52
+ description = "freshness of {0.source_name}.{0.name}".format(self.node)
53
+ fire_event(
54
+ LogStartLine(
55
+ description=description,
56
+ index=self.node_index,
57
+ total=self.num_nodes,
58
+ node_info=self.node.node_info,
59
+ )
60
+ )
61
+
62
+ def after_execute(self, result) -> None:
63
+ if hasattr(result, "node"):
64
+ source_name = result.node.source_name
65
+ table_name = result.node.name
66
+ else:
67
+ source_name = result.source_name
68
+ table_name = result.table_name
69
+ level = LogFreshnessResult.status_to_level(str(result.status))
70
+ fire_event(
71
+ LogFreshnessResult(
72
+ status=result.status,
73
+ source_name=source_name,
74
+ table_name=table_name,
75
+ index=self.node_index,
76
+ total=self.num_nodes,
77
+ execution_time=result.execution_time,
78
+ node_info=self.node.node_info,
79
+ ),
80
+ level=level,
81
+ )
82
+
83
+ def error_result(self, node, message, start_time, timing_info):
84
+ return self._build_run_result(
85
+ node=node,
86
+ start_time=start_time,
87
+ status=FreshnessStatus.RuntimeErr,
88
+ timing_info=timing_info,
89
+ message=message,
90
+ )
91
+
92
+ def _build_run_result(self, node, start_time, status, timing_info, message):
93
+ execution_time = time.time() - start_time
94
+ thread_id = threading.current_thread().name
95
+ return PartialSourceFreshnessResult(
96
+ status=status,
97
+ thread_id=thread_id,
98
+ execution_time=execution_time,
99
+ timing=timing_info,
100
+ message=message,
101
+ node=node,
102
+ adapter_response={},
103
+ failures=None,
104
+ )
105
+
106
+ def from_run_result(self, result, start_time, timing_info):
107
+ result.execution_time = time.time() - start_time
108
+ result.timing.extend(timing_info)
109
+ return result
110
+
111
+ def execute(self, compiled_node, manifest):
112
+ relation = self.adapter.Relation.create_from(self.config, compiled_node)
113
+ # given a Source, calculate its freshness.
114
+ with self.adapter.connection_named(compiled_node.unique_id, compiled_node):
115
+ self.adapter.clear_transaction()
116
+ adapter_response: Optional[AdapterResponse] = None
117
+ freshness: Optional[FreshnessResponse] = None
118
+
119
+ if compiled_node.loaded_at_query is not None:
120
+ # within the context user can have access to `this`, `source_node`(`model` will point to the same thing), etc
121
+ compiled_code = jinja.get_rendered(
122
+ compiled_node.loaded_at_query,
123
+ SourceContext(
124
+ compiled_node, self.config, manifest, RuntimeProvider(), None
125
+ ).to_dict(),
126
+ compiled_node,
127
+ )
128
+ adapter_response, freshness = self.adapter.calculate_freshness_from_custom_sql(
129
+ relation,
130
+ compiled_code,
131
+ macro_resolver=manifest,
132
+ )
133
+ status = compiled_node.freshness.status(freshness["age"])
134
+ elif compiled_node.loaded_at_field is not None:
135
+ adapter_response, freshness = self.adapter.calculate_freshness(
136
+ relation,
137
+ compiled_node.loaded_at_field,
138
+ compiled_node.freshness.filter,
139
+ macro_resolver=manifest,
140
+ )
141
+
142
+ status = compiled_node.freshness.status(freshness["age"])
143
+ elif self.adapter.supports(Capability.TableLastModifiedMetadata):
144
+ if compiled_node.freshness.filter is not None:
145
+ fire_event(
146
+ Note(
147
+ msg=f"A filter cannot be applied to a metadata freshness check on source '{compiled_node.name}'."
148
+ ),
149
+ EventLevel.WARN,
150
+ )
151
+
152
+ metadata_source = self.adapter.Relation.create_from(self.config, compiled_node)
153
+ if metadata_source in self._metadata_freshness_cache:
154
+ freshness = self._metadata_freshness_cache[metadata_source]
155
+ else:
156
+ adapter_response, freshness = self.adapter.calculate_freshness_from_metadata(
157
+ relation,
158
+ macro_resolver=manifest,
159
+ )
160
+
161
+ status = compiled_node.freshness.status(freshness["age"])
162
+ else:
163
+ raise DbtRuntimeError(
164
+ f"Could not compute freshness for source {compiled_node.name}: no 'loaded_at_field' provided and {self.adapter.type()} adapter does not support metadata-based freshness checks."
165
+ )
166
+ # adapter_response was not returned in previous versions, so this will be None
167
+ # we cannot call to_dict() on NoneType
168
+ if adapter_response:
169
+ adapter_response = adapter_response.to_dict(omit_none=True)
170
+
171
+ return SourceFreshnessResult(
172
+ node=compiled_node,
173
+ status=status,
174
+ thread_id=threading.current_thread().name,
175
+ timing=[],
176
+ execution_time=0,
177
+ message=None,
178
+ adapter_response=adapter_response or {},
179
+ failures=None,
180
+ **freshness,
181
+ )
182
+
183
+ def compile(self, manifest: Manifest):
184
+ if self.node.resource_type != NodeType.Source:
185
+ # should be unreachable...
186
+ raise DbtRuntimeError("freshness runner: got a non-Source")
187
+ # we don't do anything interesting when we compile a source node
188
+ return self.node
189
+
190
+
191
+ class FreshnessSelector(ResourceTypeSelector):
192
+ def node_is_match(self, node):
193
+ if not super().node_is_match(node):
194
+ return False
195
+ if not isinstance(node, SourceDefinition):
196
+ return False
197
+ return node.has_freshness
198
+
199
+
200
+ class FreshnessTask(RunTask):
201
+ def __init__(self, args, config, manifest) -> None:
202
+ super().__init__(args, config, manifest)
203
+
204
+ if self.args.output:
205
+ deprecations.warn(
206
+ "custom-output-path-in-source-freshness-deprecation", path=str(self.args.output)
207
+ )
208
+
209
+ self._metadata_freshness_cache: Dict[BaseRelation, FreshnessResult] = {}
210
+
211
+ def result_path(self) -> str:
212
+ if self.args.output:
213
+ return os.path.realpath(self.args.output)
214
+ else:
215
+ return os.path.join(self.config.project_target_path, SOURCE_RESULT_FILE_NAME)
216
+
217
+ def raise_on_first_error(self) -> bool:
218
+ return False
219
+
220
+ def get_node_selector(self):
221
+ if self.manifest is None or self.graph is None:
222
+ raise DbtInternalError("manifest and graph must be set to get perform node selection")
223
+ return FreshnessSelector(
224
+ graph=self.graph,
225
+ manifest=self.manifest,
226
+ previous_state=self.previous_state,
227
+ resource_types=[NodeType.Source],
228
+ )
229
+
230
+ def before_run(self, adapter: BaseAdapter, selected_uids: AbstractSet[str]) -> RunStatus:
231
+ populate_metadata_freshness_cache_status = RunStatus.Success
232
+
233
+ before_run_status = super().before_run(adapter, selected_uids)
234
+
235
+ if before_run_status == RunStatus.Success and adapter.supports(
236
+ Capability.TableLastModifiedMetadataBatch
237
+ ):
238
+ populate_metadata_freshness_cache_status = self.populate_metadata_freshness_cache(
239
+ adapter, selected_uids
240
+ )
241
+
242
+ if (
243
+ before_run_status == RunStatus.Success
244
+ and populate_metadata_freshness_cache_status == RunStatus.Success
245
+ ):
246
+ return RunStatus.Success
247
+ else:
248
+ return RunStatus.Error
249
+
250
+ def get_runner(self, node) -> BaseRunner:
251
+ freshness_runner = super().get_runner(node)
252
+ assert isinstance(freshness_runner, FreshnessRunner)
253
+ freshness_runner.set_metadata_freshness_cache(self._metadata_freshness_cache)
254
+ return freshness_runner
255
+
256
+ def get_runner_type(self, _) -> Optional[Type[BaseRunner]]:
257
+ return FreshnessRunner
258
+
259
+ def get_result(self, results, elapsed_time, generated_at):
260
+ return FreshnessResult.from_node_results(
261
+ elapsed_time=elapsed_time, generated_at=generated_at, results=results
262
+ )
263
+
264
+ def task_end_messages(self, results) -> None:
265
+ for result in results:
266
+ if result.status in (
267
+ FreshnessStatus.Error,
268
+ FreshnessStatus.RuntimeErr,
269
+ RunStatus.Error,
270
+ ):
271
+ print_run_result_error(result)
272
+
273
+ fire_event(FreshnessCheckComplete())
274
+
275
+ def get_hooks_by_type(self, hook_type: RunHookType) -> List[HookNode]:
276
+ hooks = super().get_hooks_by_type(hook_type)
277
+ if self.args.source_freshness_run_project_hooks:
278
+ return hooks
279
+ else:
280
+ if hooks:
281
+ deprecations.warn("source-freshness-project-hooks")
282
+ return []
283
+
284
+ def populate_metadata_freshness_cache(
285
+ self, adapter, selected_uids: AbstractSet[str]
286
+ ) -> RunStatus:
287
+ if self.manifest is None:
288
+ raise DbtInternalError("Manifest must be set to populate metadata freshness cache")
289
+
290
+ batch_metadata_sources: List[BaseRelation] = []
291
+ for selected_source_uid in list(selected_uids):
292
+ source = self.manifest.sources.get(selected_source_uid)
293
+ if source and source.loaded_at_field is None:
294
+ metadata_source = adapter.Relation.create_from(self.config, source)
295
+ batch_metadata_sources.append(metadata_source)
296
+
297
+ fire_event(
298
+ Note(
299
+ msg=f"Pulling freshness from warehouse metadata tables for {len(batch_metadata_sources)} sources"
300
+ ),
301
+ EventLevel.INFO,
302
+ )
303
+
304
+ try:
305
+ _, metadata_freshness_results = adapter.calculate_freshness_from_metadata_batch(
306
+ batch_metadata_sources
307
+ )
308
+ self._metadata_freshness_cache.update(metadata_freshness_results)
309
+ return RunStatus.Success
310
+ except Exception as e:
311
+ # This error handling is intentionally very coarse.
312
+ # If anything goes wrong during batch metadata calculation, we can safely
313
+ # leave _metadata_freshness_cache unpopulated.
314
+ # Downstream, this will be gracefully handled as a cache miss and non-batch
315
+ # metadata-based freshness will still be performed on a source-by-source basis.
316
+ fire_event(
317
+ Note(msg=f"Metadata freshness could not be computed in batch: {e}"),
318
+ EventLevel.WARN,
319
+ )
320
+ return RunStatus.Error
321
+
322
+ def get_freshness_metadata_cache(self) -> Dict[BaseRelation, FreshnessResult]:
323
+ return self._metadata_freshness_cache
dvt/task/function.py ADDED
@@ -0,0 +1,122 @@
1
+ import threading
2
+ from typing import Any, Dict
3
+
4
+ from dvt.artifacts.schemas.results import NodeStatus, RunStatus
5
+ from dvt.artifacts.schemas.run import RunResult
6
+ from dvt.clients.jinja import MacroGenerator
7
+ from dvt.context.providers import generate_runtime_function_context
8
+ from dvt.contracts.graph.manifest import Manifest
9
+ from dvt.contracts.graph.nodes import FunctionNode
10
+ from dvt.events.types import LogFunctionResult, LogStartLine
11
+ from dvt.task import group_lookup
12
+ from dvt.task.compile import CompileRunner
13
+
14
+ from dbt.adapters.exceptions import MissingMaterializationError
15
+ from dbt_common.clients.jinja import MacroProtocol
16
+ from dbt_common.events.base_types import EventLevel
17
+ from dbt_common.events.functions import fire_event
18
+ from dbt_common.exceptions import DbtValidationError
19
+
20
+
21
+ class FunctionRunner(CompileRunner):
22
+
23
+ def __init__(self, config, adapter, node, node_index: int, num_nodes: int) -> None:
24
+ super().__init__(config, adapter, node, node_index, num_nodes)
25
+
26
+ # doing this gives us type hints for the node :D
27
+ assert isinstance(node, FunctionNode)
28
+ self.node = node
29
+
30
+ def describe_node(self) -> str:
31
+ return f"function {self.node.name}" # TODO: add more info, similar to SeedRunner.describe_node
32
+
33
+ def before_execute(self) -> None:
34
+ fire_event(
35
+ LogStartLine(
36
+ description=self.describe_node(),
37
+ index=self.node_index,
38
+ total=self.num_nodes,
39
+ node_info=self.node.node_info,
40
+ )
41
+ )
42
+
43
+ def _get_materialization_macro(
44
+ self, compiled_node: FunctionNode, manifest: Manifest
45
+ ) -> MacroProtocol:
46
+ materialization_macro = manifest.find_materialization_macro_by_name(
47
+ self.config.project_name, compiled_node.get_materialization(), self.adapter.type()
48
+ )
49
+ if materialization_macro is None:
50
+ raise MissingMaterializationError(
51
+ materialization=compiled_node.get_materialization(),
52
+ adapter_type=self.adapter.type(),
53
+ )
54
+
55
+ return materialization_macro
56
+
57
+ def _check_lang_supported(
58
+ self, compiled_node: FunctionNode, materialization_macro: MacroProtocol
59
+ ) -> None:
60
+ # TODO: This function and its typing is a bit wonky, we should fix it
61
+ # Specifically, a MacroProtocol doesn't have a supported_languags attribute, but a macro does. We're acting
62
+ # like the materialization_macro might not have a supported_languages attribute, but we access it in an unguarded manner.
63
+ # So are we guaranteed to always have a Macro here? (because a Macro always has a supported_languages attribute)
64
+ # This logic is a copy of of the logic in the run.py file, so the same logical conundrum applies there. Also perhaps
65
+ # we can refactor to having one definition, and maybe a logically consistent one...
66
+ mat_has_supported_langs = hasattr(materialization_macro, "supported_languages")
67
+ function_lang_supported = compiled_node.language in materialization_macro.supported_languages # type: ignore
68
+ if mat_has_supported_langs and not function_lang_supported:
69
+ str_langs = [str(lang) for lang in materialization_macro.supported_languages] # type: ignore
70
+ raise DbtValidationError(
71
+ f'Materialization "{materialization_macro.name}" only supports languages {str_langs}; '
72
+ f'got "{compiled_node.language}"'
73
+ )
74
+
75
+ def build_result(self, compiled_node: FunctionNode, context: Dict[str, Any]) -> RunResult:
76
+ loaded_result = context["load_result"]("main")
77
+
78
+ return RunResult(
79
+ node=compiled_node,
80
+ status=RunStatus.Success,
81
+ timing=[],
82
+ thread_id=threading.current_thread().name,
83
+ # This gets set later in `from_run_result` called by `BaseRunner.safe_run`
84
+ execution_time=0.0,
85
+ message=str(loaded_result.response),
86
+ adapter_response=loaded_result.response.to_dict(omit_none=True),
87
+ failures=loaded_result.get("failures"),
88
+ batch_results=None,
89
+ )
90
+
91
+ def execute(self, compiled_node: FunctionNode, manifest: Manifest) -> RunResult:
92
+ materialization_macro = self._get_materialization_macro(compiled_node, manifest)
93
+ self._check_lang_supported(compiled_node, materialization_macro)
94
+ context = generate_runtime_function_context(compiled_node, self.config, manifest)
95
+
96
+ MacroGenerator(materialization_macro, context=context)()
97
+
98
+ return self.build_result(compiled_node, context)
99
+
100
+ def after_execute(self, result: RunResult) -> None:
101
+ self.print_result_line(result)
102
+
103
+ # def compile() defined on CompileRunner
104
+
105
+ def print_result_line(self, result: RunResult) -> None:
106
+ node = result.node
107
+ assert isinstance(node, FunctionNode)
108
+
109
+ group = group_lookup.get(node.unique_id)
110
+ level = EventLevel.ERROR if result.status == NodeStatus.Error else EventLevel.INFO
111
+ fire_event(
112
+ LogFunctionResult(
113
+ description=self.describe_node(),
114
+ status=result.status,
115
+ index=self.node_index,
116
+ total=self.num_nodes,
117
+ execution_time=result.execution_time,
118
+ node_info=self.node.node_info,
119
+ group=group,
120
+ ),
121
+ level=level,
122
+ )
@@ -0,0 +1,46 @@
1
+ from typing import AbstractSet, Dict, Optional, Union
2
+
3
+ from dvt.contracts.graph.manifest import Manifest
4
+ from dvt.contracts.graph.nodes import Group
5
+
6
+ _node_id_to_group_name_map: Dict[str, str] = {}
7
+ _group_name_to_group_map: Dict[str, Group] = {}
8
+
9
+
10
+ def init(manifest: Optional[Manifest], selected_ids: AbstractSet[str]) -> None:
11
+ if not manifest:
12
+ return
13
+
14
+ if not manifest.groups:
15
+ return
16
+
17
+ if not hasattr(manifest, "group_map"):
18
+ manifest.build_group_map()
19
+
20
+ _every_group_name_to_group_map = {v.name: v for v in manifest.groups.values()}
21
+
22
+ for group_name, node_ids in manifest.group_map.items():
23
+ for node_id in node_ids:
24
+ # only add node to lookup if it's selected
25
+ if node_id in selected_ids:
26
+ _node_id_to_group_name_map[node_id] = group_name
27
+
28
+ # only add group to lookup if it's not already there and if node is selected
29
+ if group_name not in _group_name_to_group_map:
30
+ _group_name_to_group_map[group_name] = _every_group_name_to_group_map[
31
+ group_name
32
+ ]
33
+
34
+
35
+ def get(node_id: str) -> Optional[Dict[str, Union[str, Dict[str, str]]]]:
36
+ group_name = _node_id_to_group_name_map.get(node_id)
37
+
38
+ if group_name is None:
39
+ return None
40
+
41
+ group = _group_name_to_group_map.get(group_name)
42
+
43
+ if group is None:
44
+ return None
45
+
46
+ return group.to_logging_dict()