diff --git a/eox_nelp/pearson_vue/decorators.py b/eox_nelp/pearson_vue/decorators.py new file mode 100644 index 00000000..381db276 --- /dev/null +++ b/eox_nelp/pearson_vue/decorators.py @@ -0,0 +1,51 @@ +""" +Module to add decorators related Pearson Vue Integration +""" +import logging + +from eox_nelp.utils import camel_to_snake + +try: + from eox_audit_model.decorators import audit_method, rename_function +except ImportError: + def rename_function(name): # pylint: disable=unused-argument + """Identity rename_function""" + return lambda x: x + + def audit_method(action): # pylint: disable=unused-argument + """Identity audit_method""" + return lambda x: x + +logger = logging.getLogger(__name__) + + +def audit_backend(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. + Finally if there is backend_data, is logged after the execution. + """ + def wrapper(self, *args, **kwargs): + + backend_name = self.__class__.__name__ + + @audit_method(action=f"Backend Execution: {backend_name}") + @rename_function(name=f"audit_backend_{camel_to_snake(backend_name)}") + def audit_backend_manager(backend_data, **kwargs): # pylint: disable=unused-argument + logger.info( + "Backend %s executed. \n backend_data: %s", + backend_name, + backend_data, + ) + + try: + return func(self, *args, **kwargs) + finally: + if self.use_audit_backend and not self.backend_data.get("catched_pearson_error"): + audit_backend_manager(backend_data=self.backend_data, **kwargs) + + return wrapper diff --git a/eox_nelp/pearson_vue/pipeline.py b/eox_nelp/pearson_vue/pipeline.py index 52b50bef..0fa0134d 100644 --- a/eox_nelp/pearson_vue/pipeline.py +++ b/eox_nelp/pearson_vue/pipeline.py @@ -231,7 +231,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}", @@ -280,7 +280,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}", diff --git a/eox_nelp/pearson_vue/rti_backend.py b/eox_nelp/pearson_vue/rti_backend.py index 290735c8..ae393b00 100644 --- a/eox_nelp/pearson_vue/rti_backend.py +++ b/eox_nelp/pearson_vue/rti_backend.py @@ -16,6 +16,7 @@ import importlib from abc import ABC, abstractmethod +from eox_nelp.pearson_vue.decorators import audit_backend from eox_nelp.pearson_vue.exceptions import PearsonBaseError from eox_nelp.pearson_vue.pipeline import ( audit_pearson_error, @@ -52,6 +53,7 @@ class AbstractBackend(ABC): get_pipeline(): Returns the pipeline, which is a list of functions to be executed (abstract method). handle_error(exception: Exception, failed_step_pipeline: str): Handles errors during pipeline execution. """ + use_audit_backend = True def __init__(self, **kwargs): """ @@ -62,6 +64,7 @@ def __init__(self, **kwargs): """ self.backend_data = kwargs.copy() + @audit_backend def run_pipeline(self): """ Executes the pipeline by iterating through the pipeline functions. @@ -78,6 +81,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 @@ -110,6 +114,7 @@ def handle_error(self, exception, failed_step_pipeline): class ErrorRealTimeImportHandler(AbstractBackend): """Class for managing validation error pipe executing the pipeline for data validation.""" + use_audit_backend = False def handle_error(self, exception, failed_step_pipeline): """ @@ -140,7 +145,6 @@ class RealTimeImport(AbstractBackend): run_pipeline(): Executes the RTI pipeline by iterating through the pipeline functions. get_pipeline(): Returns the RTI pipeline, which is a list of functions to be executed. """ - def handle_error(self, exception, failed_step_pipeline): """ Handles errors during pipeline execution. diff --git a/eox_nelp/pearson_vue/tests/test_rti_backend.py b/eox_nelp/pearson_vue/tests/test_rti_backend.py index 3ea27f7b..bbc286ea 100644 --- a/eox_nelp/pearson_vue/tests/test_rti_backend.py +++ b/eox_nelp/pearson_vue/tests/test_rti_backend.py @@ -7,6 +7,7 @@ from ddt import data, ddt +from eox_nelp.pearson_vue import decorators from eox_nelp.pearson_vue.exceptions import PearsonAttributeError, PearsonKeyError, PearsonValidationError from eox_nelp.pearson_vue.rti_backend import ( CandidateDemographicsDataImport, @@ -46,13 +47,25 @@ def test_run_pipeline(self): - Pipeline method 1 is called with the original data. - Pipeline method 2 is called with updated data. - backend_data attribute is the expected value. + - assert logs if the class has truthy `use_audit_backend` """ # Mock pipeline functions func1 = MagicMock(return_value={"updated_key": "value1"}) func2 = MagicMock(return_value={"additional_key": "value2"}) self.rti.get_pipeline = MagicMock(return_value=[func1, func2]) - self.rti.run_pipeline() + if self.rti.use_audit_backend: + with self.assertLogs(decorators.__name__, level="INFO") as logs: + self.rti.run_pipeline() + self.assertEqual( + logs.output, + [ + f"INFO:{decorators.__name__}:" + f"Backend {self.rti.__class__.__name__} executed. \n backend_data: {self.rti.backend_data}" + ] + ) + else: + self.rti.run_pipeline() func1.assert_called_once_with(**self.backend_data) func2.assert_called_once_with(**{"updated_key": "value1", "pipeline_index": 1}) @@ -165,13 +178,14 @@ class TestRealTimeImport(TestAbstractBackendMixin, unittest.TestCase): """ rti_backend_class = RealTimeImport + @patch("eox_nelp.pearson_vue.decorators.logger") + @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_logger): """ Test the execution of the RTI finished after the second function call due `launch_validation_error_pipeline` kwarg. @@ -184,6 +198,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_method_mock is not called due catched_pearson_error. """ # Mock pipeline functions func1 = MagicMock(return_value={"updated_key": "value1"}) @@ -208,6 +223,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 () }, ) @@ -217,6 +233,7 @@ def test_launch_validation_error_pipeline(self, pearson_error, rti_error_handler user_id=None, course_id=None, ) + audit_logger.info.assert_not_called() class TestExamAuthorizationDataImport(TestRealTimeImport):