dbt-bouncer 1.25.0__tar.gz → 1.27.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/PKG-INFO +4 -5
  2. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/pyproject.toml +4 -5
  3. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/catalog/check_columns.py +52 -2
  4. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_exposures.py +47 -0
  5. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_models.py +44 -0
  6. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/runner.py +29 -6
  7. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/utils.py +34 -1
  8. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/version.py +1 -1
  9. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/LICENSE +0 -0
  10. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/README.md +0 -0
  11. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/__init__.py +0 -0
  12. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/dbt_cloud/README.md +0 -0
  13. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/dbt_cloud/catalog_latest.py +0 -0
  14. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/dbt_cloud/manifest_latest.py +0 -0
  15. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/dbt_cloud/run_results_latest.py +0 -0
  16. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/parsers_catalog.py +0 -0
  17. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/parsers_common.py +0 -0
  18. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/parsers_manifest.py +0 -0
  19. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/artifact_parsers/parsers_run_results.py +0 -0
  20. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/check_base.py +0 -0
  21. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/catalog/check_catalog_sources.py +0 -0
  22. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/common.py +0 -0
  23. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_lineage.py +0 -0
  24. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_macros.py +0 -0
  25. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_metadata.py +0 -0
  26. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_semantic_models.py +0 -0
  27. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_snapshots.py +0 -0
  28. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_sources.py +0 -0
  29. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/manifest/check_unit_tests.py +0 -0
  30. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/checks/run_results/check_run_results.py +0 -0
  31. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/config_file_parser.py +0 -0
  32. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/config_file_validator.py +0 -0
  33. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/logger.py +0 -0
  34. {dbt_bouncer-1.25.0 → dbt_bouncer-1.27.0}/src/dbt_bouncer/main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbt-bouncer
3
- Version: 1.25.0
3
+ Version: 1.27.0
4
4
  Summary: Configure and enforce conventions for your dbt project.
5
5
  License: MIT
6
6
  Keywords: python,cli,dbt,CI/CD
@@ -8,12 +8,10 @@ Author: Padraic Slattery
8
8
  Author-email: pgoslatara@gmail.com
9
9
  Maintainer: Padraic Slattery
10
10
  Maintainer-email: pgoslatara@gmail.com
11
- Requires-Python: >=3.9.2,<3.14
11
+ Requires-Python: >=3.11,<3.14
12
12
  Classifier: Programming Language :: Python
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
15
  Classifier: Programming Language :: Python :: 3.11
18
16
  Classifier: Programming Language :: Python :: 3.12
19
17
  Classifier: Programming Language :: Python :: 3.13
@@ -22,12 +20,13 @@ Requires-Dist: dbt-artifacts-parser (>=0.8)
22
20
  Requires-Dist: h11 (>=0.16.0)
23
21
  Requires-Dist: jinja2 (>=3,<4)
24
22
  Requires-Dist: jinja2-simple-tags (<1)
25
- Requires-Dist: levenshtein (>=0.26.1,<1)
23
+ Requires-Dist: levenshtein (>=0.26.1,<0.27.3)
26
24
  Requires-Dist: packaging (<25)
27
25
  Requires-Dist: poetry (>=2.0.1,<3.0.0)
28
26
  Requires-Dist: progress
29
27
  Requires-Dist: pydantic (>=2,<3)
30
28
  Requires-Dist: pyyaml (<7)
29
+ Requires-Dist: rapidfuzz (<3.14.0)
31
30
  Requires-Dist: requests (>=2,<3)
32
31
  Requires-Dist: semver (<4)
33
32
  Requires-Dist: tabulate (<1)
@@ -4,8 +4,6 @@ classifiers = [
4
4
  "Programming Language :: Python",
5
5
  "Programming Language :: Python :: 3",
6
6
  "Programming Language :: Python :: 3 :: Only",
7
- "Programming Language :: Python :: 3.9",
8
- "Programming Language :: Python :: 3.10",
9
7
  "Programming Language :: Python :: 3.11",
10
8
  "Programming Language :: Python :: 3.12",
11
9
  "Programming Language :: Python :: 3.13",
@@ -16,11 +14,12 @@ dependencies =[
16
14
  "h11 (>=0.16.0)", # To fix security warning
17
15
  "jinja2 (>=3,<4)",
18
16
  "jinja2-simple-tags (<1)",
19
- "levenshtein (>=0.26.1,<1)",
17
+ "levenshtein (>=0.26.1,<0.27.3)",
20
18
  "packaging (<25)",
21
19
  "progress",
22
20
  "pydantic (>=2,<3)",
23
21
  "pyyaml (<7)",
22
+ "rapidfuzz (<3.14.0)",
24
23
  "requests (>=2,<3)",
25
24
  "tabulate (<1)",
26
25
  "toml (<1)",
@@ -39,8 +38,8 @@ maintainers = [{name="Padraic Slattery",email="pgoslatara@gmail.com"}]
39
38
  name = "dbt-bouncer"
40
39
  readme = "README.md"
41
40
  repository = "https://github.com/godatadriven/dbt-bouncer"
42
- requires-python = ">=3.9.2,<3.14"
43
- version = "1.25.0"
41
+ requires-python = ">=3.11,<3.14"
42
+ version = "1.27.0"
44
43
 
45
44
  [project.scripts]
46
45
  dbt-bouncer = "dbt_bouncer.main:cli"
@@ -3,6 +3,8 @@
3
3
  import re
4
4
  from typing import TYPE_CHECKING, List, Literal, Optional
5
5
 
6
+ from pydantic import ConfigDict, Field
7
+
6
8
  if TYPE_CHECKING:
7
9
  import warnings
8
10
 
@@ -16,7 +18,7 @@ if TYPE_CHECKING:
16
18
  DbtBouncerTestBase,
17
19
  )
18
20
 
19
- from pydantic import Field, model_validator
21
+ from pydantic import model_validator
20
22
 
21
23
  from dbt_bouncer.check_base import BaseCheck
22
24
 
@@ -130,7 +132,7 @@ class CheckColumnHasSpecifiedTest(BaseCheck):
130
132
  class CheckColumnNameCompliesToColumnType(BaseCheck):
131
133
  """Columns with the specified regexp naming pattern must have data types that comply to the specified regexp pattern or list of data types.
132
134
 
133
- Note: Oe of `type_pattern` or `types` must be specified.
135
+ Note: One of `type_pattern` or `types` must be specified.
134
136
 
135
137
  Parameters:
136
138
  column_name_pattern (str): Regex pattern to match the model name.
@@ -228,6 +230,54 @@ class CheckColumnNameCompliesToColumnType(BaseCheck):
228
230
  return self
229
231
 
230
232
 
233
+ class CheckColumnNames(BaseCheck):
234
+ """Columns must have a name that matches the supplied regex.
235
+
236
+ Parameters:
237
+ columns_name_pattern (str): Regexp the column name must match.
238
+
239
+ Receives:
240
+ catalog_node (CatalogNodes): The CatalogNodes object to check.
241
+ models (List[DbtBouncerModelBase]): List of DbtBouncerModelBase objects parsed from `manifest.json`.
242
+
243
+ 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`.
249
+
250
+ Example(s):
251
+ ```yaml
252
+ catalog_checks:
253
+ - name: check_column_names
254
+ column_name_pattern: [a-z_] # Lowercase only, underscores allowed
255
+ ```
256
+
257
+ """
258
+
259
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
260
+
261
+ catalog_node: "CatalogNodes" = Field(default=None)
262
+ column_name_pattern: str
263
+ models: List["DbtBouncerModelBase"] = Field(default=[])
264
+ name: Literal["check_column_names"]
265
+
266
+ def execute(self) -> None:
267
+ """Execute the check."""
268
+ if self.is_catalog_node_a_model(self.catalog_node, self.models):
269
+ non_complying_columns: List[str] = []
270
+ non_complying_columns.extend(
271
+ v.name
272
+ for _, v in self.catalog_node.columns.items()
273
+ if re.fullmatch(self.column_name_pattern.strip(), v.name) is None
274
+ )
275
+
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
+ )
279
+
280
+
231
281
  class CheckColumnsAreAllDocumented(BaseCheck):
232
282
  """All columns in a model should be included in the model's properties file, i.e. `.yml` file.
233
283
 
@@ -14,6 +14,53 @@ if TYPE_CHECKING:
14
14
  )
15
15
 
16
16
 
17
+ class CheckExposureOnModel(BaseCheck):
18
+ """Exposures should depend on a model.
19
+
20
+ Parameters:
21
+ max_number_of_models (Optional[int]): The maximum number of models an exposure can depend on, defaults to 100.
22
+ min_number_of_models (Optional[int]): The minimum number of models an exposure can depend on, defaults to 1.
23
+
24
+ Receives:
25
+ exposure (DbtBouncerExposureBase): The DbtBouncerExposureBase object to check.
26
+
27
+ Other Parameters:
28
+ description (Optional[str]): Description of what the check does and why it is implemented.
29
+ exclude (Optional[str]): Regex pattern to match the exposure path (i.e the .yml file where the exposure is configured). Exposure paths that match the pattern will not be checked.
30
+ include (Optional[str]): Regex pattern to match the exposure path (i.e the .yml file where the exposure is configured). Only exposure paths that match the pattern will be checked.
31
+ severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
32
+
33
+ Example(s):
34
+ ```yaml
35
+ manifest_checks:
36
+ - name: check_exposure_based_on_model
37
+ ```
38
+ ```yaml
39
+ manifest_checks:
40
+ - name: check_exposure_based_on_model
41
+ maximum_number_of_models: 3
42
+ minimum_number_of_models: 1
43
+ ```
44
+
45
+ """
46
+
47
+ exposure: "DbtBouncerExposureBase" = Field(default=None)
48
+ maximum_number_of_models: int = Field(default=100)
49
+ minimum_number_of_models: int = Field(default=1)
50
+ name: Literal["check_exposure_based_on_model"]
51
+
52
+ def execute(self) -> None:
53
+ """Execute the check."""
54
+ number_of_upstream_models = len(self.exposure.depends_on.nodes)
55
+
56
+ assert self.minimum_number_of_models <= number_of_upstream_models, (
57
+ f"`{self.exposure.name}` is based on less models ({number_of_upstream_models}) than the minimum permitted ({self.minimum_number_of_models})."
58
+ )
59
+ assert number_of_upstream_models <= self.maximum_number_of_models, (
60
+ f"`{self.exposure.name}` is based on more models ({number_of_upstream_models}) than the maximum permitted ({self.maximum_number_of_models})."
61
+ )
62
+
63
+
17
64
  class CheckExposureOnNonPublicModels(BaseCheck):
18
65
  """Exposures should be based on public models only.
19
66
 
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
14
14
  UnitTests,
15
15
  )
16
16
  from dbt_bouncer.artifact_parsers.parsers_common import (
17
+ DbtBouncerExposureBase,
17
18
  DbtBouncerManifest,
18
19
  DbtBouncerModelBase,
19
20
  DbtBouncerTestBase,
@@ -521,6 +522,49 @@ class CheckModelHasContractsEnforced(BaseCheck):
521
522
  )
522
523
 
523
524
 
525
+ class CheckModelHasExposure(BaseCheck):
526
+ """Models must have an exposure.
527
+
528
+ Receives:
529
+ exposures (List[DbtBouncerExposureBase]): List of DbtBouncerExposureBase objects parsed from `manifest.json`.
530
+ model (DbtBouncerModelBase): The DbtBouncerModelBase object to check.
531
+
532
+ Other Parameters:
533
+ description (Optional[str]): Description of what the check does and why it is implemented.
534
+ exclude (Optional[str]): Regex pattern to match the model path. Model paths that match the pattern will not be checked.
535
+ include (Optional[str]): Regex pattern to match the model path. Only model paths that match the pattern will be checked.
536
+ materialization (Optional[Literal["ephemeral", "incremental", "table", "view"]]): Limit check to models with the specified materialization.
537
+ severity (Optional[Literal["error", "warn"]]): Severity level of the check. Default: `error`.
538
+
539
+ Example(s):
540
+ ```yaml
541
+ manifest_checks:
542
+ - name: check_model_has_exposure
543
+ description: Ensure all marts are part of an exposure.
544
+ include: ^models/marts
545
+ ```
546
+
547
+ """
548
+
549
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
550
+
551
+ exposures: List["DbtBouncerExposureBase"] = Field(default=[])
552
+ model: "DbtBouncerModelBase" = Field(default=None)
553
+ name: Literal["check_model_has_exposure"]
554
+
555
+ def execute(self) -> None:
556
+ """Execute the check."""
557
+ has_exposure = False
558
+ for e in self.exposures:
559
+ for m in e.depends_on.nodes:
560
+ if m == self.model.unique_id:
561
+ has_exposure = True
562
+
563
+ assert has_exposure, (
564
+ f"`{get_clean_model_name(self.model.unique_id)}` does not have an associated exposure."
565
+ )
566
+
567
+
524
568
  class CheckModelHasMetaKeys(BaseCheck):
525
569
  """The `meta` config for models must have the specified keys.
526
570
 
@@ -14,6 +14,7 @@ from tabulate import tabulate
14
14
  from dbt_bouncer.utils import (
15
15
  create_github_comment_file,
16
16
  get_check_objects,
17
+ get_nested_value,
17
18
  resource_in_path,
18
19
  )
19
20
 
@@ -114,14 +115,36 @@ def runner(
114
115
  iterate_value = next(iter(iterate_over_value))
115
116
  for i in locals()[f"{iterate_value}s"]:
116
117
  check_i = copy.deepcopy(check)
118
+ if iterate_value in ["model", "semantic_model", "snapshot", "source"]:
119
+ try:
120
+ d = getattr(i, iterate_value).config.meta
121
+ except Exception:
122
+ d = getattr(i, iterate_value).meta
123
+ elif iterate_value in ["catalog_node", "run_result"]:
124
+ d = {}
125
+ elif iterate_value in ["macro"]:
126
+ d = i.meta
127
+ else:
128
+ try:
129
+ d = i.config.meta
130
+ except Exception:
131
+ d = i.meta
132
+ meta_config = get_nested_value(
133
+ d,
134
+ ["dbt-bouncer", "skip_checks"],
135
+ [],
136
+ )
117
137
  if resource_in_path(check_i, i) and (
118
- iterate_over_value != {"model"}
119
- or (
120
- iterate_over_value == {"model"}
121
- and check_i.materialization == i.model.config.materialized
122
- if check_i.materialization is not None
123
- else True
138
+ (
139
+ iterate_over_value != {"model"}
140
+ or (
141
+ iterate_over_value == {"model"}
142
+ and check_i.materialization == i.model.config.materialized
143
+ if check_i.materialization is not None
144
+ else True
145
+ )
124
146
  )
147
+ and (check_i.name not in meta_config if meta_config != [] else True)
125
148
  ):
126
149
  check_run_id = (
127
150
  f"{check_i.name}:{check_i.index}:{i.unique_id.split('.')[-1]}"
@@ -9,7 +9,7 @@ import re
9
9
  import sys
10
10
  from functools import lru_cache
11
11
  from pathlib import Path
12
- from typing import TYPE_CHECKING, Any, List, Mapping, Type
12
+ from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Type
13
13
 
14
14
  import click
15
15
  import yaml
@@ -58,6 +58,39 @@ def create_github_comment_file(
58
58
  f.write(md_formatted_comment)
59
59
 
60
60
 
61
+ def get_nested_value(
62
+ d: Dict[str, Any], keys: List[str], default: Optional[Any] = None
63
+ ) -> Any:
64
+ """Retrieve a value from a nested dictionary using a list of keys.
65
+
66
+ This function safely traverses a dictionary structure, allowing access to
67
+ deeply nested values. If any key in the `keys` list does not exist at
68
+ its respective level, the function returns the specified default value.
69
+
70
+ Args:
71
+ d: The dictionary to traverse.
72
+ keys: A list of strings representing the sequence of keys to follow
73
+ to reach the desired nested value.
74
+ default: The value to return if any key in the `keys` list is not
75
+ found at its corresponding level, or if an intermediate
76
+ value is not a dictionary. Defaults to None.
77
+
78
+ Returns:
79
+ The value found at the specified nested path, or the `default` value
80
+ if any part of the path is invalid or not found.
81
+
82
+ """
83
+ current_level = d
84
+ for key in keys:
85
+ if isinstance(current_level, dict):
86
+ current_level = current_level.get(key, default) # type: ignore[assignment]
87
+ if current_level is default and key != keys[-1]:
88
+ return default
89
+ else:
90
+ return default
91
+ return current_level
92
+
93
+
61
94
  def resource_in_path(check, resource) -> bool:
62
95
  """Validate that a check is applicable to a specific resource path.
63
96
 
@@ -5,4 +5,4 @@ def version() -> str:
5
5
  str: The version of `dbt-bouncer`.
6
6
 
7
7
  """
8
- return "1.25.0"
8
+ return "1.27.0"
File without changes
File without changes