dagster-dbt 0.28.0__py3-none-any.whl → 0.28.2__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.
@@ -1,5 +1,5 @@
1
- from collections.abc import Mapping
2
- from typing import Any, Callable, Optional
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any, Optional
3
3
 
4
4
  from dagster import (
5
5
  AssetsDefinition,
@@ -1,11 +1,14 @@
1
1
  import hashlib
2
2
  import os
3
+ import shutil
4
+ import tempfile
3
5
  import textwrap
4
6
  from collections import defaultdict
5
7
  from collections.abc import Iterable, Mapping, Sequence
6
8
  from pathlib import Path
7
9
  from typing import TYPE_CHECKING, AbstractSet, Annotated, Any, Final, Optional, Union # noqa: UP035
8
10
 
11
+ import yaml
9
12
  from dagster import (
10
13
  AssetCheckKey,
11
14
  AssetCheckSpec,
@@ -32,6 +35,7 @@ from dagster._core.definitions.metadata import TableMetadataSet
32
35
  from dagster._core.errors import DagsterInvalidPropertyError
33
36
  from dagster._core.types.dagster_type import Nothing
34
37
  from dagster._record import ImportFrom, record
38
+ from dagster_shared.record import replace
35
39
 
36
40
  from dagster_dbt.dbt_project import DbtProject
37
41
  from dagster_dbt.metadata_set import DbtMetadataSet
@@ -55,6 +59,10 @@ DBT_DEFAULT_SELECTOR = ""
55
59
  DBT_INDIRECT_SELECTION_ENV: Final[str] = "DBT_INDIRECT_SELECTION"
56
60
  DBT_EMPTY_INDIRECT_SELECTION: Final[str] = "empty"
57
61
 
62
+ # Threshold for switching to selector file to avoid CLI argument length limits
63
+ # https://github.com/dagster-io/dagster/issues/16997
64
+ _SELECTION_ARGS_THRESHOLD: Final[int] = 200
65
+
58
66
  DUPLICATE_ASSET_KEY_ERROR_MESSAGE = (
59
67
  "The following dbt resources are configured with identical Dagster asset keys."
60
68
  " Please ensure that each dbt resource generates a unique Dagster asset key."
@@ -443,6 +451,10 @@ def get_updated_cli_invocation_params_for_context(
443
451
  [assets_def]
444
452
  )
445
453
 
454
+ # Get project_dir from dbt_project if available
455
+ project_dir = Path(dbt_project.project_dir) if dbt_project else None
456
+ target_project = dbt_project
457
+
446
458
  selection_args, indirect_selection_override = get_subset_selection_for_context(
447
459
  context=context,
448
460
  manifest=manifest,
@@ -452,17 +464,49 @@ def get_updated_cli_invocation_params_for_context(
452
464
  dagster_dbt_translator=dagster_dbt_translator,
453
465
  current_dbt_indirect_selection_env=indirect_selection,
454
466
  )
467
+ if (
468
+ selection_args[0] == "--select"
469
+ and project_dir
470
+ and len(resources := selection_args[1].split(" ")) > _SELECTION_ARGS_THRESHOLD
471
+ ):
472
+ temp_project_dir = tempfile.mkdtemp()
473
+ shutil.copytree(project_dir, temp_project_dir, dirs_exist_ok=True)
474
+ selectors_path = Path(temp_project_dir) / "selectors.yml"
475
+
476
+ # Delete any existing selectors, we need to create our own
477
+ if selectors_path.exists():
478
+ selectors_path.unlink()
479
+
480
+ selector_name = f"dagster_run_{context.run_id}"
481
+ temp_selectors = {
482
+ "selectors": [
483
+ {
484
+ "name": selector_name,
485
+ "definition": {"union": list(resources)},
486
+ }
487
+ ]
488
+ }
489
+ selectors_path.write_text(yaml.safe_dump(temp_selectors))
490
+ logger.info(
491
+ f"DBT selection of {len(resources)} resources exceeds threshold of {_SELECTION_ARGS_THRESHOLD}. "
492
+ "This may exceed system argument length limits. "
493
+ f"Executing materialization against temporary copy of DBT project at {temp_project_dir} with ephemeral selector."
494
+ )
495
+ selection_args = ["--selector", selector_name]
496
+ target_project = replace(dbt_project, project_dir=temp_project_dir)
455
497
 
456
498
  indirect_selection = (
457
499
  indirect_selection_override if indirect_selection_override else indirect_selection
458
500
  )
501
+ else:
502
+ target_project = dbt_project
459
503
 
460
504
  return DbtCliInvocationPartialParams(
461
505
  manifest=manifest,
462
506
  dagster_dbt_translator=dagster_dbt_translator,
463
507
  selection_args=selection_args,
464
508
  indirect_selection=indirect_selection,
465
- dbt_project=dbt_project,
509
+ dbt_project=target_project,
466
510
  )
467
511
 
468
512
 
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  import shlex
3
3
  from argparse import ArgumentParser, Namespace
4
- from collections.abc import Mapping, Sequence
4
+ from collections.abc import Callable, Mapping, Sequence
5
5
  from contextlib import suppress
6
- from typing import Any, Callable, Optional, Union, cast
6
+ from typing import Any, Optional, Union, cast
7
7
 
8
8
  import dagster._check as check
9
9
  from dagster import (
@@ -1,5 +1,5 @@
1
- from collections.abc import Iterator, Mapping, Sequence
2
- from typing import Any, Callable, Optional, Union
1
+ from collections.abc import Callable, Iterator, Mapping, Sequence
2
+ from typing import Any, Optional, Union
3
3
 
4
4
  import dateutil
5
5
  from dagster import (
@@ -1,4 +1,5 @@
1
- from typing import Any, Callable, Optional
1
+ from collections.abc import Callable
2
+ from typing import Any, Optional
2
3
 
3
4
  from dagster import AssetsDefinition, multi_asset
4
5
  from dagster._annotations import public
dagster_dbt/compat.py CHANGED
@@ -1,9 +1,8 @@
1
1
  import logging
2
2
  from enum import Enum
3
- from typing import TYPE_CHECKING, Any
3
+ from typing import TYPE_CHECKING, Any, TypeAlias
4
4
 
5
5
  from packaging import version
6
- from typing_extensions import TypeAlias
7
6
 
8
7
  # it's unclear exactly which dbt import adds a handler to the root logger, but something certainly does!
9
8
  # on this line, we keep track of the set of handlers that are on the root logger BEFORE any dbt imports
@@ -1,13 +1,12 @@
1
1
  import itertools
2
2
  import json
3
- import shutil
4
3
  from collections.abc import Iterator, Mapping
5
4
  from contextlib import contextmanager
6
5
  from contextvars import ContextVar
7
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass, field, replace
8
7
  from functools import cached_property
9
8
  from pathlib import Path
10
- from typing import Annotated, Any, Literal, Optional, Union
9
+ from typing import Annotated, Any, Literal, Optional, TypeAlias, Union
11
10
 
12
11
  import dagster as dg
13
12
  from dagster._annotations import public
@@ -26,10 +25,17 @@ from dagster.components.utils.translation import (
26
25
  )
27
26
  from dagster_shared import check
28
27
  from dagster_shared.serdes.objects.models.defs_state_info import DefsStateManagementType
29
- from typing_extensions import TypeAlias
30
28
 
31
- from dagster_dbt.asset_decorator import dbt_assets
32
- from dagster_dbt.asset_utils import DBT_DEFAULT_EXCLUDE, DBT_DEFAULT_SELECT, get_node
29
+ from dagster_dbt.asset_utils import (
30
+ DAGSTER_DBT_EXCLUDE_METADATA_KEY,
31
+ DAGSTER_DBT_SELECT_METADATA_KEY,
32
+ DAGSTER_DBT_SELECTOR_METADATA_KEY,
33
+ DBT_DEFAULT_EXCLUDE,
34
+ DBT_DEFAULT_SELECT,
35
+ DBT_DEFAULT_SELECTOR,
36
+ build_dbt_specs,
37
+ get_node,
38
+ )
33
39
  from dagster_dbt.components.dbt_project.scaffolder import DbtProjectComponentScaffolder
34
40
  from dagster_dbt.core.resource import DbtCliResource
35
41
  from dagster_dbt.dagster_dbt_translator import (
@@ -40,6 +46,12 @@ from dagster_dbt.dagster_dbt_translator import (
40
46
  from dagster_dbt.dbt_manifest import validate_manifest
41
47
  from dagster_dbt.dbt_manifest_asset_selection import DbtManifestAssetSelection
42
48
  from dagster_dbt.dbt_project import DbtProject
49
+ from dagster_dbt.dbt_project_manager import (
50
+ DbtProjectArgsManager,
51
+ DbtProjectManager,
52
+ NoopDbtProjectManager,
53
+ RemoteGitDbtProjectManager,
54
+ )
43
55
  from dagster_dbt.utils import ASSET_RESOURCE_TYPES
44
56
 
45
57
 
@@ -63,29 +75,18 @@ class DbtProjectArgs(dg.Resolvable):
63
75
  state_path: Optional[str] = None
64
76
 
65
77
 
66
- def resolve_dbt_project(context: ResolutionContext, model) -> DbtProject:
67
- if isinstance(model, str):
68
- return DbtProject(
69
- context.resolve_source_relative_path(
70
- context.resolve_value(model, as_type=str),
71
- )
72
- )
73
-
74
- args = DbtProjectArgs.resolve_from_model(context, model)
75
-
76
- kwargs = {} # use optionally splatted kwargs to avoid redefining default value
77
- if args.target_path:
78
- kwargs["target_path"] = args.target_path
78
+ def resolve_dbt_project(context: ResolutionContext, model) -> DbtProjectManager:
79
+ if isinstance(model, RemoteGitDbtProjectManager.model()):
80
+ return RemoteGitDbtProjectManager.resolve_from_model(context, model)
79
81
 
80
- return DbtProject(
81
- project_dir=context.resolve_source_relative_path(args.project_dir),
82
- target=args.target,
83
- profiles_dir=args.profiles_dir,
84
- state_path=args.state_path,
85
- packaged_project_dir=args.packaged_project_dir,
86
- profile=args.profile,
87
- **kwargs,
82
+ args = (
83
+ DbtProjectArgs(project_dir=context.resolve_value(model, as_type=str))
84
+ if isinstance(model, str)
85
+ else DbtProjectArgs.resolve_from_model(context, model)
88
86
  )
87
+ # resolve the project_dir relative to where this component is defined
88
+ args = replace(args, project_dir=context.resolve_source_relative_path(args.project_dir))
89
+ return DbtProjectArgsManager(args)
89
90
 
90
91
 
91
92
  DbtMetadataAddons: TypeAlias = Literal["column_metadata", "row_count"]
@@ -128,10 +129,10 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
128
129
  """
129
130
 
130
131
  project: Annotated[
131
- DbtProject,
132
+ Union[DbtProject, DbtProjectManager],
132
133
  Resolver(
133
134
  resolve_dbt_project,
134
- model_field_type=Union[str, DbtProjectArgs.model()],
135
+ model_field_type=Union[str, DbtProjectArgs.model(), RemoteGitDbtProjectManager.model()],
135
136
  description="The path to the dbt project or a mapping defining a DbtProject",
136
137
  examples=[
137
138
  "{{ project_root }}/path/to/dbt_project",
@@ -199,6 +200,13 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
199
200
  examples=["tag:skip_dagster"],
200
201
  ),
201
202
  ] = DBT_DEFAULT_EXCLUDE
203
+ selector: Annotated[
204
+ str,
205
+ Resolver.default(
206
+ description="The dbt selector for models in the project you want to include.",
207
+ examples=["custom_selector"],
208
+ ),
209
+ ] = DBT_DEFAULT_SELECTOR
202
210
  translation: Annotated[
203
211
  Optional[TranslationFn[Mapping[str, Any]]],
204
212
  TranslationFnResolver(template_vars_for_translation_fn=lambda data: {"node": data}),
@@ -224,11 +232,38 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
224
232
  @property
225
233
  def defs_state_config(self) -> DefsStateConfig:
226
234
  return DefsStateConfig(
227
- key=f"{self.__class__.__name__}[{self.project.name}]",
235
+ key=f"DbtProjectComponent[{self._project_manager.defs_state_discriminator}]",
228
236
  management_type=DefsStateManagementType.LOCAL_FILESYSTEM,
229
237
  refresh_if_dev=self.prepare_if_dev,
230
238
  )
231
239
 
240
+ @property
241
+ def op_config_schema(self) -> Optional[type[dg.Config]]:
242
+ return None
243
+
244
+ @property
245
+ def config_cls(self) -> Optional[type[dg.Config]]:
246
+ """Internal property that returns the config schema for the op.
247
+
248
+ Delegates to op_config_schema for backwards compatibility and consistency
249
+ with other component types.
250
+ """
251
+ return self.op_config_schema
252
+
253
+ def _get_op_spec(self, project: DbtProject) -> OpSpec:
254
+ default = self.op or OpSpec(name=project.name)
255
+ # always inject required tags
256
+ return default.model_copy(
257
+ update=dict(
258
+ tags={
259
+ **(default.tags or {}),
260
+ **({DAGSTER_DBT_SELECT_METADATA_KEY: self.select} if self.select else {}),
261
+ **({DAGSTER_DBT_EXCLUDE_METADATA_KEY: self.exclude} if self.exclude else {}),
262
+ **({DAGSTER_DBT_SELECTOR_METADATA_KEY: self.selector} if self.selector else {}),
263
+ }
264
+ )
265
+ )
266
+
232
267
  @cached_property
233
268
  def translator(self) -> "DagsterDbtTranslator":
234
269
  return DbtProjectComponentTranslator(self, self.translation_settings)
@@ -309,46 +344,60 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
309
344
  return self._base_translator.get_asset_check_spec(asset_spec, manifest, unique_id, project)
310
345
 
311
346
  @cached_property
312
- def cli_resource(self):
313
- return DbtCliResource(self.project)
347
+ def _project_manager(self) -> DbtProjectManager:
348
+ if isinstance(self.project, DbtProject):
349
+ return NoopDbtProjectManager(self.project)
350
+ else:
351
+ return self.project
352
+
353
+ @cached_property
354
+ def dbt_project(self) -> DbtProject:
355
+ return self._project_manager.get_project(None)
314
356
 
315
357
  def get_asset_selection(
316
358
  self, select: str, exclude: str = DBT_DEFAULT_EXCLUDE
317
359
  ) -> DbtManifestAssetSelection:
318
360
  return DbtManifestAssetSelection.build(
319
- manifest=self.project.manifest_path,
361
+ manifest=self.dbt_project.manifest_path,
320
362
  dagster_dbt_translator=self.translator,
321
363
  select=select,
322
364
  exclude=exclude,
323
365
  )
324
366
 
325
367
  def write_state_to_path(self, state_path: Path) -> None:
326
- # compile the manifest
327
- self.project.preparer.prepare(self.project)
328
- # move the manifest to the correct path
329
- shutil.copyfile(self.project.manifest_path, state_path)
368
+ self._project_manager.prepare(state_path)
330
369
 
331
370
  def build_defs_from_state(
332
371
  self, context: dg.ComponentLoadContext, state_path: Optional[Path]
333
372
  ) -> dg.Definitions:
334
- if state_path is not None:
335
- shutil.copyfile(state_path, self.project.manifest_path)
373
+ project = self._project_manager.get_project(state_path)
336
374
 
337
375
  res_ctx = context.resolution_context
338
376
 
339
- @dbt_assets(
340
- manifest=self.project.manifest_path,
341
- project=self.project,
342
- name=self.op.name if self.op else self.project.name,
343
- op_tags=self.op.tags if self.op else None,
344
- dagster_dbt_translator=self.translator,
377
+ asset_specs, check_specs = build_dbt_specs(
378
+ translator=validate_translator(self.translator),
379
+ manifest=validate_manifest(project.manifest_path),
345
380
  select=self.select,
346
381
  exclude=self.exclude,
347
- backfill_policy=self.op.backfill_policy if self.op else None,
382
+ selector=self.selector,
383
+ project=project,
384
+ io_manager_key=None,
385
+ )
386
+ op_spec = self._get_op_spec(project)
387
+
388
+ @dg.multi_asset(
389
+ specs=asset_specs,
390
+ check_specs=check_specs,
391
+ can_subset=True,
392
+ name=op_spec.name,
393
+ op_tags=op_spec.tags,
394
+ backfill_policy=op_spec.backfill_policy,
395
+ pool=op_spec.pool,
396
+ config_schema=self.config_cls.to_fields_dict() if self.config_cls else None,
348
397
  )
349
398
  def _fn(context: dg.AssetExecutionContext):
350
399
  with _set_resolution_context(res_ctx):
351
- yield from self.execute(context=context, dbt=self.cli_resource)
400
+ yield from self.execute(context=context, dbt=DbtCliResource(project))
352
401
 
353
402
  return dg.Definitions(assets=[_fn])
354
403
 
@@ -437,7 +486,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
437
486
 
438
487
  @cached_property
439
488
  def _validated_manifest(self):
440
- return validate_manifest(self.project.manifest_path)
489
+ return validate_manifest(self.dbt_project.manifest_path)
441
490
 
442
491
  @cached_property
443
492
  def _validated_translator(self):
@@ -460,7 +509,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
460
509
  return dagster_dbt_translator.get_asset_spec(
461
510
  manifest,
462
511
  next(iter(matching_model_ids)),
463
- self.project,
512
+ self.dbt_project,
464
513
  ).key
465
514
 
466
515
 
@@ -492,4 +541,4 @@ def get_projects_from_dbt_component(components: Path) -> list[DbtProject]:
492
541
  of_type=DbtProjectComponent
493
542
  )
494
543
 
495
- return [component.project for component in project_components]
544
+ return [component.dbt_project for component in project_components]
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from pathlib import Path
3
- from typing import Optional
3
+ from typing import Any, Optional
4
4
 
5
5
  import dagster._check as check
6
6
  from dagster._core.errors import DagsterInvalidInvocationError
@@ -13,6 +13,7 @@ from pydantic import BaseModel, Field
13
13
  class DbtScaffoldParams(BaseModel):
14
14
  init: bool = Field(default=False)
15
15
  project_path: Optional[str] = None
16
+ git_url: Optional[str] = None
16
17
 
17
18
 
18
19
  class DbtProjectComponentScaffolder(Scaffolder[DbtScaffoldParams]):
@@ -20,31 +21,45 @@ class DbtProjectComponentScaffolder(Scaffolder[DbtScaffoldParams]):
20
21
  def get_scaffold_params(cls) -> type[DbtScaffoldParams]:
21
22
  return DbtScaffoldParams
22
23
 
23
- def scaffold(self, request: ScaffoldRequest[DbtScaffoldParams]) -> None:
24
- project_root = request.project_root or os.getcwd()
25
- if request.params.project_path:
26
- project_root_tmpl = "{{ project_root }}"
27
- rel_path = os.path.relpath(request.params.project_path, start=project_root)
28
- path_str = f"{project_root_tmpl}/{rel_path}"
24
+ def _init_dbt_project(self) -> None:
25
+ try:
26
+ from dbt.cli.main import dbtRunner
27
+ except ImportError:
28
+ raise DagsterInvalidInvocationError(
29
+ "dbt-core is not installed. Please install dbt to scaffold this component."
30
+ )
31
+
32
+ dbtRunner().invoke(["init"])
29
33
 
30
- elif request.params.init:
31
- try:
32
- from dbt.cli.main import dbtRunner
33
- except ImportError:
34
- raise DagsterInvalidInvocationError(
35
- "dbt-core is not installed. Please install dbt to scaffold this component."
36
- )
34
+ def _git_url_params(self, git_url: str, project_path: Optional[str]) -> dict[str, Any]:
35
+ repo_relative_path = {"repo_relative_path": project_path} if project_path else {}
36
+ return {"project": {"repo_url": git_url, **repo_relative_path}}
37
37
 
38
- dbtRunner().invoke(["init"])
38
+ def scaffold(self, request: ScaffoldRequest[DbtScaffoldParams]) -> None:
39
+ project_root = request.project_root or Path(os.getcwd())
40
+ check.param_invariant(
41
+ not (request.params.init and (request.params.git_url or request.params.project_path)),
42
+ "init and git_url/project_path cannot be set at the same time.",
43
+ )
44
+ if request.params.init:
45
+ self._init_dbt_project()
39
46
  subpaths = [
40
- path
41
- for path in Path(project_root).iterdir()
42
- if path.is_dir() and path.name != "logs"
47
+ path for path in project_root.iterdir() if path.is_dir() and path.name != "logs"
43
48
  ]
44
49
  check.invariant(len(subpaths) == 1, "Expected exactly one subpath to be created.")
45
50
  # this path should be relative to this directory
46
- path_str = subpaths[0].name
51
+ params = {"project": subpaths[0].name}
52
+ elif request.params.git_url is not None:
53
+ # project_path is the path to the dbt project within the git repository
54
+ project_path = request.params.project_path
55
+ repo_relative_path = {"repo_relative_path": project_path} if project_path else {}
56
+ params = {"project": {"repo_url": request.params.git_url, **repo_relative_path}}
57
+ elif request.params.project_path is not None:
58
+ # project_path represents a local directory
59
+ project_root_tmpl = "{{ project_root }}"
60
+ rel_path = os.path.relpath(request.params.project_path, start=project_root)
61
+ path_str = f"{project_root_tmpl}/{rel_path}"
62
+ params = {"project": path_str}
47
63
  else:
48
- path_str = None
49
-
50
- scaffold_component(request, {"project": path_str})
64
+ params = {"project": None}
65
+ scaffold_component(request, params)
@@ -8,7 +8,7 @@ import sys
8
8
  from collections.abc import Iterator, Mapping, Sequence
9
9
  from dataclasses import dataclass, field, replace
10
10
  from pathlib import Path
11
- from typing import Any, Final, NamedTuple, Optional, Union, cast
11
+ from typing import Any, Final, Literal, NamedTuple, Optional, Union, cast
12
12
 
13
13
  import orjson
14
14
  from dagster import (
@@ -24,7 +24,6 @@ from dagster import (
24
24
  from dagster._annotations import public
25
25
  from dagster._core.errors import DagsterExecutionInterruptedError
26
26
  from packaging import version
27
- from typing_extensions import Literal
28
27
 
29
28
  from dagster_dbt.compat import BaseAdapter, BaseColumn, BaseRelation
30
29
  from dagster_dbt.core.dbt_cli_event import (
@@ -1,6 +1,6 @@
1
- from collections.abc import Iterator
1
+ from collections.abc import Callable, Iterator
2
2
  from concurrent.futures import ThreadPoolExecutor
3
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
4
4
 
5
5
  from dagster import (
6
6
  AssetCheckResult,
@@ -428,7 +428,7 @@ class DbtCliResource(ConfigurableResource):
428
428
  adapter = get_adapter(config)
429
429
  manifest = ManifestLoader.load_macros(
430
430
  config,
431
- adapter.connections.set_query_header, # type: ignore
431
+ adapter.connections.set_query_header,
432
432
  base_macros_only=True,
433
433
  )
434
434
  adapter.set_macro_resolver(manifest)
@@ -627,8 +627,12 @@ class DbtCliResource(ConfigurableResource):
627
627
  dagster_dbt_translator = updated_params.dagster_dbt_translator
628
628
  selection_args = updated_params.selection_args
629
629
  indirect_selection = updated_params.indirect_selection
630
-
631
630
  target_path = target_path or self._get_unique_target_path(context=context)
631
+ project_dir = Path(
632
+ updated_params.dbt_project.project_dir
633
+ if updated_params.dbt_project
634
+ else self.project_dir
635
+ )
632
636
  env = {
633
637
  # Allow IO streaming when running in Windows.
634
638
  # Also, allow it to be overriden by the current environment.
@@ -659,7 +663,7 @@ class DbtCliResource(ConfigurableResource):
659
663
  **({"DBT_PROFILES_DIR": self.profiles_dir} if self.profiles_dir else {}),
660
664
  # The DBT_PROJECT_DIR environment variable is set to the path containing the dbt project
661
665
  # See https://docs.getdbt.com/reference/dbt_project.yml for more information.
662
- **({"DBT_PROJECT_DIR": self.project_dir} if self.project_dir else {}),
666
+ "DBT_PROJECT_DIR": str(project_dir),
663
667
  }
664
668
 
665
669
  # set dbt indirect selection if needed to execute specific dbt tests due to asset check
@@ -683,14 +687,13 @@ class DbtCliResource(ConfigurableResource):
683
687
  *profile_args,
684
688
  *selection_args,
685
689
  ]
686
- project_dir = Path(self.project_dir)
687
690
 
688
691
  if not target_path.is_absolute():
689
692
  target_path = project_dir.joinpath(target_path)
690
693
 
691
694
  # run dbt --version to get the dbt core version
692
695
  adapter: Optional[BaseAdapter] = None
693
- with pushd(self.project_dir):
696
+ with pushd(str(project_dir)):
694
697
  # we do not need to initialize the adapter if we are using the fusion engine
695
698
  if self._cli_version.major < 2:
696
699
  try:
@@ -0,0 +1,172 @@
1
+ import os
2
+ import shutil
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Annotated, Optional
7
+ from urllib.parse import quote, urlparse, urlunparse
8
+
9
+ from dagster._core.errors import DagsterInvalidDefinitionError
10
+ from dagster.components.resolved.base import Resolvable
11
+ from dagster.components.resolved.model import Resolver
12
+
13
+ from dagster_dbt.dbt_project import DbtProject
14
+
15
+ if TYPE_CHECKING:
16
+ from dagster_dbt.components.dbt_project.component import DbtProjectArgs
17
+
18
+
19
+ class DbtProjectManager(ABC):
20
+ """Helper class that wraps a dbt project that may or may not be available on local disk at the time
21
+ it is instantiated. Provides methods for syncing the project to a local path and for instantiating the
22
+ final DbtProject object when ready.
23
+ """
24
+
25
+ @property
26
+ @abstractmethod
27
+ def defs_state_discriminator(self) -> str: ...
28
+
29
+ @abstractmethod
30
+ def sync(self, state_path: Path) -> None: ...
31
+
32
+ def _local_project_dir(self, state_path: Path) -> Path:
33
+ return state_path.parent / "project"
34
+
35
+ def prepare(self, state_path: Path) -> None:
36
+ """Syncs the project to the given local path and ensures the manifest is generated
37
+ and the dependencies are installed.
38
+ """
39
+ # ensure local dir is empty
40
+ local_dir = self._local_project_dir(state_path)
41
+ shutil.rmtree(local_dir, ignore_errors=True)
42
+ local_dir.mkdir()
43
+
44
+ # ensure project exists in the dir and is compiled
45
+ self.sync(state_path)
46
+
47
+ # indicate that project has been prepared
48
+ state_path.touch()
49
+
50
+ @abstractmethod
51
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject": ...
52
+
53
+
54
+ @dataclass
55
+ class NoopDbtProjectManager(DbtProjectManager):
56
+ """Wraps a DbtProject that has already been fully instantiated. Used for cases where a
57
+ user directly provides a DbtProject to the DbtProjectComponent.
58
+ """
59
+
60
+ project: "DbtProject"
61
+
62
+ @property
63
+ def defs_state_discriminator(self) -> str:
64
+ return self.project.name
65
+
66
+ def sync(self, state_path: Path) -> None:
67
+ pass
68
+
69
+ def get_project(self, state_path: Optional[Path]) -> DbtProject:
70
+ return self.project
71
+
72
+
73
+ @dataclass
74
+ class DbtProjectArgsManager(DbtProjectManager):
75
+ """Wraps DbtProjectArgs provided to the DbtProjectComponent. Avoids instantiating the DbtProject object
76
+ immediately as this would cause errors in cases where the project_dir has not yet been synced.
77
+ """
78
+
79
+ args: "DbtProjectArgs"
80
+
81
+ @property
82
+ def defs_state_discriminator(self) -> str:
83
+ return Path(self.args.project_dir).stem
84
+
85
+ def sync(self, state_path: Path) -> None:
86
+ # we prepare the project in the original project directory rather than the new one
87
+ # so that code that does not have access to the local state path can still access
88
+ # the manifest.json file.
89
+ project = self.get_project(None)
90
+ project.preparer.prepare(project)
91
+ shutil.copytree(
92
+ self.args.project_dir, self._local_project_dir(state_path), dirs_exist_ok=True
93
+ )
94
+
95
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject":
96
+ kwargs = asdict(self.args)
97
+
98
+ project_dir = self._local_project_dir(state_path) if state_path else self.args.project_dir
99
+ return DbtProject(
100
+ project_dir=project_dir,
101
+ # allow default values on DbtProject to take precedence
102
+ **{k: v for k, v in kwargs.items() if v is not None and k != "project_dir"},
103
+ )
104
+
105
+
106
+ @dataclass
107
+ class RemoteGitDbtProjectManager(DbtProjectManager, Resolvable):
108
+ """Wraps a remote git repository containing a dbt project."""
109
+
110
+ repo_url: str
111
+ repo_relative_path: Annotated[
112
+ str,
113
+ Resolver.default(
114
+ description="The relative path to the dbt project within the repository.",
115
+ examples=["dbt/my_dbt_project"],
116
+ ),
117
+ ] = "."
118
+ token: Annotated[
119
+ Optional[str],
120
+ Resolver.default(
121
+ description="Token for authenticating to the provided repository.",
122
+ examples=["'{{ env.GITHUB_TOKEN }}'"],
123
+ ),
124
+ ] = None
125
+ profile: Annotated[
126
+ Optional[str],
127
+ Resolver.default(description="The profile to use for the dbt project."),
128
+ ] = None
129
+ target: Annotated[
130
+ Optional[str],
131
+ Resolver.default(
132
+ description="The target to use for the dbt project.",
133
+ examples=["dev", "prod"],
134
+ ),
135
+ ] = None
136
+
137
+ @property
138
+ def defs_state_discriminator(self) -> str:
139
+ return self.repo_url
140
+
141
+ def _get_clone_url(self) -> str:
142
+ # in github action environments, the token will be available automatically as an env var
143
+ raw_token = self.token or os.getenv("GITHUB_TOKEN")
144
+ if raw_token is None:
145
+ return self.repo_url
146
+ else:
147
+ # insert the token into the url
148
+ token = quote(raw_token, safe="")
149
+ parts = urlparse(self.repo_url)
150
+ return urlunparse(parts._replace(netloc=f"{token}@{parts.netloc}"))
151
+
152
+ def sync(self, state_path: Path) -> None:
153
+ # defer git import to avoid side-effects on import
154
+ from git import Repo
155
+
156
+ Repo.clone_from(self._get_clone_url(), self._local_project_dir(state_path), depth=1)
157
+ project = self.get_project(state_path)
158
+ project.preparer.prepare(project)
159
+
160
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject":
161
+ if state_path is None:
162
+ raise DagsterInvalidDefinitionError(
163
+ "Attempted to `get_project()` on a `RemoteGitDbtProjectWrapper` without a path. "
164
+ "This can happen when calling unsupported methods on the `DbtProjectComponent`."
165
+ )
166
+
167
+ local_dir = self._local_project_dir(state_path)
168
+ return DbtProject(
169
+ project_dir=local_dir / self.repo_relative_path,
170
+ profile=self.profile,
171
+ target=self.target,
172
+ )
@@ -3,7 +3,7 @@ name = "{{ project_name }}"
3
3
  version = "0.1.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
- requires-python = ">=3.9,<3.14"
6
+ requires-python = ">=3.10,<3.14"
7
7
  dependencies = [
8
8
  "dagster",
9
9
  "dagster-cloud",
dagster_dbt/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.28.0"
1
+ __version__ = "0.28.2"
@@ -1,22 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dagster-dbt
3
- Version: 0.28.0
3
+ Version: 0.28.2
4
4
  Summary: A Dagster integration for dbt
5
5
  Home-page: https://github.com/dagster-io/dagster/tree/master/python_modules/libraries/dagster-dbt
6
6
  Author: Dagster Labs
7
7
  Author-email: hello@dagsterlabs.com
8
8
  License: Apache-2.0
9
- Classifier: Programming Language :: Python :: 3.9
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
13
12
  Classifier: Programming Language :: Python :: 3.13
14
13
  Classifier: License :: OSI Approved :: Apache Software License
15
14
  Classifier: Operating System :: OS Independent
16
- Requires-Python: >=3.9,<3.14
15
+ Requires-Python: >=3.10,<3.14
17
16
  License-File: LICENSE
18
- Requires-Dist: dagster==1.12.0
17
+ Requires-Dist: dagster==1.12.2
19
18
  Requires-Dist: dbt-core<1.11,>=1.7
19
+ Requires-Dist: gitpython
20
20
  Requires-Dist: Jinja2
21
21
  Requires-Dist: networkx
22
22
  Requires-Dist: orjson
@@ -1,31 +1,32 @@
1
1
  dagster_dbt/__init__.py,sha256=rlPCxCzovXNDiqXBUEcgNVHjZrXQXwcX3kwUhe_69C4,4708
2
- dagster_dbt/asset_decorator.py,sha256=8OqlypoTwKCs6dAxOuBAO32krbHBQRLVGe6uDzpTSXE,14892
2
+ dagster_dbt/asset_decorator.py,sha256=OFFjhmDQO6dK8N3U7VQF7gergswkIS3eHnMRwAe2BZY,14892
3
3
  dagster_dbt/asset_specs.py,sha256=2EdWIhY2QZhtGXM7N-kkkeK3ClnGgYX7ayRi_X11cLg,2741
4
- dagster_dbt/asset_utils.py,sha256=bmUm9fCnSUVzGjBLwx14vUNOecizrLjfqlI2y8qxO2Q,44761
5
- dagster_dbt/compat.py,sha256=iiAez9pLskfbwLdj6LxIrFvoU-efBpy2BETXQyAsQvY,3638
4
+ dagster_dbt/asset_utils.py,sha256=6M3i5_quRys2VCQEzPQwBz5MEl2zY2NJIVZ8r2i-3p4,46651
5
+ dagster_dbt/compat.py,sha256=lqzGonzQE7Lb825yRqUaQPXeNQp8umAR4LqyD6COXdc,3609
6
6
  dagster_dbt/dagster_dbt_translator.py,sha256=skUpl7NxG7ysDfDOs1pSq1c9-ciByN7LrGvO0ZsC4W4,30274
7
7
  dagster_dbt/dbt_core_version.py,sha256=w1P62qDdbApXKv0XvUNr2p7FlwiW68csugsNaqmjNjM,38
8
8
  dagster_dbt/dbt_manifest.py,sha256=q10Qq1whh-dLfWtFbTYXYbqBVCf0oU8T1yRPyy9ASw0,1307
9
9
  dagster_dbt/dbt_manifest_asset_selection.py,sha256=KSEHcVdtfFZaEqSQDUsAx8H8BQe62jZxAOE_CLtwVlI,5072
10
10
  dagster_dbt/dbt_project.py,sha256=HtUuNYbKMAPWqTOPgPeJK4u6b1nKyqHrRNB8sYwOWVw,12203
11
+ dagster_dbt/dbt_project_manager.py,sha256=FPHSmo4ep9jNDyRjB0bDR76dOrthgRN1u3CLMklpzS0,6005
11
12
  dagster_dbt/dbt_version.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  dagster_dbt/errors.py,sha256=a8xag0tjh8OQVkiL10_uwY4hkAgRCH6HtTGaZxIbXZI,1073
13
14
  dagster_dbt/freshness_builder.py,sha256=DHAC3AGWAwIA7psDf72R0t5p8NjxDytGU9g5xnca1hc,6395
14
15
  dagster_dbt/metadata_set.py,sha256=lqjASYoYeM_Ey6r8UsPUkRMwmuAIfFCFvkNm0xW5xTg,512
15
16
  dagster_dbt/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
16
17
  dagster_dbt/utils.py,sha256=gT6xO7buRolkhc2fa5ySUPfD1eXo3e6RJWZAKkM6yFo,8513
17
- dagster_dbt/version.py,sha256=MRQGtOXBhcDKeeNOL0LiB-cllo6kfd8_KGJOvaDp0XQ,23
18
+ dagster_dbt/version.py,sha256=K-TM2fq9AmH_Dk8Cadam72wILDZ_6qftLHvY9P1Fc3I,23
18
19
  dagster_dbt/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  dagster_dbt/cli/app.py,sha256=9oBx85uzAkMtBdV39vNm5rxuAGPVYzzDUs6Ek-KL0XY,13516
20
21
  dagster_dbt/cloud/__init__.py,sha256=8WKaLuPl_pUG9Cv78GW782vrWQfqK8QtAWegkTxA9r4,441
21
- dagster_dbt/cloud/asset_defs.py,sha256=brrMeblPY-UUhAZ16WKOZ1vP7s9z1WZdHdK0SmSyq8o,29311
22
+ dagster_dbt/cloud/asset_defs.py,sha256=4mdXd3tiGd9OPBBF6xvH81DZhcyGL9pAw3WH8dB7Qu8,29311
22
23
  dagster_dbt/cloud/cli.py,sha256=VnKzBjn-BPpjn4nPZm5xSrboAKpRhlCa-4IxsN1ROCo,4525
23
24
  dagster_dbt/cloud/ops.py,sha256=rsU4qPCRUUzeHRInZph7YEz_iynwu_KidO5vMAYHX5E,4615
24
25
  dagster_dbt/cloud/resources.py,sha256=AWW3VTBS6zrSKeDjMJovbnu54sKena93FoZle6ZSSq8,31747
25
26
  dagster_dbt/cloud/types.py,sha256=gsigGlKjR3gLePppiYX09S2vx-0PyPvH7AkzOphDSMA,2886
26
- dagster_dbt/cloud/utils.py,sha256=qmeEUKCw-KksKUIAxO7W1e5Em-C7hrCXU1PEKnGMk0o,7158
27
+ dagster_dbt/cloud/utils.py,sha256=NnPxHp3r2cpJ0Vi1Gt4eBmBlNbAhdwaR7GUZRkFsLRc,7158
27
28
  dagster_dbt/cloud_v2/__init__.py,sha256=9NjPjqm8ytsKiogiW9cUpHgQFaht1q6g_3K7KSKtzsU,471
28
- dagster_dbt/cloud_v2/asset_decorator.py,sha256=KusLZ4LdY02coN9KYbQr2g4EgEVqWcvjOm98BvhRruk,3114
29
+ dagster_dbt/cloud_v2/asset_decorator.py,sha256=wHL77K2NXvnU0PDEsqyqpiRX1yp1cuQiit1oxRpNY3s,3141
29
30
  dagster_dbt/cloud_v2/cli_invocation.py,sha256=EX2a0G7HIlOJBES-Lf3DIp0y1L1vGeiO3ms1a0F9tWc,2235
30
31
  dagster_dbt/cloud_v2/client.py,sha256=V7UsepwHteJiEhQ8XT-lzFV-xDlspzVDgMJDxd6FLEg,16674
31
32
  dagster_dbt/cloud_v2/resources.py,sha256=ENzllq2rJlD4Tb3_9JTsMVsoBjyKBs28543-L3lfxLU,16925
@@ -34,25 +35,25 @@ dagster_dbt/cloud_v2/sensor_builder.py,sha256=8mAm-1ZFvoiVVdxSkSQbMxg18aQUY5kazu
34
35
  dagster_dbt/cloud_v2/types.py,sha256=dI-NIguj582LwTTMXdY5r0U4-INDn9anNy-ciGuEc1s,4136
35
36
  dagster_dbt/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
37
  dagster_dbt/components/dbt_project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- dagster_dbt/components/dbt_project/component.py,sha256=DfAif7IKnzvJSY8RJDGUzwpugSUyzZNPs_pk6E7EmFY,18661
38
- dagster_dbt/components/dbt_project/scaffolder.py,sha256=AqwRnX3cFpvCSxEeEpyQj0Vei84WSWUi2BIMCtkxaDE,1893
38
+ dagster_dbt/components/dbt_project/component.py,sha256=oVj1RGp27LBNWxdDaGhvEyw18fOZzUKN8IYwmV2PxGo,20503
39
+ dagster_dbt/components/dbt_project/scaffolder.py,sha256=tbxvFfcvF5KBdHJmjCIxiujtHeo0iLMwnuVM6Ym_iQk,2914
39
40
  dagster_dbt/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  dagster_dbt/core/dbt_cli_event.py,sha256=Yqr-UuzFF7jIcI4KRAmiAhGTX9Ay0tIMN2NaJegsrJQ,25203
41
- dagster_dbt/core/dbt_cli_invocation.py,sha256=7ZrwAgivAtZGwrHLxfpmNan004IuEEdBSaKZeKp69Mg,17534
42
- dagster_dbt/core/dbt_event_iterator.py,sha256=s0gU6wIgOpmbo50309FHp9zTqlLkIWcgLkETmidxPRo,16953
43
- dagster_dbt/core/resource.py,sha256=6HhBBa8SRAJPhDtnHRNPTjdFPQvNYIdfAXQY3_LdqHk,30782
42
+ dagster_dbt/core/dbt_cli_invocation.py,sha256=lJLoRvy630xUAEdnH76yl5PJ98-OsLx06l813XjzZOM,17505
43
+ dagster_dbt/core/dbt_event_iterator.py,sha256=IL3QsVUDMfGk04SFTkrVEfbbYu745Kn8OyMQlrlHxIE,16953
44
+ dagster_dbt/core/resource.py,sha256=1qIEneW2aZYeArF5w_QV4-Xmf7Kp9kpGaQaxUlL3ZZs,30851
44
45
  dagster_dbt/core/utils.py,sha256=ciXjLhFTNVTyDtVWP4Kjee0LWZkIuVJOltvlv0COzDo,577
45
46
  dagster_dbt/include/__init__.py,sha256=8ujr-ROlJ5x64POs-bH-0zfjZ2QHx-FgKUZAvAFbSs8,89
46
- dagster_dbt/include/pyproject.toml.jinja,sha256=h5Od0HP4c7gQLRNIDyDJLzTMpp5sc4qtrzOVM-R75IA,660
47
+ dagster_dbt/include/pyproject.toml.jinja,sha256=_BbC3zmV6ajtGnDMesyut0Yl0Fc7bY-F887KTIKMm1o,661
47
48
  dagster_dbt/include/setup.py.jinja,sha256=N3NjYuXvWmaY8bj5GftVxVvImDUGC4YbFc_MdP1-quA,618
48
49
  dagster_dbt/include/scaffold/__init__.py.jinja,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  dagster_dbt/include/scaffold/assets.py.jinja,sha256=JImqnDUP5ewy8RVti4IuXL70yJnoke54dMuZ3G8Wlx8,562
50
51
  dagster_dbt/include/scaffold/definitions.py.jinja,sha256=Hou7emwkEeh5YXTdqjYFrAc2SK-Q6MgTNsQOKA_Vy3s,364
51
52
  dagster_dbt/include/scaffold/project.py.jinja,sha256=YNtkT5Hq4VbGw-b7QcxdelhXsesIKORwVuBFGFdfeUs,432
52
53
  dagster_dbt/include/scaffold/schedules.py.jinja,sha256=Xua_VtPjYFc498A5uaBGQ36GwV1gqciO4P3D8Yt9M-Y,413
53
- dagster_dbt-0.28.0.dist-info/licenses/LICENSE,sha256=4lsMW-RCvfVD4_F57wrmpe3vX1xwUk_OAKKmV_XT7Z0,11348
54
- dagster_dbt-0.28.0.dist-info/METADATA,sha256=XUBRuIhSbaeG0yRgt_cf2xsRvAPz3Ae7r-k2k4oVpCk,1596
55
- dagster_dbt-0.28.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- dagster_dbt-0.28.0.dist-info/entry_points.txt,sha256=pbv0tVoTB7cByG-noE8rC6atvthh64qBaTo7PkQ9HbM,163
57
- dagster_dbt-0.28.0.dist-info/top_level.txt,sha256=hoOwFvw9OpJUN1azE6UVHcxMKqhUwR_BTN0Ay-iKUDA,12
58
- dagster_dbt-0.28.0.dist-info/RECORD,,
54
+ dagster_dbt-0.28.2.dist-info/licenses/LICENSE,sha256=4lsMW-RCvfVD4_F57wrmpe3vX1xwUk_OAKKmV_XT7Z0,11348
55
+ dagster_dbt-0.28.2.dist-info/METADATA,sha256=k0MPai8vmyGPfpswuSgc3l5IDf6NItMeWsKcsy3lCpU,1572
56
+ dagster_dbt-0.28.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ dagster_dbt-0.28.2.dist-info/entry_points.txt,sha256=pbv0tVoTB7cByG-noE8rC6atvthh64qBaTo7PkQ9HbM,163
58
+ dagster_dbt-0.28.2.dist-info/top_level.txt,sha256=hoOwFvw9OpJUN1azE6UVHcxMKqhUwR_BTN0Ay-iKUDA,12
59
+ dagster_dbt-0.28.2.dist-info/RECORD,,