mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-21 23:13:05 +00:00
MINOR: Enhance patch request handling by adding 'skip_on_failure' parameter (#22142)
* Enhance patch request handling by adding 'skip_on_failure' parameter * Introduced 'skip_on_failure' option in build_patch and OMetaPatchMixin methods to control behavior on patch operation failures. * Updated documentation to reflect the new parameter and its default value. * Improved error handling to log warnings instead of raising exceptions when 'skip_on_failure' is set to True. * fix: add tests for patch request with skip on failure * refactor: streamline mock patching and improve test readability in patch request tests * Consolidated import statements for unittest mock. * Enhanced readability by reducing line breaks and simplifying mock patching syntax. * Ensured consistent use of commas in function calls for clarity. * Updated tests to maintain functionality while improving code style. * fix: improve error handling in patch operations * Enhanced logging for patch operation failures in both build_patch and OMetaPatchMixin methods. * Added detailed entity information in warning and error messages to aid in debugging. * Ensured consistent behavior when 'skip_on_failure' is set, providing clearer feedback on operation outcomes. * fix: clean up whitespace in patch request error handling * Removed unnecessary whitespace in the build_patch function to improve code readability. * Ensured consistent formatting in warning and error messages for better clarity during logging. * fix: enhance error handling and improve test assertions in patch request * Updated the condition for checking 'changeDescription' in the _remove_change_description function for better clarity. * Modified exception handling in tests to raise RuntimeError instead of a generic Exception, providing more specific error feedback. * Improved assertions in tests to check for the presence of error messages, enhancing the robustness of error handling verification. * Adjusted test cases to reflect changes in expected patch operation counts and ensure accurate validation of patch operations. * fix: enhance patch operation with skip_on_failure handling * Added 'skip_on_failure' parameter to OMetaPatchMixin methods to control behavior on patch failures. * Improved error handling to log warnings and provide detailed feedback when patch operations are skipped. * Updated tests to verify the new behavior of skipping failures and improved assertions for clarity.
This commit is contained in:
parent
0b82e2e2a2
commit
fe2caf7a5d
@ -342,6 +342,7 @@ def build_patch(
|
|||||||
array_entity_fields: Optional[List] = None,
|
array_entity_fields: Optional[List] = None,
|
||||||
remove_change_description: bool = True,
|
remove_change_description: bool = True,
|
||||||
override_metadata: Optional[bool] = False,
|
override_metadata: Optional[bool] = False,
|
||||||
|
skip_on_failure: Optional[bool] = True,
|
||||||
) -> Optional[jsonpatch.JsonPatch]:
|
) -> Optional[jsonpatch.JsonPatch]:
|
||||||
"""
|
"""
|
||||||
Given an Entity type and Source entity and Destination entity,
|
Given an Entity type and Source entity and Destination entity,
|
||||||
@ -352,6 +353,10 @@ def build_patch(
|
|||||||
destination: payload with changes applied to the source.
|
destination: payload with changes applied to the source.
|
||||||
allowed_fields: List of field names to filter from source and destination models
|
allowed_fields: List of field names to filter from source and destination models
|
||||||
restrict_update_fields: List of field names which will only support add operation
|
restrict_update_fields: List of field names which will only support add operation
|
||||||
|
array_entity_fields: List of array fields to sort for consistent patching
|
||||||
|
remove_change_description: Whether to remove change description from entities
|
||||||
|
override_metadata: Whether to override existing metadata fields
|
||||||
|
skip_on_failure: Whether to skip the patch operation on failure (default: True)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
Updated Entity
|
Updated Entity
|
||||||
@ -421,10 +426,37 @@ def build_patch(
|
|||||||
patch.patch = updated_operations
|
patch.patch = updated_operations
|
||||||
|
|
||||||
return patch
|
return patch
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
logger.warning("Couldn't build patch for Entity.")
|
if skip_on_failure:
|
||||||
return None
|
entity_info = ""
|
||||||
|
try:
|
||||||
|
if hasattr(source, "fullyQualifiedName"):
|
||||||
|
entity_info = f" for '{source.fullyQualifiedName.root}'"
|
||||||
|
elif hasattr(source, "name"):
|
||||||
|
entity_info = f" for '{source.name.root}'"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to build patch{entity_info}. The patch generation was skipped. "
|
||||||
|
f"Reason: {exc}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
entity_info = ""
|
||||||
|
try:
|
||||||
|
if hasattr(source, "fullyQualifiedName"):
|
||||||
|
entity_info = f" for '{source.fullyQualifiedName.root}'"
|
||||||
|
elif hasattr(source, "name"):
|
||||||
|
entity_info = f" for '{source.name.root}'"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to build patch{entity_info}. The patch generation failed. "
|
||||||
|
f"Set 'skip_on_failure=True' to skip failed patch operations. Error: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
def _get_attribute_name(attr: T) -> str:
|
def _get_attribute_name(attr: T) -> str:
|
||||||
@ -534,7 +566,7 @@ def _remove_change_description(entity: T) -> T:
|
|||||||
We never want to patch that, and we won't have that information
|
We never want to patch that, and we won't have that information
|
||||||
from the source. It's fully handled in the server.
|
from the source. It's fully handled in the server.
|
||||||
"""
|
"""
|
||||||
if getattr(entity, "changeDescription"):
|
if hasattr(entity, "changeDescription") and getattr(entity, "changeDescription"):
|
||||||
entity.changeDescription = None
|
entity.changeDescription = None
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
|
@ -124,6 +124,7 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
restrict_update_fields: Optional[List] = None,
|
restrict_update_fields: Optional[List] = None,
|
||||||
array_entity_fields: Optional[List] = None,
|
array_entity_fields: Optional[List] = None,
|
||||||
override_metadata: Optional[bool] = False,
|
override_metadata: Optional[bool] = False,
|
||||||
|
skip_on_failure: Optional[bool] = True,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""
|
"""
|
||||||
Given an Entity type and Source entity and Destination entity,
|
Given an Entity type and Source entity and Destination entity,
|
||||||
@ -135,6 +136,9 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
destination: payload with changes applied to the source.
|
destination: payload with changes applied to the source.
|
||||||
allowed_fields: List of field names to filter from source and destination models
|
allowed_fields: List of field names to filter from source and destination models
|
||||||
restrict_update_fields: List of field names which will only support add operation
|
restrict_update_fields: List of field names which will only support add operation
|
||||||
|
array_entity_fields: List of array fields to sort for consistent patching
|
||||||
|
override_metadata: Whether to override existing metadata fields
|
||||||
|
skip_on_failure: Whether to skip the patch operation on failure (default: True)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
Updated Entity
|
Updated Entity
|
||||||
@ -147,6 +151,7 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
restrict_update_fields=restrict_update_fields,
|
restrict_update_fields=restrict_update_fields,
|
||||||
array_entity_fields=array_entity_fields,
|
array_entity_fields=array_entity_fields,
|
||||||
override_metadata=override_metadata,
|
override_metadata=override_metadata,
|
||||||
|
skip_on_failure=skip_on_failure,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not patch:
|
if not patch:
|
||||||
@ -160,9 +165,19 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
logger.warning(f"Error trying to PATCH {get_log_name(source)}: {exc}")
|
if skip_on_failure:
|
||||||
|
entity_name = get_log_name(source)
|
||||||
return None
|
logger.warning(
|
||||||
|
f"Failed to update {entity_name}. The patch operation was skipped. "
|
||||||
|
f"Reason: {exc}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
entity_name = get_log_name(source)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to update {entity_name}. The patch operation failed. "
|
||||||
|
f"Set 'skip_on_failure=True' to skip failed patches. Error: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
def patch_description(
|
def patch_description(
|
||||||
self,
|
self,
|
||||||
@ -170,6 +185,7 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
source: T,
|
source: T,
|
||||||
description: str,
|
description: str,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
skip_on_failure: bool = True,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""
|
"""
|
||||||
Given an Entity type and ID, JSON PATCH the description.
|
Given an Entity type and ID, JSON PATCH the description.
|
||||||
@ -180,33 +196,51 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
description: new description to add
|
description: new description to add
|
||||||
force: if True, we will patch any existing description. Otherwise, we will maintain
|
force: if True, we will patch any existing description. Otherwise, we will maintain
|
||||||
the existing data.
|
the existing data.
|
||||||
|
skip_on_failure: if True, return None on failure instead of raising exception
|
||||||
Returns
|
Returns
|
||||||
Updated Entity
|
Updated Entity
|
||||||
"""
|
"""
|
||||||
if isinstance(source, TestCase):
|
try:
|
||||||
instance: Optional[T] = self._fetch_entity_if_exists(
|
if isinstance(source, TestCase):
|
||||||
|
instance: Optional[T] = self._fetch_entity_if_exists(
|
||||||
|
entity=entity,
|
||||||
|
entity_id=source.id,
|
||||||
|
fields=["testDefinition", "testSuite"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
instance: Optional[T] = self._fetch_entity_if_exists(
|
||||||
|
entity=entity, entity_id=source.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not instance:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if instance.description and not force:
|
||||||
|
# If the description is already present and force is not passed,
|
||||||
|
# description will not be overridden
|
||||||
|
return None
|
||||||
|
|
||||||
|
# https://docs.pydantic.dev/latest/usage/exporting_models/#modelcopy
|
||||||
|
destination = source.model_copy(deep=True)
|
||||||
|
destination.description = Markdown(description)
|
||||||
|
|
||||||
|
return self.patch(
|
||||||
entity=entity,
|
entity=entity,
|
||||||
entity_id=source.id,
|
source=source,
|
||||||
fields=["testDefinition", "testSuite"],
|
destination=destination,
|
||||||
|
skip_on_failure=skip_on_failure,
|
||||||
)
|
)
|
||||||
else:
|
except Exception as exc:
|
||||||
instance: Optional[T] = self._fetch_entity_if_exists(
|
if skip_on_failure:
|
||||||
entity=entity, entity_id=source.id
|
logger.debug(traceback.format_exc())
|
||||||
)
|
entity_name = get_log_name(source)
|
||||||
|
logger.warning(
|
||||||
if not instance:
|
f"Failed to patch description for {entity_name}. The patch operation was skipped. "
|
||||||
return None
|
f"Reason: {exc}"
|
||||||
|
)
|
||||||
if instance.description and not force:
|
return None
|
||||||
# If the description is already present and force is not passed,
|
else:
|
||||||
# description will not be overridden
|
raise
|
||||||
return None
|
|
||||||
|
|
||||||
# https://docs.pydantic.dev/latest/usage/exporting_models/#modelcopy
|
|
||||||
destination = source.model_copy(deep=True)
|
|
||||||
destination.description = Markdown(description)
|
|
||||||
|
|
||||||
return self.patch(entity=entity, source=source, destination=destination)
|
|
||||||
|
|
||||||
def patch_table_constraints(
|
def patch_table_constraints(
|
||||||
self,
|
self,
|
||||||
@ -275,6 +309,7 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
operation: Union[
|
operation: Union[
|
||||||
PatchOperation.ADD, PatchOperation.REMOVE
|
PatchOperation.ADD, PatchOperation.REMOVE
|
||||||
] = PatchOperation.ADD,
|
] = PatchOperation.ADD,
|
||||||
|
skip_on_failure: bool = True,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""
|
"""
|
||||||
Given an Entity type and ID, JSON PATCH the tag.
|
Given an Entity type and ID, JSON PATCH the tag.
|
||||||
@ -284,29 +319,47 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
source: Source entity object
|
source: Source entity object
|
||||||
tag_label: TagLabel to add or remove
|
tag_label: TagLabel to add or remove
|
||||||
operation: Patch Operation to add or remove the tag.
|
operation: Patch Operation to add or remove the tag.
|
||||||
|
skip_on_failure: if True, return None on failure instead of raising exception
|
||||||
Returns
|
Returns
|
||||||
Updated Entity
|
Updated Entity
|
||||||
"""
|
"""
|
||||||
instance: Optional[T] = self._fetch_entity_if_exists(
|
try:
|
||||||
entity=entity, entity_id=source.id, fields=["tags"]
|
instance: Optional[T] = self._fetch_entity_if_exists(
|
||||||
)
|
entity=entity, entity_id=source.id, fields=["tags"]
|
||||||
if not instance:
|
)
|
||||||
return None
|
if not instance:
|
||||||
|
return None
|
||||||
|
|
||||||
# Initialize empty tag list or the last updated tags
|
# Initialize empty tag list or the last updated tags
|
||||||
source.tags = instance.tags or []
|
source.tags = instance.tags or []
|
||||||
destination = source.model_copy(deep=True)
|
destination = source.model_copy(deep=True)
|
||||||
|
|
||||||
tag_fqns = {label.tagFQN.root for label in tag_labels}
|
tag_fqns = {label.tagFQN.root for label in tag_labels}
|
||||||
|
|
||||||
if operation == PatchOperation.REMOVE:
|
if operation == PatchOperation.REMOVE:
|
||||||
for tag in destination.tags:
|
for tag in destination.tags:
|
||||||
if tag.tagFQN.root in tag_fqns:
|
if tag.tagFQN.root in tag_fqns:
|
||||||
destination.tags.remove(tag)
|
destination.tags.remove(tag)
|
||||||
else:
|
else:
|
||||||
destination.tags.extend(tag_labels)
|
destination.tags.extend(tag_labels)
|
||||||
|
|
||||||
return self.patch(entity=entity, source=source, destination=destination)
|
return self.patch(
|
||||||
|
entity=entity,
|
||||||
|
source=source,
|
||||||
|
destination=destination,
|
||||||
|
skip_on_failure=skip_on_failure,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if skip_on_failure:
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
entity_name = get_log_name(source)
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to patch tags for {entity_name}. The patch operation was skipped. "
|
||||||
|
f"Reason: {exc}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def patch_tag(
|
def patch_tag(
|
||||||
self,
|
self,
|
||||||
@ -316,11 +369,16 @@ class OMetaPatchMixin(OMetaPatchMixinBase):
|
|||||||
operation: Union[
|
operation: Union[
|
||||||
PatchOperation.ADD, PatchOperation.REMOVE
|
PatchOperation.ADD, PatchOperation.REMOVE
|
||||||
] = PatchOperation.ADD,
|
] = PatchOperation.ADD,
|
||||||
|
skip_on_failure: bool = True,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""Will be deprecated in 1.3"""
|
"""Will be deprecated in 1.3"""
|
||||||
logger.warning("patch_tag will be deprecated in 1.3. Use `patch_tags` instead.")
|
logger.warning("patch_tag will be deprecated in 1.3. Use `patch_tags` instead.")
|
||||||
return self.patch_tags(
|
return self.patch_tags(
|
||||||
entity=entity, source=source, tag_labels=[tag_label], operation=operation
|
entity=entity,
|
||||||
|
source=source,
|
||||||
|
tag_labels=[tag_label],
|
||||||
|
operation=operation,
|
||||||
|
skip_on_failure=skip_on_failure,
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch_owner(
|
def patch_owner(
|
||||||
|
@ -15,7 +15,7 @@ OpenMetadata high-level API Table test
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from _openmetadata_testutils.ometa import int_admin_ometa
|
from _openmetadata_testutils.ometa import int_admin_ometa
|
||||||
from metadata.generated.schema.entity.data.database import Database
|
from metadata.generated.schema.entity.data.database import Database
|
||||||
@ -680,3 +680,135 @@ class OMetaTableTest(TestCase):
|
|||||||
assert patched_table.description.root == "potato"
|
assert patched_table.description.root == "potato"
|
||||||
assert patched_table.owners.root
|
assert patched_table.owners.root
|
||||||
assert patched_table.owners.root[0].inherited
|
assert patched_table.owners.root[0].inherited
|
||||||
|
|
||||||
|
def test_patch_skip_on_failure_true(self):
|
||||||
|
"""Test that patch operation skips failures when skip_on_failure=True."""
|
||||||
|
# Create a destination with a change to trigger a patch
|
||||||
|
corrupted_destination = self.table.model_copy(deep=True)
|
||||||
|
corrupted_destination.description = Markdown("Modified description")
|
||||||
|
|
||||||
|
# Mock the client.patch to raise an exception
|
||||||
|
with mock.patch.object(self.metadata.client, "patch") as mock_patch_client:
|
||||||
|
mock_patch_client.side_effect = Exception("API error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=True (should return None)
|
||||||
|
result = self.metadata.patch(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
destination=corrupted_destination,
|
||||||
|
skip_on_failure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_patch_client.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_skip_on_failure_false(self):
|
||||||
|
"""Test that patch operation raises exception when skip_on_failure=False."""
|
||||||
|
# Create a destination with a change to trigger a patch
|
||||||
|
corrupted_destination = self.table.model_copy(deep=True)
|
||||||
|
corrupted_destination.description = Markdown("Modified description")
|
||||||
|
|
||||||
|
# Mock the client.patch to raise an exception
|
||||||
|
with mock.patch.object(self.metadata.client, "patch") as mock_patch_client:
|
||||||
|
mock_patch_client.side_effect = Exception("API error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=False (should raise exception)
|
||||||
|
with self.assertRaises(RuntimeError) as context:
|
||||||
|
self.metadata.patch(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
destination=corrupted_destination,
|
||||||
|
skip_on_failure=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "API error" in str(context.exception)
|
||||||
|
assert "Failed to update" in str(context.exception)
|
||||||
|
mock_patch_client.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_skip_on_failure_default_behavior(self):
|
||||||
|
"""Test that patch operation defaults to skip_on_failure=True."""
|
||||||
|
# Create a destination with a change to trigger a patch
|
||||||
|
corrupted_destination = self.table.model_copy(deep=True)
|
||||||
|
corrupted_destination.description = Markdown("Modified description")
|
||||||
|
|
||||||
|
# Mock the client.patch to raise an exception
|
||||||
|
with mock.patch.object(self.metadata.client, "patch") as mock_patch_client:
|
||||||
|
mock_patch_client.side_effect = Exception("API error")
|
||||||
|
|
||||||
|
# Test without explicitly setting skip_on_failure (should default to True)
|
||||||
|
result = self.metadata.patch(
|
||||||
|
entity=Table, source=self.table, destination=corrupted_destination
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_patch_client.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_description_skip_on_failure_true(self):
|
||||||
|
"""Test that patch_description skips failures when skip_on_failure=True."""
|
||||||
|
# Mock _fetch_entity_if_exists to raise an exception
|
||||||
|
with mock.patch.object(self.metadata, "_fetch_entity_if_exists") as mock_fetch:
|
||||||
|
mock_fetch.side_effect = Exception("Database error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=True
|
||||||
|
result = self.metadata.patch_description(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
description="New description",
|
||||||
|
skip_on_failure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_fetch.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_description_skip_on_failure_false(self):
|
||||||
|
"""Test that patch_description raises exception when skip_on_failure=False."""
|
||||||
|
# Mock _fetch_entity_if_exists to raise an exception
|
||||||
|
with mock.patch.object(self.metadata, "_fetch_entity_if_exists") as mock_fetch:
|
||||||
|
mock_fetch.side_effect = Exception("Database error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=False
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
self.metadata.patch_description(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
description="New description",
|
||||||
|
skip_on_failure=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(context.exception) == "Database error"
|
||||||
|
mock_fetch.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_tags_skip_on_failure_true(self):
|
||||||
|
"""Test that patch_tags skips failures when skip_on_failure=True."""
|
||||||
|
# Mock _fetch_entity_if_exists to raise an exception
|
||||||
|
with mock.patch.object(self.metadata, "_fetch_entity_if_exists") as mock_fetch:
|
||||||
|
mock_fetch.side_effect = Exception("Database error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=True
|
||||||
|
result = self.metadata.patch_tags(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
tag_labels=[PII_TAG_LABEL],
|
||||||
|
skip_on_failure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_fetch.assert_called_once()
|
||||||
|
|
||||||
|
def test_patch_tags_skip_on_failure_false(self):
|
||||||
|
"""Test that patch_tags raises exception when skip_on_failure=False."""
|
||||||
|
# Mock _fetch_entity_if_exists to raise an exception
|
||||||
|
with mock.patch.object(self.metadata, "_fetch_entity_if_exists") as mock_fetch:
|
||||||
|
mock_fetch.side_effect = Exception("Database error")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=False
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
self.metadata.patch_tags(
|
||||||
|
entity=Table,
|
||||||
|
source=self.table,
|
||||||
|
tag_labels=[PII_TAG_LABEL],
|
||||||
|
skip_on_failure=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(context.exception) == "Database error"
|
||||||
|
mock_fetch.assert_called_once()
|
||||||
|
@ -13,10 +13,12 @@
|
|||||||
Check the JSONPatch operations work as expected
|
Check the JSONPatch operations work as expected
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import jsonpatch
|
import jsonpatch
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from metadata.ingestion.models.patch_request import JsonPatchUpdater
|
from metadata.ingestion.models.patch_request import JsonPatchUpdater, build_patch
|
||||||
|
|
||||||
|
|
||||||
class JsonPatchUpdaterTest(TestCase):
|
class JsonPatchUpdaterTest(TestCase):
|
||||||
@ -104,3 +106,148 @@ class JsonPatchUpdaterTest(TestCase):
|
|||||||
updated_operations = json_patch_updater.update(json_patch)
|
updated_operations = json_patch_updater.update(json_patch)
|
||||||
|
|
||||||
self.assertEqual(expected, updated_operations)
|
self.assertEqual(expected, updated_operations)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildPatchTest(TestCase):
|
||||||
|
"""Validate build_patch function operations with skip_on_failure parameter."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
name: str
|
||||||
|
value: int
|
||||||
|
description: str = None
|
||||||
|
|
||||||
|
self.TestModel = TestModel
|
||||||
|
|
||||||
|
self.source = TestModel(name="test", value=1, description="source")
|
||||||
|
self.destination = TestModel(name="test", value=2, description="destination")
|
||||||
|
|
||||||
|
def test_build_patch_skip_on_failure_true_with_exception(self):
|
||||||
|
"""Test that build_patch returns None when skip_on_failure=True and exception occurs."""
|
||||||
|
|
||||||
|
# Mock jsonpatch.make_patch to raise an exception
|
||||||
|
with patch(
|
||||||
|
"metadata.ingestion.models.patch_request.jsonpatch.make_patch"
|
||||||
|
) as mock_make_patch:
|
||||||
|
mock_make_patch.side_effect = Exception("Test exception")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=True (default)
|
||||||
|
result = build_patch(
|
||||||
|
source=self.source, destination=self.destination, skip_on_failure=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
mock_make_patch.assert_called_once()
|
||||||
|
|
||||||
|
def test_build_patch_skip_on_failure_false_with_exception(self):
|
||||||
|
"""Test that build_patch raises exception when skip_on_failure=False and exception occurs."""
|
||||||
|
|
||||||
|
# Mock jsonpatch.make_patch to raise an exception
|
||||||
|
with patch(
|
||||||
|
"metadata.ingestion.models.patch_request.jsonpatch.make_patch"
|
||||||
|
) as mock_make_patch:
|
||||||
|
mock_make_patch.side_effect = Exception("Test exception")
|
||||||
|
|
||||||
|
# Test with skip_on_failure=False
|
||||||
|
with self.assertRaises(RuntimeError) as context:
|
||||||
|
build_patch(
|
||||||
|
source=self.source,
|
||||||
|
destination=self.destination,
|
||||||
|
skip_on_failure=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Test exception", str(context.exception))
|
||||||
|
self.assertIn("Failed to build patch", str(context.exception))
|
||||||
|
mock_make_patch.assert_called_once()
|
||||||
|
|
||||||
|
def test_build_patch_skip_on_failure_default_behavior(self):
|
||||||
|
"""Test that build_patch defaults to skip_on_failure=True."""
|
||||||
|
|
||||||
|
# Mock jsonpatch.make_patch to raise an exception
|
||||||
|
with patch(
|
||||||
|
"metadata.ingestion.models.patch_request.jsonpatch.make_patch"
|
||||||
|
) as mock_make_patch:
|
||||||
|
mock_make_patch.side_effect = Exception("Test exception")
|
||||||
|
|
||||||
|
# Test without explicitly setting skip_on_failure (should default to True)
|
||||||
|
result = build_patch(source=self.source, destination=self.destination)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
mock_make_patch.assert_called_once()
|
||||||
|
|
||||||
|
def test_build_patch_success_with_skip_on_failure_false(self):
|
||||||
|
"""Test that build_patch works normally when skip_on_failure=False and no exception occurs."""
|
||||||
|
|
||||||
|
# Create a real patch to test successful operation
|
||||||
|
result = build_patch(
|
||||||
|
source=self.source, destination=self.destination, skip_on_failure=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertIsInstance(result, jsonpatch.JsonPatch)
|
||||||
|
|
||||||
|
# Verify the patch contains the expected operations
|
||||||
|
patch_operations = result.patch
|
||||||
|
self.assertEqual(len(patch_operations), 2)
|
||||||
|
|
||||||
|
# Find the value operation
|
||||||
|
value_op = next((op for op in patch_operations if op["path"] == "/value"), None)
|
||||||
|
self.assertIsNotNone(value_op)
|
||||||
|
self.assertEqual(value_op["op"], "replace")
|
||||||
|
self.assertEqual(value_op["value"], 2)
|
||||||
|
|
||||||
|
def test_build_patch_success_with_skip_on_failure_true(self):
|
||||||
|
"""Test that build_patch works normally when skip_on_failure=True and no exception occurs."""
|
||||||
|
|
||||||
|
# Create a real patch to test successful operation
|
||||||
|
result = build_patch(
|
||||||
|
source=self.source, destination=self.destination, skip_on_failure=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertIsInstance(result, jsonpatch.JsonPatch)
|
||||||
|
|
||||||
|
# Verify the patch contains the expected operations
|
||||||
|
patch_operations = result.patch
|
||||||
|
self.assertEqual(len(patch_operations), 2)
|
||||||
|
|
||||||
|
# Find the value operation
|
||||||
|
value_op = next((op for op in patch_operations if op["path"] == "/value"), None)
|
||||||
|
self.assertIsNotNone(value_op)
|
||||||
|
self.assertEqual(value_op["op"], "replace")
|
||||||
|
self.assertEqual(value_op["value"], 2)
|
||||||
|
|
||||||
|
def test_build_patch_with_json_patch_updater_exception(self):
|
||||||
|
"""Test skip_on_failure behavior when JsonPatchUpdater.update raises an exception."""
|
||||||
|
|
||||||
|
# Mock JsonPatchUpdater.update to raise an exception
|
||||||
|
with patch(
|
||||||
|
"metadata.ingestion.models.patch_request.JsonPatchUpdater.from_restrict_update_fields"
|
||||||
|
) as mock_updater_factory:
|
||||||
|
mock_updater = Mock()
|
||||||
|
mock_updater.update.side_effect = Exception("JsonPatchUpdater exception")
|
||||||
|
mock_updater_factory.return_value = mock_updater
|
||||||
|
|
||||||
|
# Test with skip_on_failure=True
|
||||||
|
result = build_patch(
|
||||||
|
source=self.source,
|
||||||
|
destination=self.destination,
|
||||||
|
restrict_update_fields=["description"],
|
||||||
|
skip_on_failure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
# Test with skip_on_failure=False
|
||||||
|
with self.assertRaises(RuntimeError) as context:
|
||||||
|
build_patch(
|
||||||
|
source=self.source,
|
||||||
|
destination=self.destination,
|
||||||
|
restrict_update_fields=["description"],
|
||||||
|
skip_on_failure=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("JsonPatchUpdater exception", str(context.exception))
|
||||||
|
self.assertIn("Failed to build patch", str(context.exception))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user