Skip to content

Commit

Permalink
[MAINTENANCE] Raise informative errors if child objects are not persi…
Browse files Browse the repository at this point in the history
…sted before parent (#10217)
  • Loading branch information
cdkini authored Aug 19, 2024
1 parent 7314bd0 commit f6e8ccc
Show file tree
Hide file tree
Showing 11 changed files with 505 additions and 29 deletions.
31 changes: 28 additions & 3 deletions great_expectations/checkpoint/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
ExpectationSuiteIdentifier,
ValidationResultIdentifier,
)
from great_expectations.exceptions.exceptions import CheckpointRunWithoutValidationDefinitionError
from great_expectations.exceptions.exceptions import (
CheckpointNotAddedError,
CheckpointRelatedResourcesNotAddedError,
CheckpointRunWithoutValidationDefinitionError,
ResourceNotAddedError,
)
from great_expectations.render.renderer.renderer import Renderer

if TYPE_CHECKING:
Expand Down Expand Up @@ -153,8 +158,13 @@ def run(
if not self.validation_definitions:
raise CheckpointRunWithoutValidationDefinitionError()

if not self.id:
self._add_to_store()
added, errors = self.is_added()
if not added:
# The checkpoint itself is not added but all children are - we can add it for the user
if len(errors) == 1 and isinstance(errors[0], CheckpointNotAddedError):
self._add_to_store()
else:
raise CheckpointRelatedResourcesNotAddedError(errors=errors)

run_id = run_id or RunIdentifier(run_time=dt.datetime.now(dt.timezone.utc))
run_results = self._run_validation_definitions(
Expand Down Expand Up @@ -252,6 +262,21 @@ def _sort_actions(self) -> List[CheckpointAction]:

return priority_actions + secondary_actions

def is_added(self) -> tuple[bool, list[ResourceNotAddedError]]:
errs: list[ResourceNotAddedError] = []

validations_added: bool = True
for validation_definition in self.validation_definitions:
validation_added, validation_errs = validation_definition.is_added()
errs.extend(validation_errs)
validations_added = validation_added and validations_added

self_added = self.id is not None
if not self_added:
errs.append(CheckpointNotAddedError(name=self.name))

return (validations_added and self_added, errs)

@public_api
def save(self) -> None:
from great_expectations.data_context.data_context.context_factory import project_manager
Expand Down
9 changes: 9 additions & 0 deletions great_expectations/core/batch_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
# Partitioner class when we update forward refs, so we just import here.
from great_expectations.core.partitioners import ColumnPartitioner, FileNamePartitioner
from great_expectations.core.serdes import _EncodedValidationData, _IdentifierBundle
from great_expectations.exceptions.exceptions import (
BatchDefinitionNotAddedError,
ResourceNotAddedError,
)

if TYPE_CHECKING:
from great_expectations.datasource.fluent.batch_request import (
Expand Down Expand Up @@ -72,6 +76,11 @@ def get_batch(self, batch_parameters: Optional[BatchParameters] = None) -> Batch

return batch_list[-1]

def is_added(self) -> tuple[bool, list[ResourceNotAddedError]]:
if self.id:
return True, []
return False, [BatchDefinitionNotAddedError(name=self.name)]

def identifier_bundle(self) -> _EncodedValidationData:
# Utilized as a custom json_encoder
asset = self.data_asset
Expand Down
6 changes: 6 additions & 0 deletions great_expectations/core/expectation_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from great_expectations.core.serdes import _IdentifierBundle
from great_expectations.exceptions.exceptions import (
ExpectationSuiteNotAddedError,
ResourceNotAddedError,
)
from great_expectations.types import SerializableDictDot
from great_expectations.util import (
Expand Down Expand Up @@ -233,6 +234,11 @@ def save(self) -> None:
key = self._store.get_key(name=self.name, id=self.id)
self._store.update(key=key, value=self)

def is_added(self) -> tuple[bool, list[ResourceNotAddedError]]:
if self.id:
return True, []
return False, [ExpectationSuiteNotAddedError(name=self.name)]

def _has_been_saved(self) -> bool:
"""Has this ExpectationSuite been persisted to a Store?"""
# todo: this should only check local keys instead of potentially querying the remote backend
Expand Down
30 changes: 27 additions & 3 deletions great_expectations/core/validation_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
GXCloudIdentifier,
ValidationResultIdentifier,
)
from great_expectations.exceptions.exceptions import ValidationDefinitionNotAddedError
from great_expectations.exceptions.exceptions import (
ResourceNotAddedError,
ValidationDefinitionNotAddedError,
ValidationDefinitionRelatedResourcesNotAddedError,
)
from great_expectations.validator.v1_validator import Validator

if TYPE_CHECKING:
Expand Down Expand Up @@ -114,6 +118,21 @@ def asset(self) -> DataAsset:
def data_source(self) -> Datasource:
return self.asset.datasource

def is_added(self) -> tuple[bool, list[ResourceNotAddedError]]:
errors: list[ResourceNotAddedError] = []

data_added, data_errors = self.data.is_added()
errors.extend(data_errors)

suite_added, suite_errors = self.suite.is_added()
errors.extend(suite_errors)

self_added = self.id is not None
if not self_added:
errors.append(ValidationDefinitionNotAddedError(name=self.name))

return (data_added and suite_added and self_added, errors)

@validator("suite", pre=True)
def _validate_suite(cls, v: dict | ExpectationSuite):
# Input will be a dict of identifiers if being deserialized or a suite object if being constructed by a user. # noqa: E501
Expand Down Expand Up @@ -201,8 +220,13 @@ def run(
result_format: ResultFormat | dict = ResultFormat.SUMMARY,
run_id: RunIdentifier | None = None,
) -> ExpectationSuiteValidationResult:
if not self.id:
self._add_to_store()
added, errors = self.is_added()
if not added:
# The validation definition itself is not added but all children are - we can add it for the user # noqa: E501
if len(errors) == 1 and isinstance(errors[0], ValidationDefinitionNotAddedError):
self._add_to_store()
else:
raise ValidationDefinitionRelatedResourcesNotAddedError(errors=errors)

validator = Validator(
batch_definition=self.batch_definition,
Expand Down
1 change: 0 additions & 1 deletion great_expectations/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
StoreBackendUnsupportedResourceTypeError,
StoreConfigurationError,
StoreError,
SuiteEditNotebookCustomTemplateModuleNotFoundError,
SuiteParameterError,
UnavailableMetricError,
UnsupportedConfigVersionError,
Expand Down
53 changes: 41 additions & 12 deletions great_expectations/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,33 @@ def __str__(self) -> str:
return self.message


class SuiteEditNotebookCustomTemplateModuleNotFoundError(ModuleNotFoundError):
def __init__(self, custom_module) -> None:
message = f"The custom module '{custom_module}' could not be found"
super().__init__(message)


class DataContextError(GreatExpectationsError):
pass


class ExpectationSuiteError(DataContextError):
class ResourceNotAddedError(DataContextError):
pass


class ExpectationSuiteNotSavedError(DataContextError):
class ResourcesNotAddedError(ValueError):
def __init__(self, errors: list[ResourceNotAddedError]) -> None:
self._errors = errors
super().__init__("\n\t" + "\n\t".join(str(e) for e in errors))

@property
def errors(self) -> list[ResourceNotAddedError]:
return self._errors


class ExpectationSuiteError(DataContextError):
pass


class ExpectationSuiteNotAddedError(ExpectationSuiteError):
class ExpectationSuiteNotAddedError(ResourceNotAddedError):
def __init__(self, name: str) -> None:
super().__init__(
f"ExpectationSuite '{name}' must be added to the DataContext before it can be updated. "
"Please call context.suites.add(<SUITE_OBJECT>), "
"Please call `context.suites.add(<SUITE_OBJECT>)`, "
"then try your action again."
)

Expand All @@ -65,11 +69,11 @@ class ValidationDefinitionError(DataContextError):
pass


class ValidationDefinitionNotAddedError(ValidationDefinitionError):
class ValidationDefinitionNotAddedError(ResourceNotAddedError):
def __init__(self, name: str) -> None:
super().__init__(
f"ValidationDefinition '{name}' must be added to the DataContext before it can be updated. " # noqa: E501
"Please call context.validation_definitions.add(<VALIDATION_DEFINITION_OBJECT>), "
"Please call `context.validation_definitions.add(<VALIDATION_DEFINITION_OBJECT>)`, "
"then try your action again."
)

Expand All @@ -82,6 +86,15 @@ class CheckpointNotFoundError(CheckpointError):
pass


class CheckpointNotAddedError(ResourceNotAddedError):
def __init__(self, name: str) -> None:
super().__init__(
f"Checkpoint '{name}' must be added to the DataContext before it can be updated. "
"Please call `context.checkpoints.add(<CHECKPOINT_OBJECT>)`, "
"then try your action again."
)


class CheckpointRunWithoutValidationDefinitionError(CheckpointError):
def __init__(self) -> None:
super().__init__(
Expand All @@ -90,6 +103,14 @@ def __init__(self) -> None:
)


class CheckpointRelatedResourcesNotAddedError(ResourcesNotAddedError):
pass


class ValidationDefinitionRelatedResourcesNotAddedError(ResourcesNotAddedError):
pass


class StoreBackendError(DataContextError):
pass

Expand Down Expand Up @@ -416,6 +437,14 @@ def __init__(self, message) -> None:
super().__init__(self.message)


class BatchDefinitionNotAddedError(ResourceNotAddedError):
def __init__(self, name: str) -> None:
super().__init__(
f"BatchDefinition '{name}' must be added to the DataContext before it can be updated. "
"Please update using the parent asset or data source, then try your action again."
)


class BatchSpecError(DataContextError):
def __init__(self, message) -> None:
self.message = message
Expand Down
Loading

0 comments on commit f6e8ccc

Please sign in to comment.