Refactor and enhance the laborious workflow and utilities - Removed outdated test file `test_predictions_batch.py` from workflows. - Added `input_sample.json` for standardized input configuration. - Introduced `connectors_config.py` to manage database and service configurations. - Implemented a logging utility in `logger.py` for consistent logging across the application. - Created `policies.py` to define retry policies for workflows. - Developed comprehensive tests for `MLFlowRepository` in `test_model_repository.py`. - Added extensive tests for `OpcRepository` in `test_opc_repository.py`. - Updated `test_predictions_batch.py` to reflect new workflow structure and testing methodology.
606 lines
19 KiB
Python
606 lines
19 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 data with bad quality')
|
|
|
|
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 data with bad quality')
|
|
|
|
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, 'Input data with bad quality')
|
|
|
|
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, 'Input data with bad quality')
|
|
|
|
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, "Error")
|
|
|
|
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, "Transformed data not passed the content filter")
|
|
|
|
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()
|
|
|
|
|
|
@mark.asyncio
|
|
async def test_get_last_timestamp(
|
|
gates
|
|
):
|
|
input_data = {
|
|
'data': {
|
|
'variable': ['variable1', 'variable2'],
|
|
'value': [1, 2],
|
|
'timestamp': ['2021-01-01', '2021-01-02']
|
|
}
|
|
}
|
|
|
|
result = await gates.get_last_timestamp(input_data)
|
|
assert result == '2021-01-02'
|