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.
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.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/METADATA +15 -15
  31. dbt_bouncer-2.0.0rc1.dist-info/RECORD +37 -0
  32. dbt_bouncer-1.31.2rc3.dist-info/RECORD +0 -35
  33. {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/WHEEL +0 -0
  34. {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/entry_points.txt +0 -0
  35. {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  36. {dbt_bouncer-1.31.2rc3.dist-info → dbt_bouncer-2.0.0rc1.dist-info}/top_level.txt +0 -0
dbt_bouncer/main.py CHANGED
@@ -1,70 +1,43 @@
1
+ import importlib
1
2
  import logging
3
+ import sys
2
4
  from pathlib import Path, PurePath
3
- from typing import Union
4
5
 
5
6
  import click
6
7
 
8
+ from dbt_bouncer.global_context import BouncerContext, set_context
7
9
  from dbt_bouncer.logger import configure_console_logging
8
10
  from dbt_bouncer.version import version
9
11
 
10
12
 
11
- @click.command()
12
- @click.option(
13
- "--config-file",
14
- default=Path("dbt-bouncer.yml"),
15
- help="Location of the YML config file.",
16
- required=False,
17
- type=PurePath,
18
- )
19
- @click.option(
20
- "--create-pr-comment-file",
21
- default=False,
22
- help="Create a `github-comment.md` file that will be sent to GitHub as a PR comment. Defaults to True when `dbt-bouncer` is run as a GitHub Action.",
23
- hidden=True,
24
- required=False,
25
- type=click.BOOL,
26
- )
27
- @click.option(
28
- "--only",
29
- default="",
30
- help="Limit the checks run to specific categories, comma-separated. Examples: 'manifest_checks', 'catalog_checks,manifest_checks'.",
31
- required=False,
32
- type=str,
33
- )
34
- @click.option(
35
- "--output-file",
36
- default=None,
37
- help="Location of the json file where check metadata will be saved.",
38
- required=False,
39
- type=Path,
40
- )
41
- @click.option(
42
- "--output-only-failures",
43
- help="If passed then only failures will be included in the output file.",
44
- is_flag=True,
45
- )
46
- @click.option(
47
- "--show-all-failures",
48
- help="If passed then all failures will be printed to the console.",
49
- is_flag=True,
50
- )
51
- @click.option("-v", "--verbosity", help="Verbosity.", default=0, count=True)
52
- @click.pass_context
53
- @click.version_option()
54
- def cli(
55
- ctx: click.Context,
56
- config_file: PurePath,
57
- create_pr_comment_file: bool,
58
- only: str,
59
- output_file: Union[Path, None],
60
- output_only_failures: bool,
61
- show_all_failures: bool,
62
- verbosity: int,
63
- ) -> None:
64
- """Entrypoint for dbt-bouncer.
13
+ def run_bouncer(
14
+ config_file: PurePath | None = None,
15
+ create_pr_comment_file: bool = False,
16
+ only: str = "",
17
+ output_file: Path | None = None,
18
+ output_only_failures: bool = False,
19
+ show_all_failures: bool = False,
20
+ verbosity: int = 0,
21
+ config_file_source: str | None = None,
22
+ ) -> int:
23
+ """Programmatic entrypoint for dbt-bouncer.
24
+
25
+ Args:
26
+ config_file: Location of the YML config file.
27
+ create_pr_comment_file: Create a `github-comment.md` file.
28
+ only: Limit the checks run to specific categories.
29
+ output_file: Location of the json file where check metadata will be saved.
30
+ output_only_failures: Only failures will be included in the output file.
31
+ show_all_failures: All failures will be printed to the console.
32
+ verbosity: Verbosity level.
33
+ config_file_source: Source of the config file ("COMMANDLINE", "DEFAULT", etc.).
34
+
35
+ Returns:
36
+ int: Exit code (0 for success, 1 for failure).
65
37
 
66
38
  Raises:
67
- RuntimeError: If output file has an invalid extension.
39
+ AssertionError: If config_file_source is None.
40
+ RuntimeError: If output file has an invalid extension or other runtime errors.
68
41
 
69
42
  """
70
43
  configure_console_logging(verbosity)
@@ -94,11 +67,20 @@ def cli(
94
67
  load_config_file_contents,
95
68
  )
96
69
 
70
+ if config_file is None:
71
+ config_file = Path("dbt-bouncer.yml")
72
+ if config_file_source is None:
73
+ config_file_source = "DEFAULT"
74
+ else:
75
+ if config_file_source is None:
76
+ config_file_source = "COMMANDLINE"
77
+
78
+ if config_file_source is None:
79
+ raise AssertionError("config_file_source cannot be None")
80
+
97
81
  config_file_path = get_config_file_path(
98
82
  config_file=config_file,
99
- config_file_source=click.get_current_context()
100
- .get_parameter_source("config_file")
101
- .name, # type: ignore[union-attr]
83
+ config_file_source=config_file_source,
102
84
  )
103
85
  config_file_contents = load_config_file_contents(
104
86
  config_file_path, allow_default_config_file_creation=True
@@ -121,16 +103,23 @@ def cli(
121
103
  ]
122
104
  logging.debug(f"{check_categories=}")
123
105
 
124
- # Set click context object for dbt_bouncer.utils.get_check_objects()
125
- ctx.obj = {
126
- "config_file_path": config_file_path,
127
- "custom_checks_dir": config_file_contents.get("custom_checks_dir"),
128
- }
106
+ # Set global context for dbt_bouncer.utils.get_check_objects() and config_file_parser
107
+ set_context(
108
+ BouncerContext(
109
+ config_file_path=config_file_path,
110
+ custom_checks_dir=config_file_contents.get("custom_checks_dir"),
111
+ )
112
+ )
113
+
114
+ # If config_file_parser is already loaded, reload it so that check types are updated (necessary for parallel tests)
115
+ if "dbt_bouncer.config_file_parser" in sys.modules:
116
+ importlib.reload(sys.modules["dbt_bouncer.config_file_parser"])
129
117
 
130
118
  from dbt_bouncer.config_file_validator import validate_conf
131
119
 
132
120
  bouncer_config = validate_conf(
133
- check_categories=check_categories, config_file_contents=config_file_contents
121
+ check_categories=check_categories,
122
+ config_file_contents=dict(config_file_contents),
134
123
  )
135
124
  del config_file_contents
136
125
  logging.debug(f"{bouncer_config=}")
@@ -152,7 +141,9 @@ def cli(
152
141
 
153
142
  logging.debug(f"{bouncer_config=}")
154
143
 
155
- dbt_artifacts_dir = config_file.parent / bouncer_config.dbt_artifacts_dir
144
+ dbt_artifacts_dir = Path(
145
+ config_file_path.parent / (bouncer_config.dbt_artifacts_dir or "target")
146
+ )
156
147
 
157
148
  from dbt_bouncer.artifact_parsers.parsers_common import parse_dbt_artifacts
158
149
 
@@ -161,6 +152,7 @@ def cli(
161
152
  project_exposures,
162
153
  project_macros,
163
154
  project_models,
155
+ project_seeds,
164
156
  project_semantic_models,
165
157
  project_snapshots,
166
158
  project_sources,
@@ -189,6 +181,7 @@ def cli(
189
181
  output_file=output_file,
190
182
  output_only_failures=output_only_failures,
191
183
  run_results=project_run_results,
184
+ seeds=project_seeds,
192
185
  semantic_models=project_semantic_models,
193
186
  show_all_failures=show_all_failures,
194
187
  snapshots=project_snapshots,
@@ -196,4 +189,72 @@ def cli(
196
189
  tests=project_tests,
197
190
  unit_tests=project_unit_tests,
198
191
  )
199
- ctx.exit(results[0])
192
+ return results[0]
193
+
194
+
195
+ @click.command()
196
+ @click.option(
197
+ "--config-file",
198
+ default=Path("dbt-bouncer.yml"),
199
+ help="Location of the YML config file.",
200
+ required=False,
201
+ type=PurePath,
202
+ )
203
+ @click.option(
204
+ "--create-pr-comment-file",
205
+ default=False,
206
+ help="Create a `github-comment.md` file that will be sent to GitHub as a PR comment. Defaults to True when `dbt-bouncer` is run as a GitHub Action.",
207
+ hidden=True,
208
+ required=False,
209
+ type=click.BOOL,
210
+ )
211
+ @click.option(
212
+ "--only",
213
+ default="",
214
+ help="Limit the checks run to specific categories, comma-separated. Examples: 'manifest_checks', 'catalog_checks,manifest_checks'.",
215
+ required=False,
216
+ type=str,
217
+ )
218
+ @click.option(
219
+ "--output-file",
220
+ default=None,
221
+ help="Location of the json file where check metadata will be saved.",
222
+ required=False,
223
+ type=Path,
224
+ )
225
+ @click.option(
226
+ "--output-only-failures",
227
+ help="If passed then only failures will be included in the output file.",
228
+ is_flag=True,
229
+ )
230
+ @click.option(
231
+ "--show-all-failures",
232
+ help="If passed then all failures will be printed to the console.",
233
+ is_flag=True,
234
+ )
235
+ @click.option("-v", "--verbosity", help="Verbosity.", default=0, count=True)
236
+ @click.pass_context
237
+ @click.version_option()
238
+ def cli(
239
+ ctx: click.Context,
240
+ config_file: PurePath,
241
+ create_pr_comment_file: bool,
242
+ only: str,
243
+ output_file: Path | None,
244
+ output_only_failures: bool,
245
+ show_all_failures: bool,
246
+ verbosity: int,
247
+ ) -> None:
248
+ """Entrypoint for dbt-bouncer."""
249
+ config_file_source = ctx.get_parameter_source("config_file").name # type: ignore[union-attr]
250
+ exit_code = run_bouncer(
251
+ config_file=config_file,
252
+ create_pr_comment_file=create_pr_comment_file,
253
+ only=only,
254
+ output_file=output_file,
255
+ output_only_failures=output_only_failures,
256
+ show_all_failures=show_all_failures,
257
+ verbosity=verbosity,
258
+ config_file_source=config_file_source,
259
+ )
260
+ ctx.exit(exit_code)
dbt_bouncer/runner.py CHANGED
@@ -1,16 +1,19 @@
1
1
  """Assemble and run all checks."""
2
2
 
3
3
  import copy
4
+ import inspect
4
5
  import json
5
6
  import logging
6
7
  import operator
7
8
  import traceback
8
9
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Dict, List, Union
10
+ from typing import TYPE_CHECKING, Any
10
11
 
12
+ import click
11
13
  from progress.bar import Bar
12
14
  from tabulate import tabulate
13
15
 
16
+ from dbt_bouncer.checks.common import DbtBouncerFailedCheckError
14
17
  from dbt_bouncer.utils import (
15
18
  create_github_comment_file,
16
19
  get_check_objects,
@@ -20,8 +23,6 @@ from dbt_bouncer.utils import (
20
23
 
21
24
  if TYPE_CHECKING:
22
25
  from dbt_bouncer.artifact_parsers.dbt_cloud.manifest_latest import (
23
- Exposures,
24
- Macros,
25
26
  UnitTests,
26
27
  )
27
28
  from dbt_bouncer.artifact_parsers.parsers_common import (
@@ -29,11 +30,16 @@ if TYPE_CHECKING:
29
30
  DbtBouncerManifest,
30
31
  DbtBouncerModel,
31
32
  DbtBouncerRunResult,
33
+ DbtBouncerSeed,
32
34
  DbtBouncerSemanticModel,
33
35
  DbtBouncerSnapshot,
34
36
  DbtBouncerSource,
35
37
  DbtBouncerTest,
36
38
  )
39
+ from dbt_bouncer.artifact_parsers.parsers_manifest import (
40
+ DbtBouncerExposureBase,
41
+ DbtBouncerMacroBase,
42
+ )
37
43
  from dbt_bouncer.config_file_parser import (
38
44
  DbtBouncerConfAllCategories as DbtBouncerConf,
39
45
  )
@@ -41,36 +47,46 @@ if TYPE_CHECKING:
41
47
 
42
48
  def runner(
43
49
  bouncer_config: "DbtBouncerConf",
44
- catalog_nodes: List["DbtBouncerCatalogNode"],
45
- catalog_sources: List["DbtBouncerCatalogNode"],
46
- check_categories: List[str],
50
+ catalog_nodes: list["DbtBouncerCatalogNode"],
51
+ catalog_sources: list["DbtBouncerCatalogNode"],
52
+ check_categories: list[str],
47
53
  create_pr_comment_file: bool,
48
- exposures: List["Exposures"],
49
- macros: List["Macros"],
54
+ exposures: list["DbtBouncerExposureBase"],
55
+ macros: list["DbtBouncerMacroBase"],
50
56
  manifest_obj: "DbtBouncerManifest",
51
- models: List["DbtBouncerModel"],
52
- output_file: Union[Path, None],
53
- run_results: List["DbtBouncerRunResult"],
54
- semantic_models: List["DbtBouncerSemanticModel"],
57
+ models: list["DbtBouncerModel"],
58
+ output_file: Path | None,
59
+ run_results: list["DbtBouncerRunResult"],
60
+ seeds: list["DbtBouncerSeed"],
61
+ semantic_models: list["DbtBouncerSemanticModel"],
55
62
  output_only_failures: bool,
56
63
  show_all_failures: bool,
57
- snapshots: List["DbtBouncerSnapshot"],
58
- sources: List["DbtBouncerSource"],
59
- tests: List["DbtBouncerTest"],
60
- unit_tests: List["UnitTests"],
61
- ) -> tuple[int, List[Any]]:
64
+ snapshots: list["DbtBouncerSnapshot"],
65
+ sources: list["DbtBouncerSource"],
66
+ tests: list["DbtBouncerTest"],
67
+ unit_tests: list["UnitTests"],
68
+ ) -> tuple[int, list[Any]]:
62
69
  """Run dbt-bouncer checks.
63
70
 
64
71
  Returns:
65
- tuple[int, List[Any]]: A tuple containing the exit code and a list of failed checks.
72
+ tuple[int, list[Any]]: A tuple containing the exit code and a list of failed checks.
66
73
 
67
74
  Raises:
68
75
  RuntimeError: If more than one "iterate_over" argument is found.
69
76
 
70
77
  """
71
- check_classes: List[Dict[str, Union[Any, str]]] = [
72
- {"class": getattr(x, x.__name__), "source_file": x.__file__}
73
- for x in get_check_objects()
78
+ try:
79
+ ctx = click.get_current_context()
80
+ config_file_path = ctx.obj.get("config_file_path")
81
+ custom_checks_dir = ctx.obj.get("custom_checks_dir")
82
+ if custom_checks_dir:
83
+ custom_checks_dir = config_file_path.parent / custom_checks_dir
84
+ except (RuntimeError, AttributeError, KeyError):
85
+ custom_checks_dir = None
86
+
87
+ check_classes: list[dict[str, Any | str]] = [
88
+ {"class": x, "source_file": inspect.getfile(x)}
89
+ for x in get_check_objects(custom_checks_dir)
74
90
  ]
75
91
  for c in check_classes:
76
92
  locals()[c["class"].__name__] = c["class"] # type: ignore[union-attr]
@@ -83,6 +99,7 @@ def runner(
83
99
  "manifest_obj": manifest_obj,
84
100
  "models": [m.model for m in models],
85
101
  "run_results": [r.run_result for r in run_results],
102
+ "seeds": [s.seed for s in seeds],
86
103
  "semantic_models": [s.semantic_model for s in semantic_models],
87
104
  "snapshots": [s.snapshot for s in snapshots],
88
105
  "sources": sources,
@@ -103,19 +120,26 @@ def runner(
103
120
  "macro",
104
121
  "model",
105
122
  "run_result",
123
+ "seed",
106
124
  "semantic_model",
107
125
  "snapshot",
108
126
  "source",
109
127
  "unit_test",
110
128
  }
111
129
  iterate_over_value = valid_iterate_over_values.intersection(
112
- set(check.__annotations__.keys()),
130
+ set(check.__class__.__annotations__.keys()),
113
131
  )
114
132
  if len(iterate_over_value) == 1:
115
133
  iterate_value = next(iter(iterate_over_value))
116
134
  for i in locals()[f"{iterate_value}s"]:
117
135
  check_i = copy.deepcopy(check)
118
- if iterate_value in ["model", "semantic_model", "snapshot", "source"]:
136
+ if iterate_value in [
137
+ "model",
138
+ "seed",
139
+ "semantic_model",
140
+ "snapshot",
141
+ "source",
142
+ ]:
119
143
  try:
120
144
  d = getattr(i, iterate_value).config.meta
121
145
  except Exception:
@@ -153,7 +177,9 @@ def runner(
153
177
  )
154
178
  setattr(check_i, iterate_value, getattr(i, iterate_value, i))
155
179
 
156
- for x in parsed_data.keys() & check_i.__annotations__.keys():
180
+ for x in (
181
+ parsed_data.keys() & check_i.__class__.__annotations__.keys()
182
+ ):
157
183
  setattr(check_i, x, parsed_data[x])
158
184
 
159
185
  checks_to_run.append(
@@ -169,7 +195,7 @@ def runner(
169
195
  )
170
196
  else:
171
197
  check_run_id = f"{check.name}:{check.index}"
172
- for x in parsed_data.keys() & check.__annotations__.keys():
198
+ for x in parsed_data.keys() & check.__class__.__annotations__.keys():
173
199
  setattr(check, x, parsed_data[x])
174
200
  checks_to_run.append(
175
201
  {
@@ -188,21 +214,25 @@ def runner(
188
214
  check["check"].execute()
189
215
  check["outcome"] = "success"
190
216
  except Exception as e:
191
- failure_message_full = list(
192
- traceback.TracebackException.from_exception(e).format(),
193
- )
194
- failure_message = failure_message_full[-1].strip()
217
+ if isinstance(e, DbtBouncerFailedCheckError):
218
+ failure_message = e.message
219
+ else:
220
+ failure_message_full = list(
221
+ traceback.TracebackException.from_exception(e).format(),
222
+ )
223
+ failure_message = failure_message_full[-1].strip()
224
+
195
225
  if check["check"].description:
196
226
  failure_message = f"{check['check'].description} - {failure_message}"
197
227
 
198
228
  logging.debug(
199
- f"Check {check['check_run_id']} failed: {' '.join(failure_message_full)}"
229
+ f"Check {check['check_run_id']} failed: {' '.join(failure_message)}"
200
230
  )
201
231
  check["outcome"] = "failed"
202
232
  check["failure_message"] = failure_message
203
233
 
204
234
  # If a check encountered an issue, change severity to warn
205
- if not isinstance(e, AssertionError):
235
+ if not isinstance(e, DbtBouncerFailedCheckError):
206
236
  check["severity"] = "warn"
207
237
  check["failure_message"] = (
208
238
  f"`dbt-bouncer` encountered an error ({failure_message}), run with `-v` to see more details or report an issue at https://github.com/godatadriven/dbt-bouncer/issues."