Skip to content

Commit

Permalink
feat: use decorator for audit backend (#192)
Browse files Browse the repository at this point in the history
* feat: use decorator for audit backend

* fix: audit could not serialize classes(self)

* feat: add test based in `use_audit_backend`
  • Loading branch information
johanseto authored Jul 12, 2024
1 parent f3da71e commit a640038
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 6 deletions.
51 changes: 51 additions & 0 deletions eox_nelp/pearson_vue/decorators.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions eox_nelp/pearson_vue/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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}",
Expand Down
6 changes: 5 additions & 1 deletion eox_nelp/pearson_vue/rti_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 20 additions & 3 deletions eox_nelp/pearson_vue/tests/test_rti_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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.
Expand All @@ -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"})
Expand All @@ -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 ()
},
)
Expand All @@ -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):
Expand Down

0 comments on commit a640038

Please sign in to comment.