dbt-bouncer 1.31.2rc2__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.
Files changed (36) hide show
  1. dbt_bouncer/artifact_parsers/dbt_cloud/catalog_latest.py +21 -21
  2. dbt_bouncer/artifact_parsers/dbt_cloud/manifest_latest.py +1745 -1745
  3. dbt_bouncer/artifact_parsers/dbt_cloud/run_results_latest.py +22 -22
  4. dbt_bouncer/artifact_parsers/parsers_catalog.py +26 -24
  5. dbt_bouncer/artifact_parsers/parsers_common.py +57 -36
  6. dbt_bouncer/artifact_parsers/parsers_manifest.py +98 -69
  7. dbt_bouncer/artifact_parsers/parsers_run_results.py +32 -19
  8. dbt_bouncer/check_base.py +22 -11
  9. dbt_bouncer/checks/catalog/check_catalog_sources.py +22 -12
  10. dbt_bouncer/checks/catalog/check_columns.py +175 -105
  11. dbt_bouncer/checks/common.py +24 -3
  12. dbt_bouncer/checks/manifest/check_exposures.py +79 -52
  13. dbt_bouncer/checks/manifest/check_lineage.py +69 -40
  14. dbt_bouncer/checks/manifest/check_macros.py +177 -104
  15. dbt_bouncer/checks/manifest/check_metadata.py +28 -18
  16. dbt_bouncer/checks/manifest/check_models.py +842 -496
  17. dbt_bouncer/checks/manifest/check_seeds.py +63 -0
  18. dbt_bouncer/checks/manifest/check_semantic_models.py +28 -20
  19. dbt_bouncer/checks/manifest/check_snapshots.py +57 -33
  20. dbt_bouncer/checks/manifest/check_sources.py +246 -137
  21. dbt_bouncer/checks/manifest/check_unit_tests.py +97 -54
  22. dbt_bouncer/checks/run_results/check_run_results.py +34 -20
  23. dbt_bouncer/config_file_parser.py +47 -28
  24. dbt_bouncer/config_file_validator.py +11 -8
  25. dbt_bouncer/global_context.py +31 -0
  26. dbt_bouncer/main.py +128 -67
  27. dbt_bouncer/runner.py +61 -31
  28. dbt_bouncer/utils.py +146 -50
  29. dbt_bouncer/version.py +1 -1
  30. {dbt_bouncer-1.31.2rc2.dist-info → dbt_bouncer-2.0.0.dist-info}/METADATA +15 -15
  31. dbt_bouncer-2.0.0.dist-info/RECORD +37 -0
  32. dbt_bouncer-1.31.2rc2.dist-info/RECORD +0 -35
  33. {dbt_bouncer-1.31.2rc2.dist-info → dbt_bouncer-2.0.0.dist-info}/WHEEL +0 -0
  34. {dbt_bouncer-1.31.2rc2.dist-info → dbt_bouncer-2.0.0.dist-info}/entry_points.txt +0 -0
  35. {dbt_bouncer-1.31.2rc2.dist-info → dbt_bouncer-2.0.0.dist-info}/licenses/LICENSE +0 -0
  36. {dbt_bouncer-1.31.2rc2.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, List, Literal, Optional
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 (List[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
34
+ models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
32
35
 
33
36
  Other Parameters:
34
- description (Optional[str]): Description of what the check does and why it is implemented.
35
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
36
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
37
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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
- models: List["DbtBouncerModelBase"] = Field(default=[])
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
- if model.columns.get(
61
- v.name
62
- ) is None or not self.is_description_populated(
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
- assert not non_complying_columns, (
68
- f"`{self.catalog_node.unique_id.split('.')[-1]}` has columns that do not have a populated description: {non_complying_columns}"
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 (List[DbtBouncerTestBase]): List of DbtBouncerTestBase objects parsed from `manifest.json`.
97
+ tests (list[DbtBouncerTestBase]): List of DbtBouncerTestBase objects parsed from `manifest.json`.
82
98
 
83
99
  Other Parameters:
84
- description (Optional[str]): Description of what the check does and why it is implemented.
85
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
86
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
87
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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: List["DbtBouncerTestBase"] = Field(default=[])
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) is not None
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 [f"{t.attached_node}.{t.column_name}" for t in relevant_tests]
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
- assert not non_complying_columns, (
128
- f"`{self.catalog_node.unique_id.split('.')[-1]}` has columns that should have a `{self.test_name}` test: {non_complying_columns}"
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 (Optional[str]): Regex pattern to match the data types.
140
- types (Optional[List[str]]): List of data types to check.
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 (Optional[str]): Description of what the check does and why it is implemented.
147
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
148
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
149
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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: Optional[str] = None
195
- types: Optional[List[str]] = None
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
- assert not non_complying_columns, (
209
- f"`{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}"
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) is None
256
+ and re.compile(self.column_name_pattern.strip()).match(str(v.name))
257
+ is None
218
258
  ]
219
259
 
220
- assert not non_complying_columns, (
221
- f"`{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}"
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 (List[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
282
+ models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
242
283
 
243
284
  Other Parameters:
244
- description (Optional[str]): Description of what the check does and why it is implemented.
245
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
246
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
247
- materialization (Optional[Literal["ephemeral", "incremental", "table", "view"]]): Limit check to models with the specified materialization.
248
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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: List["DbtBouncerModelBase"] = Field(default=[])
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: List[str] = []
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
- assert not non_complying_columns, (
277
- f"`{self.catalog_node.unique_id.split('.')[-1]}` has columns ({non_complying_columns}) that do not match the supplied regex: `{self.column_name_pattern.strip()}`."
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 (Optional[bool]): 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`.
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 (List[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
337
+ models (list[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
289
338
 
290
339
  Other Parameters:
291
- description (Optional[str]): Description of what the check does and why it is implemented.
292
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
293
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
294
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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: Optional[bool] = Field(default=True)
305
- catalog_node: "CatalogNodes" = Field(default=None)
306
- manifest_obj: "DbtBouncerManifest" = Field(default=None)
307
- models: List["DbtBouncerModelBase"] = Field(default=[])
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 model.columns
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 model.columns]
389
+ if v.name.lower() not in [c.lower() for c in model_columns]
331
390
  ]
332
391
 
333
- assert not undocumented_columns, (
334
- f"`{self.catalog_node.unique_id.split('.')[-1]}` has columns that are not included in the models properties file: {undocumented_columns}"
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
- models (List[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
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 (Optional[str]): Description of what the check does and why it is implemented.
347
- exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
348
- include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
349
- severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
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
- models: List["DbtBouncerModelBase"] = Field(default=[])
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
- column_config = model.columns.get(v.name)
373
- if (
374
- column_config is None
375
- or len(column_config.description.strip()) < 4
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
- assert not non_complying_columns, (
380
- f"`{self.catalog_node.unique_id.split('.')[-1]}` is a public model but has columns that don't have a populated description: {non_complying_columns}"
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
+ )
@@ -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: Union[Dict[str, "NestedDict"], List["NestedDict"], str]
28
+ root: dict[str, "NestedDict"] | list["NestedDict"] | str