dbt-bouncer 1.31.2rc3__py3-none-any.whl → 2.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbt_bouncer/artifact_parsers/dbt_cloud/catalog_latest.py +21 -21
- dbt_bouncer/artifact_parsers/dbt_cloud/manifest_latest.py +1745 -1745
- dbt_bouncer/artifact_parsers/dbt_cloud/run_results_latest.py +22 -22
- dbt_bouncer/artifact_parsers/parsers_catalog.py +26 -24
- dbt_bouncer/artifact_parsers/parsers_common.py +57 -36
- dbt_bouncer/artifact_parsers/parsers_manifest.py +98 -69
- dbt_bouncer/artifact_parsers/parsers_run_results.py +32 -19
- dbt_bouncer/check_base.py +22 -11
- dbt_bouncer/checks/catalog/check_catalog_sources.py +22 -12
- dbt_bouncer/checks/catalog/check_columns.py +175 -105
- dbt_bouncer/checks/common.py +24 -3
- dbt_bouncer/checks/manifest/check_exposures.py +79 -52
- dbt_bouncer/checks/manifest/check_lineage.py +69 -40
- dbt_bouncer/checks/manifest/check_macros.py +177 -104
- dbt_bouncer/checks/manifest/check_metadata.py +28 -18
- dbt_bouncer/checks/manifest/check_models.py +842 -496
- dbt_bouncer/checks/manifest/check_seeds.py +63 -0
- dbt_bouncer/checks/manifest/check_semantic_models.py +28 -20
- dbt_bouncer/checks/manifest/check_snapshots.py +57 -33
- dbt_bouncer/checks/manifest/check_sources.py +246 -137
- dbt_bouncer/checks/manifest/check_unit_tests.py +97 -54
- dbt_bouncer/checks/run_results/check_run_results.py +34 -20
- dbt_bouncer/config_file_parser.py +47 -28
- dbt_bouncer/config_file_validator.py +11 -8
- dbt_bouncer/global_context.py +31 -0
- dbt_bouncer/main.py +128 -67
- dbt_bouncer/runner.py +61 -31
- dbt_bouncer/utils.py +146 -50
- dbt_bouncer/version.py +1 -1
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/METADATA +15 -15
- dbt_bouncer-2.0.0rc1.dist-info/RECORD +37 -0
- dbt_bouncer-1.31.2rc3.dist-info/RECORD +0 -35
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/WHEEL +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/entry_points.txt +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/licenses/LICENSE +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import warnings
|
|
3
|
-
from typing import TYPE_CHECKING, Any,
|
|
3
|
+
from typing import TYPE_CHECKING, Any, TypeAlias
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
from dbt_bouncer.artifact_parsers.parsers_common import load_dbt_artifact
|
|
29
29
|
|
|
30
|
-
DbtBouncerRunResultBase =
|
|
30
|
+
DbtBouncerRunResultBase: TypeAlias = RunResultOutput_v5 | RunResultOutput_Latest
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class DbtBouncerRunResult(BaseModel):
|
|
@@ -39,8 +39,8 @@ class DbtBouncerRunResult(BaseModel):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def parse_run_results(
|
|
42
|
-
run_results:
|
|
43
|
-
) ->
|
|
42
|
+
run_results: dict[str, Any],
|
|
43
|
+
) -> RunResultsV5 | RunResultsLatest:
|
|
44
44
|
"""Parse run-results.json.
|
|
45
45
|
|
|
46
46
|
Args:
|
|
@@ -50,7 +50,7 @@ def parse_run_results(
|
|
|
50
50
|
ValueError: If the schema version is not supported.
|
|
51
51
|
|
|
52
52
|
Returns:
|
|
53
|
-
|
|
53
|
+
RunResultsV5 | RunResultsLatest:
|
|
54
54
|
|
|
55
55
|
"""
|
|
56
56
|
dbt_schema_version = run_results["metadata"]["dbt_schema_version"]
|
|
@@ -64,12 +64,21 @@ def parse_run_results(
|
|
|
64
64
|
def parse_run_results_artifact(
|
|
65
65
|
artifact_dir: "Path",
|
|
66
66
|
manifest_obj: "DbtBouncerManifest",
|
|
67
|
-
package_name:
|
|
68
|
-
) ->
|
|
67
|
+
package_name: str | None = None,
|
|
68
|
+
) -> list[DbtBouncerRunResult]:
|
|
69
69
|
"""Parse the run_results.json artifact.
|
|
70
70
|
|
|
71
|
+
Args:
|
|
72
|
+
artifact_dir (Path): Path to the dbt artifacts directory.
|
|
73
|
+
manifest_obj (DbtBouncerManifest): The manifest object.
|
|
74
|
+
package_name (str | None): The package name to filter results by. If None,
|
|
75
|
+
uses the project name from the manifest.
|
|
76
|
+
|
|
71
77
|
Returns:
|
|
72
|
-
|
|
78
|
+
list[DbtBouncerRunResult]: A list of DbtBouncerRunResult objects.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
TypeError: If the loaded artifact is not of the expected type.
|
|
73
82
|
|
|
74
83
|
"""
|
|
75
84
|
|
|
@@ -93,24 +102,28 @@ def parse_run_results_artifact(
|
|
|
93
102
|
manifest_obj.manifest.exposures[unique_id].original_file_path
|
|
94
103
|
)
|
|
95
104
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
unit_tests = getattr(manifest_obj.manifest, "unit_tests", {})
|
|
106
|
+
if unique_id in unit_tests:
|
|
107
|
+
return clean_path_str(unit_tests[unique_id].original_file_path)
|
|
108
|
+
return ""
|
|
99
109
|
|
|
100
|
-
run_results_obj
|
|
110
|
+
run_results_obj = load_dbt_artifact(
|
|
101
111
|
artifact_name="run_results.json",
|
|
102
112
|
dbt_artifacts_dir=artifact_dir,
|
|
103
113
|
)
|
|
104
114
|
|
|
115
|
+
if not isinstance(run_results_obj, (RunResultsV5, RunResultsLatest)):
|
|
116
|
+
raise TypeError(
|
|
117
|
+
f"Expected RunResultsV5 or RunResultsLatest, got {type(run_results_obj)}"
|
|
118
|
+
)
|
|
119
|
+
|
|
105
120
|
project_run_results = [
|
|
106
121
|
DbtBouncerRunResult(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"unique_id": r.unique_id,
|
|
113
|
-
},
|
|
122
|
+
original_file_path=get_clean_path_str(
|
|
123
|
+
unique_id=r.unique_id, manifest_obj=manifest_obj
|
|
124
|
+
),
|
|
125
|
+
run_result=r,
|
|
126
|
+
unique_id=r.unique_id,
|
|
114
127
|
)
|
|
115
128
|
for r in run_results_obj.results
|
|
116
129
|
if r.unique_id.split(".")[1]
|
dbt_bouncer/check_base.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
1
|
+
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field
|
|
4
4
|
|
|
5
|
+
from dbt_bouncer.utils import is_description_populated
|
|
6
|
+
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
8
|
import warnings
|
|
7
9
|
|
|
@@ -20,42 +22,44 @@ class BaseCheck(BaseModel):
|
|
|
20
22
|
|
|
21
23
|
model_config = ConfigDict(extra="forbid")
|
|
22
24
|
|
|
23
|
-
description:
|
|
25
|
+
description: str | None = Field(
|
|
24
26
|
default=None,
|
|
25
27
|
description="Description of what the check does and why it is implemented.",
|
|
26
28
|
)
|
|
27
|
-
exclude:
|
|
29
|
+
exclude: str | None = Field(
|
|
28
30
|
default=None,
|
|
29
31
|
description="Regexp to match which paths to exclude.",
|
|
30
32
|
)
|
|
31
|
-
include:
|
|
33
|
+
include: str | None = Field(
|
|
32
34
|
default=None,
|
|
33
35
|
description="Regexp to match which paths to include.",
|
|
34
36
|
)
|
|
35
|
-
index:
|
|
37
|
+
index: int | None = Field(
|
|
36
38
|
default=None,
|
|
37
39
|
description="Index to uniquely identify the check, calculated at runtime.",
|
|
38
40
|
)
|
|
39
|
-
materialization:
|
|
41
|
+
materialization: Literal["ephemeral", "incremental", "table", "view"] | None = (
|
|
40
42
|
Field(
|
|
41
43
|
default=None,
|
|
42
44
|
description="Limit check to models with the specified materialization.",
|
|
43
45
|
)
|
|
44
46
|
)
|
|
45
|
-
severity:
|
|
47
|
+
severity: Literal["error", "warn"] | None = Field(
|
|
46
48
|
default="error",
|
|
47
49
|
description="Severity of the check, one of 'error' or 'warn'.",
|
|
48
50
|
)
|
|
49
51
|
|
|
52
|
+
_min_description_length: ClassVar[int] = 4
|
|
53
|
+
|
|
50
54
|
# Helper methods
|
|
51
55
|
def is_catalog_node_a_model(
|
|
52
|
-
self, catalog_node: "CatalogNodes", models:
|
|
56
|
+
self, catalog_node: "CatalogNodes", models: list["DbtBouncerModelBase"]
|
|
53
57
|
) -> bool:
|
|
54
58
|
"""Check if a catalog node is a model.
|
|
55
59
|
|
|
56
60
|
Args:
|
|
57
61
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
58
|
-
models (
|
|
62
|
+
models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
|
|
59
63
|
|
|
60
64
|
Returns:
|
|
61
65
|
bool: Whether a catalog node is a model.
|
|
@@ -69,14 +73,21 @@ class BaseCheck(BaseModel):
|
|
|
69
73
|
else:
|
|
70
74
|
return False
|
|
71
75
|
|
|
72
|
-
def
|
|
76
|
+
def _is_description_populated(
|
|
77
|
+
self, description: str, min_description_length: int | None
|
|
78
|
+
) -> bool:
|
|
73
79
|
"""Check if a description is populated.
|
|
74
80
|
|
|
75
81
|
Args:
|
|
76
82
|
description (str): Description.
|
|
83
|
+
min_description_length (int): Minimum length of the description.
|
|
77
84
|
|
|
78
85
|
Returns:
|
|
79
86
|
bool: Whether a description is validly populated.
|
|
80
87
|
|
|
81
88
|
"""
|
|
82
|
-
return
|
|
89
|
+
return is_description_populated(
|
|
90
|
+
description=description,
|
|
91
|
+
min_description_length=min_description_length
|
|
92
|
+
or self._min_description_length,
|
|
93
|
+
)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
1
|
+
from typing import TYPE_CHECKING, Literal
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
5
|
from dbt_bouncer.check_base import BaseCheck
|
|
6
|
+
from dbt_bouncer.checks.common import DbtBouncerFailedCheckError
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
9
|
import warnings
|
|
@@ -23,13 +24,13 @@ class CheckSourceColumnsAreAllDocumented(BaseCheck):
|
|
|
23
24
|
|
|
24
25
|
Receives:
|
|
25
26
|
catalog_source (CatalogNodes): The CatalogNodes object to check.
|
|
26
|
-
sources (
|
|
27
|
+
sources (list[DbtBouncerSourceBase]): List of DbtBouncerSourceBase objects parsed from `catalog.json`.
|
|
27
28
|
|
|
28
29
|
Other Parameters:
|
|
29
|
-
description (
|
|
30
|
-
exclude (
|
|
31
|
-
include (
|
|
32
|
-
severity (
|
|
30
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
31
|
+
exclude (str | None): Regex pattern to match the source path (i.e the .yml file where the source is configured). Source paths that match the pattern will not be checked.
|
|
32
|
+
include (str | None): Regex pattern to match the source path (i.e the .yml file where the source is configured). Only source paths that match the pattern will be checked.
|
|
33
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
33
34
|
|
|
34
35
|
Example(s):
|
|
35
36
|
```yaml
|
|
@@ -39,12 +40,20 @@ class CheckSourceColumnsAreAllDocumented(BaseCheck):
|
|
|
39
40
|
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
|
-
catalog_source: "CatalogNodes" = Field(default=None)
|
|
43
|
+
catalog_source: "CatalogNodes | None" = Field(default=None)
|
|
43
44
|
name: Literal["check_source_columns_are_all_documented"]
|
|
44
|
-
sources:
|
|
45
|
+
sources: list["DbtBouncerSourceBase"] = Field(default=[])
|
|
45
46
|
|
|
46
47
|
def execute(self) -> None:
|
|
47
|
-
"""Execute the check.
|
|
48
|
+
"""Execute the check.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
DbtBouncerFailedCheckError: If columns are undocumented.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
if self.catalog_source is None:
|
|
55
|
+
raise DbtBouncerFailedCheckError("self.catalog_source is None")
|
|
56
|
+
|
|
48
57
|
source = next(
|
|
49
58
|
s for s in self.sources if s.unique_id == self.catalog_source.unique_id
|
|
50
59
|
)
|
|
@@ -53,6 +62,7 @@ class CheckSourceColumnsAreAllDocumented(BaseCheck):
|
|
|
53
62
|
for _, v in self.catalog_source.columns.items()
|
|
54
63
|
if v.name not in source.columns
|
|
55
64
|
]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
if undocumented_columns:
|
|
66
|
+
raise DbtBouncerFailedCheckError(
|
|
67
|
+
f"`{self.catalog_source.unique_id}` has columns that are not included in the sources properties file: {undocumented_columns}"
|
|
68
|
+
)
|