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 CHANGED
@@ -1 +1 @@
1
- version = "1.7.0"
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 # type: ignore
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:
@@ -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 typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Union, Set, Type
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 # type: ignore
13
- import jinja2.ext # type: ignore
14
- import jinja2.nativetypes # type: ignore
15
- import jinja2.nodes # type: ignore
16
- import jinja2.parser # type: ignore
17
- import jinja2.sandbox # type: ignore
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") # type: ignore
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 # type: ignore
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(self, source, name, filename):
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__(self, hint=None, obj=None, name=None, exc=None):
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(template, ctx: Dict[str, Any], node=None) -> str:
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
 
@@ -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.extractall(dest_dir)
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