dbt-common 1.7.0__py3-none-any.whl → 1.9.0__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.
- dbt_common/__about__.py +1 -1
- dbt_common/behavior_flags.py +139 -0
- dbt_common/clients/agate_helper.py +2 -2
- dbt_common/clients/jinja.py +83 -55
- dbt_common/clients/system.py +27 -1
- dbt_common/context.py +2 -2
- dbt_common/contracts/config/base.py +95 -19
- dbt_common/contracts/util.py +10 -4
- dbt_common/dataclass_schema.py +4 -5
- dbt_common/events/contextvars.py +1 -1
- dbt_common/events/types.proto +15 -0
- dbt_common/events/types.py +20 -0
- dbt_common/events/types_pb2.py +49 -45
- dbt_common/exceptions/base.py +13 -13
- dbt_common/exceptions/jinja.py +9 -4
- dbt_common/exceptions/system.py +2 -2
- dbt_common/invocation.py +1 -1
- dbt_common/record.py +8 -2
- dbt_common/semver.py +48 -36
- dbt_common/utils/casting.py +3 -2
- dbt_common/utils/connection.py +2 -1
- dbt_common/utils/jinja.py +7 -5
- {dbt_common-1.7.0.dist-info → dbt_common-1.9.0.dist-info}/METADATA +1 -1
- {dbt_common-1.7.0.dist-info → dbt_common-1.9.0.dist-info}/RECORD +26 -25
- {dbt_common-1.7.0.dist-info → dbt_common-1.9.0.dist-info}/WHEEL +0 -0
- {dbt_common-1.7.0.dist-info → dbt_common-1.9.0.dist-info}/licenses/LICENSE +0 -0
dbt_common/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
version = "1.
|
1
|
+
version = "1.9.0"
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import Any, Dict, List, TypedDict
|
3
|
+
|
4
|
+
try:
|
5
|
+
from typing import NotRequired
|
6
|
+
except ImportError:
|
7
|
+
# NotRequired was introduced in Python 3.11
|
8
|
+
# This is the suggested way to implement a TypedDict with optional arguments
|
9
|
+
from typing import Optional as NotRequired
|
10
|
+
|
11
|
+
from dbt_common.events.functions import fire_event
|
12
|
+
from dbt_common.events.types import BehaviorChangeEvent
|
13
|
+
from dbt_common.exceptions import CompilationError, DbtInternalError
|
14
|
+
|
15
|
+
|
16
|
+
class BehaviorFlag(TypedDict):
|
17
|
+
"""
|
18
|
+
Configuration used to create a BehaviorFlagRendered instance
|
19
|
+
|
20
|
+
Args:
|
21
|
+
name: the name of the behavior flag
|
22
|
+
default: default setting, starts as False, becomes True after a bake-in period
|
23
|
+
description: an additional message to send when the flag evaluates to False
|
24
|
+
docs_url: the url to the relevant docs on docs.getdbt.com
|
25
|
+
|
26
|
+
*Note*:
|
27
|
+
While `description` and `docs_url` are both listed as `NotRequired`, at least one of them is required.
|
28
|
+
This is validated when the flag is rendered in `BehaviorFlagRendered` below.
|
29
|
+
The goal of this restriction is to provide the end user with context so they can make an informed decision
|
30
|
+
about if, and when, to enable the behavior flag.
|
31
|
+
"""
|
32
|
+
|
33
|
+
name: str
|
34
|
+
default: bool
|
35
|
+
source: NotRequired[str]
|
36
|
+
description: NotRequired[str]
|
37
|
+
docs_url: NotRequired[str]
|
38
|
+
|
39
|
+
|
40
|
+
class BehaviorFlagRendered:
|
41
|
+
"""
|
42
|
+
A rendered behavior flag that gets used throughout dbt packages
|
43
|
+
|
44
|
+
Args:
|
45
|
+
flag: the configuration for the behavior flag
|
46
|
+
user_overrides: a set of user settings, one of which may be an override on this behavior flag
|
47
|
+
"""
|
48
|
+
|
49
|
+
def __init__(self, flag: BehaviorFlag, user_overrides: Dict[str, Any]) -> None:
|
50
|
+
self._validate(flag)
|
51
|
+
|
52
|
+
self.name = flag["name"]
|
53
|
+
self.setting = user_overrides.get(flag["name"], flag["default"])
|
54
|
+
|
55
|
+
default_description = (
|
56
|
+
f"""The behavior controlled by `{flag["name"]}` is currently turned off.\n"""
|
57
|
+
)
|
58
|
+
default_docs_url = "https://docs.getdbt.com/reference/global-configs/behavior-changes"
|
59
|
+
self._behavior_change_event = BehaviorChangeEvent(
|
60
|
+
flag_name=flag["name"],
|
61
|
+
flag_source=flag.get("source", self._default_source()),
|
62
|
+
description=flag.get("description", default_description),
|
63
|
+
docs_url=flag.get("docs_url", default_docs_url),
|
64
|
+
)
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def _validate(flag: BehaviorFlag) -> None:
|
68
|
+
if flag.get("description") is None and flag.get("docs_url") is None:
|
69
|
+
raise DbtInternalError(
|
70
|
+
"Behavior change flags require at least one of `description` and `docs_url`."
|
71
|
+
)
|
72
|
+
|
73
|
+
@property
|
74
|
+
def setting(self) -> bool:
|
75
|
+
if self._setting is False:
|
76
|
+
fire_event(self._behavior_change_event)
|
77
|
+
return self._setting
|
78
|
+
|
79
|
+
@setting.setter
|
80
|
+
def setting(self, value: bool) -> None:
|
81
|
+
self._setting = value
|
82
|
+
|
83
|
+
@property
|
84
|
+
def no_warn(self) -> bool:
|
85
|
+
return self._setting
|
86
|
+
|
87
|
+
@staticmethod
|
88
|
+
def _default_source() -> str:
|
89
|
+
"""
|
90
|
+
If the maintainer did not provide a source, default to the module that called this class.
|
91
|
+
For adapters, this will likely be `dbt.adapters.<foo>.impl` for `dbt-foo`.
|
92
|
+
"""
|
93
|
+
for frame in inspect.stack():
|
94
|
+
if module := inspect.getmodule(frame[0]):
|
95
|
+
if module.__name__ != __name__:
|
96
|
+
return module.__name__
|
97
|
+
return "Unknown"
|
98
|
+
|
99
|
+
def __bool__(self) -> bool:
|
100
|
+
return self.setting
|
101
|
+
|
102
|
+
|
103
|
+
class Behavior:
|
104
|
+
"""
|
105
|
+
A collection of behavior flags
|
106
|
+
|
107
|
+
This is effectively a dictionary that supports dot notation for easy reference, e.g.:
|
108
|
+
```python
|
109
|
+
if adapter.behavior.my_flag:
|
110
|
+
...
|
111
|
+
|
112
|
+
if adapter.behavior.my_flag.no_warn: # this will not fire the behavior change event
|
113
|
+
...
|
114
|
+
```
|
115
|
+
```jinja
|
116
|
+
{% if adapter.behavior.my_flag %}
|
117
|
+
...
|
118
|
+
{% endif %}
|
119
|
+
|
120
|
+
{% if adapter.behavior.my_flag.no_warn %} {# this will not fire the behavior change event #}
|
121
|
+
...
|
122
|
+
{% endif %}
|
123
|
+
```
|
124
|
+
|
125
|
+
Args:
|
126
|
+
flags: a list of configurations, one for each behavior flag
|
127
|
+
user_overrides: a set of user settings, which may include overrides on one or more of the behavior flags
|
128
|
+
"""
|
129
|
+
|
130
|
+
_flags: List[BehaviorFlagRendered]
|
131
|
+
|
132
|
+
def __init__(self, flags: List[BehaviorFlag], user_overrides: Dict[str, Any]) -> None:
|
133
|
+
self._flags = [BehaviorFlagRendered(flag, user_overrides) for flag in flags]
|
134
|
+
|
135
|
+
def __getattr__(self, name: str) -> BehaviorFlagRendered:
|
136
|
+
for flag in self._flags:
|
137
|
+
if flag.name == name:
|
138
|
+
return flag
|
139
|
+
raise CompilationError(f"The flag {name} has not be registered.")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from codecs import BOM_UTF8
|
2
2
|
|
3
|
-
import agate
|
3
|
+
import agate
|
4
4
|
import datetime
|
5
5
|
import isodate
|
6
6
|
import json
|
@@ -149,7 +149,7 @@ def as_matrix(table):
|
|
149
149
|
return [r.values() for r in table.rows.values()]
|
150
150
|
|
151
151
|
|
152
|
-
def from_csv(abspath, text_columns, delimiter=","):
|
152
|
+
def from_csv(abspath, text_columns, delimiter=",") -> agate.Table:
|
153
153
|
type_tester = build_type_tester(text_columns=text_columns)
|
154
154
|
with open(abspath, encoding="utf-8") as fp:
|
155
155
|
if fp.read(1) != BOM:
|
dbt_common/clients/jinja.py
CHANGED
@@ -6,15 +6,29 @@ from ast import literal_eval
|
|
6
6
|
from collections import ChainMap
|
7
7
|
from contextlib import contextmanager
|
8
8
|
from itertools import chain, islice
|
9
|
-
from
|
9
|
+
from types import CodeType
|
10
|
+
from typing import (
|
11
|
+
Any,
|
12
|
+
Callable,
|
13
|
+
Dict,
|
14
|
+
Iterator,
|
15
|
+
List,
|
16
|
+
Mapping,
|
17
|
+
Optional,
|
18
|
+
Union,
|
19
|
+
Set,
|
20
|
+
Type,
|
21
|
+
NoReturn,
|
22
|
+
)
|
23
|
+
|
10
24
|
from typing_extensions import Protocol
|
11
25
|
|
12
|
-
import jinja2
|
13
|
-
import jinja2.ext
|
14
|
-
import jinja2.nativetypes
|
15
|
-
import jinja2.nodes
|
16
|
-
import jinja2.parser
|
17
|
-
import jinja2.sandbox
|
26
|
+
import jinja2
|
27
|
+
import jinja2.ext
|
28
|
+
import jinja2.nativetypes
|
29
|
+
import jinja2.nodes
|
30
|
+
import jinja2.parser
|
31
|
+
import jinja2.sandbox
|
18
32
|
|
19
33
|
from dbt_common.tests import test_caching_enabled
|
20
34
|
from dbt_common.utils.jinja import (
|
@@ -39,10 +53,17 @@ from dbt_common.exceptions.macros import MacroReturn, UndefinedMacroError, Caugh
|
|
39
53
|
SUPPORTED_LANG_ARG = jinja2.nodes.Name("supported_languages", "param")
|
40
54
|
|
41
55
|
# Global which can be set by dependents of dbt-common (e.g. core via flag parsing)
|
42
|
-
MACRO_DEBUGGING = False
|
56
|
+
MACRO_DEBUGGING: Union[str, bool] = False
|
57
|
+
|
58
|
+
_ParseReturn = Union[jinja2.nodes.Node, List[jinja2.nodes.Node]]
|
59
|
+
|
60
|
+
|
61
|
+
# Temporary type capturing the concept the functions in this file expect for a "node"
|
62
|
+
class _NodeProtocol(Protocol):
|
63
|
+
pass
|
43
64
|
|
44
65
|
|
45
|
-
def _linecache_inject(source, write):
|
66
|
+
def _linecache_inject(source: str, write: bool) -> str:
|
46
67
|
if write:
|
47
68
|
# this is the only reliable way to accomplish this. Obviously, it's
|
48
69
|
# really darn noisy and will fill your temporary directory
|
@@ -58,18 +79,18 @@ def _linecache_inject(source, write):
|
|
58
79
|
else:
|
59
80
|
# `codecs.encode` actually takes a `bytes` as the first argument if
|
60
81
|
# the second argument is 'hex' - mypy does not know this.
|
61
|
-
rnd = codecs.encode(os.urandom(12), "hex")
|
82
|
+
rnd = codecs.encode(os.urandom(12), "hex")
|
62
83
|
filename = rnd.decode("ascii")
|
63
84
|
|
64
85
|
# put ourselves in the cache
|
65
86
|
cache_entry = (len(source), None, [line + "\n" for line in source.splitlines()], filename)
|
66
87
|
# linecache does in fact have an attribute `cache`, thanks
|
67
|
-
linecache.cache[filename] = cache_entry
|
88
|
+
linecache.cache[filename] = cache_entry
|
68
89
|
return filename
|
69
90
|
|
70
91
|
|
71
92
|
class MacroFuzzParser(jinja2.parser.Parser):
|
72
|
-
def parse_macro(self):
|
93
|
+
def parse_macro(self) -> jinja2.nodes.Macro:
|
73
94
|
node = jinja2.nodes.Macro(lineno=next(self.stream).lineno)
|
74
95
|
|
75
96
|
# modified to fuzz macros defined in the same file. this way
|
@@ -83,16 +104,13 @@ class MacroFuzzParser(jinja2.parser.Parser):
|
|
83
104
|
|
84
105
|
|
85
106
|
class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment):
|
86
|
-
def _parse(
|
107
|
+
def _parse(
|
108
|
+
self, source: str, name: Optional[str], filename: Optional[str]
|
109
|
+
) -> jinja2.nodes.Template:
|
87
110
|
return MacroFuzzParser(self, source, name, filename).parse()
|
88
111
|
|
89
|
-
def _compile(self, source, filename):
|
112
|
+
def _compile(self, source: str, filename: str) -> CodeType:
|
90
113
|
"""
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
114
|
Override jinja's compilation. Use to stash the rendered source inside
|
97
115
|
the python linecache for debugging when the appropriate environment
|
98
116
|
variable is set.
|
@@ -108,7 +126,7 @@ class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment):
|
|
108
126
|
|
109
127
|
|
110
128
|
class MacroFuzzTemplate(jinja2.nativetypes.NativeTemplate):
|
111
|
-
environment_class = MacroFuzzEnvironment
|
129
|
+
environment_class = MacroFuzzEnvironment # type: ignore
|
112
130
|
|
113
131
|
def new_context(
|
114
132
|
self,
|
@@ -124,6 +142,7 @@ class MacroFuzzTemplate(jinja2.nativetypes.NativeTemplate):
|
|
124
142
|
"shared or locals parameters."
|
125
143
|
)
|
126
144
|
|
145
|
+
vars = {} if vars is None else vars
|
127
146
|
parent = ChainMap(vars, self.globals) if self.globals else vars
|
128
147
|
|
129
148
|
return self.environment.context_class(self.environment, parent, self.name, self.blocks)
|
@@ -170,11 +189,11 @@ class NumberMarker(NativeMarker):
|
|
170
189
|
pass
|
171
190
|
|
172
191
|
|
173
|
-
def _is_number(value) -> bool:
|
192
|
+
def _is_number(value: Any) -> bool:
|
174
193
|
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
175
194
|
|
176
195
|
|
177
|
-
def quoted_native_concat(nodes):
|
196
|
+
def quoted_native_concat(nodes: Iterator[str]) -> Any:
|
178
197
|
"""Handle special case for native_concat from the NativeTemplate.
|
179
198
|
|
180
199
|
This is almost native_concat from the NativeTemplate, except in the
|
@@ -212,7 +231,7 @@ def quoted_native_concat(nodes):
|
|
212
231
|
class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore
|
213
232
|
environment_class = NativeSandboxEnvironment # type: ignore
|
214
233
|
|
215
|
-
def render(self, *args, **kwargs):
|
234
|
+
def render(self, *args: Any, **kwargs: Any) -> Any:
|
216
235
|
"""Render the template to produce a native Python type.
|
217
236
|
|
218
237
|
If the result is a single node, its value is returned. Otherwise,
|
@@ -228,6 +247,11 @@ class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore
|
|
228
247
|
return self.environment.handle_exception()
|
229
248
|
|
230
249
|
|
250
|
+
class MacroProtocol(Protocol):
|
251
|
+
name: str
|
252
|
+
macro_sql: str
|
253
|
+
|
254
|
+
|
231
255
|
NativeSandboxEnvironment.template_class = NativeSandboxTemplate # type: ignore
|
232
256
|
|
233
257
|
|
@@ -235,7 +259,7 @@ class TemplateCache:
|
|
235
259
|
def __init__(self) -> None:
|
236
260
|
self.file_cache: Dict[str, jinja2.Template] = {}
|
237
261
|
|
238
|
-
def get_node_template(self, node) -> jinja2.Template:
|
262
|
+
def get_node_template(self, node: MacroProtocol) -> jinja2.Template:
|
239
263
|
key = node.macro_sql
|
240
264
|
|
241
265
|
if key in self.file_cache:
|
@@ -250,7 +274,7 @@ class TemplateCache:
|
|
250
274
|
self.file_cache[key] = template
|
251
275
|
return template
|
252
276
|
|
253
|
-
def clear(self):
|
277
|
+
def clear(self) -> None:
|
254
278
|
self.file_cache.clear()
|
255
279
|
|
256
280
|
|
@@ -261,13 +285,13 @@ class BaseMacroGenerator:
|
|
261
285
|
def __init__(self, context: Optional[Dict[str, Any]] = None) -> None:
|
262
286
|
self.context: Optional[Dict[str, Any]] = context
|
263
287
|
|
264
|
-
def get_template(self):
|
288
|
+
def get_template(self) -> jinja2.Template:
|
265
289
|
raise NotImplementedError("get_template not implemented!")
|
266
290
|
|
267
291
|
def get_name(self) -> str:
|
268
292
|
raise NotImplementedError("get_name not implemented!")
|
269
293
|
|
270
|
-
def get_macro(self):
|
294
|
+
def get_macro(self) -> Callable:
|
271
295
|
name = self.get_name()
|
272
296
|
template = self.get_template()
|
273
297
|
# make the module. previously we set both vars and local, but that's
|
@@ -285,7 +309,7 @@ class BaseMacroGenerator:
|
|
285
309
|
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
|
286
310
|
raise CaughtMacroError(e)
|
287
311
|
|
288
|
-
def call_macro(self, *args, **kwargs):
|
312
|
+
def call_macro(self, *args: Any, **kwargs: Any) -> Any:
|
289
313
|
# called from __call__ methods
|
290
314
|
if self.context is None:
|
291
315
|
raise DbtInternalError("Context is still None in call_macro!")
|
@@ -300,11 +324,6 @@ class BaseMacroGenerator:
|
|
300
324
|
return e.value
|
301
325
|
|
302
326
|
|
303
|
-
class MacroProtocol(Protocol):
|
304
|
-
name: str
|
305
|
-
macro_sql: str
|
306
|
-
|
307
|
-
|
308
327
|
class CallableMacroGenerator(BaseMacroGenerator):
|
309
328
|
def __init__(
|
310
329
|
self,
|
@@ -314,7 +333,7 @@ class CallableMacroGenerator(BaseMacroGenerator):
|
|
314
333
|
super().__init__(context)
|
315
334
|
self.macro = macro
|
316
335
|
|
317
|
-
def get_template(self):
|
336
|
+
def get_template(self) -> jinja2.Template:
|
318
337
|
return template_cache.get_node_template(self.macro)
|
319
338
|
|
320
339
|
def get_name(self) -> str:
|
@@ -331,14 +350,14 @@ class CallableMacroGenerator(BaseMacroGenerator):
|
|
331
350
|
raise e
|
332
351
|
|
333
352
|
# this makes MacroGenerator objects callable like functions
|
334
|
-
def __call__(self, *args, **kwargs):
|
353
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
335
354
|
return self.call_macro(*args, **kwargs)
|
336
355
|
|
337
356
|
|
338
357
|
class MaterializationExtension(jinja2.ext.Extension):
|
339
358
|
tags = ["materialization"]
|
340
359
|
|
341
|
-
def parse(self, parser):
|
360
|
+
def parse(self, parser: jinja2.parser.Parser) -> _ParseReturn:
|
342
361
|
node = jinja2.nodes.Macro(lineno=next(parser.stream).lineno)
|
343
362
|
materialization_name = parser.parse_assign_target(name_only=True).name
|
344
363
|
|
@@ -381,7 +400,7 @@ class MaterializationExtension(jinja2.ext.Extension):
|
|
381
400
|
class DocumentationExtension(jinja2.ext.Extension):
|
382
401
|
tags = ["docs"]
|
383
402
|
|
384
|
-
def parse(self, parser):
|
403
|
+
def parse(self, parser: jinja2.parser.Parser) -> _ParseReturn:
|
385
404
|
node = jinja2.nodes.Macro(lineno=next(parser.stream).lineno)
|
386
405
|
docs_name = parser.parse_assign_target(name_only=True).name
|
387
406
|
|
@@ -395,7 +414,7 @@ class DocumentationExtension(jinja2.ext.Extension):
|
|
395
414
|
class TestExtension(jinja2.ext.Extension):
|
396
415
|
tags = ["test"]
|
397
416
|
|
398
|
-
def parse(self, parser):
|
417
|
+
def parse(self, parser: jinja2.parser.Parser) -> _ParseReturn:
|
399
418
|
node = jinja2.nodes.Macro(lineno=next(parser.stream).lineno)
|
400
419
|
test_name = parser.parse_assign_target(name_only=True).name
|
401
420
|
|
@@ -405,13 +424,19 @@ class TestExtension(jinja2.ext.Extension):
|
|
405
424
|
return node
|
406
425
|
|
407
426
|
|
408
|
-
def _is_dunder_name(name):
|
427
|
+
def _is_dunder_name(name: str) -> bool:
|
409
428
|
return name.startswith("__") and name.endswith("__")
|
410
429
|
|
411
430
|
|
412
|
-
def create_undefined(node=None):
|
431
|
+
def create_undefined(node: Optional[_NodeProtocol] = None) -> Type[jinja2.Undefined]:
|
413
432
|
class Undefined(jinja2.Undefined):
|
414
|
-
def __init__(
|
433
|
+
def __init__(
|
434
|
+
self,
|
435
|
+
hint: Optional[str] = None,
|
436
|
+
obj: Any = None,
|
437
|
+
name: Optional[str] = None,
|
438
|
+
exc: Any = None,
|
439
|
+
) -> None:
|
415
440
|
super().__init__(hint=hint, name=name)
|
416
441
|
self.node = node
|
417
442
|
self.name = name
|
@@ -421,12 +446,12 @@ def create_undefined(node=None):
|
|
421
446
|
self.unsafe_callable = False
|
422
447
|
self.alters_data = False
|
423
448
|
|
424
|
-
def __getitem__(self, name):
|
449
|
+
def __getitem__(self, name: Any) -> "Undefined":
|
425
450
|
# Propagate the undefined value if a caller accesses this as if it
|
426
451
|
# were a dictionary
|
427
452
|
return self
|
428
453
|
|
429
|
-
def __getattr__(self, name):
|
454
|
+
def __getattr__(self, name: str) -> "Undefined":
|
430
455
|
if name == "name" or _is_dunder_name(name):
|
431
456
|
raise AttributeError(
|
432
457
|
"'{}' object has no attribute '{}'".format(type(self).__name__, name)
|
@@ -436,11 +461,11 @@ def create_undefined(node=None):
|
|
436
461
|
|
437
462
|
return self.__class__(hint=self.hint, name=self.name)
|
438
463
|
|
439
|
-
def __call__(self, *args, **kwargs):
|
464
|
+
def __call__(self, *args: Any, **kwargs: Any) -> "Undefined":
|
440
465
|
return self
|
441
466
|
|
442
|
-
def __reduce__(self):
|
443
|
-
raise UndefinedCompilationError(name=self.name, node=node)
|
467
|
+
def __reduce__(self) -> NoReturn:
|
468
|
+
raise UndefinedCompilationError(name=self.name or "unknown", node=node)
|
444
469
|
|
445
470
|
return Undefined
|
446
471
|
|
@@ -462,7 +487,7 @@ TEXT_FILTERS: Dict[str, Callable[[Any], Any]] = {
|
|
462
487
|
|
463
488
|
|
464
489
|
def get_environment(
|
465
|
-
node=None,
|
490
|
+
node: Optional[_NodeProtocol] = None,
|
466
491
|
capture_macros: bool = False,
|
467
492
|
native: bool = False,
|
468
493
|
) -> jinja2.Environment:
|
@@ -471,7 +496,7 @@ def get_environment(
|
|
471
496
|
}
|
472
497
|
|
473
498
|
if capture_macros:
|
474
|
-
args["undefined"] = create_undefined(node)
|
499
|
+
args["undefined"] = create_undefined(node) # type: ignore
|
475
500
|
|
476
501
|
args["extensions"].append(MaterializationExtension)
|
477
502
|
args["extensions"].append(DocumentationExtension)
|
@@ -492,7 +517,7 @@ def get_environment(
|
|
492
517
|
|
493
518
|
|
494
519
|
@contextmanager
|
495
|
-
def catch_jinja(node=None) -> Iterator[None]:
|
520
|
+
def catch_jinja(node: Optional[_NodeProtocol] = None) -> Iterator[None]:
|
496
521
|
try:
|
497
522
|
yield
|
498
523
|
except jinja2.exceptions.TemplateSyntaxError as e:
|
@@ -505,16 +530,16 @@ def catch_jinja(node=None) -> Iterator[None]:
|
|
505
530
|
raise
|
506
531
|
|
507
532
|
|
508
|
-
_TESTING_PARSE_CACHE: Dict[str, jinja2.Template] = {}
|
533
|
+
_TESTING_PARSE_CACHE: Dict[str, jinja2.nodes.Template] = {}
|
509
534
|
|
510
535
|
|
511
|
-
def parse(string):
|
536
|
+
def parse(string: Any) -> jinja2.nodes.Template:
|
512
537
|
str_string = str(string)
|
513
538
|
if test_caching_enabled() and str_string in _TESTING_PARSE_CACHE:
|
514
539
|
return _TESTING_PARSE_CACHE[str_string]
|
515
540
|
|
516
541
|
with catch_jinja():
|
517
|
-
parsed = get_environment().parse(str(string))
|
542
|
+
parsed: jinja2.nodes.Template = get_environment().parse(str(string))
|
518
543
|
if test_caching_enabled():
|
519
544
|
_TESTING_PARSE_CACHE[str_string] = parsed
|
520
545
|
return parsed
|
@@ -523,10 +548,10 @@ def parse(string):
|
|
523
548
|
def get_template(
|
524
549
|
string: str,
|
525
550
|
ctx: Dict[str, Any],
|
526
|
-
node=None,
|
551
|
+
node: Optional[_NodeProtocol] = None,
|
527
552
|
capture_macros: bool = False,
|
528
553
|
native: bool = False,
|
529
|
-
):
|
554
|
+
) -> jinja2.Template:
|
530
555
|
with catch_jinja(node):
|
531
556
|
env = get_environment(node, capture_macros, native=native)
|
532
557
|
|
@@ -534,7 +559,9 @@ def get_template(
|
|
534
559
|
return env.from_string(template_source, globals=ctx)
|
535
560
|
|
536
561
|
|
537
|
-
def render_template(
|
562
|
+
def render_template(
|
563
|
+
template: jinja2.Template, ctx: Dict[str, Any], node: Optional[_NodeProtocol] = None
|
564
|
+
) -> str:
|
538
565
|
with catch_jinja(node):
|
539
566
|
return template.render(ctx)
|
540
567
|
|
@@ -544,6 +571,7 @@ _TESTING_BLOCKS_CACHE: Dict[int, List[Union[BlockData, BlockTag]]] = {}
|
|
544
571
|
|
545
572
|
def _get_blocks_hash(text: str, allowed_blocks: Optional[Set[str]], collect_raw_data: bool) -> int:
|
546
573
|
"""Provides a hash function over the arguments to extract_toplevel_blocks, in order to support caching."""
|
574
|
+
allowed_blocks = allowed_blocks or set()
|
547
575
|
allowed_tuple = tuple(sorted(allowed_blocks) or [])
|
548
576
|
return text.__hash__() + allowed_tuple.__hash__() + collect_raw_data.__hash__()
|
549
577
|
|
dbt_common/clients/system.py
CHANGED
@@ -52,6 +52,7 @@ class FindMatchingParams:
|
|
52
52
|
root_path: str
|
53
53
|
relative_paths_to_search: List[str]
|
54
54
|
file_pattern: str
|
55
|
+
|
55
56
|
# ignore_spec: Optional[PathSpec] = None
|
56
57
|
|
57
58
|
def __init__(
|
@@ -608,11 +609,36 @@ def rename(from_path: str, to_path: str, force: bool = False) -> None:
|
|
608
609
|
shutil.move(from_path, to_path)
|
609
610
|
|
610
611
|
|
612
|
+
def safe_extract(tarball: tarfile.TarFile, path: str = ".") -> None:
|
613
|
+
"""
|
614
|
+
Fix for CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
|
615
|
+
Solution copied from https://github.com/mindsdb/mindsdb/blob/main/mindsdb/utilities/fs.py
|
616
|
+
"""
|
617
|
+
|
618
|
+
def _is_within_directory(directory, target):
|
619
|
+
abs_directory = os.path.abspath(directory)
|
620
|
+
abs_target = os.path.abspath(target)
|
621
|
+
prefix = os.path.commonprefix([abs_directory, abs_target])
|
622
|
+
return prefix == abs_directory
|
623
|
+
|
624
|
+
# for py >= 3.12
|
625
|
+
if hasattr(tarball, "data_filter"):
|
626
|
+
tarball.extractall(path, filter="data")
|
627
|
+
else:
|
628
|
+
members = tarball.getmembers()
|
629
|
+
for member in members:
|
630
|
+
member_path = os.path.join(path, member.name)
|
631
|
+
if not _is_within_directory(path, member_path):
|
632
|
+
raise tarfile.OutsideDestinationError(member, path)
|
633
|
+
|
634
|
+
tarball.extractall(path, members=members)
|
635
|
+
|
636
|
+
|
611
637
|
def untar_package(tar_path: str, dest_dir: str, rename_to: Optional[str] = None) -> None:
|
612
638
|
tar_path = convert_path(tar_path)
|
613
639
|
tar_dir_name = None
|
614
640
|
with tarfile.open(tar_path, "r:gz") as tarball:
|
615
|
-
tarball
|
641
|
+
safe_extract(tarball, dest_dir)
|
616
642
|
tar_dir_name = os.path.commonprefix(tarball.getnames())
|
617
643
|
if rename_to:
|
618
644
|
downloaded_path = os.path.join(dest_dir, tar_dir_name)
|
dbt_common/context.py
CHANGED
@@ -6,7 +6,7 @@ from dbt_common.constants import PRIVATE_ENV_PREFIX, SECRET_ENV_PREFIX
|
|
6
6
|
from dbt_common.record import Recorder
|
7
7
|
|
8
8
|
|
9
|
-
class CaseInsensitiveMapping(Mapping):
|
9
|
+
class CaseInsensitiveMapping(Mapping[str, str]):
|
10
10
|
def __init__(self, env: Mapping[str, str]):
|
11
11
|
self._env = {k.casefold(): (k, v) for k, v in env.items()}
|
12
12
|
|
@@ -65,7 +65,7 @@ _INVOCATION_CONTEXT_VAR: ContextVar[InvocationContext] = ContextVar("DBT_INVOCAT
|
|
65
65
|
|
66
66
|
|
67
67
|
def reliably_get_invocation_var() -> ContextVar[InvocationContext]:
|
68
|
-
invocation_var: Optional[ContextVar] = next(
|
68
|
+
invocation_var: Optional[ContextVar[InvocationContext]] = next(
|
69
69
|
(cv for cv in copy_context() if cv.name == _INVOCATION_CONTEXT_VAR.name), None
|
70
70
|
)
|
71
71
|
|