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.
- {dagster_dbt-0.28.0/dagster_dbt.egg-info → dagster_dbt-0.28.1}/PKG-INFO +3 -2
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_utils.py +45 -1
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/dbt_project/component.py +39 -42
- dagster_dbt-0.28.1/dagster_dbt/components/dbt_project/scaffolder.py +65 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/resource.py +8 -5
- dagster_dbt-0.28.1/dagster_dbt/dbt_project_manager.py +170 -0
- dagster_dbt-0.28.1/dagster_dbt/version.py +1 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1/dagster_dbt.egg-info}/PKG-INFO +3 -2
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/SOURCES.txt +1 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/requires.txt +2 -1
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/setup.py +2 -1
- dagster_dbt-0.28.0/dagster_dbt/components/dbt_project/scaffolder.py +0 -50
- dagster_dbt-0.28.0/dagster_dbt/version.py +0 -1
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/LICENSE +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/MANIFEST.in +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/README.md +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_decorator.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/asset_specs.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cli/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cli/app.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/asset_defs.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/cli.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/ops.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/resources.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/types.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud/utils.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/asset_decorator.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/cli_invocation.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/client.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/resources.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/run_handler.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/sensor_builder.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/cloud_v2/types.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/compat.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/components/dbt_project/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_cli_event.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_cli_invocation.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/dbt_event_iterator.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/core/utils.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dagster_dbt_translator.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_core_version.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_manifest.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_manifest_asset_selection.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_project.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/dbt_version.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/errors.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/freshness_builder.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/__init__.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/pyproject.toml.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/__init__.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/assets.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/definitions.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/project.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/scaffold/schedules.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/include/setup.py.jinja +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/metadata_set.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/py.typed +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt/utils.py +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/dependency_links.txt +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/entry_points.txt +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/not-zip-safe +0 -0
- {dagster_dbt-0.28.0 → dagster_dbt-0.28.1}/dagster_dbt.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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=
|
|
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) ->
|
|
67
|
-
if isinstance(model,
|
|
68
|
-
return
|
|
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
|
-
|
|
81
|
-
project_dir=context.
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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
|
|
313
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
341
|
-
project=
|
|
342
|
-
name=self.op.name if self.op else
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|