SIENTIAPDE-1712
Remove code validation script and refactor imports in activities and workflows - Deleted the `validate.sh` script, which was responsible for running code quality checks. - Cleaned up import statements in `activities.py`, `gates.py`, `mlflow.py`, and `storage.py` by removing unused imports and organizing them. - Refactored initialization methods in `MinioManager` and `MLFlow` classes for improved readability. - Updated various workflows to ensure compatibility with the new structure and removed unnecessary comments. - Enhanced test cases to accommodate changes in the activities and workflows, ensuring proper mocking of dependencies.
This commit is contained in:
@@ -17,9 +17,11 @@ from laborious.activities.storage import Storage
|
||||
@patch('laborious.activities.activities.Gates.__init__')
|
||||
@patch('laborious.activities.activities.ModelMetrics.__init__')
|
||||
@patch('laborious.activities.activities.API.__init__')
|
||||
@patch('laborious.activities.activities.MinioRepository')
|
||||
@patch('laborious.activities.activities.MetricsController')
|
||||
def test___init__(
|
||||
mock_metrics_controller,
|
||||
mock_minio_repository,
|
||||
mock_api_init,
|
||||
mock_model_metrics_init,
|
||||
mock_gates_init,
|
||||
@@ -43,6 +45,7 @@ def test___init__(
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
'retention_hours': 24,
|
||||
}
|
||||
|
||||
mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'}
|
||||
@@ -89,7 +92,8 @@ def test___init__(
|
||||
dbname=postgres_config['dbname'],
|
||||
min_connections=postgres_config['min_connections'],
|
||||
max_connections=postgres_config['max_connections'],
|
||||
minio_config=minio_config,
|
||||
retention_hours=minio_config['retention_hours'],
|
||||
minio_repository=mock_minio_repository.return_value,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
@@ -101,7 +105,7 @@ def test___init__(
|
||||
mlflow_port=mlflow_config['port'],
|
||||
mlflow_username=mlflow_config['username'],
|
||||
mlflow_password=mlflow_config['password'],
|
||||
minio_config=minio_config,
|
||||
minio_repository=mock_minio_repository.return_value,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
@@ -117,6 +121,7 @@ def test___init__(
|
||||
|
||||
mock_gates_init.assert_called_once_with(
|
||||
ANY,
|
||||
minio_repository=mock_minio_repository.return_value,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
@@ -139,6 +144,16 @@ def test___init__(
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
mock_minio_repository.assert_called_once_with(
|
||||
endpoint_url=minio_config['endpoint_url'],
|
||||
access_key=minio_config['access_key'],
|
||||
secret_key=minio_config['secret_key'],
|
||||
bucket=minio_config['default_bucket'],
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.activities.Storage')
|
||||
@@ -147,7 +162,9 @@ def test___init__(
|
||||
@patch('laborious.activities.activities.Gates')
|
||||
@patch('laborious.activities.activities.ModelMetrics')
|
||||
@patch('laborious.activities.activities.API')
|
||||
@patch('laborious.activities.activities.MinioRepository')
|
||||
async def test_shutdown(
|
||||
_mock_minio_repository,
|
||||
mock_api_init,
|
||||
mock_model_metrics_init,
|
||||
mock_gates_init,
|
||||
@@ -172,6 +189,7 @@ async def test_shutdown(
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
'retention_hours': 24,
|
||||
}
|
||||
|
||||
mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'}
|
||||
|
||||
@@ -61,6 +61,60 @@ def base_input_data():
|
||||
}
|
||||
|
||||
|
||||
@patch('laborious.activities.api.PIWebAPIClient')
|
||||
def test_get_pi_web_api_core_labels_without_operation_type(mock_pi_web_api_client):
|
||||
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
|
||||
|
||||
api_instance = API(
|
||||
base_url='https://test-pi-server.com',
|
||||
auth_type='bearer',
|
||||
auth_token='test_token',
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
with patch.object(
|
||||
SientiaMonitoring,
|
||||
'get_core_labels',
|
||||
return_value={
|
||||
'pod_id': 'test_pod',
|
||||
'model_name': 'test_model',
|
||||
'operation_type': '-',
|
||||
},
|
||||
):
|
||||
labels = api_instance.get_pi_web_api_core_labels(
|
||||
metadata=metadata['metadata'], operation_type=None
|
||||
)
|
||||
assert 'operation_type' not in labels
|
||||
|
||||
|
||||
@patch('laborious.activities.api.PIWebAPIClient')
|
||||
def test_get_pi_web_api_core_labels_with_operation_type(mock_pi_web_api_client):
|
||||
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
|
||||
|
||||
api_instance = API(
|
||||
base_url='https://test-pi-server.com',
|
||||
auth_type='bearer',
|
||||
auth_token='test_token',
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
with patch.object(
|
||||
SientiaMonitoring,
|
||||
'get_core_labels',
|
||||
return_value={
|
||||
'pod_id': 'test_pod',
|
||||
'model_name': 'test_model',
|
||||
'operation_type': 'write',
|
||||
},
|
||||
):
|
||||
labels = api_instance.get_pi_web_api_core_labels(
|
||||
metadata=metadata['metadata'], operation_type='write'
|
||||
)
|
||||
assert labels['operation_type'] == 'write'
|
||||
|
||||
|
||||
def test__init__():
|
||||
api = API(
|
||||
base_url='https://test-pi-server.com',
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pandas import DataFrame
|
||||
from pytest import fixture, mark
|
||||
from sientia_do.notifications.models import NotificationLevel
|
||||
|
||||
from laborious.activities.gates import Gates
|
||||
|
||||
|
||||
def _minio_payload(retrieve_return, status=None):
|
||||
"""
|
||||
Build a MinioDataFramePayload-like test double with async retrieve.
|
||||
|
||||
Args:
|
||||
retrieve_return: Value returned from await retrieve(minio_repo, metadata).
|
||||
status: Optional status dict for MLflow response gate (payload.status).
|
||||
|
||||
Return:
|
||||
MagicMock: Object with async retrieve and optional status.
|
||||
"""
|
||||
p = MagicMock()
|
||||
p.retrieve = AsyncMock(return_value=retrieve_return)
|
||||
p.status = status
|
||||
return p
|
||||
|
||||
|
||||
@fixture
|
||||
def gates_activity():
|
||||
gates = Gates(
|
||||
@@ -40,7 +58,7 @@ async def test_input_gate_invalid_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'INVALID_FILTER': {'POLICY': 'STOP'}},
|
||||
'data': {'value': [1, 2, 3]},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
|
||||
@@ -65,7 +83,7 @@ async def test_input_gate_filter_exception(mock_input_filter_functions, gates_ac
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'EMPTY_DATA': {'policy': 'STOP', 'config': {}}},
|
||||
'data': {'value': []},
|
||||
'data': _minio_payload(DataFrame({'value': []})),
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
|
||||
@@ -90,7 +108,7 @@ async def test_input_gate_no_filters(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {},
|
||||
'data': {'value': [1, 2, 3]},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'path_priority': ['CONTINUE', 'STOP', 'REPEAT'],
|
||||
}
|
||||
|
||||
@@ -108,7 +126,7 @@ async def test_input_gate_with_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'EMPTY_DATA': {'policy': 'STOP', 'config': {}}},
|
||||
'data': {'value': []},
|
||||
'data': _minio_payload(DataFrame({'value': []})),
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
|
||||
@@ -126,7 +144,7 @@ async def test_input_gate_with_filter_not_caught(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'EMPTY_DATA': {'policy': 'STOP', 'config': {}}},
|
||||
'data': {'value': [1, 2, 3]},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
|
||||
@@ -144,7 +162,10 @@ async def test_mlflow_response_gate_invalid_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'INVALID_FILTER': {'POLICY': 'STOP'}},
|
||||
'data': {'content': {'message': 'success'}},
|
||||
'data': _minio_payload(
|
||||
{'content': {'message': 'success'}},
|
||||
status={'success': True},
|
||||
),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -169,7 +190,10 @@ async def test_mlflow_response_gate_filter_exception(
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'INVALID_FILTER': {'POLICY': 'STOP'}},
|
||||
'data': {'content': {'message': 'success'}},
|
||||
'data': _minio_payload(
|
||||
{'content': {'message': 'success'}},
|
||||
status={'success': True},
|
||||
),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -195,7 +219,10 @@ async def test_mlflow_response_gate_no_filters(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {},
|
||||
'data': {'content': {'message': 'success'}},
|
||||
'data': _minio_payload(
|
||||
{'content': {'message': 'success'}},
|
||||
status={'success': True},
|
||||
),
|
||||
'type': 'test',
|
||||
'path_priority': ['CONTINUE', 'STOP', 'REPEAT'],
|
||||
}
|
||||
@@ -214,10 +241,10 @@ async def test_mlflow_response_gate_with_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'API_ERROR': {'policy': 'STOP'}},
|
||||
'data': {
|
||||
'success': False,
|
||||
'content': {'message': 'API error occurred', 'traceback': 'error trace'},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
{'content': {'message': 'API error occurred', 'traceback': 'error trace'}},
|
||||
status={'success': False, 'message': 'API error occurred'},
|
||||
),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -237,10 +264,10 @@ async def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'API_ERROR': {'policy': 'STOP'}},
|
||||
'data': {
|
||||
'success': True,
|
||||
'content': {'message': 'success'},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
{'content': {'message': 'success'}},
|
||||
status={'success': True},
|
||||
),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -259,10 +286,7 @@ async def test_mlflow_content_gate_invalid_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'INVALID_FILTER': {'POLICY': 'STOP'}},
|
||||
'data': {
|
||||
'success': True,
|
||||
'content': {'message': 'success'},
|
||||
},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -287,10 +311,7 @@ async def test_mlflow_content_gate_filter_exception(
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'API_ERROR': {'POLICY': 'STOP'}},
|
||||
'data': {
|
||||
'success': False,
|
||||
'content': {'message': 'API error occurred', 'traceback': 'error trace'},
|
||||
},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -317,7 +338,7 @@ async def test_mlflow_content_gate_no_filters(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {},
|
||||
'data': {'value': [1, 2, 3]},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['CONTINUE', 'STOP', 'REPEAT'],
|
||||
}
|
||||
@@ -336,7 +357,7 @@ async def test_mlflow_content_gate_with_filter(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'NAN_VALUES': {'policy': 'STOP', 'config': {}}},
|
||||
'data': {'value': [None, None, None]},
|
||||
'data': _minio_payload(DataFrame({'value': [None, None, None]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -356,7 +377,7 @@ async def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'API_ERROR': {'POLICY': 'STOP'}},
|
||||
'data': {'content': {'message': 'success'}},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
@@ -369,6 +390,22 @@ async def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_mlflow_content_gate_filter_returns_false(gates_activity):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'NAN_VALUES': {'policy': 'STOP', 'config': {}}},
|
||||
'data': _minio_payload(DataFrame({'value': [1, 2, 3]})),
|
||||
'type': 'test',
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
}
|
||||
|
||||
result = await gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
assert result == (None, 0, '')
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
def test_get_prediction_store_policy_invalid_policy(gates_activity):
|
||||
# Arrange
|
||||
prediction_store_policy = 'INVALID_POLICY'
|
||||
@@ -430,10 +467,14 @@ async def test_format_prediction_no_timestamp(gates_activity):
|
||||
# Arrange
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'prediction': {'2023-05-26 11:12:27': 1},
|
||||
'response_time': {'2023-05-26 11:12:27': 0.1},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'prediction': {'2023-05-26 11:12:27': 1},
|
||||
'response_time': {'2023-05-26 11:12:27': 0.1},
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'prediction_confidence': 0.9,
|
||||
'prediction_store_policy': 'lts:1',
|
||||
@@ -457,18 +498,22 @@ async def test_format_prediction_with_timestamp_erl(gates_activity):
|
||||
# Arrange
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'prediction': {
|
||||
'2023-05-26 11:12:27': 1,
|
||||
'2023-05-26 11:12:28': 2,
|
||||
'2023-05-26 11:12:29': 3,
|
||||
},
|
||||
'response_time': {
|
||||
'2023-05-26 11:12:27': 0.1,
|
||||
'2023-05-26 11:12:28': 0.2,
|
||||
'2023-05-26 11:12:29': 0.3,
|
||||
},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'prediction': {
|
||||
'2023-05-26 11:12:27': 1,
|
||||
'2023-05-26 11:12:28': 2,
|
||||
'2023-05-26 11:12:29': 3,
|
||||
},
|
||||
'response_time': {
|
||||
'2023-05-26 11:12:27': 0.1,
|
||||
'2023-05-26 11:12:28': 0.2,
|
||||
'2023-05-26 11:12:29': 0.3,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'prediction_confidence': 0.9,
|
||||
'prediction_store_policy': 'erl:2',
|
||||
@@ -492,18 +537,22 @@ async def test_format_prediction_with_timestamp_lts(gates_activity):
|
||||
# Arrange
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'prediction': {
|
||||
'2023-05-26 11:12:27': 1,
|
||||
'2023-05-26 11:12:28': 2,
|
||||
'2023-05-26 11:12:29': 3,
|
||||
},
|
||||
'response_time': {
|
||||
'2023-05-26 11:12:27': 0.1,
|
||||
'2023-05-26 11:12:28': 0.2,
|
||||
'2023-05-26 11:12:29': 0.3,
|
||||
},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'prediction': {
|
||||
'2023-05-26 11:12:27': 1,
|
||||
'2023-05-26 11:12:28': 2,
|
||||
'2023-05-26 11:12:29': 3,
|
||||
},
|
||||
'response_time': {
|
||||
'2023-05-26 11:12:27': 0.1,
|
||||
'2023-05-26 11:12:28': 0.2,
|
||||
'2023-05-26 11:12:29': 0.3,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'prediction_confidence': 0.9,
|
||||
'prediction_store_policy': 'lts:2',
|
||||
@@ -527,11 +576,19 @@ async def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
|
||||
# Arrange
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'prediction': [1, 2, 3],
|
||||
'response_time': [0.1, 0.2, 0.3],
|
||||
'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28', '2023-05-26 11:12:29'],
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'prediction': [1, 2, 3],
|
||||
'response_time': [0.1, 0.2, 0.3],
|
||||
'timestamp': [
|
||||
'2023-05-26 11:12:27',
|
||||
'2023-05-26 11:12:28',
|
||||
'2023-05-26 11:12:29',
|
||||
],
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'prediction_confidence': 0.9,
|
||||
'prediction_store_policy': 'lts:2',
|
||||
@@ -547,76 +604,106 @@ async def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_format_transformed_data_single_row(gates_activity):
|
||||
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
|
||||
async def test_format_transformed_data_single_row(mock_from_dataframe, gates_activity):
|
||||
# Arrange
|
||||
payload_result = MagicMock()
|
||||
mock_from_dataframe.return_value = payload_result
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'var1': {'2023-05-26 11:12:27': 1.0},
|
||||
'var2': {'2023-05-26 11:12:27': 2.0},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'var1': {'2023-05-26 11:12:27': 1.0},
|
||||
'var2': {'2023-05-26 11:12:27': 2.0},
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'model_name': 'test_model',
|
||||
}
|
||||
|
||||
# Act
|
||||
result = await gates_activity.format_transformed_data(input_data)
|
||||
|
||||
# Assert
|
||||
assert result['timestamp'] == {0: '2023-05-26 11:12:27', 1: '2023-05-26 11:12:27'}
|
||||
assert result['variable'] == {0: 'var1', 1: 'var2'}
|
||||
assert result['value'] == {0: 1.0, 1: 2.0}
|
||||
assert result['model_id'] == {0: 'test_model', 1: 'test_model'}
|
||||
assert result is payload_result
|
||||
mock_from_dataframe.assert_called_once()
|
||||
kwargs = mock_from_dataframe.call_args.kwargs
|
||||
assert kwargs['model_name'] == 'test_model'
|
||||
assert kwargs['operation'] == 'transform'
|
||||
assert kwargs['workflow_metadata'] == metadata['metadata']
|
||||
assert kwargs['minio_repo'] is gates_activity.minio_repository
|
||||
assert 'dataframe' in kwargs
|
||||
gates_activity.info.assert_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_format_transformed_data_multiple_rows(gates_activity):
|
||||
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
|
||||
async def test_format_transformed_data_multiple_rows(mock_from_dataframe, gates_activity):
|
||||
# Arrange
|
||||
payload_result = MagicMock()
|
||||
mock_from_dataframe.return_value = payload_result
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'var1': {
|
||||
'2023-05-26 11:12:27': 1.0,
|
||||
'2023-05-26 11:12:28': 2.0,
|
||||
},
|
||||
'var2': {
|
||||
'2023-05-26 11:12:27': 3.0,
|
||||
'2023-05-26 11:12:28': 4.0,
|
||||
},
|
||||
},
|
||||
'data': _minio_payload(
|
||||
DataFrame(
|
||||
{
|
||||
'var1': {
|
||||
'2023-05-26 11:12:27': 1.0,
|
||||
'2023-05-26 11:12:28': 2.0,
|
||||
},
|
||||
'var2': {
|
||||
'2023-05-26 11:12:27': 3.0,
|
||||
'2023-05-26 11:12:28': 4.0,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
'model_id': 'test_model',
|
||||
'model_name': 'test_model',
|
||||
}
|
||||
|
||||
# Act
|
||||
result = await gates_activity.format_transformed_data(input_data)
|
||||
|
||||
# Assert
|
||||
assert len(result['timestamp']) == 4
|
||||
assert len(result['variable']) == 4
|
||||
assert len(result['value']) == 4
|
||||
assert len(result['model_id']) == 4
|
||||
assert all(v == 'test_model' for v in result['model_id'].values())
|
||||
assert set(result['variable'].values()) == {'var1', 'var2'}
|
||||
assert result is payload_result
|
||||
mock_from_dataframe.assert_called_once()
|
||||
kwargs = mock_from_dataframe.call_args.kwargs
|
||||
assert kwargs['model_name'] == 'test_model'
|
||||
assert kwargs['operation'] == 'transform'
|
||||
assert kwargs['workflow_metadata'] == metadata['metadata']
|
||||
assert kwargs['minio_repo'] is gates_activity.minio_repository
|
||||
assert 'dataframe' in kwargs
|
||||
gates_activity.info.assert_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_format_transformed_data_empty_data(gates_activity):
|
||||
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
|
||||
async def test_format_transformed_data_empty_data(mock_from_dataframe, gates_activity):
|
||||
# Arrange
|
||||
payload_result = MagicMock()
|
||||
mock_from_dataframe.return_value = payload_result
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {},
|
||||
'data': _minio_payload(DataFrame()),
|
||||
'model_id': 'test_model',
|
||||
'model_name': 'test_model',
|
||||
}
|
||||
|
||||
# Act
|
||||
result = await gates_activity.format_transformed_data(input_data)
|
||||
|
||||
# Assert
|
||||
assert result['timestamp'] == {}
|
||||
assert result['variable'] == {}
|
||||
assert result['value'] == {}
|
||||
assert result['model_id'] == {}
|
||||
assert result is payload_result
|
||||
mock_from_dataframe.assert_called_once()
|
||||
kwargs = mock_from_dataframe.call_args.kwargs
|
||||
assert kwargs['model_name'] == 'test_model'
|
||||
assert kwargs['operation'] == 'transform'
|
||||
assert kwargs['workflow_metadata'] == metadata['metadata']
|
||||
assert kwargs['minio_repo'] is gates_activity.minio_repository
|
||||
assert 'dataframe' in kwargs
|
||||
gates_activity.info.assert_called()
|
||||
|
||||
|
||||
@@ -711,31 +798,6 @@ async def test_format_retrain_report_failure(gates_activity):
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_get_last_timestamp_with_data(gates_activity):
|
||||
# Arrange
|
||||
input_data = {**metadata, 'data': {'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28']}}
|
||||
|
||||
# Act
|
||||
result = await gates_activity.get_last_timestamp(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == '2023-05-26 11:12:28'
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_get_last_timestamp_no_data(gates_activity):
|
||||
# Arrange
|
||||
input_data = {'data': {}, **metadata}
|
||||
|
||||
# Act
|
||||
result = await gates_activity.get_last_timestamp(input_data)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, str) # Should be a timestamp string
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.gates.metrics')
|
||||
async def test_write_metrics(mock_metrics, gates_activity):
|
||||
|
||||
@@ -11,21 +11,27 @@ from laborious.activities.mlflow import MLFlow
|
||||
@patch('laborious.activities.mlflow.MLFlowRepository')
|
||||
@patch('laborious.activities.mlflow.MinioRepository')
|
||||
def test___init__(mock_minio_repository, mock_mlflow_repository):
|
||||
logger = MagicMock()
|
||||
notification_handler = MagicMock()
|
||||
metrics_controller = AsyncMock()
|
||||
minio_repo = mock_minio_repository(
|
||||
endpoint='localhost:9000',
|
||||
access_key='minio',
|
||||
secret_key='minio123',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
bucket='test',
|
||||
)
|
||||
mlflow = MLFlow(
|
||||
mlflow_host='http://localhost',
|
||||
mlflow_port=5000,
|
||||
mlflow_username='admin',
|
||||
mlflow_password='admin',
|
||||
minio_config={
|
||||
'endpoint_url': 'http://localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
minio_repository=minio_repo,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
|
||||
assert mlflow.mlflow_host == 'http://localhost'
|
||||
@@ -52,21 +58,27 @@ def test___init__(mock_minio_repository, mock_mlflow_repository):
|
||||
@patch('laborious.activities.mlflow.MLFlowRepository')
|
||||
@patch('laborious.activities.mlflow.MinioRepository')
|
||||
def mlflow(mock_minio_repository, mock_mlflow_repository):
|
||||
logger = MagicMock()
|
||||
notification_handler = MagicMock()
|
||||
metrics_controller = AsyncMock()
|
||||
minio_repo = mock_minio_repository(
|
||||
endpoint='localhost:9000',
|
||||
access_key='minio',
|
||||
secret_key='minio123',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
bucket='test',
|
||||
)
|
||||
mlflow = MLFlow(
|
||||
mlflow_host='http://localhost:5000',
|
||||
mlflow_port=5000,
|
||||
mlflow_username='admin',
|
||||
mlflow_password='admin',
|
||||
minio_config={
|
||||
'endpoint_url': 'http://localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
minio_repository=minio_repo,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
|
||||
mlflow.model_monitoring_repository = AsyncMock()
|
||||
@@ -96,165 +108,161 @@ metadata = {
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.dataframe_from_wire',
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch('laborious.activities.mlflow.max')
|
||||
async def test_request_transform_success(mock_max, mock_dataframe_from_wire, mlflow):
|
||||
mock_max.return_value = '2024-01-02'
|
||||
async def test_request_transform_success(mock_from_dataframe, mlflow):
|
||||
data_mock = MagicMock()
|
||||
mock_dataframe_from_wire.return_value = data_mock
|
||||
# Mock input data
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=data_mock)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': [
|
||||
{
|
||||
'timestamp': '2024-01-01',
|
||||
'variable': 'var1',
|
||||
'value': 1.0,
|
||||
'created_at': '2024-01-01 12:00:00',
|
||||
},
|
||||
{
|
||||
'timestamp': '2024-01-01',
|
||||
'variable': 'var2',
|
||||
'value': 2.0,
|
||||
'created_at': '2024-01-01 12:00:00',
|
||||
},
|
||||
{
|
||||
'timestamp': '2024-01-02',
|
||||
'variable': 'var1',
|
||||
'value': 3.0,
|
||||
'created_at': '2024-01-02 12:00:00',
|
||||
},
|
||||
{
|
||||
'timestamp': '2024-01-02',
|
||||
'variable': 'var2',
|
||||
'value': 4.0,
|
||||
'created_at': '2024-01-02 12:00:00',
|
||||
},
|
||||
{
|
||||
'timestamp': '2024-01-02',
|
||||
'variable': 'var1',
|
||||
'value': 1.0,
|
||||
'created_at': '2024-01-01 12:00:00',
|
||||
},
|
||||
{
|
||||
'timestamp': '2024-01-02',
|
||||
'variable': 'var2',
|
||||
'value': 1.0,
|
||||
'created_at': '2024-01-01 12:00:00',
|
||||
},
|
||||
],
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {},
|
||||
}
|
||||
|
||||
# Mock the transform response
|
||||
expected_response = {'prediction': [0.5, 0.6], 'timestamp': ['2024-01-01', '2024-01-02']}
|
||||
mlflow.model_monitoring_repository.transform.return_value = expected_response
|
||||
transform_response = {'success': True, 'content': MagicMock()}
|
||||
mlflow.model_monitoring_repository.transform.return_value = transform_response
|
||||
|
||||
data_mock.sort_values.return_value = data_mock
|
||||
data_mock.drop_duplicates.return_value = data_mock
|
||||
data_mock.pivot.return_value = data_mock
|
||||
|
||||
# Call the method
|
||||
response_data = await mlflow.request_transform(input_data)
|
||||
|
||||
# Verify the data was correctly transformed
|
||||
data_mock.pivot.assert_called_once_with(
|
||||
index='timestamp', columns='variable', values='value'
|
||||
)
|
||||
data_mock.fillna.assert_called_once_with(np.nan, inplace=True)
|
||||
# mock_dataframe.reset_index.assert_called_once()
|
||||
data_mock.columns.name = None
|
||||
|
||||
# Verify the response
|
||||
assert response_data == expected_response
|
||||
|
||||
# Verify the repository was called with correct arguments
|
||||
mlflow.model_monitoring_repository.transform.assert_called_once_with(
|
||||
'test_model', data_mock, {}, metadata['metadata']
|
||||
)
|
||||
mock_from_dataframe.assert_called_once()
|
||||
assert response_data == mock_from_dataframe.return_value
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.dataframe_from_wire',
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
@patch('laborious.activities.mlflow.max')
|
||||
async def test_request_predict(mock_max, mock_to_datetime, mock_dataframe_from_wire, mlflow):
|
||||
mock_max.return_value = '2024-01-02'
|
||||
async def test_request_transform_failure(mock_from_dataframe, mlflow):
|
||||
data_mock = MagicMock()
|
||||
mock_dataframe_from_wire.return_value = data_mock
|
||||
# Mock input data
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=data_mock)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': {
|
||||
'variable': {
|
||||
'2024-01-01': 'var1',
|
||||
'2024-01-02': 'var2',
|
||||
'2024-01-03': 'var1',
|
||||
'2024-01-04': 'var2',
|
||||
},
|
||||
'value': {'2024-01-01': 1.0, '2024-01-02': 2.0, '2024-01-03': 3.0, '2024-01-04': 4.0},
|
||||
},
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {},
|
||||
}
|
||||
|
||||
# Mock the predict response
|
||||
expected_response = {'prediction': [0.5, 0.6]}
|
||||
mlflow.model_monitoring_repository.predict.return_value = expected_response
|
||||
transform_response = {'success': False, 'message': 'Transform failed'}
|
||||
mlflow.model_monitoring_repository.transform.return_value = transform_response
|
||||
|
||||
# Call the method
|
||||
response_data = await mlflow.request_predict(input_data)
|
||||
data_mock.sort_values.return_value = data_mock
|
||||
data_mock.drop_duplicates.return_value = data_mock
|
||||
data_mock.pivot.return_value = data_mock
|
||||
|
||||
data_mock.replace.assert_called_once_with(np.nan, None, inplace=True)
|
||||
data_mock.__setitem__.assert_any_call(
|
||||
'timestamp', mock_to_datetime.return_value.dt.strftime.return_value
|
||||
)
|
||||
data_mock.__setitem__.assert_any_call(
|
||||
'timestamp', mock_to_datetime.return_value.dt.strftime.return_value
|
||||
)
|
||||
|
||||
mock_to_datetime.assert_called_once_with(
|
||||
data_mock.__getitem__.return_value, format=DATETIME_FORMAT_WITH_TZ
|
||||
)
|
||||
mock_to_datetime.return_value.dt.strftime.assert_called_once_with(DATETIME_FORMAT)
|
||||
|
||||
mock_to_datetime.assert_called_once_with(
|
||||
data_mock.__getitem__.return_value, format=DATETIME_FORMAT_WITH_TZ
|
||||
)
|
||||
mock_to_datetime.return_value.dt.strftime.assert_called_once_with(DATETIME_FORMAT)
|
||||
|
||||
# Verify the response
|
||||
assert response_data == expected_response
|
||||
|
||||
# Verify the repository was called with correct arguments
|
||||
mlflow.model_monitoring_repository.predict.assert_called_once_with(
|
||||
'test_model', data_mock, {}, metadata['metadata']
|
||||
response_data = await mlflow.request_transform(input_data)
|
||||
|
||||
mock_from_dataframe.assert_called_once_with(
|
||||
dataframe=None,
|
||||
minio_repo=mlflow.minio_repository,
|
||||
model_name='test_model',
|
||||
operation='transform',
|
||||
status=transform_response,
|
||||
workflow_metadata=metadata['metadata'],
|
||||
)
|
||||
assert response_data == mock_from_dataframe.return_value
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.mlflow.read_parquet')
|
||||
@patch(
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
async def test_retrain_model_success_data_success_retrain(mock_to_datetime, mock_read_parquet, mlflow):
|
||||
async def test_request_predict(mock_to_datetime, mock_from_dataframe, mlflow):
|
||||
data_mock = MagicMock()
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=data_mock)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {},
|
||||
}
|
||||
|
||||
predict_response = {'success': True, 'content': MagicMock()}
|
||||
mlflow.model_monitoring_repository.predict.return_value = predict_response
|
||||
|
||||
response_data = await mlflow.request_predict(input_data)
|
||||
|
||||
data_mock.replace.assert_called_once_with(np.nan, None, inplace=True)
|
||||
mock_to_datetime.assert_called_once_with(
|
||||
data_mock.__getitem__.return_value, format=DATETIME_FORMAT_WITH_TZ
|
||||
)
|
||||
mock_to_datetime.return_value.dt.strftime.assert_called_once_with(DATETIME_FORMAT)
|
||||
|
||||
mlflow.model_monitoring_repository.predict.assert_called_once_with(
|
||||
'test_model', data_mock, {}, metadata['metadata']
|
||||
)
|
||||
mock_from_dataframe.assert_called_once()
|
||||
assert response_data == mock_from_dataframe.return_value
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'laborious.activities.mlflow.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
async def test_request_predict_failure(mock_to_datetime, mock_from_dataframe, mlflow):
|
||||
data_mock = MagicMock()
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=data_mock)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {},
|
||||
}
|
||||
|
||||
predict_response = {'success': False, 'message': 'Predict failed'}
|
||||
mlflow.model_monitoring_repository.predict.return_value = predict_response
|
||||
|
||||
response_data = await mlflow.request_predict(input_data)
|
||||
|
||||
mock_from_dataframe.assert_called_once_with(
|
||||
dataframe=None,
|
||||
minio_repo=mlflow.minio_repository,
|
||||
model_name='test_model',
|
||||
operation='predict',
|
||||
status=predict_response,
|
||||
workflow_metadata=metadata['metadata'],
|
||||
)
|
||||
assert response_data == mock_from_dataframe.return_value
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
async def test_retrain_model_success_data_success_retrain(mock_to_datetime, mlflow):
|
||||
mlflow.model_monitoring_repository.retrain_model.return_value = {
|
||||
'success': True,
|
||||
'experiment': 'test_experiment',
|
||||
'message': 'Model retrained successfully.',
|
||||
}
|
||||
|
||||
mlflow.minio_repository.download_file.return_value = b'parquet-bytes'
|
||||
mock_read_parquet.return_value = MagicMock()
|
||||
raw_data = MagicMock(columns=['variable', 'timestamp', 'value'])
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=raw_data)
|
||||
|
||||
response = await mlflow.retrain_model(
|
||||
{
|
||||
**metadata,
|
||||
'object_key': 'test_object_key',
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {
|
||||
'target': 'target',
|
||||
@@ -264,8 +272,6 @@ async def test_retrain_model_success_data_success_retrain(mock_to_datetime, mock
|
||||
}
|
||||
)
|
||||
|
||||
raw_data = mock_read_parquet.return_value
|
||||
|
||||
timestamp = raw_data.__getitem__.return_value.max.return_value
|
||||
|
||||
raw_data.sort_values.assert_not_called()
|
||||
@@ -317,22 +323,22 @@ async def test_retrain_model_success_data_success_retrain(mock_to_datetime, mock
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.mlflow.MinioDataFramePayload.dataframe_from_wire', new_callable=AsyncMock)
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
async def test_retrain_model_success_with_payload_data(
|
||||
mock_to_datetime, mock_dataframe_from_wire, mlflow
|
||||
):
|
||||
async def test_retrain_model_success_with_payload_data(mock_to_datetime, mlflow):
|
||||
mlflow.model_monitoring_repository.retrain_model.return_value = {
|
||||
'success': True,
|
||||
'experiment': 'test_experiment',
|
||||
'message': 'Model retrained successfully.',
|
||||
}
|
||||
mock_dataframe_from_wire.return_value = MagicMock()
|
||||
|
||||
raw_data = MagicMock(columns=['variable', 'timestamp', 'value', 'created_at'])
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=raw_data)
|
||||
|
||||
response = await mlflow.retrain_model(
|
||||
{
|
||||
**metadata,
|
||||
'data': {'data': {'a': [1]}},
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {
|
||||
'target': 'target',
|
||||
@@ -347,24 +353,22 @@ async def test_retrain_model_success_with_payload_data(
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.mlflow.read_parquet')
|
||||
@patch('laborious.activities.mlflow.to_datetime')
|
||||
async def test_retrain_model_success_data_fail_retrain(mock_to_datetime, mock_read_parquet, mlflow):
|
||||
async def test_retrain_model_success_data_fail_retrain(mock_to_datetime, mlflow):
|
||||
mlflow.model_monitoring_repository.retrain_model.return_value = {
|
||||
'success': False,
|
||||
'traceback': 'test_traceback',
|
||||
'message': 'Model retrained failed.',
|
||||
}
|
||||
|
||||
mlflow.minio_repository.download_file.return_value = b'parquet-bytes'
|
||||
mock_read_parquet.return_value = MagicMock(
|
||||
columns=['variable', 'timestamp', 'value', 'created_at']
|
||||
)
|
||||
raw_data = MagicMock(columns=['variable', 'timestamp', 'value', 'created_at'])
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=raw_data)
|
||||
|
||||
response = await mlflow.retrain_model(
|
||||
{
|
||||
**metadata,
|
||||
'object_key': 'test_object_key',
|
||||
'data': payload,
|
||||
'model_name': 'test_model',
|
||||
'model_config': {
|
||||
'target': 'target',
|
||||
@@ -374,8 +378,6 @@ async def test_retrain_model_success_data_fail_retrain(mock_to_datetime, mock_re
|
||||
}
|
||||
)
|
||||
|
||||
raw_data = mock_read_parquet.return_value
|
||||
|
||||
timestamp = raw_data.__getitem__.return_value.max.return_value
|
||||
|
||||
raw_data.sort_values.assert_called_once_with('created_at', ascending=False)
|
||||
@@ -439,14 +441,9 @@ async def test_retrain_model_success_data_fail_retrain(mock_to_datetime, mock_re
|
||||
|
||||
@mark.asyncio
|
||||
async def test_retrain_model_data_error(mlflow):
|
||||
mlflow.minio_repository.download_file.side_effect = Exception(
|
||||
'Error loading retrain data'
|
||||
)
|
||||
|
||||
response = await mlflow.retrain_model(
|
||||
{
|
||||
**metadata,
|
||||
'object_key': 'test_object_key',
|
||||
'model_name': 'test_model',
|
||||
'model_config': {
|
||||
'target': 'target',
|
||||
@@ -458,7 +455,7 @@ async def test_retrain_model_data_error(mlflow):
|
||||
|
||||
assert response == {
|
||||
'success': False,
|
||||
'message': 'Error loading retrain data: Error loading retrain data',
|
||||
'message': "Error loading retrain data: 'data'",
|
||||
'traceback': ANY,
|
||||
'timestamp': ANY,
|
||||
}
|
||||
|
||||
@@ -743,6 +743,101 @@ async def test_get_drift_metrics_univariate_error(
|
||||
raise AssertionError('Expected Exception')
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.model_metrics.to_datetime')
|
||||
@patch('laborious.activities.model_metrics.time.time')
|
||||
@patch('laborious.activities.model_metrics.ModelAnalysis')
|
||||
@patch('laborious.activities.model_metrics.metrics')
|
||||
async def test_get_drift_metrics_multivariate_error(
|
||||
mock_metrics, mock_model_analysis, mock_time, mock_to_datetime, model_metrics_activity
|
||||
):
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
mock_model_analysis.return_value.detect_univariate_drift.return_value = MagicMock()
|
||||
mock_model_analysis.return_value.detect_multivariate_drift.side_effect = Exception(
|
||||
'Multivariate drift error'
|
||||
)
|
||||
|
||||
reference_data = DataFrame(
|
||||
{'timestamp': ['2023-05-26 11:12:27'], 'target': [1.0], 'feature1': [1.0]}
|
||||
)
|
||||
target_data = DataFrame(
|
||||
{'timestamp': ['2023-05-26 11:12:27'], 'target': [1.0], 'feature1': [1.0]}
|
||||
)
|
||||
reference_columns = reference_data.drop(
|
||||
columns=['target', 'timestamp'], errors='ignore'
|
||||
).columns
|
||||
|
||||
try:
|
||||
await model_metrics_activity.get_drift_metrics(
|
||||
reference_data=reference_data,
|
||||
target_data=target_data,
|
||||
target_name='target',
|
||||
reference_columns=reference_columns,
|
||||
drift_metrics=['ks_test'],
|
||||
chunk_period='min',
|
||||
metadata=metadata['metadata'],
|
||||
)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Multivariate drift error'
|
||||
model_metrics_activity.error.assert_called_once_with(
|
||||
'Error detecting multivariate drift: Multivariate drift error', metadata['metadata']
|
||||
)
|
||||
model_metrics_activity.emit_metric.assert_called_with(
|
||||
metric_object=mock_metrics.MODEL_ANALYZE_ERROR_COUNT, tags=ANY
|
||||
)
|
||||
else:
|
||||
raise AssertionError('Expected Exception')
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.model_metrics.to_datetime')
|
||||
@patch('laborious.activities.model_metrics.time.time')
|
||||
@patch('laborious.activities.model_metrics.ModelAnalysis')
|
||||
@patch('laborious.activities.model_metrics.metrics')
|
||||
async def test_get_drift_metrics_dataframe_error(
|
||||
mock_metrics, mock_model_analysis, mock_time, mock_to_datetime, model_metrics_activity
|
||||
):
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
mock_model_analysis.return_value.detect_univariate_drift.return_value = MagicMock()
|
||||
mock_model_analysis.return_value.detect_multivariate_drift.return_value = MagicMock()
|
||||
mock_model_analysis.return_value.get_drift_metrics_dataframe.side_effect = Exception(
|
||||
'Dataframe error'
|
||||
)
|
||||
|
||||
reference_data = DataFrame(
|
||||
{'timestamp': ['2023-05-26 11:12:27'], 'target': [1.0], 'feature1': [1.0]}
|
||||
)
|
||||
target_data = DataFrame(
|
||||
{'timestamp': ['2023-05-26 11:12:27'], 'target': [1.0], 'feature1': [1.0]}
|
||||
)
|
||||
reference_columns = reference_data.drop(
|
||||
columns=['target', 'timestamp'], errors='ignore'
|
||||
).columns
|
||||
|
||||
try:
|
||||
await model_metrics_activity.get_drift_metrics(
|
||||
reference_data=reference_data,
|
||||
target_data=target_data,
|
||||
target_name='target',
|
||||
reference_columns=reference_columns,
|
||||
drift_metrics=['ks_test'],
|
||||
chunk_period='min',
|
||||
metadata=metadata['metadata'],
|
||||
)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Dataframe error'
|
||||
model_metrics_activity.error.assert_called_once_with(
|
||||
'Error getting drift metrics: Dataframe error', metadata['metadata']
|
||||
)
|
||||
model_metrics_activity.emit_metric.assert_called_with(
|
||||
metric_object=mock_metrics.MODEL_ANALYZE_ERROR_COUNT, tags=ANY
|
||||
)
|
||||
else:
|
||||
raise AssertionError('Expected Exception')
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_calculate_simple_metrics_success_all_metrics(model_metrics_activity):
|
||||
# Arrange
|
||||
@@ -987,3 +1082,27 @@ async def test_calculate_simple_metrics_success_multiple_metrics_subset(model_me
|
||||
model_metrics_activity.info.assert_called_once_with(
|
||||
"Calculating simple metrics for model test_model_id: ['rmse', 'mae']", metadata['metadata']
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_calculate_simple_metrics_unknown_metric_ignored(model_metrics_activity):
|
||||
target_data = DataFrame(
|
||||
{
|
||||
'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28'],
|
||||
'target': [1.0, 2.0],
|
||||
'prediction': [1.1, 2.1],
|
||||
}
|
||||
)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'model_id': 'test_model_id',
|
||||
'target_data': target_data.to_dict(),
|
||||
'metrics': ['unknown_metric', 'rmse'],
|
||||
'interval_minutes': 5,
|
||||
}
|
||||
|
||||
result = DataFrame(await model_metrics_activity.calculate_simple_metrics(input_data))
|
||||
|
||||
assert len(result['metric']) == 1
|
||||
assert result['metric'].values[0] == 'rmse'
|
||||
|
||||
@@ -29,13 +29,8 @@ def storage(mock_minio_repository):
|
||||
dbname='postgres',
|
||||
min_connections=1,
|
||||
max_connections=10,
|
||||
minio_config={
|
||||
'endpoint_url': 'localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
retention_hours=24,
|
||||
minio_repository=mock_minio_repository.return_value,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
@@ -47,6 +42,7 @@ def test___init___not_hasattr(mock_minio_repository):
|
||||
logger = MagicMock()
|
||||
notification_handler = MagicMock()
|
||||
metrics_controller = AsyncMock()
|
||||
minio_repo = mock_minio_repository.return_value
|
||||
storage = Storage(
|
||||
host='localhost',
|
||||
port=5432,
|
||||
@@ -55,28 +51,16 @@ def test___init___not_hasattr(mock_minio_repository):
|
||||
dbname='postgres',
|
||||
min_connections=1,
|
||||
max_connections=10,
|
||||
minio_config={
|
||||
'endpoint_url': 'localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
retention_hours=24,
|
||||
minio_repository=minio_repo,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
assert isinstance(storage, Postgres)
|
||||
|
||||
mock_minio_repository.assert_called_once_with(
|
||||
endpoint='localhost:9000',
|
||||
access_key='minio',
|
||||
secret_key='minio123',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
bucket='test',
|
||||
)
|
||||
assert storage.minio_repository is minio_repo
|
||||
mock_minio_repository.assert_not_called()
|
||||
|
||||
|
||||
@patch('laborious.activities.storage.MinioRepository')
|
||||
@@ -93,27 +77,15 @@ def test___init___none_minio_repository(mock_minio_repository, storage):
|
||||
dbname='postgres',
|
||||
min_connections=1,
|
||||
max_connections=10,
|
||||
minio_config={
|
||||
'endpoint_url': 'localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
retention_hours=24,
|
||||
minio_repository=None,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
|
||||
mock_minio_repository.assert_called_once_with(
|
||||
endpoint='localhost:9000',
|
||||
access_key='minio',
|
||||
secret_key='minio123',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
bucket='test',
|
||||
)
|
||||
assert storage.minio_repository is None
|
||||
mock_minio_repository.assert_not_called()
|
||||
|
||||
|
||||
@patch('laborious.activities.storage.MinioRepository')
|
||||
@@ -126,13 +98,8 @@ def test___init___done_repository(mock_minio_repository, storage):
|
||||
dbname='postgres',
|
||||
min_connections=1,
|
||||
max_connections=10,
|
||||
minio_config={
|
||||
'endpoint_url': 'localhost:9000',
|
||||
'access_key': 'minio',
|
||||
'secret_key': 'minio123',
|
||||
'region_name': 'us-east-1',
|
||||
'default_bucket': 'test',
|
||||
},
|
||||
retention_hours=24,
|
||||
minio_repository=mock_minio_repository.return_value,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
@@ -231,55 +198,58 @@ def test___del__(storage):
|
||||
storage.close.assert_called_once()
|
||||
|
||||
|
||||
def test_estimate_payload_size_bytes(storage):
|
||||
assert storage._estimate_payload_size_bytes({'x': 1}) > 0
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_load_query_with_minio_offload_no_rows(storage):
|
||||
storage.load_custom_query = AsyncMock(return_value=None)
|
||||
result = await storage.load_query_with_minio_offload(
|
||||
{**metadata, 'query': 'SELECT 1', 'model_name': 'm', 'key_prefix': 'predictions/s'}
|
||||
)
|
||||
assert result['success'] is False
|
||||
storage_result = {'success': False}
|
||||
with patch(
|
||||
'laborious.activities.storage.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
return_value=storage_result,
|
||||
) as mock_from_dataframe:
|
||||
result = await storage.load_query_with_minio_offload(
|
||||
{**metadata, 'query': 'SELECT 1', 'model_name': 'm', 'key_prefix': 'predictions/s'}
|
||||
)
|
||||
assert result == storage_result
|
||||
mock_from_dataframe.assert_awaited_once()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_load_query_with_minio_offload_inline(storage):
|
||||
storage.load_custom_query = AsyncMock(return_value=[{'a': 1}])
|
||||
result = await storage.load_query_with_minio_offload(
|
||||
{**metadata, 'query': 'SELECT 1', 'model_name': 'my-model', 'key_prefix': 'predictions/s'}
|
||||
)
|
||||
assert result.get('success') is True
|
||||
assert 'data' in result
|
||||
assert result.get('object_key') is None
|
||||
storage_result = {'success': True, 'data': {'a': [1]}, 'object_key': None}
|
||||
with patch(
|
||||
'laborious.activities.storage.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
return_value=storage_result,
|
||||
) as mock_from_dataframe:
|
||||
result = await storage.load_query_with_minio_offload(
|
||||
{
|
||||
**metadata,
|
||||
'query': 'SELECT 1',
|
||||
'model_name': 'my-model',
|
||||
'key_prefix': 'predictions/s',
|
||||
}
|
||||
)
|
||||
assert result == storage_result
|
||||
mock_from_dataframe.assert_awaited_once()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.utils.models.minio_dataframe_payload.MinioDataFramePayload.estimate_size_bytes')
|
||||
async def test_load_query_with_minio_offload_minio(mock_estimate, storage):
|
||||
mock_estimate.return_value = 10**9
|
||||
async def test_load_query_with_minio_offload_minio(storage):
|
||||
storage.load_custom_query = AsyncMock(return_value=[{'a': 1}])
|
||||
storage.minio_repository.upload_file = AsyncMock(
|
||||
return_value={
|
||||
'minio_object_name': 'sientia/streamlit-connectors/training_datasets/m/m-initial-2024-01-15_12-30-45.parquet'
|
||||
}
|
||||
)
|
||||
storage.minio_repository.bucket = 'test'
|
||||
|
||||
fixed = datetime.datetime(2024, 1, 15, 12, 30, 45)
|
||||
with patch('laborious.utils.models.minio_dataframe_payload.now', return_value=fixed):
|
||||
storage_result = {'success': True, 'data': None, 'object_key': 'object-key'}
|
||||
with patch(
|
||||
'laborious.activities.storage.MinioDataFramePayload.from_dataframe',
|
||||
new_callable=AsyncMock,
|
||||
return_value=storage_result,
|
||||
) as mock_from_dataframe:
|
||||
result = await storage.load_query_with_minio_offload(
|
||||
{**metadata, 'query': 'SELECT 1', 'model_name': 'm', 'key_prefix': 'predictions/s'}
|
||||
)
|
||||
|
||||
assert result.get('success') is True
|
||||
assert result.get('data') is None
|
||||
assert (
|
||||
result['object_key']
|
||||
== 'sientia/streamlit-connectors/training_datasets/m/m-initial-2024-01-15_12-30-45.parquet'
|
||||
)
|
||||
storage.minio_repository.upload_file.assert_called_once()
|
||||
assert result == storage_result
|
||||
mock_from_dataframe.assert_awaited_once()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@@ -297,12 +267,117 @@ async def test_cleanup_minio_objects_expired(mock_now, storage):
|
||||
storage.send_notification_async = AsyncMock()
|
||||
|
||||
result = await storage.cleanup_minio_objects_expired(
|
||||
{**metadata, 'prefixes': ['training_datasets/m']}
|
||||
{**metadata, 'prefix': 'training_datasets/m'}
|
||||
)
|
||||
|
||||
assert result['success'] is True
|
||||
assert result['deleted_count'] == 1
|
||||
assert result['failed_count'] == 0
|
||||
deleted_key = (
|
||||
'sientia/streamlit-connectors/training_datasets/m/m-initial-2024-12-01_00-00-00.parquet'
|
||||
)
|
||||
assert deleted_key in result['deleted']
|
||||
assert result['deleted'][deleted_key]['success'] is True
|
||||
storage.minio_repository.list_objects.assert_called_once_with(
|
||||
prefix='training_datasets/m',
|
||||
recursive=True,
|
||||
metadata=metadata['metadata'],
|
||||
)
|
||||
storage.minio_repository.delete_file.assert_called_once_with(
|
||||
object_name='sientia/streamlit-connectors/training_datasets/m/m-initial-2024-12-01_00-00-00.parquet',
|
||||
metadata=metadata['metadata'],
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_load_query_with_minio_offload_minio_not_initialized(storage):
|
||||
storage.minio_repository = None
|
||||
|
||||
with raises(ValueError, match='Minio repository not initialized'):
|
||||
await storage.load_query_with_minio_offload(
|
||||
{**metadata, 'query': 'SELECT 1', 'model_name': 'm'}
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_export_payload_to_postgres(storage):
|
||||
payload = AsyncMock()
|
||||
payload.retrieve = AsyncMock(return_value=MagicMock())
|
||||
storage.export_data_to_postgres = AsyncMock(return_value={'success': True})
|
||||
|
||||
result = await storage.export_payload_to_postgres(
|
||||
{**metadata, 'data': payload, 'schema': 'public', 'table': 't'}
|
||||
)
|
||||
|
||||
payload.retrieve.assert_awaited_once_with(storage.minio_repository, metadata['metadata'])
|
||||
storage.export_data_to_postgres.assert_awaited_once()
|
||||
assert result == {'success': True}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_cleanup_minio_objects_expired_minio_not_initialized(storage):
|
||||
storage.minio_repository = None
|
||||
|
||||
with raises(ValueError, match='Minio repository not initialized'):
|
||||
await storage.cleanup_minio_objects_expired({**metadata, 'prefix': 'test'})
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.storage.now')
|
||||
async def test_cleanup_minio_objects_expired_unparseable_key(mock_now, storage):
|
||||
mock_now.return_value = datetime.datetime(2025, 1, 10, 12, 0, 0)
|
||||
storage.minio_repository.list_objects = AsyncMock(
|
||||
return_value=['some/random/key-without-timestamp.parquet']
|
||||
)
|
||||
storage.minio_repository.delete_file = AsyncMock()
|
||||
storage.send_notification_async = AsyncMock()
|
||||
|
||||
result = await storage.cleanup_minio_objects_expired({**metadata, 'prefix': 'test'})
|
||||
|
||||
assert result['deleted_count'] == 0
|
||||
assert result['failed_count'] == 0
|
||||
storage.minio_repository.delete_file.assert_not_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.storage.now')
|
||||
async def test_cleanup_minio_objects_expired_delete_fails(mock_now, storage):
|
||||
mock_now.return_value = datetime.datetime(2025, 1, 10, 12, 0, 0)
|
||||
old_key = 'training_datasets/m/m-initial-2024-12-01_00-00-00.parquet'
|
||||
storage.minio_repository.list_objects = AsyncMock(return_value=[old_key])
|
||||
storage.minio_repository.delete_file = AsyncMock(side_effect=Exception('delete error'))
|
||||
storage.send_notification_async = AsyncMock()
|
||||
|
||||
result = await storage.cleanup_minio_objects_expired(
|
||||
{**metadata, 'prefix': 'training_datasets/m'}
|
||||
)
|
||||
|
||||
assert result['deleted_count'] == 0
|
||||
assert result['failed_count'] == 1
|
||||
assert old_key in result['failed']
|
||||
assert result['failed'][old_key]['success'] is False
|
||||
assert result['failed'][old_key]['message'] == 'delete error'
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('laborious.activities.storage.now')
|
||||
async def test_cleanup_minio_objects_expired_list_objects_error(mock_now, storage):
|
||||
mock_now.return_value = datetime.datetime(2025, 1, 10, 12, 0, 0)
|
||||
storage.minio_repository.list_objects = AsyncMock(side_effect=Exception('list error'))
|
||||
storage.send_notification_async = AsyncMock()
|
||||
storage.error = MagicMock()
|
||||
|
||||
result = await storage.cleanup_minio_objects_expired(
|
||||
{**metadata, 'prefix': 'training_datasets/m'}
|
||||
)
|
||||
|
||||
assert result['deleted_count'] == 0
|
||||
assert result['failed_count'] == 0
|
||||
storage.send_notification_async.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='ERROR_CLEANUP_MINIO_OBJECTS_EXPIRED',
|
||||
message='Error cleaning up MinIO objects: list error',
|
||||
block='cleanup_minio_objects_expired',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
storage.error.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user