Skip to content

Commit

Permalink
feat: add audit for succesfull imports
Browse files Browse the repository at this point in the history
fix: call upper method xD

feat: refactor using decorator

feat: use finally block python
  • Loading branch information
johanseto committed Jul 9, 2024
1 parent 115e025 commit abbc564
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 15 deletions.
44 changes: 42 additions & 2 deletions eox_nelp/pearson_vue/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from eox_nelp.pearson_vue.utils import generate_client_authorization_id, update_xml_with_dict
from eox_nelp.signals.utils import get_completed_and_graded
from eox_nelp.utils import camel_to_snake

try:
from eox_audit_model.decorators import audit_method, rename_function
Expand Down Expand Up @@ -227,7 +228,7 @@ def import_candidate_demographics(cdd_request, **kwargs): # pylint: disable=unu
response = api_client.import_candidate_demographics(payload)

if response.get("status", "error") == "accepted":
return response
return {"cdd_import": response}

raise PearsonImportError(
exception_reason=f"Import candidate demographics pipeline has failed with the following response: {response}",
Expand Down Expand Up @@ -276,7 +277,7 @@ def import_exam_authorization(ead_request, **kwargs): # pylint: disable=unused-
response = api_client.import_exam_authorization(payload)

if response.get("status", "error") == "accepted":
return response
return {"ead_import": response}

raise PearsonImportError(
exception_reason=f"Import exam authorization pipeline has failed with the following response: {response}",
Expand Down Expand Up @@ -613,3 +614,42 @@ def get_enrollment_from_id(enrollment_id, enrollment=None, **kwargs): # pylint:
pass

return {}


def save_audit_pipeline(
pipeline_backend,
last_step,
cdd_import=None,
ead_import=None,
**kwargs
): # pylint: disable=unused-argument
"""This is not a pipe step, could be but its no the intention because inside a pipeline could
be more than one import. so this is a helper for pipelines.
Args:
pipeline_backend (str): pipeline class name
last_step (str): last function executed
cdd_import (dict, optional): Defaults to None.
ead_import (dict, optional): Defaults to None.
"""

@audit_method(action=f"Pearson Vue Pipeline Execution: {pipeline_backend}")
@rename_function(name=f"audit_pipeline_{camel_to_snake(pipeline_backend)}")
def executed_pipeline(pipeline_backend, last_step, cdd_import, ead_import):
logger.info(
"Pipeline %s executed until last step %s",
pipeline_backend,
last_step,
)
logger.info(
"Succesfull imports. cdd: %s. ead: %s",
cdd_import,
ead_import,
)

executed_pipeline(
pipeline_backend=pipeline_backend,
last_step=last_step,
cdd_import=cdd_import,
ead_import=ead_import,
)
30 changes: 29 additions & 1 deletion eox_nelp/pearson_vue/rti_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,36 @@
handle_course_completion_status,
import_candidate_demographics,
import_exam_authorization,
save_audit_pipeline,
validate_cdd_request,
validate_ead_request,
)


def audit_pipeline(func):
"""Decorator that wraps a class method with a try-finally block.
Args:
func: The method to be decorated.
Returns:
A wrapper function that executes the decorated method with a try-finally block.
"""
def wrapper(self, *args, **kwargs):
try:
# Call the original function with self, args, and kwargs
return func(self, *args, **kwargs)
finally:
if self.use_audit_pipeline and not self.backend_data.get("catched_pearson_error"):
save_audit_pipeline(
pipeline_backend=self.__class__.__name__,
last_step=self.get_pipeline()[self.backend_data.get("pipeline_index", 0)].__name__,
**self.backend_data
)

return wrapper


class AbstractBackend(ABC):
"""
Base class for managing backend operations and executing pipelines.
Expand All @@ -53,15 +78,17 @@ class AbstractBackend(ABC):
handle_error(exception: Exception, failed_step_pipeline: str): Handles errors during pipeline execution.
"""

def __init__(self, **kwargs):
def __init__(self, use_audit_pipeline=True, **kwargs):
"""
Initializes the AbstractBackend instance with the provided keyword arguments.
Args:
**kwargs: Additional keyword arguments to configure the AbstractBackend instance.
"""
self.backend_data = kwargs.copy()
self.use_audit_pipeline = use_audit_pipeline

@audit_pipeline
def run_pipeline(self):
"""
Executes the pipeline by iterating through the pipeline functions.
Expand All @@ -78,6 +105,7 @@ def run_pipeline(self):
try:
result = func(**self.backend_data) or {}
except PearsonBaseError as pearson_error:
self.backend_data["catched_pearson_error"] = True
self.handle_error(pearson_error, func.__name__)
break

Expand Down
57 changes: 57 additions & 0 deletions eox_nelp/pearson_vue/tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
handle_course_completion_status,
import_candidate_demographics,
import_exam_authorization,
save_audit_pipeline,
validate_cdd_request,
validate_ead_request,
)
Expand Down Expand Up @@ -1231,3 +1232,59 @@ def test_get_enrollment_with_provided_enrollment(self):

# Verify mock calls
CourseEnrollment.objects.get.assert_not_called()


class TestSaveAuditPipeline(unittest.TestCase):
"""
Unit tests for the save_audit_pipeline method.
"""

def test_audit_succesfull_cdd(self):
"""Test correct behaviour calling save_audit_pipeline.
Expected behavior:
- The result is the expected value(None).
- Expected log info.
"""
pipeline_backend = "CandidateDemographicsDataImport"
last_step = "import_candidate_demographics"
cdd_import = {}
log_info = [
f"INFO:{pipeline.__name__}:"
f"Pipeline {pipeline_backend} executed until last step {last_step}",
f"INFO:{pipeline.__name__}:"
f"Succesfull imports. cdd: {cdd_import}. ead: {None}",
]

with self.assertLogs(pipeline.__name__, level="INFO") as logs:
self.assertIsNone(save_audit_pipeline(
pipeline_backend=pipeline_backend,
last_step=last_step,
cdd_import=cdd_import,
))
self.assertListEqual(log_info, logs.output)

def test_audit_succesfull_ead(self):
"""Test correct behaviour calling audit_sucessfull_import.
Expected behavior:
- The result is the expected value(None).
- Expected log info.
"""
pipeline_backend = "ExamAuthorizationDataImport"
last_step = "import_exam_authorization"
ead_import = {}
log_info = [
f"INFO:{pipeline.__name__}:"
f"Pipeline {pipeline_backend} executed until last step {last_step}",
f"INFO:{pipeline.__name__}:"
f"Succesfull imports. cdd: {None}. ead: {ead_import}",
]

with self.assertLogs(pipeline.__name__, level="INFO") as logs:
self.assertIsNone(save_audit_pipeline(
pipeline_backend=pipeline_backend,
last_step=last_step,
ead_import=ead_import,
))
self.assertListEqual(log_info, logs.output)
42 changes: 30 additions & 12 deletions eox_nelp/pearson_vue/tests/test_rti_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def setUp(self): # pylint: disable=invalid-name
Set up the test environment.
"""
self.backend_data = {"pipeline_index": 0}
self.rti = self.rti_backend_class(**self.backend_data)
self.rti = self.rti_backend_class(**self.backend_data, use_audit_pipeline=True)

def test_init(self):
"""
Expand All @@ -38,18 +38,20 @@ def test_init(self):
"""
self.assertDictEqual(self.rti.backend_data, self.backend_data)

def test_run_pipeline(self):
@patch("eox_nelp.pearson_vue.rti_backend.save_audit_pipeline")
def test_run_pipeline(self, audit_pipeline_mock):
"""
Test the execution of the RTI pipeline.
Expected behavior:
- Pipeline method 1 is called with the original data.
- Pipeline method 2 is called with updated data.
- backend_data attribute is the expected value.
- audit_pipeline mock called with desired parameters.
"""
# Mock pipeline functions
func1 = MagicMock(return_value={"updated_key": "value1"})
func2 = MagicMock(return_value={"additional_key": "value2"})
func1 = MagicMock(return_value={"updated_key": "value1"}, __name__="func1")
func2 = MagicMock(return_value={"additional_key": "value2"}, __name__="func2")
self.rti.get_pipeline = MagicMock(return_value=[func1, func2])

self.rti.run_pipeline()
Expand All @@ -64,6 +66,11 @@ def test_run_pipeline(self):
**func2(), # Include data from func2
},
)
audit_pipeline_mock.assert_called_once_with(
pipeline_backend=self.rti.__class__.__name__,
last_step=func2.__name__,
**self.rti.backend_data
)

def test_pipeline_index(self):
"""
Expand All @@ -81,7 +88,7 @@ def test_pipeline_index(self):
func3 = MagicMock()
func3_output = {"last_value": "value3"}
func3.side_effect = [Exception("Test exception"), func3_output]
rti = self.rti_backend_class(pipeline_index=0)
rti = self.rti_backend_class(pipeline_index=0, use_audit_pipeline=False)
rti.get_pipeline = MagicMock(return_value=[func1, func2, func3])

with self.assertRaises(Exception):
Expand All @@ -107,7 +114,8 @@ def test_pipeline_index(self):
},
)

def test_safely_pipeline_termination(self):
@patch("eox_nelp.pearson_vue.rti_backend.save_audit_pipeline")
def test_safely_pipeline_termination(self, audit_pipeline_mock):
"""
Test the execution of the RTI finished after the second function call due
`safely_pipeline_termination` kwarg.
Expand All @@ -119,12 +127,13 @@ def test_safely_pipeline_termination(self):
- Pipeline method 4 is not called.
- backend_data attribute is the expected value.
Without func3,func4 data and pipeline index in the last.
- audit_pipeline mock called with desired parameters.
"""
# Mock pipeline functions
func1 = MagicMock(return_value={"updated_key": "value1"})
func2 = MagicMock(return_value={"safely_pipeline_termination": True})
func3 = MagicMock(return_value={"additional_key": "value3"})
func4 = MagicMock(return_value={"additional_key": "value4"})
func1 = MagicMock(return_value={"updated_key": "value1"}, __name__="func1")
func2 = MagicMock(return_value={"safely_pipeline_termination": True}, __name__="func2")
func3 = MagicMock(return_value={"additional_key": "value3"}, __name__="func3")
func4 = MagicMock(return_value={"additional_key": "value4"}, __name__="func4")

self.rti.get_pipeline = MagicMock(return_value=[func1, func2, func2])

Expand All @@ -143,6 +152,11 @@ def test_safely_pipeline_termination(self):
**func2(), # Include data from func2 (with safely_pipeline_termination)
},
)
audit_pipeline_mock.assert_called_once_with(
pipeline_backend=self.rti.__class__.__name__,
last_step=func2.__name__,
**self.rti.backend_data
)

def test_get_pipeline(self):
"""
Expand All @@ -165,13 +179,14 @@ class TestRealTimeImport(TestAbstractBackendMixin, unittest.TestCase):
"""
rti_backend_class = RealTimeImport

@patch("eox_nelp.pearson_vue.rti_backend.save_audit_pipeline")
@patch("eox_nelp.pearson_vue.tasks.rti_error_handler_task")
@data(
PearsonValidationError(inspect.currentframe(), "error: ['String to short.']"),
PearsonKeyError(inspect.currentframe(), "eligibility_appt_date_first"),
PearsonAttributeError(inspect.currentframe(), "Settings' object has no attribute PERITA")
)
@patch("eox_nelp.pearson_vue.tasks.rti_error_handler_task")
def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler_task_mock):
def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler_task_mock, audit_pipeline_mock):
"""
Test the execution of the RTI finished after the second function call due
`launch_validation_error_pipeline` kwarg.
Expand All @@ -184,6 +199,7 @@ def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler
- backend_data attribute is the expected value.
Without func3,func4 data and pipeline index in the last.
- rti_error_handler_task is called with executed__pipeline_kwargs and error_validation_kwargs.
- audit pipeline is not called due catched_pearson_error.
"""
# Mock pipeline functions
func1 = MagicMock(return_value={"updated_key": "value1"})
Expand All @@ -208,6 +224,7 @@ def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler
self.rti.backend_data,
{
"pipeline_index": 1, # includes the pipe executed until break due exception
'catched_pearson_error': True, # pipeline error flag
**func1(), # Include data from func1 ()
},
)
Expand All @@ -217,6 +234,7 @@ def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler
user_id=None,
course_id=None,
)
audit_pipeline_mock.assert_not_called()


class TestExamAuthorizationDataImport(TestRealTimeImport):
Expand Down

0 comments on commit abbc564

Please sign in to comment.