Refactor activity methods and update requirements.txt to enhance functionality and remove deprecated filters. Added detailed docstrings for clarity and improved error handling in data processing workflows.
588 lines
18 KiB
Python
588 lines
18 KiB
Python
from unittest.mock import ANY, MagicMock, patch
|
|
from pandas import DataFrame
|
|
from pytest import fixture, mark
|
|
|
|
from laborious.activities.gates import Gates
|
|
from sientia_do.notifications.models import NotificationLevel
|
|
|
|
|
|
@fixture
|
|
def gates():
|
|
return Gates(
|
|
logger=MagicMock(),
|
|
notification_handler=MagicMock()
|
|
)
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_specific_variables_null_values_with_stop_policy_only(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=True)
|
|
empty_data_mock = MagicMock(return_value=False)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == ('stop', -1)
|
|
|
|
input_args = specific_variables_null_values_mock.call_args
|
|
assert input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert input_args[0][1] == input_data['filters']['SPECIFIC_VARIABLES_NULL_VALUES']
|
|
|
|
empty_data_mock.assert_not_called()
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_specific_variables_null_values_with_continue_policy_only(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=True)
|
|
empty_data_mock = MagicMock(return_value=False)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'continue',
|
|
'VARIABLES': ['variable2']
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == ('continue', 2)
|
|
|
|
input_args = specific_variables_null_values_mock.call_args
|
|
assert input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert input_args[0][1] == input_data['filters']['SPECIFIC_VARIABLES_NULL_VALUES']
|
|
|
|
empty_data_mock.assert_not_called()
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_specific_variables_null_values_no_filtered(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=False)
|
|
empty_data_mock = MagicMock(return_value=False)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == (None, 0)
|
|
|
|
specific_variables_null_values_input_args = specific_variables_null_values_mock.call_args
|
|
|
|
assert specific_variables_null_values_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert specific_variables_null_values_input_args[0][
|
|
1] == input_data['filters']['SPECIFIC_VARIABLES_NULL_VALUES']
|
|
|
|
empty_data_mock.assert_not_called()
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_one_stop_policy(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=True)
|
|
empty_data_mock = MagicMock(return_value=True)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
},
|
|
'EMPTY_DATA': {
|
|
'POLICY': 'continue',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == ('stop', -1)
|
|
|
|
specific_variables_null_values_input_args = specific_variables_null_values_mock.call_args
|
|
assert specific_variables_null_values_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert specific_variables_null_values_input_args[0][
|
|
1] == input_data['filters']['SPECIFIC_VARIABLES_NULL_VALUES']
|
|
|
|
empty_data_input_args = empty_data_mock.call_args
|
|
assert empty_data_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert empty_data_input_args[0][1] == input_data['filters']['EMPTY_DATA']
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_one_continue_policy(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=False)
|
|
empty_data_mock = MagicMock(return_value=True)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
},
|
|
'EMPTY_DATA': {
|
|
'POLICY': 'continue',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == ('continue', 2)
|
|
|
|
specific_variables_null_values_input_args = specific_variables_null_values_mock.call_args
|
|
assert specific_variables_null_values_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert specific_variables_null_values_input_args[0][
|
|
1] == input_data['filters']['SPECIFIC_VARIABLES_NULL_VALUES']
|
|
|
|
empty_data_input_args = empty_data_mock.call_args
|
|
assert empty_data_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert empty_data_input_args[0][1] == input_data['filters']['EMPTY_DATA']
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_no_filtered(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
specific_variables_null_values_mock = MagicMock(return_value=False)
|
|
empty_data_mock = MagicMock(return_value=False)
|
|
|
|
def functions_side_effect(x):
|
|
if x == 'SPECIFIC_VARIABLES_NULL_VALUES':
|
|
return specific_variables_null_values_mock
|
|
if x == 'path_confidence':
|
|
return {
|
|
'stop': -1,
|
|
'continue': 2,
|
|
'repeat': -1
|
|
}
|
|
return empty_data_mock
|
|
|
|
input_filter_functions_mock.__getitem__.side_effect = functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
},
|
|
'EMPTY_DATA': {
|
|
'POLICY': 'continue',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat']
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == (None, 0)
|
|
|
|
specific_variables_null_values_input_args = specific_variables_null_values_mock.call_args
|
|
assert specific_variables_null_values_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
|
|
empty_data_input_args = empty_data_mock.call_args
|
|
assert empty_data_input_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert empty_data_input_args[0][1] == input_data['filters']['EMPTY_DATA']
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.input_filter_functions')
|
|
async def test_input_gate_error(
|
|
input_filter_functions_mock,
|
|
gates
|
|
):
|
|
input_filter_functions_mock.__getitem__.side_effect = KeyError('test')
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'SPECIFIC_VARIABLES_NULL_VALUES': {
|
|
'POLICY': 'stop',
|
|
'VARIABLES': ['variable2']
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat'],
|
|
}
|
|
|
|
result = await gates.input_gate(input_data)
|
|
assert result == (None, 0)
|
|
|
|
gates.notification_handler.build_and_send_notification.assert_called_once_with(
|
|
notification_id='INTPUT_GATE_ERROR__SPECIFIC_VARIABLES_NULL_VALUES',
|
|
message="Error in filter SPECIFIC_VARIABLES_NULL_VALUES:{'POLICY': 'stop', 'VARIABLES': ['variable2']}: \n 'test'",
|
|
block='input_gate',
|
|
level=NotificationLevel.ERROR,
|
|
attachment_content=ANY
|
|
)
|
|
|
|
|
|
transform_filter_path_confidence = {
|
|
'stop': -1,
|
|
'continue': 255,
|
|
'repeat': -1
|
|
}
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.mlflow_response_filter_functions')
|
|
async def test_mlflow_response_gate_no_filtered(
|
|
mlflow_response_filter_functions_mock,
|
|
gates
|
|
):
|
|
api_error_filter_mock = MagicMock(return_value=False)
|
|
|
|
def transform_filter_functions_side_effect(x: str):
|
|
if x == 'API_ERROR':
|
|
return api_error_filter_mock
|
|
if x == 'path_confidence':
|
|
return transform_filter_path_confidence
|
|
|
|
mlflow_response_filter_functions_mock.__getitem__.side_effect = transform_filter_functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'API_ERROR': {
|
|
'POLICY': 'stop',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat'],
|
|
'type': 'predict'
|
|
}
|
|
|
|
result = await gates.mlflow_response_gate(input_data)
|
|
assert result == (None, 0)
|
|
|
|
api_error_filter_mock.assert_called_once_with(
|
|
input_data['data'],
|
|
input_data['filters']['API_ERROR']
|
|
)
|
|
|
|
gates.notification_handler.build_and_send_notification.assert_not_called()
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.mlflow_response_filter_functions')
|
|
async def test_mlflow_response_gate_filtered(
|
|
mlflow_response_filter_functions_mock,
|
|
gates
|
|
):
|
|
api_error_filter_mock = MagicMock(return_value=True)
|
|
|
|
def transform_filter_functions_side_effect(x: str):
|
|
if x == 'API_ERROR':
|
|
return api_error_filter_mock
|
|
if x == 'path_confidence':
|
|
return transform_filter_path_confidence
|
|
|
|
mlflow_response_filter_functions_mock.__getitem__.side_effect = transform_filter_functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'API_ERROR': {
|
|
'POLICY': 'continue',
|
|
}
|
|
},
|
|
'data': {
|
|
'success': False,
|
|
'content': {
|
|
'message': 'Error',
|
|
'traceback': 'Error'
|
|
}
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat'],
|
|
'type': 'predict'
|
|
}
|
|
|
|
result = await gates.mlflow_response_gate(input_data)
|
|
assert result == ('continue', 255)
|
|
|
|
api_error_filter_mock.assert_called_once_with(
|
|
input_data['data'],
|
|
input_data['filters']['API_ERROR']
|
|
)
|
|
|
|
gates.notification_handler.build_and_send_notification.assert_called_once_with(
|
|
notification_id='PREDICT_GATE_RESPONSE_FILTER__API_ERROR',
|
|
message=input_data['data']['content']['message'],
|
|
block='mlflow_gate',
|
|
level=NotificationLevel.WARNING,
|
|
attachment_content=input_data['data']['content']['traceback']
|
|
)
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.mlflow_content_filter_functions')
|
|
async def test_mlflow_content_gate_no_filtered(
|
|
mlflow_content_filter_functions_mock,
|
|
gates
|
|
):
|
|
nan_values_filter_mock = MagicMock(return_value=False)
|
|
|
|
def transform_filter_functions_side_effect(x: str):
|
|
if x == 'NAN_VALUES':
|
|
return nan_values_filter_mock
|
|
if x == 'path_confidence':
|
|
return transform_filter_path_confidence
|
|
|
|
mlflow_content_filter_functions_mock.__getitem__.side_effect = transform_filter_functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'NAN_VALUES': {
|
|
'POLICY': 'repeat',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat'],
|
|
'type': 'predict'
|
|
}
|
|
|
|
result = await gates.mlflow_content_gate(input_data)
|
|
assert result == (None, 0)
|
|
|
|
nan_values_filter_mock_args = nan_values_filter_mock.call_args
|
|
assert nan_values_filter_mock_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert nan_values_filter_mock_args[0][1] == input_data['filters']['NAN_VALUES']
|
|
|
|
gates.notification_handler.build_and_send_notification.assert_not_called()
|
|
|
|
|
|
@mark.asyncio
|
|
@patch('laborious.activities.gates.mlflow_content_filter_functions')
|
|
async def test_mlflow_content_gate_filtered(
|
|
mlflow_content_filter_functions_mock,
|
|
gates
|
|
):
|
|
nan_values_filter_mock = MagicMock(return_value=True)
|
|
|
|
def transform_filter_functions_side_effect(x: str):
|
|
if x == 'NAN_VALUES':
|
|
return nan_values_filter_mock
|
|
if x == 'path_confidence':
|
|
return transform_filter_path_confidence
|
|
|
|
mlflow_content_filter_functions_mock.__getitem__.side_effect = transform_filter_functions_side_effect
|
|
|
|
input_data = {
|
|
'filters': {
|
|
'NAN_VALUES': {
|
|
'POLICY': 'repeat',
|
|
}
|
|
},
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'path_priority': ['stop', 'continue', 'repeat'],
|
|
'type': 'predict'
|
|
}
|
|
|
|
result = await gates.mlflow_content_gate(input_data)
|
|
assert result == ('repeat', -1)
|
|
|
|
nan_values_filter_mock_args = nan_values_filter_mock.call_args
|
|
assert nan_values_filter_mock_args[0][0].equals(DataFrame(
|
|
{'variable': ['variable1', 'variable2'], 'value': [1, 2]}))
|
|
assert nan_values_filter_mock_args[0][1] == input_data['filters']['NAN_VALUES']
|
|
|
|
gates.notification_handler.build_and_send_notification.assert_called_once_with(
|
|
notification_id='PREDICT_GATE_CONTENT_FILTER__NAN_VALUES',
|
|
message="Data not passed the content filter NAN_VALUES:{'POLICY': 'repeat'}",
|
|
block='mlflow_gate',
|
|
level=NotificationLevel.WARNING,
|
|
attachment_content=DataFrame(input_data['data']).to_string()
|
|
)
|
|
|
|
|
|
@mark.asyncio
|
|
async def test_format_prediction(
|
|
gates
|
|
):
|
|
input_data = {
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2]
|
|
},
|
|
'timestamp': '2021-01-01',
|
|
'model_id': 'model_id',
|
|
'prediction_confidence': 0.95
|
|
}
|
|
|
|
expected_output = DataFrame(input_data['data'])
|
|
expected_output['timestamp'] = input_data['timestamp']
|
|
expected_output['model_id'] = input_data['model_id']
|
|
expected_output['prediction_confidence'] = input_data['prediction_confidence']
|
|
expected_output['prediction_status'] = 'Good'
|
|
expected_output['comment'] = ''
|
|
|
|
result = await gates.format_prediction(input_data)
|
|
assert result == expected_output.to_dict()
|
|
|
|
|
|
@mark.asyncio
|
|
async def test_format_default_prediction(
|
|
gates
|
|
):
|
|
input_data = {
|
|
'timestamp': '2021-01-01',
|
|
'model_id': 'model_id',
|
|
'prediction_confidence': 0.95,
|
|
'comment': 'Comment'
|
|
}
|
|
|
|
expected_output = DataFrame({
|
|
'prediction': [0],
|
|
'response_time': [0],
|
|
'timestamp': [input_data['timestamp']],
|
|
'model_id': [input_data['model_id']],
|
|
'prediction_confidence': [input_data['prediction_confidence']],
|
|
'prediction_status': ['Bad'],
|
|
'comment': [input_data['comment']]
|
|
})
|
|
|
|
result = await gates.format_default_prediction(input_data)
|
|
assert result == expected_output.to_dict()
|