fal 0.9.2__py3-none-any.whl → 0.9.4__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 fal might be problematic. Click here for more details.
- _fal_testing/utils.py +2 -2
- dbt/adapters/fal/__init__.py +21 -0
- dbt/adapters/fal/__version__.py +1 -0
- dbt/adapters/fal/connections.py +18 -0
- dbt/adapters/fal/impl.py +93 -0
- dbt/adapters/fal/load_db_profile.py +80 -0
- dbt/adapters/fal/wrappers.py +113 -0
- dbt/adapters/fal_experimental/__init__.py +11 -0
- dbt/adapters/fal_experimental/__version__.py +1 -0
- dbt/adapters/fal_experimental/adapter.py +149 -0
- dbt/adapters/fal_experimental/adapter_support.py +234 -0
- dbt/adapters/fal_experimental/connections.py +72 -0
- dbt/adapters/fal_experimental/impl.py +240 -0
- dbt/adapters/fal_experimental/support/athena.py +92 -0
- dbt/adapters/fal_experimental/support/bigquery.py +74 -0
- dbt/adapters/fal_experimental/support/duckdb.py +28 -0
- dbt/adapters/fal_experimental/support/postgres.py +88 -0
- dbt/adapters/fal_experimental/support/redshift.py +56 -0
- dbt/adapters/fal_experimental/support/snowflake.py +76 -0
- dbt/adapters/fal_experimental/support/trino.py +26 -0
- dbt/adapters/fal_experimental/telemetry/__init__.py +1 -0
- dbt/adapters/fal_experimental/telemetry/telemetry.py +411 -0
- dbt/adapters/fal_experimental/teleport.py +192 -0
- dbt/adapters/fal_experimental/teleport_adapter_support.py +23 -0
- dbt/adapters/fal_experimental/teleport_support/duckdb.py +122 -0
- dbt/adapters/fal_experimental/teleport_support/snowflake.py +72 -0
- dbt/adapters/fal_experimental/utils/__init__.py +50 -0
- dbt/adapters/fal_experimental/utils/environments.py +302 -0
- dbt/fal/adapters/python/__init__.py +3 -0
- dbt/fal/adapters/python/connections.py +319 -0
- dbt/fal/adapters/python/impl.py +291 -0
- dbt/fal/adapters/teleport/__init__.py +3 -0
- dbt/fal/adapters/teleport/impl.py +103 -0
- dbt/fal/adapters/teleport/info.py +73 -0
- dbt/include/fal/__init__.py +3 -0
- dbt/include/fal/dbt_project.yml +5 -0
- dbt/include/fal/macros/materializations/table.sql +46 -0
- dbt/include/fal/macros/teleport_duckdb.sql +8 -0
- dbt/include/fal/macros/teleport_snowflake.sql +31 -0
- dbt/include/fal_experimental/__init__.py +3 -0
- dbt/include/fal_experimental/dbt_project.yml +5 -0
- dbt/include/fal_experimental/macros/materializations/table.sql +36 -0
- fal/__init__.py +61 -11
- fal/dbt/__init__.py +11 -0
- fal/dbt/cli/__init__.py +1 -0
- fal/{cli → dbt/cli}/args.py +7 -2
- fal/{cli → dbt/cli}/cli.py +18 -3
- fal/{cli → dbt/cli}/dbt_runner.py +1 -1
- fal/{cli → dbt/cli}/fal_runner.py +6 -6
- fal/{cli → dbt/cli}/flow_runner.py +9 -9
- fal/{cli → dbt/cli}/model_generator/model_generator.py +5 -5
- fal/{cli → dbt/cli}/selectors.py +2 -2
- fal/{fal_script.py → dbt/fal_script.py} +4 -4
- {faldbt → fal/dbt/integration}/lib.py +2 -2
- {faldbt → fal/dbt/integration}/magics.py +2 -2
- {faldbt → fal/dbt/integration}/parse.py +7 -7
- {faldbt → fal/dbt/integration}/project.py +7 -7
- fal/dbt/integration/utils/yaml_helper.py +80 -0
- fal/dbt/new/project.py +43 -0
- fal/{node_graph.py → dbt/node_graph.py} +2 -2
- fal/{packages → dbt/packages}/dependency_analysis.py +32 -38
- fal/{packages → dbt/packages}/environments/__init__.py +3 -3
- fal/{packages → dbt/packages}/environments/base.py +2 -2
- fal/{packages → dbt/packages}/environments/conda.py +3 -3
- fal/{packages → dbt/packages}/environments/virtual_env.py +3 -3
- fal/{packages → dbt/packages}/isolated_runner.py +5 -5
- fal/{planner → dbt/planner}/executor.py +4 -4
- fal/{planner → dbt/planner}/plan.py +3 -3
- fal/{planner → dbt/planner}/schedule.py +5 -5
- fal/{planner → dbt/planner}/tasks.py +5 -5
- fal/{telemetry → dbt/telemetry}/telemetry.py +4 -4
- fal/{typing.py → dbt/typing.py} +2 -2
- fal/{utils.py → dbt/utils.py} +2 -2
- {fal-0.9.2.dist-info → fal-0.9.4.dist-info}/METADATA +98 -117
- fal-0.9.4.dist-info/RECORD +91 -0
- fal-0.9.4.dist-info/entry_points.txt +4 -0
- fal/cli/__init__.py +0 -1
- fal-0.9.2.dist-info/RECORD +0 -47
- fal-0.9.2.dist-info/entry_points.txt +0 -3
- {faldbt → dbt/adapters/fal_experimental}/utils/yaml_helper.py +0 -0
- /fal/{cli → dbt/cli}/model_generator/__init__.py +0 -0
- /fal/{cli → dbt/cli}/model_generator/module_check.py +0 -0
- /fal/{feature_store → dbt/feature_store}/__init__.py +0 -0
- /fal/{feature_store → dbt/feature_store}/feature.py +0 -0
- /fal/{packages → dbt/integration}/__init__.py +0 -0
- {faldbt → fal/dbt/integration}/logger.py +0 -0
- /fal/{planner → dbt/integration/utils}/__init__.py +0 -0
- {faldbt → fal/dbt/integration}/version.py +0 -0
- /fal/{telemetry → dbt/packages}/__init__.py +0 -0
- /fal/{packages → dbt/packages}/bridge.py +0 -0
- {faldbt → fal/dbt/planner}/__init__.py +0 -0
- {faldbt/utils → fal/dbt/telemetry}/__init__.py +0 -0
- {fal-0.9.2.dist-info → fal-0.9.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import os
|
|
3
|
+
from time import sleep
|
|
4
|
+
import sys
|
|
5
|
+
from multiprocessing.synchronize import RLock
|
|
6
|
+
from threading import get_ident
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Dict,
|
|
10
|
+
Tuple,
|
|
11
|
+
Hashable,
|
|
12
|
+
Optional,
|
|
13
|
+
List,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
Iterable,
|
|
17
|
+
Callable,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from dbt.exceptions import (
|
|
21
|
+
NotImplementedError,
|
|
22
|
+
InvalidConnectionError,
|
|
23
|
+
DbtInternalError,
|
|
24
|
+
CompilationError,
|
|
25
|
+
FailedToConnectError,
|
|
26
|
+
)
|
|
27
|
+
from dbt.contracts.connection import (
|
|
28
|
+
Connection,
|
|
29
|
+
Identifier,
|
|
30
|
+
ConnectionState,
|
|
31
|
+
AdapterRequiredConfig,
|
|
32
|
+
LazyHandle,
|
|
33
|
+
AdapterResponse,
|
|
34
|
+
)
|
|
35
|
+
from dbt.events import AdapterLogger
|
|
36
|
+
from dbt.events.functions import fire_event
|
|
37
|
+
from dbt.events.types import (
|
|
38
|
+
NewConnection,
|
|
39
|
+
ConnectionReused,
|
|
40
|
+
ConnectionLeftOpen,
|
|
41
|
+
ConnectionClosed,
|
|
42
|
+
)
|
|
43
|
+
from dbt import flags
|
|
44
|
+
|
|
45
|
+
SleepTime = Union[int, float] # As taken by time.sleep.
|
|
46
|
+
AdapterHandle = Any # Adapter connection handle objects can be any class.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PythonConnectionManager(metaclass=abc.ABCMeta):
|
|
50
|
+
"""Methods to implement:
|
|
51
|
+
- open
|
|
52
|
+
- execute
|
|
53
|
+
|
|
54
|
+
You must also set the 'TYPE' class attribute with a class-unique constant
|
|
55
|
+
string.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
TYPE: str = NotImplemented
|
|
59
|
+
|
|
60
|
+
def __init__(self, profile: AdapterRequiredConfig):
|
|
61
|
+
self.profile = profile
|
|
62
|
+
if profile.credentials.type == self.TYPE:
|
|
63
|
+
self.credentials = profile.credentials
|
|
64
|
+
else:
|
|
65
|
+
self.credentials = profile.python_adapter_credentials
|
|
66
|
+
|
|
67
|
+
self.thread_connections: Dict[Hashable, Connection] = {}
|
|
68
|
+
self.lock: RLock = flags.MP_CONTEXT.RLock()
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def get_thread_identifier() -> Hashable:
|
|
72
|
+
# note that get_ident() may be re-used, but we should never experience
|
|
73
|
+
# that within a single process
|
|
74
|
+
return (os.getpid(), get_ident())
|
|
75
|
+
|
|
76
|
+
def get_thread_connection(self) -> Connection:
|
|
77
|
+
key = self.get_thread_identifier()
|
|
78
|
+
with self.lock:
|
|
79
|
+
if key not in self.thread_connections:
|
|
80
|
+
raise InvalidConnectionError(key, list(self.thread_connections))
|
|
81
|
+
return self.thread_connections[key]
|
|
82
|
+
|
|
83
|
+
def set_thread_connection(self, conn: Connection) -> None:
|
|
84
|
+
key = self.get_thread_identifier()
|
|
85
|
+
if key in self.thread_connections:
|
|
86
|
+
raise DbtInternalError(
|
|
87
|
+
"In set_thread_connection, existing connection exists for {}"
|
|
88
|
+
)
|
|
89
|
+
self.thread_connections[key] = conn
|
|
90
|
+
|
|
91
|
+
def get_if_exists(self) -> Optional[Connection]:
|
|
92
|
+
key = self.get_thread_identifier()
|
|
93
|
+
with self.lock:
|
|
94
|
+
return self.thread_connections.get(key)
|
|
95
|
+
|
|
96
|
+
def clear_thread_connection(self) -> None:
|
|
97
|
+
key = self.get_thread_identifier()
|
|
98
|
+
with self.lock:
|
|
99
|
+
if key in self.thread_connections:
|
|
100
|
+
del self.thread_connections[key]
|
|
101
|
+
|
|
102
|
+
def set_connection_name(self, name: Optional[str] = None) -> Connection:
|
|
103
|
+
conn_name: str
|
|
104
|
+
if name is None:
|
|
105
|
+
# if a name isn't specified, we'll re-use a single handle
|
|
106
|
+
# named 'master'
|
|
107
|
+
conn_name = "master"
|
|
108
|
+
else:
|
|
109
|
+
if not isinstance(name, str):
|
|
110
|
+
raise CompilationError(
|
|
111
|
+
f"For connection name, got {name} - not a string!"
|
|
112
|
+
)
|
|
113
|
+
assert isinstance(name, str)
|
|
114
|
+
conn_name = name
|
|
115
|
+
|
|
116
|
+
conn = self.get_if_exists()
|
|
117
|
+
if conn is None:
|
|
118
|
+
conn = Connection(
|
|
119
|
+
type=Identifier(self.TYPE),
|
|
120
|
+
name=None,
|
|
121
|
+
state=ConnectionState.INIT,
|
|
122
|
+
transaction_open=False,
|
|
123
|
+
handle=None,
|
|
124
|
+
credentials=self.credentials,
|
|
125
|
+
)
|
|
126
|
+
self.set_thread_connection(conn)
|
|
127
|
+
|
|
128
|
+
if conn.name == conn_name and conn.state == "open":
|
|
129
|
+
return conn
|
|
130
|
+
|
|
131
|
+
fire_event(NewConnection(conn_name=conn_name, conn_type=self.TYPE))
|
|
132
|
+
|
|
133
|
+
if conn.state == "open":
|
|
134
|
+
fire_event(ConnectionReused(conn_name=conn_name))
|
|
135
|
+
else:
|
|
136
|
+
conn.handle = LazyHandle(self.open)
|
|
137
|
+
|
|
138
|
+
conn.name = conn_name
|
|
139
|
+
return conn
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def retry_connection(
|
|
143
|
+
cls,
|
|
144
|
+
connection: Connection,
|
|
145
|
+
connect: Callable[[], AdapterHandle],
|
|
146
|
+
logger: AdapterLogger,
|
|
147
|
+
retryable_exceptions: Iterable[Type[Exception]],
|
|
148
|
+
retry_limit: int = 1,
|
|
149
|
+
retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1,
|
|
150
|
+
_attempts: int = 0,
|
|
151
|
+
) -> Connection:
|
|
152
|
+
"""Given a Connection, set its handle by calling connect.
|
|
153
|
+
|
|
154
|
+
The calls to connect will be retried up to retry_limit times to deal with transient
|
|
155
|
+
connection errors. By default, one retry will be attempted if retryable_exceptions is set.
|
|
156
|
+
|
|
157
|
+
:param Connection connection: An instance of a Connection that needs a handle to be set,
|
|
158
|
+
usually when attempting to open it.
|
|
159
|
+
:param connect: A callable that returns the appropiate connection handle for a
|
|
160
|
+
given adapter. This callable will be retried retry_limit times if a subclass of any
|
|
161
|
+
Exception in retryable_exceptions is raised by connect.
|
|
162
|
+
:type connect: Callable[[], AdapterHandle]
|
|
163
|
+
:param AdapterLogger logger: A logger to emit messages on retry attempts or errors. When
|
|
164
|
+
handling expected errors, we call debug, and call warning on unexpected errors or when
|
|
165
|
+
all retry attempts have been exhausted.
|
|
166
|
+
:param retryable_exceptions: An iterable of exception classes that if raised by
|
|
167
|
+
connect should trigger a retry.
|
|
168
|
+
:type retryable_exceptions: Iterable[Type[Exception]]
|
|
169
|
+
:param int retry_limit: How many times to retry the call to connect. If this limit
|
|
170
|
+
is exceeded before a successful call, a FailedToConnectError will be raised.
|
|
171
|
+
Must be non-negative.
|
|
172
|
+
:param retry_timeout: Time to wait between attempts to connect. Can also take a
|
|
173
|
+
Callable that takes the number of attempts so far, beginning at 0, and returns an int
|
|
174
|
+
or float to be passed to time.sleep.
|
|
175
|
+
:type retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1
|
|
176
|
+
:param int _attempts: Parameter used to keep track of the number of attempts in calling the
|
|
177
|
+
connect function across recursive calls. Passed as an argument to retry_timeout if it
|
|
178
|
+
is a Callable. This parameter should not be set by the initial caller.
|
|
179
|
+
:raises dbt.exceptions.FailedToConnectError: Upon exhausting all retry attempts without
|
|
180
|
+
successfully acquiring a handle.
|
|
181
|
+
:return: The given connection with its appropriate state and handle attributes set
|
|
182
|
+
depending on whether we successfully acquired a handle or not.
|
|
183
|
+
"""
|
|
184
|
+
timeout = retry_timeout(_attempts) if callable(retry_timeout) else retry_timeout
|
|
185
|
+
if timeout < 0:
|
|
186
|
+
raise FailedToConnectError(
|
|
187
|
+
"retry_timeout cannot be negative or return a negative time."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if retry_limit < 0 or retry_limit > sys.getrecursionlimit():
|
|
191
|
+
# This guard is not perfect others may add to the recursion limit (e.g. built-ins).
|
|
192
|
+
connection.handle = None
|
|
193
|
+
connection.state = ConnectionState.FAIL
|
|
194
|
+
raise FailedToConnectError("retry_limit cannot be negative")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
connection.handle = connect()
|
|
198
|
+
connection.state = ConnectionState.OPEN
|
|
199
|
+
return connection
|
|
200
|
+
|
|
201
|
+
except tuple(retryable_exceptions) as e:
|
|
202
|
+
if retry_limit <= 0:
|
|
203
|
+
connection.handle = None
|
|
204
|
+
connection.state = ConnectionState.FAIL
|
|
205
|
+
raise FailedToConnectError(str(e))
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"Got a retryable error when attempting to open a {cls.TYPE} connection.\n"
|
|
209
|
+
f"{retry_limit} attempts remaining. Retrying in {timeout} seconds.\n"
|
|
210
|
+
f"Error:\n{e}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
sleep(timeout)
|
|
214
|
+
return cls.retry_connection(
|
|
215
|
+
connection=connection,
|
|
216
|
+
connect=connect,
|
|
217
|
+
logger=logger,
|
|
218
|
+
retry_limit=retry_limit - 1,
|
|
219
|
+
retry_timeout=retry_timeout,
|
|
220
|
+
retryable_exceptions=retryable_exceptions,
|
|
221
|
+
_attempts=_attempts + 1,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
connection.handle = None
|
|
226
|
+
connection.state = ConnectionState.FAIL
|
|
227
|
+
raise FailedToConnectError(str(e))
|
|
228
|
+
|
|
229
|
+
@abc.abstractmethod
|
|
230
|
+
def cancel(self, connection: Connection):
|
|
231
|
+
"""Cancel the given connection."""
|
|
232
|
+
raise NotImplementedError("`cancel` is not implemented for this adapter!")
|
|
233
|
+
|
|
234
|
+
def cancel_open(self) -> List[str]:
|
|
235
|
+
names = []
|
|
236
|
+
this_connection = self.get_if_exists()
|
|
237
|
+
with self.lock:
|
|
238
|
+
for connection in self.thread_connections.values():
|
|
239
|
+
if connection is this_connection:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
# if the connection failed, the handle will be None so we have
|
|
243
|
+
# nothing to cancel.
|
|
244
|
+
if (
|
|
245
|
+
connection.handle is not None
|
|
246
|
+
and connection.state == ConnectionState.OPEN
|
|
247
|
+
):
|
|
248
|
+
self.cancel(connection)
|
|
249
|
+
if connection.name is not None:
|
|
250
|
+
names.append(connection.name)
|
|
251
|
+
return names
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
@abc.abstractmethod
|
|
255
|
+
def open(cls, connection: Connection) -> Connection:
|
|
256
|
+
"""Open the given connection on the adapter and return it.
|
|
257
|
+
|
|
258
|
+
This may mutate the given connection (in particular, its state and its
|
|
259
|
+
handle).
|
|
260
|
+
|
|
261
|
+
This should be thread-safe, or hold the lock if necessary. The given
|
|
262
|
+
connection should not be in either in_use or available.
|
|
263
|
+
"""
|
|
264
|
+
raise NotImplementedError("`open` is not implemented for this adapter!")
|
|
265
|
+
|
|
266
|
+
def release(self) -> None:
|
|
267
|
+
with self.lock:
|
|
268
|
+
conn = self.get_if_exists()
|
|
269
|
+
if conn is None:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
# always close the connection
|
|
274
|
+
self.close(conn)
|
|
275
|
+
except Exception:
|
|
276
|
+
# if rollback or close failed, remove our busted connection
|
|
277
|
+
self.clear_thread_connection()
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
def cleanup_all(self) -> None:
|
|
281
|
+
with self.lock:
|
|
282
|
+
for connection in self.thread_connections.values():
|
|
283
|
+
if connection.state not in {"closed", "init"}:
|
|
284
|
+
fire_event(ConnectionLeftOpen(conn_name=connection.name))
|
|
285
|
+
else:
|
|
286
|
+
fire_event(ConnectionClosed(conn_name=connection.name))
|
|
287
|
+
self.close(connection)
|
|
288
|
+
|
|
289
|
+
# garbage collect these connections
|
|
290
|
+
self.thread_connections.clear()
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def _close_handle(cls, connection: Connection) -> None:
|
|
294
|
+
"""Perform the actual close operation."""
|
|
295
|
+
# On windows, sometimes connection handles don't have a close() attr.
|
|
296
|
+
if hasattr(connection.handle, "close"):
|
|
297
|
+
connection.handle.close()
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def close(cls, connection: Connection) -> Connection:
|
|
301
|
+
# if the connection is in closed or init, there's nothing to do
|
|
302
|
+
if connection.state in {ConnectionState.CLOSED, ConnectionState.INIT}:
|
|
303
|
+
return connection
|
|
304
|
+
|
|
305
|
+
cls._close_handle(connection)
|
|
306
|
+
connection.state = ConnectionState.CLOSED
|
|
307
|
+
|
|
308
|
+
return connection
|
|
309
|
+
|
|
310
|
+
@abc.abstractmethod
|
|
311
|
+
def execute(self, compiled_code: str) -> Tuple[AdapterResponse, Any]:
|
|
312
|
+
"""Execute the given Python code.
|
|
313
|
+
|
|
314
|
+
:param str compiled_code: The Python code to execute.
|
|
315
|
+
:return: A tuple of the run status and results
|
|
316
|
+
(type is left to be decided by the Adapter implementation for now).
|
|
317
|
+
:rtype: Tuple[AdapterResponse, Any]
|
|
318
|
+
"""
|
|
319
|
+
raise NotImplementedError("`execute` is not implemented for this adapter!")
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
import time
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Type,
|
|
7
|
+
Dict,
|
|
8
|
+
Any,
|
|
9
|
+
Mapping,
|
|
10
|
+
Iterator,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from dbt.adapters.factory import get_adapter
|
|
14
|
+
from dbt.exceptions import (
|
|
15
|
+
NotImplementedError,
|
|
16
|
+
DbtRuntimeError,
|
|
17
|
+
)
|
|
18
|
+
from dbt.contracts.graph.nodes import ResultNode
|
|
19
|
+
from dbt.adapters.protocol import AdapterConfig, ConnectionManagerProtocol
|
|
20
|
+
from dbt.events.functions import fire_event
|
|
21
|
+
from dbt.events.types import (
|
|
22
|
+
CodeExecution,
|
|
23
|
+
CodeExecutionStatus,
|
|
24
|
+
)
|
|
25
|
+
from dbt.adapters.base.meta import AdapterMeta, available
|
|
26
|
+
from dbt.contracts.connection import Credentials, Connection, AdapterResponse
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def log_code_execution(code_execution_function):
|
|
30
|
+
# decorator to log code and execution time
|
|
31
|
+
if code_execution_function.__name__ != "submit_python_job":
|
|
32
|
+
raise ValueError("this should be only used to log submit_python_job now")
|
|
33
|
+
|
|
34
|
+
def execution_with_log(*args):
|
|
35
|
+
self = args[0]
|
|
36
|
+
connection_name = self.connections.get_thread_connection().name
|
|
37
|
+
fire_event(CodeExecution(conn_name=connection_name, code_content=args[2]))
|
|
38
|
+
start_time = time.time()
|
|
39
|
+
response = code_execution_function(*args)
|
|
40
|
+
fire_event(
|
|
41
|
+
CodeExecutionStatus(
|
|
42
|
+
status=response._message, elapsed=round((time.time() - start_time), 2)
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
return execution_with_log
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PythonJobHelper:
|
|
51
|
+
def __init__(self, parsed_model: Dict, credential: Credentials) -> None:
|
|
52
|
+
raise NotImplementedError("PythonJobHelper is not implemented yet")
|
|
53
|
+
|
|
54
|
+
def submit(self, compiled_code: str) -> Any:
|
|
55
|
+
raise NotImplementedError(
|
|
56
|
+
"PythonJobHelper submit function is not implemented yet"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PythonAdapter(metaclass=AdapterMeta):
|
|
61
|
+
"""The PythonAdapter provides an abstract base class for Python adapters.
|
|
62
|
+
|
|
63
|
+
Adapters must implement the following methods and macros. Some of the
|
|
64
|
+
methods can be safely overridden as a noop, where it makes sense. Those
|
|
65
|
+
methods are marked with a (passable) in their docstrings. Check docstrings
|
|
66
|
+
for type information, etc.
|
|
67
|
+
|
|
68
|
+
To implement a macro, implement "${adapter_type}__${macro_name}" in the
|
|
69
|
+
adapter's internal project.
|
|
70
|
+
|
|
71
|
+
To invoke a method in an adapter macro, call it on the 'adapter' Jinja
|
|
72
|
+
object using dot syntax.
|
|
73
|
+
|
|
74
|
+
To invoke a method in model code, add the @available decorator atop a method
|
|
75
|
+
declaration. Methods are invoked as macros.
|
|
76
|
+
|
|
77
|
+
Methods:
|
|
78
|
+
- is_cancelable
|
|
79
|
+
|
|
80
|
+
- python_submission_helpers
|
|
81
|
+
- default_python_submission_method
|
|
82
|
+
- generate_python_submission_response
|
|
83
|
+
or
|
|
84
|
+
- submit_python_job
|
|
85
|
+
|
|
86
|
+
Macros:
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
ConnectionManager: Type[ConnectionManagerProtocol]
|
|
91
|
+
|
|
92
|
+
# A set of clobber config fields accepted by this adapter
|
|
93
|
+
# for use in materializations
|
|
94
|
+
AdapterSpecificConfigs: Type[AdapterConfig] = AdapterConfig
|
|
95
|
+
|
|
96
|
+
def __init__(self, config):
|
|
97
|
+
self.config = config
|
|
98
|
+
self.connections = self.ConnectionManager(config)
|
|
99
|
+
|
|
100
|
+
# HACK: A Python adapter does not have _available_ all the attributes a DB adapter does.
|
|
101
|
+
# Since we use the DB adapter as the storage for the Python adapter, we must proxy to it
|
|
102
|
+
# all the unhandled calls.
|
|
103
|
+
#
|
|
104
|
+
# Another option is to write a PythonAdapter-specific DBWrapper (PythonWrapper?) that is
|
|
105
|
+
# aware of this case. This may be appealing because a _complete_ adapter (DB+Python) would
|
|
106
|
+
# then be more easily used to replace the Python part of any other adapter.
|
|
107
|
+
|
|
108
|
+
self._db_adapter = get_adapter(config)
|
|
109
|
+
self.Relation = self._db_adapter.Relation
|
|
110
|
+
self.Column = self._db_adapter.Column
|
|
111
|
+
|
|
112
|
+
def cache_added(self, *args, **kwargs):
|
|
113
|
+
return self._db_adapter.cache_added(*args, **kwargs)
|
|
114
|
+
|
|
115
|
+
@available.parse_none
|
|
116
|
+
def get_relation(self, *args, **kwargs):
|
|
117
|
+
return self._db_adapter.get_relation(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def date_function(cls):
|
|
121
|
+
# HACK: to appease the ProviderContext
|
|
122
|
+
return """
|
|
123
|
+
import datetime
|
|
124
|
+
return datetime.datetime.now()
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
###
|
|
128
|
+
# Methods that pass through to the connection manager
|
|
129
|
+
###
|
|
130
|
+
def acquire_connection(self, name=None) -> Connection:
|
|
131
|
+
return self.connections.set_connection_name(name)
|
|
132
|
+
|
|
133
|
+
def release_connection(self) -> None:
|
|
134
|
+
self.connections.release()
|
|
135
|
+
|
|
136
|
+
def cleanup_connections(self) -> None:
|
|
137
|
+
self.connections.cleanup_all()
|
|
138
|
+
|
|
139
|
+
def nice_connection_name(self) -> str:
|
|
140
|
+
conn = self.connections.get_if_exists()
|
|
141
|
+
if conn is None or conn.name is None:
|
|
142
|
+
return "<None>"
|
|
143
|
+
return conn.name
|
|
144
|
+
|
|
145
|
+
@contextmanager
|
|
146
|
+
def connection_named(
|
|
147
|
+
self, name: str, node: Optional[ResultNode] = None
|
|
148
|
+
) -> Iterator[None]:
|
|
149
|
+
try:
|
|
150
|
+
self.acquire_connection(name)
|
|
151
|
+
yield
|
|
152
|
+
finally:
|
|
153
|
+
self.release_connection()
|
|
154
|
+
|
|
155
|
+
@contextmanager
|
|
156
|
+
def connection_for(self, node: ResultNode) -> Iterator[None]:
|
|
157
|
+
with self.connection_named(node.unique_id, node):
|
|
158
|
+
yield
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
@abc.abstractmethod
|
|
162
|
+
def is_cancelable(cls) -> bool:
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
"`is_cancelable` is not implemented for this adapter!"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
###
|
|
168
|
+
# Methods that should never be overridden
|
|
169
|
+
###
|
|
170
|
+
@classmethod
|
|
171
|
+
def type(cls) -> str:
|
|
172
|
+
"""Get the type of this adapter. Types must be class-unique and
|
|
173
|
+
consistent.
|
|
174
|
+
|
|
175
|
+
:return: The type name
|
|
176
|
+
:rtype: str
|
|
177
|
+
"""
|
|
178
|
+
return cls.ConnectionManager.TYPE
|
|
179
|
+
|
|
180
|
+
###
|
|
181
|
+
# ODBC FUNCTIONS -- these should not need to change for every adapter,
|
|
182
|
+
# although some adapters may override them
|
|
183
|
+
###
|
|
184
|
+
def cancel_open_connections(self):
|
|
185
|
+
"""Cancel all open connections."""
|
|
186
|
+
return self.connections.cancel_open()
|
|
187
|
+
|
|
188
|
+
def pre_model_hook(self, config: Mapping[str, Any]) -> Any:
|
|
189
|
+
"""A hook for running some operation before the model materialization
|
|
190
|
+
runs. The hook can assume it has a connection available.
|
|
191
|
+
|
|
192
|
+
The only parameter is a configuration dictionary (the same one
|
|
193
|
+
available in the materialization context). It should be considered
|
|
194
|
+
read-only.
|
|
195
|
+
|
|
196
|
+
The pre-model hook may return anything as a context, which will be
|
|
197
|
+
passed to the post-model hook.
|
|
198
|
+
"""
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def post_model_hook(self, config: Mapping[str, Any], context: Any) -> None:
|
|
202
|
+
"""A hook for running some operation after the model materialization
|
|
203
|
+
runs. The hook can assume it has a connection available.
|
|
204
|
+
|
|
205
|
+
The first parameter is a configuration dictionary (the same one
|
|
206
|
+
available in the materialization context). It should be considered
|
|
207
|
+
read-only.
|
|
208
|
+
|
|
209
|
+
The second parameter is the value returned by pre_mdoel_hook.
|
|
210
|
+
"""
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
def get_compiler(self):
|
|
214
|
+
from dbt.compilation import Compiler
|
|
215
|
+
|
|
216
|
+
return Compiler(self.config)
|
|
217
|
+
|
|
218
|
+
###
|
|
219
|
+
# Python
|
|
220
|
+
###
|
|
221
|
+
@property
|
|
222
|
+
def python_submission_helpers(self) -> Dict[str, Type[PythonJobHelper]]:
|
|
223
|
+
raise NotImplementedError("python_submission_helpers is not specified")
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def default_python_submission_method(self) -> str:
|
|
227
|
+
raise NotImplementedError("default_python_submission_method is not specified")
|
|
228
|
+
|
|
229
|
+
@log_code_execution
|
|
230
|
+
def submit_python_job(
|
|
231
|
+
self, parsed_model: dict, compiled_code: str
|
|
232
|
+
) -> AdapterResponse:
|
|
233
|
+
submission_method = parsed_model["config"].get(
|
|
234
|
+
"submission_method", self.default_python_submission_method
|
|
235
|
+
)
|
|
236
|
+
if submission_method not in self.python_submission_helpers:
|
|
237
|
+
raise NotImplementedError(
|
|
238
|
+
"Submission method {} is not supported for current adapter".format(
|
|
239
|
+
submission_method
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
job_helper = self.python_submission_helpers[submission_method](
|
|
243
|
+
parsed_model, self.connections.credentials
|
|
244
|
+
)
|
|
245
|
+
submission_result = job_helper.submit(compiled_code)
|
|
246
|
+
# process submission result to generate adapter response
|
|
247
|
+
return self.generate_python_submission_response(submission_result)
|
|
248
|
+
|
|
249
|
+
def generate_python_submission_response(
|
|
250
|
+
self, submission_result: Any
|
|
251
|
+
) -> AdapterResponse:
|
|
252
|
+
raise NotImplementedError(
|
|
253
|
+
"Your adapter need to implement generate_python_submission_response"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def valid_incremental_strategies(self):
|
|
257
|
+
"""The set of standard builtin strategies which this adapter supports out-of-the-box.
|
|
258
|
+
Not used to validate custom strategies defined by end users.
|
|
259
|
+
"""
|
|
260
|
+
return ["append"]
|
|
261
|
+
|
|
262
|
+
def builtin_incremental_strategies(self):
|
|
263
|
+
return ["append", "delete+insert", "merge", "insert_overwrite"]
|
|
264
|
+
|
|
265
|
+
@available.parse_none
|
|
266
|
+
def get_incremental_strategy_macro(self, model_context, strategy: str):
|
|
267
|
+
# Construct macro_name from strategy name
|
|
268
|
+
if strategy is None:
|
|
269
|
+
strategy = "default"
|
|
270
|
+
|
|
271
|
+
# validate strategies for this adapter
|
|
272
|
+
valid_strategies = self.valid_incremental_strategies()
|
|
273
|
+
valid_strategies.append("default")
|
|
274
|
+
builtin_strategies = self.builtin_incremental_strategies()
|
|
275
|
+
if strategy in builtin_strategies and strategy not in valid_strategies:
|
|
276
|
+
raise DbtRuntimeError(
|
|
277
|
+
f"The incremental strategy '{strategy}' is not valid for this adapter"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
strategy = strategy.replace("+", "_")
|
|
281
|
+
macro_name = f"get_incremental_{strategy}_sql"
|
|
282
|
+
# The model_context should have MacroGenerator callable objects for all macros
|
|
283
|
+
if macro_name not in model_context:
|
|
284
|
+
raise DbtRuntimeError(
|
|
285
|
+
'dbt could not find an incremental strategy macro with the name "{}" in {}'.format(
|
|
286
|
+
macro_name, self.config.project_name
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# This returns a callable macro
|
|
291
|
+
return model_context[macro_name]
|