Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add audit for succesfull imports #192

Merged
merged 3 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -227,7 +227,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}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we will need this ?


raise PearsonImportError(
exception_reason=f"Import candidate demographics pipeline has failed with the following response: {response}",
Expand Down Expand Up @@ -276,7 +276,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}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above


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
Loading