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/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
|
+
)
|
dvt/task/group_lookup.py
ADDED
|
@@ -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()
|