Code import - branch release/SIENTIAPDE-1645

This commit is contained in:
2026-08-05 13:53:37 +00:00
commit d481e0acff
116 changed files with 92848 additions and 0 deletions

View File

View File

@@ -0,0 +1,75 @@
"""Unit tests for ExperimentStatus enum."""
from model_manager.utils.models.experiment_status import ExperimentStatus
def test_experiment_status_values():
"""Test that all expected status values exist."""
assert ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR == 'ORCHESTRATOR_VALIDATION_ERROR'
assert ExperimentStatus.ORCHESTRATOR_WAITING_PROC == 'ORCHESTRATOR_WAITING_PROC'
assert ExperimentStatus.TRAINING_SUCCESS == 'TRAINING_SUCCESS'
assert ExperimentStatus.TRAINING_ERROR == 'TRAINING_ERROR'
def test_experiment_status_count():
"""Test that enum has exactly 4 status values."""
assert len(ExperimentStatus) == 4
def test_experiment_status_is_string():
"""Test that enum values are strings."""
for status in ExperimentStatus:
assert isinstance(status.value, str)
assert isinstance(status, str)
def test_experiment_status_membership():
"""Test membership checks for status values."""
assert 'ORCHESTRATOR_VALIDATION_ERROR' in [s.value for s in ExperimentStatus]
assert 'ORCHESTRATOR_WAITING_PROC' in [s.value for s in ExperimentStatus]
assert 'TRAINING_SUCCESS' in [s.value for s in ExperimentStatus]
assert 'TRAINING_ERROR' in [s.value for s in ExperimentStatus]
def test_experiment_status_iteration():
"""Test that enum can be iterated."""
statuses = list(ExperimentStatus)
assert len(statuses) == 4
assert ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR in statuses
assert ExperimentStatus.ORCHESTRATOR_WAITING_PROC in statuses
assert ExperimentStatus.TRAINING_SUCCESS in statuses
assert ExperimentStatus.TRAINING_ERROR in statuses
def test_experiment_status_comparison():
"""Test that enum values can be compared with strings."""
assert ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR == 'ORCHESTRATOR_VALIDATION_ERROR'
assert ExperimentStatus.ORCHESTRATOR_WAITING_PROC == 'ORCHESTRATOR_WAITING_PROC'
assert ExperimentStatus.TRAINING_SUCCESS == 'TRAINING_SUCCESS'
assert str(ExperimentStatus.TRAINING_ERROR) != 'TRAINING_SUCCESS'
def test_experiment_status_access_by_name():
"""Test accessing enum members by name."""
assert (
ExperimentStatus['ORCHESTRATOR_VALIDATION_ERROR']
== ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR
)
assert (
ExperimentStatus['ORCHESTRATOR_WAITING_PROC'] == ExperimentStatus.ORCHESTRATOR_WAITING_PROC
)
assert ExperimentStatus['TRAINING_SUCCESS'] == ExperimentStatus.TRAINING_SUCCESS
assert ExperimentStatus['TRAINING_ERROR'] == ExperimentStatus.TRAINING_ERROR
def test_experiment_status_access_by_value():
"""Test accessing enum members by value."""
assert (
ExperimentStatus('ORCHESTRATOR_VALIDATION_ERROR')
== ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR
)
assert (
ExperimentStatus('ORCHESTRATOR_WAITING_PROC') == ExperimentStatus.ORCHESTRATOR_WAITING_PROC
)
assert ExperimentStatus('TRAINING_SUCCESS') == ExperimentStatus.TRAINING_SUCCESS
assert ExperimentStatus('TRAINING_ERROR') == ExperimentStatus.TRAINING_ERROR

View File

@@ -0,0 +1,51 @@
"""Unit tests for models __init__.py module."""
from model_manager.utils.models import (
ExperimentStatus,
TrainModelParams,
TrainModelResult,
)
def test_experiment_status_import():
"""Test that ExperimentStatus can be imported from models package."""
assert ExperimentStatus is not None
assert hasattr(ExperimentStatus, 'ORCHESTRATOR_WAITING_PROC')
assert hasattr(ExperimentStatus, 'TRAINING_SUCCESS')
def test_train_model_params_import():
"""Test that TrainModelParams can be imported from models package."""
assert TrainModelParams is not None
assert callable(TrainModelParams)
def test_train_model_result_import():
"""Test that TrainModelResult can be imported from models package."""
assert TrainModelResult is not None
# Dataclasses have __dataclass_fields__
assert hasattr(TrainModelResult, '__dataclass_fields__')
def test_all_exports():
"""Test that __all__ contains all expected exports."""
from model_manager.utils.models import __all__
assert 'ExperimentStatus' in __all__
assert 'TrainModelParams' in __all__
assert 'TrainModelResult' in __all__
assert len(__all__) == 3
def test_no_extra_exports():
"""Test that only expected items are exported."""
import model_manager.utils.models as models_module
# Get all public attributes (not starting with _)
public_attrs = [attr for attr in dir(models_module) if not attr.startswith('_')]
# Should only have the 3 main classes
expected_public = {'ExperimentStatus', 'TrainModelParams', 'TrainModelResult'}
# Check that our expected classes are present
assert expected_public.issubset(set(public_attrs))

View File

@@ -0,0 +1,321 @@
"""Unit tests for TrainModelParams (current schema)."""
import copy
from unittest.mock import patch
import pytest
from model_manager.utils.models.train_model_params import (
DEFAULT_TRAIN_DATE_FORMAT,
TrainModelParams,
validate_frontend_date_format,
)
@pytest.fixture
def minimal_model_metadata() -> dict:
"""Minimal truthy metadata so validate_business_rules passes schema lookup."""
return {'schemas': {'components': {'schemas': {}}}}
@pytest.fixture
def valid_train_params_dict(minimal_model_metadata) -> dict:
"""Valid dictionary for TrainModelParams.from_dict."""
return {
'variable_columns': ['var1', 'var2'],
'target_variable': 'target',
'bucket_name': 'test-bucket',
'file_name': 'test-file.csv',
'line_separator': ',',
'decimal_separator': '.',
'date_column': 'timestamp',
'date_format': 'yyyy-MM-dd HH:mm:ss',
'train_size': 80,
'shuffle': True,
'random_state': 42,
'experiment_run_id': 1,
'model_name': 'Linear Regression',
'val_file_name': None,
'data_model_kwargs': {},
'model_kwargs': {},
'opt_params': {},
'model_type': 'linear_regression',
'model_id': None,
'model_metadata': minimal_model_metadata,
}
def test_from_dict_success(valid_train_params_dict):
"""from_dict builds params and experiment_name from model_name."""
params = TrainModelParams.from_dict(valid_train_params_dict)
assert params.variable_columns == ['var1', 'var2']
assert params.target_variable == 'target'
assert params.bucket_name == 'test-bucket'
assert params.experiment_run_id == 1
assert params.experiment_name == 'Linear Regression'
assert params.model_metadata is valid_train_params_dict['model_metadata']
def test_from_dict_date_format_omitted_uses_default(valid_train_params_dict):
"""Missing date_format defaults to DEFAULT_TRAIN_DATE_FORMAT."""
d = copy.deepcopy(valid_train_params_dict)
del d['date_format']
params = TrainModelParams.from_dict(d)
assert params.date_format == DEFAULT_TRAIN_DATE_FORMAT
def test_from_dict_date_format_blank_uses_default(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['date_format'] = ' '
params = TrainModelParams.from_dict(d)
assert params.date_format == DEFAULT_TRAIN_DATE_FORMAT
def test_from_dict_superfluous_date_column_camel_key_is_ignored(valid_train_params_dict):
"""Only snake_case keys are read; dateColumn does not populate date_column."""
d = copy.deepcopy(valid_train_params_dict)
d['dateColumn'] = 'wrong_name'
params = TrainModelParams.from_dict(d)
assert params.date_column == 'timestamp'
def test_from_dict_missing_date_column_raises(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
del d['date_column']
with pytest.raises(ValueError, match='date_column is required'):
TrainModelParams.from_dict(d)
def test_from_dict_date_format_non_string_raises(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['date_format'] = 12345
with pytest.raises(TypeError, match='date_format must be a string'):
TrainModelParams.from_dict(d)
def test_from_dict_coerces_experiment_run_id_string(valid_train_params_dict):
"""Numeric string experiment_run_id is coerced to int."""
d = copy.deepcopy(valid_train_params_dict)
d['experiment_run_id'] = '42'
params = TrainModelParams.from_dict(d)
assert params.experiment_run_id == 42
def test_from_dict_model_metadata_none(valid_train_params_dict):
"""model_metadata may be None before load_model_metadata activity."""
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = None
params = TrainModelParams.from_dict(d)
assert params.model_metadata is None
def test_coerce_experiment_run_id_rejects_bool():
"""Boolean must not be accepted as experiment_run_id."""
with pytest.raises(TypeError, match='experiment_run_id must be an integer'):
TrainModelParams._coerce_experiment_run_id(True)
def test_parse_optional_model_metadata_rejects_list():
"""model_metadata must be dict or None."""
with pytest.raises(TypeError, match='model_metadata must be a dict or None'):
TrainModelParams._parse_optional_model_metadata([])
def test_check_none_raises_value_error():
with pytest.raises(ValueError, match='test_field is required'):
TrainModelParams._check_none(None, str, 'test_field')
def test_check_none_raises_type_error():
with pytest.raises(TypeError, match='test_field must be of type str'):
TrainModelParams._check_none(123, str, 'test_field')
def test_validate_business_rules_success(valid_train_params_dict):
params = TrainModelParams.from_dict(valid_train_params_dict)
params.validate_business_rules()
def test_validate_business_rules_missing_model_metadata(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = None
params = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='model_metadata is required'):
params.validate_business_rules()
def test_validate_business_rules_train_size_out_of_range(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['train_size'] = 5
params = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='train_size must be between'):
params.validate_business_rules()
def test_validate_business_rules_empty_variable_columns(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['variable_columns'] = []
params = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='variable_columns cannot be empty'):
params.validate_business_rules()
def test_validate_business_rules_empty_target(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['target_variable'] = ' '
params = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='target_variable cannot be empty'):
params.validate_business_rules()
def test_validate_business_rules_whitespace_date_column(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['date_column'] = ' '
params = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='date_column cannot be empty'):
params.validate_business_rules()
def test_from_dict_missing_required_key(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
del d['bucket_name']
with pytest.raises(ValueError, match='bucket_name is required'):
TrainModelParams.from_dict(d)
def test_to_dict_roundtrip_keys(valid_train_params_dict):
params = TrainModelParams.from_dict(valid_train_params_dict)
d = params.to_dict()
assert 'variable_columns' in d
assert d['experiment_run_id'] == 1
def test_coerce_experiment_run_id_float():
assert TrainModelParams._coerce_experiment_run_id(2.0) == 2
def test_coerce_experiment_run_id_none_raises():
with pytest.raises(ValueError, match='experiment_run_id is required'):
TrainModelParams._coerce_experiment_run_id(None)
def test_coerce_experiment_run_id_invalid_type():
with pytest.raises(TypeError, match='integer or numeric string'):
TrainModelParams._coerce_experiment_run_id([1])
def test_validate_model_param_schema_validation_error(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {
'schemas': {
'components': {
'schemas': {
'data_model': {
'type': 'object',
'properties': {'x': {'type': 'integer'}},
'required': ['x'],
},
}
}
}
}
p = TrainModelParams.from_dict(d)
p.data_model_kwargs = {}
with pytest.raises(ValueError, match='Model parameters validation failed'):
p.validate_business_rules()
def test_validate_model_param_unexpected_validator_error(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {
'schemas': {
'components': {
'schemas': {
'data_model': {'type': 'object'},
}
}
}
}
p = TrainModelParams.from_dict(d)
with patch('model_manager.utils.models.train_model_params.Draft202012Validator') as m:
m.return_value.validate.side_effect = RuntimeError('boom')
with pytest.raises(RuntimeError, match='boom'):
p.validate_business_rules()
def test_validate_business_rules_date_format_invalid(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['date_format'] = 'not-an-allowed-format'
p = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match='Invalid date_format'):
p.validate_business_rules()
def test_validate_required_strings_whitespace_bucket_file_model(valid_train_params_dict):
for field, msg in [
('bucket_name', 'bucket_name cannot be empty'),
('file_name', 'file_name cannot be empty'),
('model_name', 'model_name cannot be empty'),
]:
d = copy.deepcopy(valid_train_params_dict)
d[field] = ' '
p = TrainModelParams.from_dict(d)
with pytest.raises(ValueError, match=msg):
p.validate_business_rules()
def test_validate_model_param_only_data_model_schema(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {
'schemas': {'components': {'schemas': {'data_model': {'type': 'object'}}}}
}
p = TrainModelParams.from_dict(d)
p.data_model_kwargs = {}
p.validate_business_rules()
def test_validate_model_param_only_model_schema(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {'schemas': {'components': {'schemas': {'model': {'type': 'object'}}}}}
p = TrainModelParams.from_dict(d)
p.model_kwargs = {}
p.validate_business_rules()
def test_validate_model_param_only_opt_params_schema(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {
'schemas': {'components': {'schemas': {'opt_params': {'type': 'object'}}}}
}
p = TrainModelParams.from_dict(d)
p.opt_params = {}
p.validate_business_rules()
def test_validate_model_param_all_schema_branches(valid_train_params_dict):
d = copy.deepcopy(valid_train_params_dict)
d['model_metadata'] = {
'schemas': {
'components': {
'schemas': {
'data_model': {'type': 'object'},
'model': {'type': 'object'},
'opt_params': {'type': 'object'},
}
}
}
}
p = TrainModelParams.from_dict(d)
p.data_model_kwargs = {}
p.model_kwargs = {}
p.opt_params = {}
p.validate_business_rules()
def test_validate_frontend_date_format_whitespace_returns():
validate_frontend_date_format(' ')
def test_validate_frontend_date_format_valid_returns():
validate_frontend_date_format('dd/MM/yyyy HH:mm:ss')

View File

@@ -0,0 +1,71 @@
"""Unit tests for TrainModelResult dataclass."""
import pandas as pd
import pytest
from model_manager.utils.models.train_model_params import TrainModelParams
from model_manager.utils.models.train_model_result import TrainModelResult
@pytest.fixture
def sample_params() -> TrainModelParams:
"""Minimal TrainModelParams for TrainModelResult tests."""
return TrainModelParams.from_dict(
{
'variable_columns': ['a'],
'target_variable': 't',
'bucket_name': 'b',
'file_name': 'f.csv',
'line_separator': '\n',
'decimal_separator': '.',
'date_column': 'timestamp',
'date_format': 'yyyy-MM-dd HH:mm:ss',
'train_size': 80,
'shuffle': True,
'random_state': 42,
'experiment_run_id': 1,
'model_name': 'Linear Regression',
'val_file_name': None,
'data_model_kwargs': {},
'model_kwargs': {},
'opt_params': {},
'model_type': 'linear_regression',
'model_id': None,
'model_metadata': {'schemas': {'components': {'schemas': {}}}},
}
)
@pytest.fixture
def sample_frames():
train = pd.DataFrame({'a': [1, 2], 't': [1.0, 2.0]})
val = pd.DataFrame({'a': [3], 't': [3.0]})
return train, val
def test_train_model_result_creation(sample_params, sample_frames):
train, val = sample_frames
result = TrainModelResult(params=sample_params, train_data=train, val_data=val)
assert result.params is sample_params
assert result.train_data.equals(train)
assert result.val_data.equals(val)
assert result.run_name is None
def test_train_model_result_optional_paths(sample_params, sample_frames):
train, val = sample_frames
result = TrainModelResult(
params=sample_params,
train_data=train,
val_data=val,
run_name='run-1',
run_id='rid',
run_dir='/tmp/x',
mse_val=0.1,
mae_val=0.2,
r2_val=0.99,
)
assert result.run_name == 'run-1'
assert result.run_id == 'rid'
assert result.run_dir == '/tmp/x'
assert result.mse_val == 0.1