Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit c8f3d9d

Browse files
committed
custom errors for dbt_parser
1 parent 9ffb136 commit c8f3d9d

File tree

4 files changed

+98
-42
lines changed

4 files changed

+98
-42
lines changed

data_diff/dbt_parser.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from dbt_artifacts_parser.parser import parse_run_results, parse_manifest
1212
from dbt.config.renderer import ProfileRenderer
1313

14+
from data_diff.errors import DbtBigQueryOauthOnlyError, DbtConnectionNotImplementedError, DbtCoreNoRunnerError, DbtNoSuccessfulModelsInRunError, DbtProfileNotFoundError, DbtProjectVarsNotFoundError, DbtRedshiftPasswordOnlyError, DbtRunResultsVersionError, DbtSelectNoMatchingModelsError, DbtSelectUnexpectedError, DbtSelectVersionTooLowError, DbtSnowflakeSetConnectionError
15+
1416
from .utils import getLogger, get_from_dict_with_raise
1517
from .version import __version__
1618

@@ -93,9 +95,9 @@ def __init__(self, profiles_dir_override: str, project_dir_override: str) -> Non
9395

9496
def get_datadiff_variables(self) -> dict:
9597
doc_url = "https://docs.datafold.com/development_testing/open_source#configure-your-dbt-project"
96-
error_message = f"vars: data_diff: section not found in dbt_project.yml.\n\nTo solve this, please configure your dbt project: \n{doc_url}\n"
97-
vars = get_from_dict_with_raise(self.project_dict, "vars", error_message)
98-
return get_from_dict_with_raise(vars, "data_diff", error_message)
98+
exception = DbtProjectVarsNotFoundError(f"vars: data_diff: section not found in dbt_project.yml.\n\nTo solve this, please configure your dbt project: \n{doc_url}\n")
99+
vars_dict = get_from_dict_with_raise(self.project_dict, "vars", exception)
100+
return get_from_dict_with_raise(vars_dict, "data_diff", exception)
99101

100102
def get_datadiff_model_config(self, model_meta: dict) -> TDatadiffModelConfig:
101103
where_filter = None
@@ -120,11 +122,11 @@ def get_models(self, dbt_selection: Optional[str] = None):
120122
return self.get_dbt_selection_models(dbt_selection)
121123
# edge case if running data-diff from a separate env than dbt (likely local development)
122124
else:
123-
raise Exception(
125+
raise DbtCoreNoRunnerError(
124126
"data-diff is using a dbt-core version < 1.5, update the environment's dbt-core version via pip install 'dbt-core>=1.5' in order to use `--select`"
125127
)
126128
else:
127-
raise Exception(
129+
raise DbtSelectVersionTooLowError(
128130
f"The `--select` feature requires dbt >= 1.5, but your project's manifest.json is from dbt v{dbt_version}. Please follow these steps to use the `--select` feature: \n 1. Update your dbt-core version via pip install 'dbt-core>=1.5'. Details: https://docs.getdbt.com/docs/core/pip-install#change-dbt-core-versions \n 2. Execute any `dbt` command (`run`, `compile`, `build`) to create a new manifest.json."
129131
)
130132
else:
@@ -154,15 +156,17 @@ def get_dbt_selection_models(self, dbt_selection: str) -> List[str]:
154156
)
155157
if results.exception:
156158
raise results.exception
157-
elif results.success and results.result:
159+
160+
if results.success and results.result:
158161
model_list = [json.loads(model)["unique_id"] for model in results.result]
159162
models = [self.manifest_obj.nodes.get(x) for x in model_list]
160163
return models
161-
elif not results.result:
162-
raise Exception(f"No dbt models found for `--select {dbt_selection}`")
163-
else:
164-
logger.debug(str(results))
165-
raise Exception("Encountered an unexpected error while finding `--select` models")
164+
165+
if not results.result:
166+
raise DbtSelectNoMatchingModelsError(f"No dbt models found for `--select {dbt_selection}`")
167+
168+
logger.debug(str(results))
169+
raise DbtSelectUnexpectedError("Encountered an unexpected error while finding `--select` models")
166170

167171
def get_run_results_models(self):
168172
with open(self.project_dir / RUN_RESULTS_PATH) as run_results:
@@ -176,16 +180,16 @@ def get_run_results_models(self):
176180
self.profiles_dir = legacy_profiles_dir()
177181

178182
if dbt_version < parse_version(LOWER_DBT_V):
179-
raise Exception(f"Found dbt: v{dbt_version} Expected the dbt project's version to be >= {LOWER_DBT_V}")
180-
elif dbt_version >= parse_version(UPPER_DBT_V):
183+
raise DbtRunResultsVersionError(f"Found dbt: v{dbt_version} Expected the dbt project's version to be >= {LOWER_DBT_V}")
184+
if dbt_version >= parse_version(UPPER_DBT_V):
181185
logger.warning(
182186
f"{dbt_version} is a recent version of dbt and may not be fully tested with data-diff! \nPlease report any issues to https://github.com/datafold/data-diff/issues"
183187
)
184188

185189
success_models = [x.unique_id for x in run_results_obj.results if x.status.name == "success"]
186190
models = [self.manifest_obj.nodes.get(x) for x in success_models]
187191
if not models:
188-
raise ValueError("Expected > 0 successful models runs from the last dbt command.")
192+
raise DbtNoSuccessfulModelsInRunError("Expected > 0 successful models runs from the last dbt command.")
189193

190194
print(f"Running with data-diff={__version__}\n")
191195
return models
@@ -212,25 +216,25 @@ def get_connection_creds(self) -> Tuple[Dict[str, str], str]:
212216
dbt_profile_var = self.project_dict.get("profile")
213217

214218
profile = get_from_dict_with_raise(
215-
profiles, dbt_profile_var, f"No profile '{dbt_profile_var}' found in '{profiles_path}'."
219+
profiles, dbt_profile_var, DbtProfileNotFoundError(f"No profile '{dbt_profile_var}' found in '{profiles_path}'.")
216220
)
217221
# values can contain env_vars
218222
rendered_profile = ProfileRenderer().render_data(profile)
219223
profile_target = get_from_dict_with_raise(
220-
rendered_profile, "target", f"No target found in profile '{dbt_profile_var}' in '{profiles_path}'."
224+
rendered_profile, "target", DbtProfileNotFoundError(f"No target found in profile '{dbt_profile_var}' in '{profiles_path}'.")
221225
)
222226
outputs = get_from_dict_with_raise(
223-
rendered_profile, "outputs", f"No outputs found in profile '{dbt_profile_var}' in '{profiles_path}'."
227+
rendered_profile, "outputs", DbtProfileNotFoundError(f"No outputs found in profile '{dbt_profile_var}' in '{profiles_path}'.")
224228
)
225229
credentials = get_from_dict_with_raise(
226230
outputs,
227231
profile_target,
228-
f"No credentials found for target '{profile_target}' in profile '{dbt_profile_var}' in '{profiles_path}'.",
232+
DbtProfileNotFoundError(f"No credentials found for target '{profile_target}' in profile '{dbt_profile_var}' in '{profiles_path}'."),
229233
)
230234
conn_type = get_from_dict_with_raise(
231235
credentials,
232236
"type",
233-
f"No type found for target '{profile_target}' in profile '{dbt_profile_var}' in '{profiles_path}'.",
237+
DbtProfileNotFoundError(f"No type found for target '{profile_target}' in profile '{dbt_profile_var}' in '{profiles_path}'."),
234238
)
235239
conn_type = conn_type.lower()
236240

@@ -256,7 +260,7 @@ def set_connection(self):
256260

257261
if credentials.get("private_key_path") is not None:
258262
if credentials.get("password") is not None:
259-
raise Exception("Cannot use password and key at the same time")
263+
raise DbtSnowflakeSetConnectionError("Cannot use password and key at the same time")
260264
conn_info["key"] = credentials.get("private_key_path")
261265
conn_info["private_key_passphrase"] = credentials.get("private_key_passphrase")
262266
elif credentials.get("authenticator") is not None:
@@ -265,13 +269,13 @@ def set_connection(self):
265269
elif credentials.get("password") is not None:
266270
conn_info["password"] = credentials.get("password")
267271
else:
268-
raise Exception("Snowflake: unsupported auth method")
272+
raise DbtSnowflakeSetConnectionError("Snowflake: unsupported auth method")
269273
elif conn_type == "bigquery":
270274
method = credentials.get("method")
271275
# there are many connection types https://docs.getdbt.com/reference/warehouse-setups/bigquery-setup#oauth-via-gcloud
272276
# this assumes that the user is auth'd via `gcloud auth application-default login`
273277
if method is None or method != "oauth":
274-
raise Exception("Oauth is the current method supported for Big Query.")
278+
raise DbtBigQueryOauthOnlyError("Oauth is the current method supported for Big Query.")
275279
conn_info = {
276280
"driver": conn_type,
277281
"project": credentials.get("project"),
@@ -287,7 +291,7 @@ def set_connection(self):
287291
if (credentials.get("pass") is None and credentials.get("password") is None) or credentials.get(
288292
"method"
289293
) == "iam":
290-
raise Exception("Only password authentication is currently supported for Redshift.")
294+
raise DbtRedshiftPasswordOnlyError("Only password authentication is currently supported for Redshift.")
291295
conn_info = {
292296
"driver": conn_type,
293297
"host": credentials.get("host"),
@@ -318,7 +322,7 @@ def set_connection(self):
318322
}
319323
self.threads = credentials.get("threads")
320324
else:
321-
raise NotImplementedError(f"Provider {conn_type} is not yet supported for dbt diffs")
325+
raise DbtConnectionNotImplementedError(f"Provider {conn_type} is not yet supported for dbt diffs")
322326

323327
self.connection = conn_info
324328

data_diff/errors.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
class DbtProjectVarsNotFoundError(Exception):
3+
"Raised when an expected dbt_project.yml section is missing."
4+
5+
class DbtProfileNotFoundError(Exception):
6+
"Raised when an expected profiles.yml section is missing."
7+
8+
class DbtNoSuccessfulModelsInRunError(Exception):
9+
"Raised when there are no successful model runs in the run_results.json"
10+
11+
class DbtRunResultsVersionError(Exception):
12+
"Raised when the dbt version in run_results.json is lower than the minimum version."
13+
14+
class DbtSelectNoMatchingModelsError(Exception):
15+
"Raised when the `--select` flag returns no models."
16+
17+
class DbtSelectUnexpectedError(Exception):
18+
"Catch all for unexpected dbt list --select results."
19+
20+
class DbtSnowflakeSetConnectionError(Exception):
21+
"Raised when a dbt snowflake profile has unexpected values."
22+
23+
class DbtBigQueryOauthOnlyError(Exception):
24+
"Raised when trying to use a method other than oauth with BigQuery."
25+
26+
class DbtRedshiftPasswordOnlyError(Exception):
27+
"Raised when using a non-password connection method with Redshift."
28+
29+
class DbtConnectionNotImplementedError(Exception):
30+
"Raised when trying to use an unsupported dbt connection method."
31+
32+
class DbtCoreNoRunnerError(Exception):
33+
"Raised when the manifest version >= 1.5, but the dbt-core package is < 1.5. This is an edge case most likely to occur in development."
34+
35+
class DbtSelectVersionTooLowError(Exception):
36+
"Raised when attempting to use `--select` with a dbt-core version < 1.5."

data_diff/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ def truncate_error(error: str):
8181
return re.sub("'(.*?)'", "'***'", first_line)
8282

8383

84-
def get_from_dict_with_raise(dictionary: Dict, key: str, error_message: str):
84+
def get_from_dict_with_raise(dictionary: Dict, key: str, exception: Exception):
85+
if dictionary is None:
86+
raise exception
8587
result = dictionary.get(key)
8688
if result is None:
87-
raise ValueError(error_message)
89+
raise exception
8890
return result
8991

9092

tests/test_dbt.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from data_diff.cloud.datafold_api import TCloudApiDataSource
55
from data_diff.cloud.datafold_api import TCloudApiOrgMeta
66
from data_diff.diff_tables import Algorithm
7+
from data_diff.errors import DbtBigQueryOauthOnlyError, DbtConnectionNotImplementedError, DbtCoreNoRunnerError, DbtNoSuccessfulModelsInRunError, DbtProfileNotFoundError, DbtProjectVarsNotFoundError, DbtRedshiftPasswordOnlyError, DbtRunResultsVersionError, DbtSelectVersionTooLowError, DbtSnowflakeSetConnectionError
78
from .test_cli import run_datadiff_cli
89

910
from data_diff.dbt import (
@@ -40,7 +41,7 @@ def test_get_datadiff_variables_none(self):
4041
mock_self = Mock()
4142
mock_self.project_dict = none_dict
4243

43-
with self.assertRaises(Exception):
44+
with self.assertRaises(DbtProjectVarsNotFoundError):
4445
DbtParser.get_datadiff_variables(mock_self)
4546

4647
def test_get_datadiff_variables_empty(self):
@@ -49,7 +50,7 @@ def test_get_datadiff_variables_empty(self):
4950
mock_self = Mock()
5051
mock_self.project_dict = empty_dict
5152

52-
with self.assertRaises(Exception):
53+
with self.assertRaises(DbtProjectVarsNotFoundError):
5354
DbtParser.get_datadiff_variables(mock_self)
5455

5556
def test_get_models(self):
@@ -72,7 +73,7 @@ def test_get_models_unsupported_manifest_version(self):
7273
mock_return_value = Mock()
7374
mock_self.get_dbt_selection_models.return_value = mock_return_value
7475

75-
with self.assertRaises(Exception):
76+
with self.assertRaises(DbtSelectVersionTooLowError):
7677
_ = DbtParser.get_models(mock_self, selection)
7778
mock_self.get_dbt_selection_models.assert_not_called()
7879

@@ -85,7 +86,7 @@ def test_get_models_no_runner(self):
8586
mock_return_value = Mock()
8687
mock_self.get_dbt_selection_models.return_value = mock_return_value
8788

88-
with self.assertRaises(Exception):
89+
with self.assertRaises(DbtCoreNoRunnerError):
8990
_ = DbtParser.get_models(mock_self, selection)
9091
mock_self.get_dbt_selection_models.assert_not_called()
9192

@@ -135,7 +136,7 @@ def test_get_run_results_models_bad_lower_dbt_version(self, mock_open, mock_arti
135136
mock_artifact_parser.return_value = mock_run_results
136137
mock_run_results.metadata.dbt_version = "0.19.0"
137138

138-
with self.assertRaises(Exception) as ex:
139+
with self.assertRaises(DbtRunResultsVersionError) as ex:
139140
DbtParser.get_run_results_models(mock_self)
140141

141142
mock_open.assert_called_once_with(Path(RUN_RESULTS_PATH))
@@ -158,7 +159,7 @@ def test_get_run_results_models_no_success(self, mock_open, mock_artifact_parser
158159
mock_failed_result.status.name = "failed"
159160
mock_run_results.results = [mock_failed_result]
160161

161-
with self.assertRaises(Exception):
162+
with self.assertRaises(DbtNoSuccessfulModelsInRunError):
162163
DbtParser.get_run_results_models(mock_self)
163164

164165
mock_open.assert_any_call(Path(RUN_RESULTS_PATH))
@@ -235,7 +236,7 @@ def test_set_connection_snowflake_no_key_or_password(self):
235236
mock_self = Mock()
236237
mock_self.get_connection_creds.return_value = (expected_credentials, expected_driver)
237238

238-
with self.assertRaises(Exception):
239+
with self.assertRaises(DbtSnowflakeSetConnectionError):
239240
DbtParser.set_connection(mock_self)
240241

241242
self.assertNotIsInstance(mock_self.connection, dict)
@@ -259,7 +260,7 @@ def test_set_connection_snowflake_key_and_password(self):
259260
mock_self = Mock()
260261
mock_self.get_connection_creds.return_value = (expected_credentials, expected_driver)
261262

262-
with self.assertRaises(Exception):
263+
with self.assertRaises(DbtSnowflakeSetConnectionError):
263264
DbtParser.set_connection(mock_self)
264265

265266
self.assertNotIsInstance(mock_self.connection, dict)
@@ -291,7 +292,20 @@ def test_set_connection_bigquery_not_oauth(self):
291292

292293
mock_self = Mock()
293294
mock_self.get_connection_creds.return_value = (expected_credentials, expected_driver)
294-
with self.assertRaises(Exception):
295+
with self.assertRaises(DbtBigQueryOauthOnlyError):
296+
DbtParser.set_connection(mock_self)
297+
298+
self.assertNotIsInstance(mock_self.connection, dict)
299+
300+
def test_set_connection_redshift_not_password(self):
301+
driver = "redshift"
302+
credentials = {
303+
"method": "not_password",
304+
}
305+
306+
mock_self = Mock()
307+
mock_self.get_connection_creds.return_value = (credentials, driver)
308+
with self.assertRaises(DbtRedshiftPasswordOnlyError):
295309
DbtParser.set_connection(mock_self)
296310

297311
self.assertNotIsInstance(mock_self.connection, dict)
@@ -301,7 +315,7 @@ def test_set_connection_not_implemented(self):
301315

302316
mock_self = Mock()
303317
mock_self.get_connection_creds.return_value = (None, expected_driver)
304-
with self.assertRaises(NotImplementedError):
318+
with self.assertRaises(DbtConnectionNotImplementedError):
305319
DbtParser.set_connection(mock_self)
306320

307321
self.assertNotIsInstance(mock_self.connection, dict)
@@ -340,7 +354,7 @@ def test_get_connection_no_matching_profile(self, mock_open, mock_profile_render
340354
mock_yaml.safe_load.return_value = profiles_dict
341355
profile = profiles_dict["a_profile"]
342356
mock_profile_renderer().render_data.return_value = profile
343-
with self.assertRaises(ValueError):
357+
with self.assertRaises(DbtProfileNotFoundError):
344358
_, _ = DbtParser.get_connection_creds(mock_self)
345359

346360
@patch("data_diff.dbt_parser.yaml")
@@ -360,7 +374,7 @@ def test_get_connection_no_target(self, mock_open, mock_profile_renderer, mock_y
360374
mock_profile_renderer().render_data.return_value = profile
361375
mock_self.project_dict = {"profile": "a_profile"}
362376
mock_yaml.safe_load.return_value = profiles_dict
363-
with self.assertRaises(ValueError):
377+
with self.assertRaises(DbtProfileNotFoundError):
364378
_, _ = DbtParser.get_connection_creds(mock_self)
365379

366380
profile_yaml_no_outputs = """
@@ -379,7 +393,7 @@ def test_get_connection_no_outputs(self, mock_open, mock_profile_renderer, mock_
379393
profile = profiles_dict["a_profile"]
380394
mock_profile_renderer().render_data.return_value = profile
381395
mock_yaml.safe_load.return_value = profiles_dict
382-
with self.assertRaises(ValueError):
396+
with self.assertRaises(DbtProfileNotFoundError):
383397
_, _ = DbtParser.get_connection_creds(mock_self)
384398

385399
@patch("data_diff.dbt_parser.yaml")
@@ -398,7 +412,7 @@ def test_get_connection_no_credentials(self, mock_open, mock_profile_renderer, m
398412
mock_yaml.safe_load.return_value = profiles_dict
399413
profile = profiles_dict["a_profile"]
400414
mock_profile_renderer().render_data.return_value = profile
401-
with self.assertRaises(ValueError):
415+
with self.assertRaises(DbtProfileNotFoundError):
402416
_, _ = DbtParser.get_connection_creds(mock_self)
403417

404418
@patch("data_diff.dbt_parser.yaml")
@@ -419,7 +433,7 @@ def test_get_connection_no_target_credentials(self, mock_open, mock_profile_rend
419433
profile = profiles_dict["a_profile"]
420434
mock_profile_renderer().render_data.return_value = profile
421435
mock_yaml.safe_load.return_value = profiles_dict
422-
with self.assertRaises(ValueError):
436+
with self.assertRaises(DbtProfileNotFoundError):
423437
_, _ = DbtParser.get_connection_creds(mock_self)
424438

425439
@patch("data_diff.dbt_parser.yaml")
@@ -438,7 +452,7 @@ def test_get_connection_no_type(self, mock_open, mock_profile_renderer, mock_yam
438452
mock_yaml.safe_load.return_value = profiles_dict
439453
profile = profiles_dict["a_profile"]
440454
mock_profile_renderer().render_data.return_value = profile
441-
with self.assertRaises(ValueError):
455+
with self.assertRaises(DbtProfileNotFoundError):
442456
_, _ = DbtParser.get_connection_creds(mock_self)
443457

444458

0 commit comments

Comments
 (0)