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/tracking.py ADDED
@@ -0,0 +1,529 @@
1
+ import os
2
+ import platform
3
+ import traceback
4
+ import uuid
5
+ from contextlib import contextmanager
6
+ from datetime import datetime
7
+ from typing import Optional
8
+
9
+ import pytz
10
+ import requests
11
+ from dvt import version as dbt_version
12
+ from dvt.clients.yaml_helper import safe_load, yaml # noqa:F401
13
+ from dvt.events.types import (
14
+ DisableTracking,
15
+ FlushEvents,
16
+ FlushEventsFailure,
17
+ MainEncounteredError,
18
+ SendEventFailure,
19
+ SendingEvent,
20
+ TrackingInitializeFailure,
21
+ )
22
+ from packaging.version import Version
23
+ from snowplow_tracker import Emitter, SelfDescribingJson, Subject, Tracker
24
+ from snowplow_tracker import __version__ as snowplow_version # type: ignore
25
+ from snowplow_tracker import logger as sp_logger
26
+ from snowplow_tracker.events import StructuredEvent
27
+
28
+ from dbt.adapters.exceptions import FailedToConnectError
29
+ from dbt_common.events.base_types import EventMsg
30
+ from dbt_common.events.functions import fire_event, get_invocation_id, msg_to_dict
31
+ from dbt_common.exceptions import NotImplementedError
32
+
33
+ sp_logger.setLevel(100)
34
+
35
+ COLLECTOR_URL = "fishtownanalytics.sinter-collect.com"
36
+ COLLECTOR_PROTOCOL = "https"
37
+ DBT_INVOCATION_ENV = "DBT_INVOCATION_ENV"
38
+
39
+ ADAPTER_INFO_SPEC = "iglu:com.dbt/adapter_info/jsonschema/1-0-1"
40
+ DEPRECATION_WARN_SPEC = "iglu:com.dbt/deprecation_warn/jsonschema/1-0-0"
41
+ BEHAVIOR_CHANGE_WARN_SPEC = "iglu:com.dbt/behavior_change_warn/jsonschema/1-0-0"
42
+ EXPERIMENTAL_PARSER = "iglu:com.dbt/experimental_parser/jsonschema/1-0-0"
43
+ INVOCATION_ENV_SPEC = "iglu:com.dbt/invocation_env/jsonschema/1-0-0"
44
+ INVOCATION_SPEC = "iglu:com.dbt/invocation/jsonschema/1-0-2"
45
+ LOAD_ALL_TIMING_SPEC = "iglu:com.dbt/load_all_timing/jsonschema/1-0-3"
46
+ PACKAGE_INSTALL_SPEC = "iglu:com.dbt/package_install/jsonschema/1-0-0"
47
+ PARTIAL_PARSER = "iglu:com.dbt/partial_parser/jsonschema/1-0-1"
48
+ PLATFORM_SPEC = "iglu:com.dbt/platform/jsonschema/1-0-0"
49
+ PROJECT_ID_SPEC = "iglu:com.dbt/project_id/jsonschema/1-0-1"
50
+ RESOURCE_COUNTS = "iglu:com.dbt/resource_counts/jsonschema/1-0-1"
51
+ RPC_REQUEST_SPEC = "iglu:com.dbt/rpc_request/jsonschema/1-0-1"
52
+ RUNNABLE_TIMING = "iglu:com.dbt/runnable/jsonschema/1-0-0"
53
+ RUN_MODEL_SPEC = "iglu:com.dbt/run_model/jsonschema/1-1-0"
54
+ PLUGIN_GET_NODES = "iglu:com.dbt/plugin_get_nodes/jsonschema/1-0-0"
55
+ ARTIFACT_UPLOAD = "iglu:com.dbt/artifact_upload/jsonschema/1-0-0"
56
+
57
+ SNOWPLOW_TRACKER_VERSION = Version(snowplow_version)
58
+
59
+ # workaround in case real snowplow tracker is in the env
60
+ # the argument was renamed in https://github.com/snowplow/snowplow-python-tracker/commit/39fd50a3aff98a5efdd5c5c7fb5518fe4761305b
61
+ INIT_KW_ARGS = (
62
+ {"buffer_size": 30} if SNOWPLOW_TRACKER_VERSION < Version("0.13.0") else {"batch_size": 30}
63
+ )
64
+
65
+
66
+ class TimeoutEmitter(Emitter):
67
+ def __init__(self) -> None:
68
+ super().__init__(
69
+ COLLECTOR_URL,
70
+ protocol=COLLECTOR_PROTOCOL,
71
+ on_failure=self.handle_failure,
72
+ method="post",
73
+ # don't set this.
74
+ byte_limit=None,
75
+ **INIT_KW_ARGS,
76
+ )
77
+
78
+ @staticmethod
79
+ def handle_failure(num_ok, unsent):
80
+ # num_ok will always be 0, unsent will always be 1 entry long, because
81
+ # the buffer is length 1, so not much to talk about
82
+ fire_event(DisableTracking())
83
+ disable_tracking()
84
+
85
+ def _log_request(self, request, payload):
86
+ sp_logger.info(f"Sending {request} request to {self.endpoint}...")
87
+ sp_logger.debug(f"Payload: {payload}")
88
+
89
+ def _log_result(self, request, status_code):
90
+ msg = f"{request} request finished with status code: {status_code}"
91
+ if self.is_good_status_code(status_code):
92
+ sp_logger.info(msg)
93
+ else:
94
+ sp_logger.warning(msg)
95
+
96
+ def http_post(self, payload):
97
+ self._log_request("POST", payload)
98
+
99
+ r = requests.post(
100
+ self.endpoint,
101
+ data=payload,
102
+ headers={"content-type": "application/json; charset=utf-8"},
103
+ timeout=5.0,
104
+ )
105
+
106
+ self._log_result("GET", r.status_code)
107
+ return r
108
+
109
+ def http_get(self, payload):
110
+ self._log_request("GET", payload)
111
+
112
+ r = requests.get(self.endpoint, params=payload, timeout=5.0)
113
+
114
+ self._log_result("GET", r.status_code)
115
+ return r
116
+
117
+
118
+ emitter = TimeoutEmitter()
119
+ tracker = Tracker(
120
+ emitters=emitter,
121
+ namespace="cf",
122
+ app_id="dbt",
123
+ )
124
+
125
+
126
+ class User:
127
+ def __init__(self, cookie_dir) -> None:
128
+ self.do_not_track = True
129
+ self.cookie_dir = cookie_dir
130
+
131
+ self.id = None
132
+ self.invocation_id = get_invocation_id()
133
+ self.run_started_at = datetime.now(tz=pytz.utc)
134
+
135
+ def state(self):
136
+ return "do not track" if self.do_not_track else "tracking"
137
+
138
+ @property
139
+ def cookie_path(self):
140
+ return os.path.join(self.cookie_dir, ".user.yml")
141
+
142
+ def initialize(self):
143
+ self.do_not_track = False
144
+
145
+ cookie = self.get_cookie()
146
+ self.id = cookie.get("id")
147
+
148
+ subject = Subject()
149
+ subject.set_user_id(self.id)
150
+ tracker.set_subject(subject)
151
+
152
+ def disable_tracking(self):
153
+ self.do_not_track = True
154
+ self.id = None
155
+ self.cookie_dir = None
156
+ tracker.set_subject(None)
157
+
158
+ def set_cookie(self):
159
+ # If the user points dbt to a profile directory which exists AND
160
+ # contains a profiles.yml file, then we can set a cookie. If the
161
+ # specified folder does not exist, or if there is not a profiles.yml
162
+ # file in this folder, then an inconsistent cookie can be used. This
163
+ # will change in every dbt invocation until the user points to a
164
+ # profile dir file which contains a valid profiles.yml file.
165
+ #
166
+ # See: https://github.com/dbt-labs/dbt-core/issues/1645
167
+
168
+ user = {"id": str(uuid.uuid4())}
169
+
170
+ cookie_path = os.path.abspath(self.cookie_dir)
171
+ profiles_file = os.path.join(cookie_path, "profiles.yml")
172
+ if os.path.exists(cookie_path) and os.path.exists(profiles_file):
173
+ with open(self.cookie_path, "w") as fh:
174
+ yaml.dump(user, fh)
175
+
176
+ return user
177
+
178
+ def get_cookie(self):
179
+ if not os.path.isfile(self.cookie_path):
180
+ user = self.set_cookie()
181
+ else:
182
+ with open(self.cookie_path, "r") as fh:
183
+ try:
184
+ user = safe_load(fh)
185
+ if user is None:
186
+ user = self.set_cookie()
187
+ except yaml.reader.ReaderError:
188
+ user = self.set_cookie()
189
+ return user
190
+
191
+
192
+ active_user: Optional[User] = None
193
+
194
+
195
+ def get_platform_context():
196
+ data = {
197
+ "platform": platform.platform(),
198
+ "python": platform.python_version(),
199
+ "python_version": platform.python_implementation(),
200
+ }
201
+
202
+ return SelfDescribingJson(PLATFORM_SPEC, data)
203
+
204
+
205
+ def get_dbt_env_context():
206
+ default = "manual"
207
+
208
+ dbt_invocation_env = os.getenv(DBT_INVOCATION_ENV, default)
209
+ if dbt_invocation_env == "":
210
+ dbt_invocation_env = default
211
+
212
+ data = {
213
+ "environment": dbt_invocation_env,
214
+ }
215
+
216
+ return SelfDescribingJson(INVOCATION_ENV_SPEC, data)
217
+
218
+
219
+ def track(user, *args, **kwargs):
220
+ if user.do_not_track:
221
+ return
222
+
223
+ fire_event(SendingEvent(kwargs=str(kwargs)))
224
+ try:
225
+ tracker.track(StructuredEvent(*args, **kwargs))
226
+ except Exception:
227
+ fire_event(SendEventFailure())
228
+
229
+
230
+ def track_project_id(options):
231
+ assert active_user is not None, "Cannot track project_id when active user is None"
232
+ context = [SelfDescribingJson(PROJECT_ID_SPEC, options)]
233
+
234
+ track(
235
+ active_user,
236
+ category="dbt",
237
+ action="project_id",
238
+ label=get_invocation_id(),
239
+ context=context,
240
+ )
241
+
242
+
243
+ def track_adapter_info(options):
244
+ assert active_user is not None, "Cannot track adapter_info when active user is None"
245
+ context = [SelfDescribingJson(ADAPTER_INFO_SPEC, options)]
246
+
247
+ track(
248
+ active_user,
249
+ category="dbt",
250
+ action="adapter_info",
251
+ label=get_invocation_id(),
252
+ context=context,
253
+ )
254
+
255
+
256
+ def track_invocation_start(invocation_context):
257
+ data = {"progress": "start", "result_type": None, "result": None}
258
+ data.update(invocation_context)
259
+ context = [
260
+ SelfDescribingJson(INVOCATION_SPEC, data),
261
+ get_platform_context(),
262
+ get_dbt_env_context(),
263
+ ]
264
+
265
+ track(active_user, category="dbt", action="invocation", label="start", context=context)
266
+
267
+
268
+ def track_project_load(options):
269
+ context = [SelfDescribingJson(LOAD_ALL_TIMING_SPEC, options)]
270
+ assert active_user is not None, "Cannot track project loading time when active user is None"
271
+
272
+ track(
273
+ active_user,
274
+ category="dbt",
275
+ action="load_project",
276
+ label=get_invocation_id(),
277
+ context=context,
278
+ )
279
+
280
+
281
+ def track_resource_counts(resource_counts):
282
+ context = [SelfDescribingJson(RESOURCE_COUNTS, resource_counts)]
283
+ assert active_user is not None, "Cannot track resource counts when active user is None"
284
+
285
+ track(
286
+ active_user,
287
+ category="dbt",
288
+ action="resource_counts",
289
+ label=get_invocation_id(),
290
+ context=context,
291
+ )
292
+
293
+
294
+ def track_model_run(options):
295
+ context = [SelfDescribingJson(RUN_MODEL_SPEC, options)]
296
+ assert active_user is not None, "Cannot track model runs when active user is None"
297
+
298
+ track(
299
+ active_user, category="dbt", action="run_model", label=get_invocation_id(), context=context
300
+ )
301
+
302
+
303
+ def track_rpc_request(options):
304
+ context = [SelfDescribingJson(RPC_REQUEST_SPEC, options)]
305
+ assert active_user is not None, "Cannot track rpc requests when active user is None"
306
+
307
+ track(
308
+ active_user,
309
+ category="dbt",
310
+ action="rpc_request",
311
+ label=get_invocation_id(),
312
+ context=context,
313
+ )
314
+
315
+
316
+ def get_base_invocation_context():
317
+ assert (
318
+ active_user is not None
319
+ ), "initialize active user before calling get_base_invocation_context"
320
+ return {
321
+ "project_id": None,
322
+ "user_id": active_user.id,
323
+ "invocation_id": active_user.invocation_id,
324
+ "command": None,
325
+ "options": None,
326
+ "version": str(dbt_version.installed),
327
+ "run_type": "regular",
328
+ "adapter_type": None,
329
+ "adapter_unique_id": None,
330
+ }
331
+
332
+
333
+ def track_package_install(command_name: str, project_hashed_name: Optional[str], options):
334
+ assert active_user is not None, "Cannot track package installs when active user is None"
335
+
336
+ invocation_data = get_base_invocation_context()
337
+
338
+ invocation_data.update({"project_id": project_hashed_name, "command": command_name})
339
+
340
+ context = [
341
+ SelfDescribingJson(INVOCATION_SPEC, invocation_data),
342
+ SelfDescribingJson(PACKAGE_INSTALL_SPEC, options),
343
+ ]
344
+
345
+ track(
346
+ active_user,
347
+ category="dbt",
348
+ action="package",
349
+ label=get_invocation_id(),
350
+ property_="install",
351
+ context=context,
352
+ )
353
+
354
+
355
+ def track_deprecation_warn(options):
356
+
357
+ assert active_user is not None, "Cannot track deprecation warnings when active user is None"
358
+
359
+ context = [SelfDescribingJson(DEPRECATION_WARN_SPEC, options)]
360
+
361
+ track(
362
+ active_user,
363
+ category="dbt",
364
+ action="deprecation",
365
+ label=get_invocation_id(),
366
+ property_="warn",
367
+ context=context,
368
+ )
369
+
370
+
371
+ def track_behavior_change_warn(msg: EventMsg) -> None:
372
+ if msg.info.name != "BehaviorChangeEvent" or active_user is None:
373
+ return
374
+
375
+ context = [SelfDescribingJson(BEHAVIOR_CHANGE_WARN_SPEC, msg_to_dict(msg))]
376
+ track(
377
+ active_user,
378
+ category="dbt",
379
+ action=msg.info.name,
380
+ label=get_invocation_id(),
381
+ context=context,
382
+ )
383
+
384
+
385
+ def track_invocation_end(invocation_context, result_type=None):
386
+ data = {"progress": "end", "result_type": result_type, "result": None}
387
+ data.update(invocation_context)
388
+ context = [
389
+ SelfDescribingJson(INVOCATION_SPEC, data),
390
+ get_platform_context(),
391
+ get_dbt_env_context(),
392
+ ]
393
+
394
+ assert active_user is not None, "Cannot track invocation end when active user is None"
395
+
396
+ track(active_user, category="dbt", action="invocation", label="end", context=context)
397
+
398
+
399
+ def track_invalid_invocation(args=None, result_type=None):
400
+ assert active_user is not None, "Cannot track invalid invocations when active user is None"
401
+ invocation_context = get_base_invocation_context()
402
+ invocation_context.update({"command": args.which})
403
+ data = {"progress": "invalid", "result_type": result_type, "result": None}
404
+ data.update(invocation_context)
405
+ context = [
406
+ SelfDescribingJson(INVOCATION_SPEC, data),
407
+ get_platform_context(),
408
+ get_dbt_env_context(),
409
+ ]
410
+ track(active_user, category="dbt", action="invocation", label="invalid", context=context)
411
+
412
+
413
+ def track_experimental_parser_sample(options):
414
+ context = [SelfDescribingJson(EXPERIMENTAL_PARSER, options)]
415
+ assert (
416
+ active_user is not None
417
+ ), "Cannot track experimental parser info when active user is None"
418
+
419
+ track(
420
+ active_user,
421
+ category="dbt",
422
+ action="experimental_parser",
423
+ label=get_invocation_id(),
424
+ context=context,
425
+ )
426
+
427
+
428
+ def track_partial_parser(options):
429
+ context = [SelfDescribingJson(PARTIAL_PARSER, options)]
430
+ assert active_user is not None, "Cannot track partial parser info when active user is None"
431
+
432
+ track(
433
+ active_user,
434
+ category="dbt",
435
+ action="partial_parser",
436
+ label=get_invocation_id(),
437
+ context=context,
438
+ )
439
+
440
+
441
+ def track_plugin_get_nodes(options):
442
+ context = [SelfDescribingJson(PLUGIN_GET_NODES, options)]
443
+ assert active_user is not None, "Cannot track plugin node info when active user is None"
444
+
445
+ track(
446
+ active_user,
447
+ category="dbt",
448
+ action="plugin_get_nodes",
449
+ label=get_invocation_id(),
450
+ context=context,
451
+ )
452
+
453
+
454
+ def track_runnable_timing(options):
455
+ context = [SelfDescribingJson(RUNNABLE_TIMING, options)]
456
+ assert active_user is not None, "Cannot track runnable info when active user is None"
457
+ track(
458
+ active_user,
459
+ category="dbt",
460
+ action="runnable_timing",
461
+ label=get_invocation_id(),
462
+ context=context,
463
+ )
464
+
465
+
466
+ def track_artifact_upload(options):
467
+ context = [SelfDescribingJson(ARTIFACT_UPLOAD, options)]
468
+ assert active_user is not None, "Cannot track artifact upload when active user is None"
469
+
470
+ track(
471
+ active_user,
472
+ category="dbt",
473
+ action="artifact_upload",
474
+ label=get_invocation_id(),
475
+ context=context,
476
+ )
477
+
478
+
479
+ def flush():
480
+ fire_event(FlushEvents())
481
+ try:
482
+ tracker.flush()
483
+ except Exception:
484
+ fire_event(FlushEventsFailure())
485
+
486
+
487
+ def disable_tracking():
488
+ global active_user
489
+ if active_user is not None:
490
+ active_user.disable_tracking()
491
+ else:
492
+ active_user = User(None)
493
+
494
+
495
+ def do_not_track():
496
+ global active_user
497
+ active_user = User(None)
498
+
499
+
500
+ def initialize_from_flags(send_anonymous_usage_stats, profiles_dir):
501
+ global active_user
502
+ if send_anonymous_usage_stats:
503
+ active_user = User(profiles_dir)
504
+ try:
505
+ active_user.initialize()
506
+ except Exception:
507
+ fire_event(TrackingInitializeFailure(exc_info=traceback.format_exc()))
508
+ active_user = User(None)
509
+ else:
510
+ active_user = User(None)
511
+
512
+
513
+ @contextmanager
514
+ def track_run(run_command=None):
515
+ invocation_context = get_base_invocation_context()
516
+ invocation_context["command"] = run_command
517
+
518
+ track_invocation_start(invocation_context)
519
+ try:
520
+ yield
521
+ track_invocation_end(invocation_context, result_type="ok")
522
+ except (NotImplementedError, FailedToConnectError) as e:
523
+ fire_event(MainEncounteredError(exc=str(e)))
524
+ track_invocation_end(invocation_context, result_type="error")
525
+ except Exception:
526
+ track_invocation_end(invocation_context, result_type="error")
527
+ raise
528
+ finally:
529
+ flush()
dvt/utils/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # re-export utils
2
+ from .utils import * # noqa: F403
3
+ from .utils import _coerce_decimal # noqa: F401 somewhere in the codebase we use this
@@ -0,0 +1,151 @@
1
+ import time
2
+ import uuid
3
+ import zipfile
4
+
5
+ import dvt.tracking
6
+ import requests
7
+ from dvt._pydantic_shim import BaseSettings # type: ignore
8
+ from dvt.config.runtime import UnsetProfile, load_project
9
+ from dvt.constants import MANIFEST_FILE_NAME, RUN_RESULTS_FILE_NAME
10
+ from dvt.events.types import ArtifactUploadSkipped, ArtifactUploadSuccess
11
+ from dvt.exceptions import DbtProjectError
12
+
13
+ from dbt_common.events.functions import fire_event
14
+ from dbt_common.exceptions import DbtBaseException as DbtException
15
+
16
+ MAX_RETRIES = 3
17
+
18
+ EXECUTION_ARTIFACTS = [MANIFEST_FILE_NAME, RUN_RESULTS_FILE_NAME]
19
+
20
+ PRODUCED_ARTIFACTS_PATHS: set[str] = set()
21
+
22
+
23
+ # artifact paths calling this will be uploaded to dbt Cloud
24
+ def add_artifact_produced(artifact_path: str):
25
+ PRODUCED_ARTIFACTS_PATHS.add(artifact_path)
26
+
27
+
28
+ class ArtifactUploadConfig(BaseSettings):
29
+ tenant_hostname: str
30
+ DBT_CLOUD_TOKEN: str
31
+ DBT_CLOUD_ACCOUNT_ID: str
32
+ DBT_CLOUD_ENVIRONMENT_ID: str
33
+
34
+ def get_ingest_url(self):
35
+ return f"https://{self.tenant_hostname}/api/private/accounts/{self.DBT_CLOUD_ACCOUNT_ID}/environments/{self.DBT_CLOUD_ENVIRONMENT_ID}/ingests/"
36
+
37
+ def get_complete_url(self, ingest_id):
38
+ return f"{self.get_ingest_url()}{ingest_id}/"
39
+
40
+ def get_headers(self, invocation_id=None):
41
+ if invocation_id is None:
42
+ invocation_id = str(uuid.uuid4())
43
+ return {
44
+ "Accept": "application/json",
45
+ "X-Invocation-Id": invocation_id,
46
+ "Authorization": f"Token {self.DBT_CLOUD_TOKEN}",
47
+ }
48
+
49
+
50
+ def _retry_with_backoff(operation_name, func, max_retries=MAX_RETRIES, retry_codes=None):
51
+ """Execute a function with exponential backoff retry logic.
52
+
53
+ Args:
54
+ operation_name: Name of the operation for error messages
55
+ func: Function to execute that returns (success, result)
56
+ max_retries: Maximum number of retry attempts
57
+
58
+ Returns:
59
+ The result from the function if successful
60
+
61
+ Raises:
62
+ DbtException: If all retry attempts fail
63
+ """
64
+ if retry_codes is None:
65
+ retry_codes = [500, 502, 503, 504]
66
+ retry_delay = 1
67
+ for attempt in range(max_retries + 1):
68
+ try:
69
+ success, result = func()
70
+ if success:
71
+ return result
72
+
73
+ if result.status_code not in retry_codes:
74
+ raise DbtException(f"Error {operation_name}: {result}")
75
+ if attempt == max_retries: # Last attempt
76
+ raise DbtException(f"Error {operation_name}: {result}")
77
+ except requests.RequestException as e:
78
+ if attempt == max_retries: # Last attempt
79
+ raise DbtException(f"Error {operation_name}: {str(e)}")
80
+
81
+ time.sleep(retry_delay)
82
+ retry_delay *= 2 # exponential backoff
83
+
84
+
85
+ def upload_artifacts(project_dir, target_path, command):
86
+ # Check if there are artifacts to upload for this command
87
+ if not PRODUCED_ARTIFACTS_PATHS:
88
+ fire_event(ArtifactUploadSkipped(msg="No artifacts to upload for current command"))
89
+ return
90
+
91
+ # read configurations
92
+ try:
93
+ project = load_project(
94
+ project_dir, version_check=False, profile=UnsetProfile(), cli_vars=None
95
+ )
96
+ if not project.dbt_cloud or "tenant_hostname" not in project.dbt_cloud:
97
+ raise DbtProjectError("dbt_cloud.tenant_hostname not found in dbt_project.yml")
98
+ tenant_hostname = project.dbt_cloud["tenant_hostname"]
99
+ if not tenant_hostname:
100
+ raise DbtProjectError("dbt_cloud.tenant_hostname is empty in dbt_project.yml")
101
+ except Exception as e:
102
+ raise DbtProjectError(
103
+ f"Error reading dbt_cloud.tenant_hostname from dbt_project.yml: {str(e)}"
104
+ )
105
+
106
+ config = ArtifactUploadConfig(tenant_hostname=tenant_hostname)
107
+
108
+ if not target_path:
109
+ target_path = "target"
110
+
111
+ # Create zip file with artifacts
112
+ zip_file_name = "target.zip"
113
+ with zipfile.ZipFile(zip_file_name, "w") as z:
114
+ for artifact_path in PRODUCED_ARTIFACTS_PATHS:
115
+ z.write(artifact_path, artifact_path.split("/")[-1])
116
+
117
+ # Step 1: Create ingest request with retry
118
+ def create_ingest():
119
+ response = requests.post(url=config.get_ingest_url(), headers=config.get_headers())
120
+ return response.status_code == 200, response
121
+
122
+ response = _retry_with_backoff("creating ingest request", create_ingest)
123
+ response_data = response.json()
124
+ ingest_id = response_data["data"]["id"]
125
+ upload_url = response_data["data"]["upload_url"]
126
+
127
+ # Step 2: Upload the zip file to the provided URL with retry
128
+ with open(zip_file_name, "rb") as f:
129
+ file_data = f.read()
130
+
131
+ def upload_file():
132
+ upload_response = requests.put(url=upload_url, data=file_data)
133
+ return upload_response.status_code in (200, 204), upload_response
134
+
135
+ _retry_with_backoff("uploading artifacts", upload_file)
136
+
137
+ # Step 3: Mark the ingest as successful with retry
138
+ def complete_ingest():
139
+ complete_response = requests.patch(
140
+ url=config.get_complete_url(ingest_id),
141
+ headers=config.get_headers(),
142
+ json={"upload_status": "SUCCESS"},
143
+ )
144
+ return complete_response.status_code == 204, complete_response
145
+
146
+ _retry_with_backoff("completing ingest", complete_ingest)
147
+
148
+ fire_event(ArtifactUploadSuccess(msg=f"command {command} completed successfully"))
149
+ if dbt.tracking.active_user is not None:
150
+ dbt.tracking.track_artifact_upload({"command": command})
151
+ PRODUCED_ARTIFACTS_PATHS.clear()