dagster-dbt 0.28.0__tar.gz → 0.28.1__tar.gz

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.
Files changed (68) hide show
  1. {dagster_dbt-0.28.0/dagster_dbt.egg-info → dagster_dbt-0.28.1}/PKG-INFO +3 -2
  2. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_utils.py +45 -1
  3. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/dbt_project/component.py +39 -42
  4. dagster_dbt-0.28.1/dagster_dbt/components/dbt_project/scaffolder.py +65 -0
  5. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/resource.py +8 -5
  6. dagster_dbt-0.28.1/dagster_dbt/dbt_project_manager.py +170 -0
  7. dagster_dbt-0.28.1/dagster_dbt/version.py +1 -0
  8. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1/dagster_dbt.egg-info}/PKG-INFO +3 -2
  9. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/SOURCES.txt +1 -0
  10. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/requires.txt +2 -1
  11. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/setup.py +2 -1
  12. dagster_dbt-0.28.0/dagster_dbt/components/dbt_project/scaffolder.py +0 -50
  13. dagster_dbt-0.28.0/dagster_dbt/version.py +0 -1
  14. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/LICENSE +0 -0
  15. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/MANIFEST.in +0 -0
  16. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/README.md +0 -0
  17. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/__init__.py +0 -0
  18. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_decorator.py +0 -0
  19. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_specs.py +0 -0
  20. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cli/__init__.py +0 -0
  21. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cli/app.py +0 -0
  22. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/__init__.py +0 -0
  23. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/asset_defs.py +0 -0
  24. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/cli.py +0 -0
  25. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/ops.py +0 -0
  26. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/resources.py +0 -0
  27. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/types.py +0 -0
  28. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/utils.py +0 -0
  29. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/__init__.py +0 -0
  30. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/asset_decorator.py +0 -0
  31. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/cli_invocation.py +0 -0
  32. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/client.py +0 -0
  33. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/resources.py +0 -0
  34. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/run_handler.py +0 -0
  35. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/sensor_builder.py +0 -0
  36. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/types.py +0 -0
  37. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/compat.py +0 -0
  38. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/__init__.py +0 -0
  39. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/dbt_project/__init__.py +0 -0
  40. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/__init__.py +0 -0
  41. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_cli_event.py +0 -0
  42. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_cli_invocation.py +0 -0
  43. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_event_iterator.py +0 -0
  44. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/utils.py +0 -0
  45. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dagster_dbt_translator.py +0 -0
  46. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_core_version.py +0 -0
  47. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_manifest.py +0 -0
  48. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_manifest_asset_selection.py +0 -0
  49. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_project.py +0 -0
  50. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_version.py +0 -0
  51. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/errors.py +0 -0
  52. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/freshness_builder.py +0 -0
  53. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/__init__.py +0 -0
  54. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/pyproject.toml.jinja +0 -0
  55. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/__init__.py.jinja +0 -0
  56. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/assets.py.jinja +0 -0
  57. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/definitions.py.jinja +0 -0
  58. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/project.py.jinja +0 -0
  59. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/schedules.py.jinja +0 -0
  60. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/setup.py.jinja +0 -0
  61. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/metadata_set.py +0 -0
  62. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/py.typed +0 -0
  63. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/utils.py +0 -0
  64. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/dependency_links.txt +0 -0
  65. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/entry_points.txt +0 -0
  66. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/not-zip-safe +0 -0
  67. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/top_level.txt +0 -0
  68. {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dagster-dbt
3
- Version: 0.28.0
3
+ Version: 0.28.1
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
@@ -15,8 +15,9 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.9,<3.14
17
17
  License-File: LICENSE
18
- Requires-Dist: dagster==1.12.0
18
+ Requires-Dist: dagster==1.12.1
19
19
  Requires-Dist: dbt-core<1.11,>=1.7
20
+ Requires-Dist: gitpython
20
21
  Requires-Dist: Jinja2
21
22
  Requires-Dist: networkx
22
23
  Requires-Dist: orjson
@@ -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,10 +1,9 @@
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
9
  from typing import Annotated, Any, Literal, Optional, Union
@@ -40,6 +39,12 @@ from dagster_dbt.dagster_dbt_translator import (
40
39
  from dagster_dbt.dbt_manifest import validate_manifest
41
40
  from dagster_dbt.dbt_manifest_asset_selection import DbtManifestAssetSelection
42
41
  from dagster_dbt.dbt_project import DbtProject
42
+ from dagster_dbt.dbt_project_manager import (
43
+ DbtProjectArgsManager,
44
+ DbtProjectManager,
45
+ NoopDbtProjectManager,
46
+ RemoteGitDbtProjectManager,
47
+ )
43
48
  from dagster_dbt.utils import ASSET_RESOURCE_TYPES
44
49
 
45
50
 
@@ -63,29 +68,18 @@ class DbtProjectArgs(dg.Resolvable):
63
68
  state_path: Optional[str] = None
64
69
 
65
70
 
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
71
+ def resolve_dbt_project(context: ResolutionContext, model) -> DbtProjectManager:
72
+ if isinstance(model, RemoteGitDbtProjectManager.model()):
73
+ return RemoteGitDbtProjectManager.resolve_from_model(context, model)
79
74
 
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,
75
+ args = (
76
+ DbtProjectArgs(project_dir=context.resolve_value(model, as_type=str))
77
+ if isinstance(model, str)
78
+ else DbtProjectArgs.resolve_from_model(context, model)
88
79
  )
80
+ # resolve the project_dir relative to where this component is defined
81
+ args = replace(args, project_dir=context.resolve_source_relative_path(args.project_dir))
82
+ return DbtProjectArgsManager(args)
89
83
 
90
84
 
91
85
  DbtMetadataAddons: TypeAlias = Literal["column_metadata", "row_count"]
@@ -128,10 +122,10 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
128
122
  """
129
123
 
130
124
  project: Annotated[
131
- DbtProject,
125
+ Union[DbtProject, DbtProjectManager],
132
126
  Resolver(
133
127
  resolve_dbt_project,
134
- model_field_type=Union[str, DbtProjectArgs.model()],
128
+ model_field_type=Union[str, DbtProjectArgs.model(), RemoteGitDbtProjectManager.model()],
135
129
  description="The path to the dbt project or a mapping defining a DbtProject",
136
130
  examples=[
137
131
  "{{ project_root }}/path/to/dbt_project",
@@ -224,7 +218,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
224
218
  @property
225
219
  def defs_state_config(self) -> DefsStateConfig:
226
220
  return DefsStateConfig(
227
- key=f"{self.__class__.__name__}[{self.project.name}]",
221
+ key=f"{self.__class__.__name__}[{self._project_manager.defs_state_discriminator}]",
228
222
  management_type=DefsStateManagementType.LOCAL_FILESYSTEM,
229
223
  refresh_if_dev=self.prepare_if_dev,
230
224
  )
@@ -309,37 +303,40 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
309
303
  return self._base_translator.get_asset_check_spec(asset_spec, manifest, unique_id, project)
310
304
 
311
305
  @cached_property
312
- def cli_resource(self):
313
- return DbtCliResource(self.project)
306
+ def _project_manager(self) -> DbtProjectManager:
307
+ if isinstance(self.project, DbtProject):
308
+ return NoopDbtProjectManager(self.project)
309
+ else:
310
+ return self.project
311
+
312
+ @cached_property
313
+ def dbt_project(self) -> DbtProject:
314
+ return self._project_manager.get_project(None)
314
315
 
315
316
  def get_asset_selection(
316
317
  self, select: str, exclude: str = DBT_DEFAULT_EXCLUDE
317
318
  ) -> DbtManifestAssetSelection:
318
319
  return DbtManifestAssetSelection.build(
319
- manifest=self.project.manifest_path,
320
+ manifest=self.dbt_project.manifest_path,
320
321
  dagster_dbt_translator=self.translator,
321
322
  select=select,
322
323
  exclude=exclude,
323
324
  )
324
325
 
325
326
  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)
327
+ self._project_manager.prepare(state_path)
330
328
 
331
329
  def build_defs_from_state(
332
330
  self, context: dg.ComponentLoadContext, state_path: Optional[Path]
333
331
  ) -> dg.Definitions:
334
- if state_path is not None:
335
- shutil.copyfile(state_path, self.project.manifest_path)
332
+ project = self._project_manager.get_project(state_path)
336
333
 
337
334
  res_ctx = context.resolution_context
338
335
 
339
336
  @dbt_assets(
340
- manifest=self.project.manifest_path,
341
- project=self.project,
342
- name=self.op.name if self.op else self.project.name,
337
+ manifest=project.manifest_path,
338
+ project=project,
339
+ name=self.op.name if self.op else project.name,
343
340
  op_tags=self.op.tags if self.op else None,
344
341
  dagster_dbt_translator=self.translator,
345
342
  select=self.select,
@@ -348,7 +345,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
348
345
  )
349
346
  def _fn(context: dg.AssetExecutionContext):
350
347
  with _set_resolution_context(res_ctx):
351
- yield from self.execute(context=context, dbt=self.cli_resource)
348
+ yield from self.execute(context=context, dbt=DbtCliResource(project))
352
349
 
353
350
  return dg.Definitions(assets=[_fn])
354
351
 
@@ -437,7 +434,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
437
434
 
438
435
  @cached_property
439
436
  def _validated_manifest(self):
440
- return validate_manifest(self.project.manifest_path)
437
+ return validate_manifest(self.dbt_project.manifest_path)
441
438
 
442
439
  @cached_property
443
440
  def _validated_translator(self):
@@ -460,7 +457,7 @@ class DbtProjectComponent(StateBackedComponent, dg.Resolvable):
460
457
  return dagster_dbt_translator.get_asset_spec(
461
458
  manifest,
462
459
  next(iter(matching_model_ids)),
463
- self.project,
460
+ self.dbt_project,
464
461
  ).key
465
462
 
466
463
 
@@ -492,4 +489,4 @@ def get_projects_from_dbt_component(components: Path) -> list[DbtProject]:
492
489
  of_type=DbtProjectComponent
493
490
  )
494
491
 
495
- return [component.project for component in project_components]
492
+ return [component.dbt_project for component in project_components]
@@ -0,0 +1,65 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Any, Optional
4
+
5
+ import dagster._check as check
6
+ from dagster._core.errors import DagsterInvalidInvocationError
7
+ from dagster.components.component.component_scaffolder import Scaffolder
8
+ from dagster.components.component_scaffolding import scaffold_component
9
+ from dagster.components.scaffold.scaffold import ScaffoldRequest
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class DbtScaffoldParams(BaseModel):
14
+ init: bool = Field(default=False)
15
+ project_path: Optional[str] = None
16
+ git_url: Optional[str] = None
17
+
18
+
19
+ class DbtProjectComponentScaffolder(Scaffolder[DbtScaffoldParams]):
20
+ @classmethod
21
+ def get_scaffold_params(cls) -> type[DbtScaffoldParams]:
22
+ return DbtScaffoldParams
23
+
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"])
33
+
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
+
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()
46
+ subpaths = [
47
+ path for path in project_root.iterdir() if path.is_dir() and path.name != "logs"
48
+ ]
49
+ check.invariant(len(subpaths) == 1, "Expected exactly one subpath to be created.")
50
+ # this path should be relative to this directory
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}
63
+ else:
64
+ params = {"project": None}
65
+ scaffold_component(request, params)
@@ -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,170 @@
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
+ from git import Repo
13
+
14
+ from dagster_dbt.dbt_project import DbtProject
15
+
16
+ if TYPE_CHECKING:
17
+ from dagster_dbt.components.dbt_project.component import DbtProjectArgs
18
+
19
+
20
+ class DbtProjectManager(ABC):
21
+ """Helper class that wraps a dbt project that may or may not be available on local disk at the time
22
+ it is instantiated. Provides methods for syncing the project to a local path and for instantiating the
23
+ final DbtProject object when ready.
24
+ """
25
+
26
+ @property
27
+ @abstractmethod
28
+ def defs_state_discriminator(self) -> str: ...
29
+
30
+ @abstractmethod
31
+ def sync(self, state_path: Path) -> None: ...
32
+
33
+ def _local_project_dir(self, state_path: Path) -> Path:
34
+ return state_path.parent / "project"
35
+
36
+ def prepare(self, state_path: Path) -> None:
37
+ """Syncs the project to the given local path and ensures the manifest is generated
38
+ and the dependencies are installed.
39
+ """
40
+ # ensure local dir is empty
41
+ local_dir = self._local_project_dir(state_path)
42
+ shutil.rmtree(local_dir, ignore_errors=True)
43
+ local_dir.mkdir()
44
+
45
+ # ensure project exists in the dir and is compiled
46
+ self.sync(state_path)
47
+
48
+ # indicate that project has been prepared
49
+ state_path.touch()
50
+
51
+ @abstractmethod
52
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject": ...
53
+
54
+
55
+ @dataclass
56
+ class NoopDbtProjectManager(DbtProjectManager):
57
+ """Wraps a DbtProject that has already been fully instantiated. Used for cases where a
58
+ user directly provides a DbtProject to the DbtProjectComponent.
59
+ """
60
+
61
+ project: "DbtProject"
62
+
63
+ @property
64
+ def defs_state_discriminator(self) -> str:
65
+ return self.project.name
66
+
67
+ def sync(self, state_path: Path) -> None:
68
+ pass
69
+
70
+ def get_project(self, state_path: Optional[Path]) -> DbtProject:
71
+ return self.project
72
+
73
+
74
+ @dataclass
75
+ class DbtProjectArgsManager(DbtProjectManager):
76
+ """Wraps DbtProjectArgs provided to the DbtProjectComponent. Avoids instantiating the DbtProject object
77
+ immediately as this would cause errors in cases where the project_dir has not yet been synced.
78
+ """
79
+
80
+ args: "DbtProjectArgs"
81
+
82
+ @property
83
+ def defs_state_discriminator(self) -> str:
84
+ return Path(self.args.project_dir).stem
85
+
86
+ def sync(self, state_path: Path) -> None:
87
+ # we prepare the project in the original project directory rather than the new one
88
+ # so that code that does not have access to the local state path can still access
89
+ # the manifest.json file.
90
+ project = self.get_project(None)
91
+ project.preparer.prepare(project)
92
+ shutil.copytree(
93
+ self.args.project_dir, self._local_project_dir(state_path), dirs_exist_ok=True
94
+ )
95
+
96
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject":
97
+ kwargs = asdict(self.args)
98
+
99
+ project_dir = self._local_project_dir(state_path) if state_path else self.args.project_dir
100
+ return DbtProject(
101
+ project_dir=project_dir,
102
+ # allow default values on DbtProject to take precedence
103
+ **{k: v for k, v in kwargs.items() if v is not None and k != "project_dir"},
104
+ )
105
+
106
+
107
+ @dataclass
108
+ class RemoteGitDbtProjectManager(DbtProjectManager, Resolvable):
109
+ """Wraps a remote git repository containing a dbt project."""
110
+
111
+ repo_url: str
112
+ repo_relative_path: Annotated[
113
+ str,
114
+ Resolver.default(
115
+ description="The relative path to the dbt project within the repository.",
116
+ examples=["dbt/my_dbt_project"],
117
+ ),
118
+ ] = "."
119
+ token: Annotated[
120
+ Optional[str],
121
+ Resolver.default(
122
+ description="Token for authenticating to the provided repository.",
123
+ examples=["'{{ env.GITHUB_TOKEN }}'"],
124
+ ),
125
+ ] = None
126
+ profile: Annotated[
127
+ Optional[str],
128
+ Resolver.default(description="The profile to use for the dbt project."),
129
+ ] = None
130
+ target: Annotated[
131
+ Optional[str],
132
+ Resolver.default(
133
+ description="The target to use for the dbt project.",
134
+ examples=["dev", "prod"],
135
+ ),
136
+ ] = None
137
+
138
+ @property
139
+ def defs_state_discriminator(self) -> str:
140
+ return self.repo_url
141
+
142
+ def _get_clone_url(self) -> str:
143
+ # in github action environments, the token will be available automatically as an env var
144
+ raw_token = self.token or os.getenv("GITHUB_TOKEN")
145
+ if raw_token is None:
146
+ return self.repo_url
147
+ else:
148
+ # insert the token into the url
149
+ token = quote(raw_token, safe="")
150
+ parts = urlparse(self.repo_url)
151
+ return urlunparse(parts._replace(netloc=f"{token}@{parts.netloc}"))
152
+
153
+ def sync(self, state_path: Path) -> None:
154
+ Repo.clone_from(self._get_clone_url(), self._local_project_dir(state_path), depth=1)
155
+ project = self.get_project(state_path)
156
+ project.preparer.prepare(project)
157
+
158
+ def get_project(self, state_path: Optional[Path]) -> "DbtProject":
159
+ if state_path is None:
160
+ raise DagsterInvalidDefinitionError(
161
+ "Attempted to `get_project()` on a `RemoteGitDbtProjectWrapper` without a path. "
162
+ "This can happen when calling unsupported methods on the `DbtProjectComponent`."
163
+ )
164
+
165
+ local_dir = self._local_project_dir(state_path)
166
+ return DbtProject(
167
+ project_dir=local_dir / self.repo_relative_path,
168
+ profile=self.profile,
169
+ target=self.target,
170
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.28.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dagster-dbt
3
- Version: 0.28.0
3
+ Version: 0.28.1
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
@@ -15,8 +15,9 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.9,<3.14
17
17
  License-File: LICENSE
18
- Requires-Dist: dagster==1.12.0
18
+ Requires-Dist: dagster==1.12.1
19
19
  Requires-Dist: dbt-core<1.11,>=1.7
20
+ Requires-Dist: gitpython
20
21
  Requires-Dist: Jinja2
21
22
  Requires-Dist: networkx
22
23
  Requires-Dist: orjson
@@ -13,6 +13,7 @@ dagster_dbt/dbt_core_version.py
13
13
  dagster_dbt/dbt_manifest.py
14
14
  dagster_dbt/dbt_manifest_asset_selection.py
15
15
  dagster_dbt/dbt_project.py
16
+ dagster_dbt/dbt_project_manager.py
16
17
  dagster_dbt/dbt_version.py
17
18
  dagster_dbt/errors.py
18
19
  dagster_dbt/freshness_builder.py
@@ -1,5 +1,6 @@
1
- dagster==1.12.0
1
+ dagster==1.12.1
2
2
  dbt-core<1.11,>=1.7
3
+ gitpython
3
4
  Jinja2
4
5
  networkx
5
6
  orjson
@@ -40,9 +40,10 @@ setup(
40
40
  include_package_data=True,
41
41
  python_requires=">=3.9,<3.14",
42
42
  install_requires=[
43
- "dagster==1.12.0",
43
+ "dagster==1.12.1",
44
44
  # Follow the version support constraints for dbt Core: https://docs.getdbt.com/docs/dbt-versions/core
45
45
  f"dbt-core>=1.7,<{DBT_CORE_VERSION_UPPER_BOUND}",
46
+ "gitpython",
46
47
  "Jinja2",
47
48
  "networkx",
48
49
  "orjson",
@@ -1,50 +0,0 @@
1
- import os
2
- from pathlib import Path
3
- from typing import Optional
4
-
5
- import dagster._check as check
6
- from dagster._core.errors import DagsterInvalidInvocationError
7
- from dagster.components.component.component_scaffolder import Scaffolder
8
- from dagster.components.component_scaffolding import scaffold_component
9
- from dagster.components.scaffold.scaffold import ScaffoldRequest
10
- from pydantic import BaseModel, Field
11
-
12
-
13
- class DbtScaffoldParams(BaseModel):
14
- init: bool = Field(default=False)
15
- project_path: Optional[str] = None
16
-
17
-
18
- class DbtProjectComponentScaffolder(Scaffolder[DbtScaffoldParams]):
19
- @classmethod
20
- def get_scaffold_params(cls) -> type[DbtScaffoldParams]:
21
- return DbtScaffoldParams
22
-
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}"
29
-
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
- )
37
-
38
- dbtRunner().invoke(["init"])
39
- subpaths = [
40
- path
41
- for path in Path(project_root).iterdir()
42
- if path.is_dir() and path.name != "logs"
43
- ]
44
- check.invariant(len(subpaths) == 1, "Expected exactly one subpath to be created.")
45
- # this path should be relative to this directory
46
- path_str = subpaths[0].name
47
- else:
48
- path_str = None
49
-
50
- scaffold_component(request, {"project": path_str})
@@ -1 +0,0 @@
1
- __version__ = "0.28.0"
File without changes
File without changes
File without changes
File without changes