dbt-bouncer 1.31.2rc3__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbt_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.0.dist-info}/METADATA +15 -15
- dbt_bouncer-2.0.0.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.0.dist-info}/WHEEL +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0.dist-info}/entry_points.txt +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# mypy: disable-error-code="union-attr"
|
|
2
|
-
|
|
3
1
|
import re
|
|
4
|
-
from typing import TYPE_CHECKING,
|
|
2
|
+
from typing import TYPE_CHECKING, Literal
|
|
5
3
|
|
|
6
4
|
from pydantic import ConfigDict, Field
|
|
7
5
|
|
|
6
|
+
from dbt_bouncer.checks.common import DbtBouncerFailedCheckError
|
|
7
|
+
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
import warnings
|
|
10
10
|
|
|
@@ -13,8 +13,8 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from dbt_artifacts_parser.parsers.catalog.catalog_v1 import (
|
|
14
14
|
Nodes as CatalogNodes,
|
|
15
15
|
)
|
|
16
|
-
from dbt_bouncer.artifact_parsers.parsers_common import DbtBouncerManifest
|
|
17
16
|
from dbt_bouncer.artifact_parsers.parsers_manifest import (
|
|
17
|
+
DbtBouncerManifest,
|
|
18
18
|
DbtBouncerModelBase,
|
|
19
19
|
DbtBouncerTestBase,
|
|
20
20
|
)
|
|
@@ -26,15 +26,18 @@ from dbt_bouncer.check_base import BaseCheck
|
|
|
26
26
|
class CheckColumnDescriptionPopulated(BaseCheck):
|
|
27
27
|
"""Columns must have a populated description.
|
|
28
28
|
|
|
29
|
+
Parameters:
|
|
30
|
+
min_description_length (int | None): Minimum length required for the description to be considered populated.
|
|
31
|
+
|
|
29
32
|
Receives:
|
|
30
33
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
31
|
-
models (
|
|
34
|
+
models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
|
|
32
35
|
|
|
33
36
|
Other Parameters:
|
|
34
|
-
description (
|
|
35
|
-
exclude (
|
|
36
|
-
include (
|
|
37
|
-
severity (
|
|
37
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
38
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
39
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
40
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
38
41
|
|
|
39
42
|
Example(s):
|
|
40
43
|
```yaml
|
|
@@ -42,31 +45,44 @@ class CheckColumnDescriptionPopulated(BaseCheck):
|
|
|
42
45
|
- name: check_column_description_populated
|
|
43
46
|
include: ^models/marts
|
|
44
47
|
```
|
|
48
|
+
```yaml
|
|
49
|
+
manifest_checks:
|
|
50
|
+
- name: check_column_description_populated
|
|
51
|
+
min_description_length: 25 # Setting a stricter requirement for description length
|
|
52
|
+
```
|
|
45
53
|
|
|
46
54
|
"""
|
|
47
55
|
|
|
48
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
49
|
-
|
|
56
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
57
|
+
min_description_length: int | None = Field(default=None)
|
|
58
|
+
models: list["DbtBouncerModelBase"] = Field(default=[])
|
|
50
59
|
name: Literal["check_column_description_populated"]
|
|
51
60
|
|
|
52
61
|
def execute(self) -> None:
|
|
53
|
-
"""Execute the check.
|
|
62
|
+
"""Execute the check.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
DbtBouncerFailedCheckError: If description is not populated.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
if self.catalog_node is None:
|
|
69
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
54
70
|
if self.is_catalog_node_a_model(self.catalog_node, self.models):
|
|
55
71
|
model = next(
|
|
56
72
|
m for m in self.models if m.unique_id == self.catalog_node.unique_id
|
|
57
73
|
)
|
|
58
74
|
non_complying_columns = []
|
|
59
75
|
for _, v in self.catalog_node.columns.items():
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
model.columns[v.name].description
|
|
76
|
+
columns = model.columns or {}
|
|
77
|
+
if columns.get(v.name) is None or not self._is_description_populated(
|
|
78
|
+
columns[v.name].description or "", self.min_description_length
|
|
64
79
|
):
|
|
65
80
|
non_complying_columns.append(v.name)
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
if non_complying_columns:
|
|
83
|
+
raise DbtBouncerFailedCheckError(
|
|
84
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns that do not have a populated description: {non_complying_columns}"
|
|
85
|
+
)
|
|
70
86
|
|
|
71
87
|
|
|
72
88
|
class CheckColumnHasSpecifiedTest(BaseCheck):
|
|
@@ -78,13 +94,13 @@ class CheckColumnHasSpecifiedTest(BaseCheck):
|
|
|
78
94
|
|
|
79
95
|
Receives:
|
|
80
96
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
81
|
-
tests (
|
|
97
|
+
tests (list[DbtBouncerTestBase]): List of DbtBouncerTestBase objects parsed from `manifest.json`.
|
|
82
98
|
|
|
83
99
|
Other Parameters:
|
|
84
|
-
description (
|
|
85
|
-
exclude (
|
|
86
|
-
include (
|
|
87
|
-
severity (
|
|
100
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
101
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
102
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
103
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
88
104
|
|
|
89
105
|
Example(s):
|
|
90
106
|
```yaml
|
|
@@ -96,37 +112,52 @@ class CheckColumnHasSpecifiedTest(BaseCheck):
|
|
|
96
112
|
|
|
97
113
|
"""
|
|
98
114
|
|
|
99
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
115
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
100
116
|
column_name_pattern: str
|
|
101
117
|
name: Literal["check_column_has_specified_test"]
|
|
102
118
|
test_name: str
|
|
103
|
-
tests:
|
|
119
|
+
tests: list["DbtBouncerTestBase"] = Field(default=[])
|
|
104
120
|
|
|
105
121
|
def execute(self) -> None:
|
|
106
|
-
"""Execute the check.
|
|
122
|
+
"""Execute the check.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
DbtBouncerFailedCheckError: If column does not have specified test.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
if self.catalog_node is None:
|
|
129
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
107
130
|
columns_to_check = [
|
|
108
131
|
v.name
|
|
109
132
|
for _, v in self.catalog_node.columns.items()
|
|
110
|
-
if re.compile(self.column_name_pattern.strip()).match(v.name)
|
|
111
|
-
|
|
112
|
-
relevant_tests = [
|
|
113
|
-
t
|
|
114
|
-
for t in self.tests
|
|
115
|
-
if hasattr(t, "test_metadata") is True
|
|
116
|
-
and hasattr(t, "attached_node") is True
|
|
117
|
-
and t.test_metadata.name == self.test_name
|
|
118
|
-
and t.attached_node == self.catalog_node.unique_id
|
|
133
|
+
if re.compile(self.column_name_pattern.strip()).match(str(v.name))
|
|
134
|
+
is not None
|
|
119
135
|
]
|
|
136
|
+
relevant_tests = []
|
|
137
|
+
for t in self.tests:
|
|
138
|
+
test_metadata = getattr(t, "test_metadata", None)
|
|
139
|
+
attached_node = getattr(t, "attached_node", None)
|
|
140
|
+
if (
|
|
141
|
+
test_metadata
|
|
142
|
+
and attached_node
|
|
143
|
+
and getattr(test_metadata, "name", None) == self.test_name
|
|
144
|
+
and attached_node == self.catalog_node.unique_id
|
|
145
|
+
):
|
|
146
|
+
relevant_tests.append(t)
|
|
120
147
|
non_complying_columns = [
|
|
121
148
|
c
|
|
122
149
|
for c in columns_to_check
|
|
123
150
|
if f"{self.catalog_node.unique_id}.{c}"
|
|
124
|
-
not in [
|
|
151
|
+
not in [
|
|
152
|
+
f"{getattr(t, 'attached_node', '')}.{getattr(t, 'column_name', '')}"
|
|
153
|
+
for t in relevant_tests
|
|
154
|
+
]
|
|
125
155
|
]
|
|
126
156
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
if non_complying_columns:
|
|
158
|
+
raise DbtBouncerFailedCheckError(
|
|
159
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns that should have a `{self.test_name}` test: {non_complying_columns}"
|
|
160
|
+
)
|
|
130
161
|
|
|
131
162
|
|
|
132
163
|
class CheckColumnNameCompliesToColumnType(BaseCheck):
|
|
@@ -136,17 +167,17 @@ class CheckColumnNameCompliesToColumnType(BaseCheck):
|
|
|
136
167
|
|
|
137
168
|
Parameters:
|
|
138
169
|
column_name_pattern (str): Regex pattern to match the model name.
|
|
139
|
-
type_pattern (
|
|
140
|
-
types (
|
|
170
|
+
type_pattern (str | None): Regex pattern to match the data types.
|
|
171
|
+
types (list[str] | None): List of data types to check.
|
|
141
172
|
|
|
142
173
|
Receives:
|
|
143
174
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
144
175
|
|
|
145
176
|
Other Parameters:
|
|
146
|
-
description (
|
|
147
|
-
exclude (
|
|
148
|
-
include (
|
|
149
|
-
severity (
|
|
177
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
178
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
179
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
180
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
150
181
|
|
|
151
182
|
Example(s):
|
|
152
183
|
```yaml
|
|
@@ -188,38 +219,48 @@ class CheckColumnNameCompliesToColumnType(BaseCheck):
|
|
|
188
219
|
|
|
189
220
|
"""
|
|
190
221
|
|
|
191
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
222
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
192
223
|
column_name_pattern: str
|
|
193
224
|
name: Literal["check_column_name_complies_to_column_type"]
|
|
194
|
-
type_pattern:
|
|
195
|
-
types:
|
|
225
|
+
type_pattern: str | None = None
|
|
226
|
+
types: list[str] | None = None
|
|
196
227
|
|
|
197
228
|
def execute(self) -> None:
|
|
198
|
-
"""Execute the check.
|
|
229
|
+
"""Execute the check.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
DbtBouncerFailedCheckError: If column name does not comply to column type.
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
if self.catalog_node is None:
|
|
236
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
199
237
|
if self.type_pattern:
|
|
200
238
|
non_complying_columns = [
|
|
201
239
|
v.name
|
|
202
240
|
for _, v in self.catalog_node.columns.items()
|
|
203
|
-
if re.compile(self.type_pattern.strip()).match(v.type) is None
|
|
204
|
-
and re.compile(self.column_name_pattern.strip()).match(v.name)
|
|
241
|
+
if re.compile(self.type_pattern.strip()).match(str(v.type)) is None
|
|
242
|
+
and re.compile(self.column_name_pattern.strip()).match(str(v.name))
|
|
205
243
|
is not None
|
|
206
244
|
]
|
|
207
245
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
if non_complying_columns:
|
|
247
|
+
raise DbtBouncerFailedCheckError(
|
|
248
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns that don't comply with the specified data type regexp pattern (`{self.column_name_pattern}`): {non_complying_columns}"
|
|
249
|
+
)
|
|
211
250
|
|
|
212
251
|
elif self.types:
|
|
213
252
|
non_complying_columns = [
|
|
214
253
|
v.name
|
|
215
254
|
for _, v in self.catalog_node.columns.items()
|
|
216
255
|
if v.type in self.types
|
|
217
|
-
and re.compile(self.column_name_pattern.strip()).match(v.name)
|
|
256
|
+
and re.compile(self.column_name_pattern.strip()).match(str(v.name))
|
|
257
|
+
is None
|
|
218
258
|
]
|
|
219
259
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
if non_complying_columns:
|
|
261
|
+
raise DbtBouncerFailedCheckError(
|
|
262
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns that don't comply with the specified regexp pattern (`{self.column_name_pattern}`): {non_complying_columns}"
|
|
263
|
+
)
|
|
223
264
|
|
|
224
265
|
@model_validator(mode="after")
|
|
225
266
|
def _check_type_pattern_or_types(self) -> "CheckColumnNameCompliesToColumnType":
|
|
@@ -238,14 +279,14 @@ class CheckColumnNames(BaseCheck):
|
|
|
238
279
|
|
|
239
280
|
Receives:
|
|
240
281
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
241
|
-
models (
|
|
282
|
+
models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
|
|
242
283
|
|
|
243
284
|
Other Parameters:
|
|
244
|
-
description (
|
|
245
|
-
exclude (
|
|
246
|
-
include (
|
|
247
|
-
materialization (
|
|
248
|
-
severity (
|
|
285
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
286
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
287
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
288
|
+
materialization (Literal["ephemeral", "incremental", "table", "view"] | None): Limit check to models with the specified materialization.
|
|
289
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
249
290
|
|
|
250
291
|
Example(s):
|
|
251
292
|
```yaml
|
|
@@ -258,40 +299,48 @@ class CheckColumnNames(BaseCheck):
|
|
|
258
299
|
|
|
259
300
|
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
|
260
301
|
|
|
261
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
302
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
262
303
|
column_name_pattern: str
|
|
263
|
-
models:
|
|
304
|
+
models: list["DbtBouncerModelBase"] = Field(default=[])
|
|
264
305
|
name: Literal["check_column_names"]
|
|
265
306
|
|
|
266
307
|
def execute(self) -> None:
|
|
267
|
-
"""Execute the check.
|
|
308
|
+
"""Execute the check.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
DbtBouncerFailedCheckError: If column name does not match regex.
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
if self.catalog_node is None:
|
|
315
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
268
316
|
if self.is_catalog_node_a_model(self.catalog_node, self.models):
|
|
269
|
-
non_complying_columns:
|
|
317
|
+
non_complying_columns: list[str] = []
|
|
270
318
|
non_complying_columns.extend(
|
|
271
319
|
v.name
|
|
272
320
|
for _, v in self.catalog_node.columns.items()
|
|
273
|
-
if re.fullmatch(self.column_name_pattern.strip(), v.name) is None
|
|
321
|
+
if re.fullmatch(self.column_name_pattern.strip(), str(v.name)) is None
|
|
274
322
|
)
|
|
275
323
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
324
|
+
if non_complying_columns:
|
|
325
|
+
raise DbtBouncerFailedCheckError(
|
|
326
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns ({non_complying_columns}) that do not match the supplied regex: `{self.column_name_pattern.strip()}`."
|
|
327
|
+
)
|
|
279
328
|
|
|
280
329
|
|
|
281
330
|
class CheckColumnsAreAllDocumented(BaseCheck):
|
|
282
331
|
"""All columns in a model should be included in the model's properties file, i.e. `.yml` file.
|
|
283
332
|
|
|
284
333
|
Receives:
|
|
285
|
-
case_sensitive (
|
|
334
|
+
case_sensitive (bool | None): Whether the column names are case sensitive or not. Necessary for adapters like `dbt-snowflake` where the column in `catalog.json` is uppercase but the column in `manifest.json` can be lowercase. Defaults to `false` for `dbt-snowflake`, otherwise `true`.
|
|
286
335
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
287
336
|
manifest_obj (DbtBouncerManifest): The DbtBouncerManifest object parsed from `manifest.json`.
|
|
288
|
-
models (
|
|
337
|
+
models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
|
|
289
338
|
|
|
290
339
|
Other Parameters:
|
|
291
|
-
description (
|
|
292
|
-
exclude (
|
|
293
|
-
include (
|
|
294
|
-
severity (
|
|
340
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
341
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
342
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
343
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
295
344
|
|
|
296
345
|
Example(s):
|
|
297
346
|
```yaml
|
|
@@ -301,14 +350,23 @@ class CheckColumnsAreAllDocumented(BaseCheck):
|
|
|
301
350
|
|
|
302
351
|
"""
|
|
303
352
|
|
|
304
|
-
case_sensitive:
|
|
305
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
306
|
-
manifest_obj: "DbtBouncerManifest" = Field(default=None)
|
|
307
|
-
models:
|
|
353
|
+
case_sensitive: bool | None = Field(default=True)
|
|
354
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
355
|
+
manifest_obj: "DbtBouncerManifest | None" = Field(default=None)
|
|
356
|
+
models: list["DbtBouncerModelBase"] = Field(default=[])
|
|
308
357
|
name: Literal["check_columns_are_all_documented"]
|
|
309
358
|
|
|
310
359
|
def execute(self) -> None:
|
|
311
|
-
"""Execute the check.
|
|
360
|
+
"""Execute the check.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
DbtBouncerFailedCheckError: If columns are undocumented.
|
|
364
|
+
|
|
365
|
+
"""
|
|
366
|
+
if self.catalog_node is None:
|
|
367
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
368
|
+
if self.manifest_obj is None:
|
|
369
|
+
raise DbtBouncerFailedCheckError("self.manifest_obj is None")
|
|
312
370
|
if self.is_catalog_node_a_model(self.catalog_node, self.models):
|
|
313
371
|
model = next(
|
|
314
372
|
m for m in self.models if m.unique_id == self.catalog_node.unique_id
|
|
@@ -317,22 +375,24 @@ class CheckColumnsAreAllDocumented(BaseCheck):
|
|
|
317
375
|
if self.manifest_obj.manifest.metadata.adapter_type in ["snowflake"]:
|
|
318
376
|
self.case_sensitive = False
|
|
319
377
|
|
|
378
|
+
model_columns = model.columns or {}
|
|
320
379
|
if self.case_sensitive:
|
|
321
380
|
undocumented_columns = [
|
|
322
381
|
v.name
|
|
323
382
|
for _, v in self.catalog_node.columns.items()
|
|
324
|
-
if v.name not in
|
|
383
|
+
if v.name not in model_columns
|
|
325
384
|
]
|
|
326
385
|
else:
|
|
327
386
|
undocumented_columns = [
|
|
328
387
|
v.name
|
|
329
388
|
for _, v in self.catalog_node.columns.items()
|
|
330
|
-
if v.name.lower() not in [c.lower() for c in
|
|
389
|
+
if v.name.lower() not in [c.lower() for c in model_columns]
|
|
331
390
|
]
|
|
332
391
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
392
|
+
if undocumented_columns:
|
|
393
|
+
raise DbtBouncerFailedCheckError(
|
|
394
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` has columns that are not included in the models properties file: {undocumented_columns}"
|
|
395
|
+
)
|
|
336
396
|
|
|
337
397
|
|
|
338
398
|
class CheckColumnsAreDocumentedInPublicModels(BaseCheck):
|
|
@@ -340,13 +400,14 @@ class CheckColumnsAreDocumentedInPublicModels(BaseCheck):
|
|
|
340
400
|
|
|
341
401
|
Receives:
|
|
342
402
|
catalog_node (CatalogNodes): The CatalogNodes object to check.
|
|
343
|
-
|
|
403
|
+
min_description_length (int | None): Minimum length required for the description to be considered populated.
|
|
404
|
+
models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
|
|
344
405
|
|
|
345
406
|
Other Parameters:
|
|
346
|
-
description (
|
|
347
|
-
exclude (
|
|
348
|
-
include (
|
|
349
|
-
severity (
|
|
407
|
+
description (str | None): Description of what the check does and why it is implemented.
|
|
408
|
+
exclude (str | None): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
|
|
409
|
+
include (str | None): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
|
|
410
|
+
severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.
|
|
350
411
|
|
|
351
412
|
Example(s):
|
|
352
413
|
```yaml
|
|
@@ -356,26 +417,35 @@ class CheckColumnsAreDocumentedInPublicModels(BaseCheck):
|
|
|
356
417
|
|
|
357
418
|
"""
|
|
358
419
|
|
|
359
|
-
catalog_node: "CatalogNodes" = Field(default=None)
|
|
360
|
-
|
|
420
|
+
catalog_node: "CatalogNodes | None" = Field(default=None)
|
|
421
|
+
min_description_length: int | None = Field(default=None)
|
|
422
|
+
models: list["DbtBouncerModelBase"] = Field(default=[])
|
|
361
423
|
name: Literal["check_columns_are_documented_in_public_models"]
|
|
362
424
|
|
|
363
425
|
def execute(self) -> None:
|
|
364
|
-
"""Execute the check.
|
|
426
|
+
"""Execute the check.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
DbtBouncerFailedCheckError: If columns are undocumented in public model.
|
|
430
|
+
|
|
431
|
+
"""
|
|
432
|
+
if self.catalog_node is None:
|
|
433
|
+
raise DbtBouncerFailedCheckError("self.catalog_node is None")
|
|
365
434
|
if self.is_catalog_node_a_model(self.catalog_node, self.models):
|
|
366
435
|
model = next(
|
|
367
436
|
m for m in self.models if m.unique_id == self.catalog_node.unique_id
|
|
368
437
|
)
|
|
369
438
|
non_complying_columns = []
|
|
370
439
|
for _, v in self.catalog_node.columns.items():
|
|
371
|
-
if model.access.value == "public":
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
440
|
+
if model.access and model.access.value == "public":
|
|
441
|
+
model_columns = model.columns or {}
|
|
442
|
+
column_config = model_columns.get(v.name)
|
|
443
|
+
if column_config is None or not self._is_description_populated(
|
|
444
|
+
column_config.description or "", self.min_description_length
|
|
376
445
|
):
|
|
377
446
|
non_complying_columns.append(v.name)
|
|
378
447
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
448
|
+
if non_complying_columns:
|
|
449
|
+
raise DbtBouncerFailedCheckError(
|
|
450
|
+
f"`{str(self.catalog_node.unique_id).split('.')[-1]}` is a public model but has columns that don't have a populated description: {non_complying_columns}"
|
|
451
|
+
)
|
dbt_bouncer/checks/common.py
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
from typing import Dict, List, Union
|
|
2
|
-
|
|
3
1
|
from pydantic import RootModel
|
|
4
2
|
|
|
5
3
|
|
|
4
|
+
class DbtBouncerFailedCheckError(Exception):
|
|
5
|
+
"""A custom exception class for failing dbt-bouncer checks."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str):
|
|
8
|
+
"""Initialize the DbtBouncerFailedCheck exception.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
message (str): The exception message.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
self.message = message
|
|
15
|
+
super().__init__(self.message)
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
"""Return the string representation of the exception.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: The exception message.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
return self.message
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
class NestedDict(RootModel): # type: ignore[type-arg]
|
|
7
|
-
root:
|
|
28
|
+
root: dict[str, "NestedDict"] | list["NestedDict"] | str
|