Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/simdb/cli/commands/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def simulation_push(
schemas = api.get_validation_schemas()
try:
for schema in schemas:
Validator(schema).validate(simulation)
Validator(schema, config).validate(simulation)
except ValidationError as err:
raise click.ClickException(f"Simulation does not validate: {err}") from err

Expand Down Expand Up @@ -471,7 +471,7 @@ def simulation_validate(

click.echo("validating metadata ... ", nl=False)
for schema in schemas:
Validator(schema).validate(simulation)
Validator(schema, config).validate(simulation)

ids_list = []
for file in chain(simulation.inputs, simulation.outputs):
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/cli/remote_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from .manifest import DataType

if TYPE_CHECKING:
from simdb.database.models import File, Simulation, Watcher
from simdb.database.models import File, Watcher

if TYPE_CHECKING or "sphinx" in sys.modules:
# Only importing these for type checking and documentation generation in order to
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/database/models/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def meta(self) -> List[MetaDataWrapper]:
Returns a list of MetaDataWrapper objects from the JSON metadata.
"""
meta_dict = self._get_metadata_dict()
return [MetaDataWrapper(k, v) for k, v in meta_dict.items()]
return list(itertools.starmap(MetaDataWrapper, meta_dict.items()))

def _get_metadata_dict(self) -> Dict[str, Any]:
if self._metadata is None:
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/remote/apis/v1/simulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _update_simulation_status(
def _validate(simulation, user) -> Dict:
schema = Validator.validation_schema()
try:
Validator(schema).validate(simulation)
Validator(schema, current_app.simdb_config).validate(simulation)
_update_simulation_status(simulation, models_sim.Simulation.Status.PASSED, user)
return {
"passed": True,
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/remote/apis/v1_1/simulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _validate(simulation, user) -> Dict:
schemas = Validator.validation_schemas(current_app.simdb_config, simulation)
try:
for schema in schemas:
Validator(schema).validate(simulation)
Validator(schema, current_app.simdb_config).validate(simulation)
_update_simulation_status(
simulation, models_sim.Simulation.Status.PASSED, user
)
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/remote/apis/v1_2/simulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _validate(simulation, user) -> ValidationResult:
schemas = Validator.validation_schemas(current_app.simdb_config, simulation)
try:
for schema in schemas:
Validator(schema).validate(simulation)
Validator(schema, current_app.simdb_config).validate(simulation)
_update_simulation_status(
simulation, models_sim.Simulation.Status.PASSED, user
)
Expand Down
2 changes: 1 addition & 1 deletion src/simdb/validation/file/ids_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def configure(self, arguments: dict):
.split(",")
]

### Define logic for rule_filter
# Define logic for rule_filter
list_of_filter_names = (
arguments.get("rule_filter_name", "").strip('"').split(",")
)
Expand Down
42 changes: 40 additions & 2 deletions src/simdb/validation/validator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import warnings
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast

Expand Down Expand Up @@ -200,9 +201,46 @@ def validation_schemas(

return schemas

def __init__(self, schema: Dict):
def _custom_validation_ext(self, config: Config):

module_path = config.get_option("validation.custom_validator", default=None)
if module_path is None:
return CustomValidator

if not isinstance(module_path, str):
raise TypeError(
"Expected 'custom_validator config value' to be a string, "
f"got {type(module_path).__name__}"
)

if "." not in module_path:
raise ValueError(
f"Invalid validator path '{module_path}'."
"Expected format: 'package.module.ClassName'"
)

module_name, class_name = module_path.rsplit(".", 1)

try:
module = import_module(module_name)
except ModuleNotFoundError as err:
raise ImportError(
f"Unable to import module '{module_name}': {err}. "
"Please ensure the necessary validation package is installed"
) from err
try:
validation_cls = getattr(module, class_name)
except AttributeError as err:
raise AttributeError(
f"Module '{module_name}' does not have class or attribute "
f"'{class_name}'"
) from err
return validation_cls

def __init__(self, schema: Dict, config: Config):
try:
self._validator = CustomValidator(schema)
validation_cls = self._custom_validation_ext(config)
self._validator = validation_cls(schema)
self._validator.allow_unknown = True
except cerberus.SchemaError as err:
raise LoadError("Failed to parse validation schema") from err
Expand Down
102 changes: 102 additions & 0 deletions tests/cli/test_validator_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from unittest import mock

import pytest

from simdb.validation.validator import CustomValidator, Validator


class TestCustomValidatorExt:
@pytest.fixture
def validator_instance(self):
return Validator.__new__(Validator)

def test_validator_ext_when_none(self, validator_instance):
config = mock.MagicMock()
config.get_option.return_value = None

result = validator_instance._custom_validation_ext(config)

assert result is CustomValidator

config.get_option.assert_called_once_with(
"validation.custom_validator", default=None
)

def test_raises_type_error_for_non_string(self, validator_instance):
config = mock.MagicMock()
config.get_option.return_value = 12345

with pytest.raises(TypeError) as exc_info:
validator_instance._custom_validation_ext(config)

assert "Expected 'custom_validator config value' to be a string" in str(
exc_info.value
)

config.get_option.assert_called_once_with(
"validation.custom_validator", default=None
)

def test_raises_value_error_for_missing_dot(self, validator_instance):
config = mock.MagicMock()
config.get_option.return_value = "InvalidPathWithoutDot"

with pytest.raises(ValueError) as exc_info:
validator_instance._custom_validation_ext(config)

assert "Expected format: 'package.module.ClassName'" in str(exc_info.value)

config.get_option.assert_called_once_with(
"validation.custom_validator", default=None
)

@mock.patch("simdb.validation.validator.import_module")
def test_raises_import_error_on_module_not_found(
self, mock_import_module, validator_instance
):
config = mock.MagicMock()
config.get_option.return_value = "non_existent_pkg.module.MyValidator"
mock_import_module.side_effect = ModuleNotFoundError(
"No module named 'non_existent_pkg'"
)

with pytest.raises(ImportError):
validator_instance._custom_validation_ext(config)

mock_import_module.assert_called_once_with("non_existent_pkg.module")

@mock.patch("simdb.validation.validator.import_module")
def test_returns_custom_class_successfully(
self, mock_import_module, validator_instance
):
config = mock.MagicMock()
config.get_option.return_value = "mypackage.validator.MyValidator"

mock_module = mock.MagicMock()
mock_validator_class = mock.MagicMock()

mock_module.MyValidator = mock_validator_class

mock_import_module.return_value = mock_module

result = validator_instance._custom_validation_ext(config)

assert result is mock_validator_class
mock_import_module.assert_called_once_with("mypackage.validator")

@mock.patch("simdb.validation.validator.import_module")
def test_raise_attribute_error_when_class_missing(
self, mock_import_module, validator_instance
):
config = mock.MagicMock()
config.get_option.return_value = "mypackage.validator.MyValidator"

mock_module = mock.MagicMock(spec=[])
mock_import_module.return_value = mock_module

with pytest.raises(
AttributeError,
match="does not have class or attribute 'MyValidator'",
):
validator_instance._custom_validation_ext(config)
mock_import_module.assert_called_once_with("mypackage.validator")
1 change: 0 additions & 1 deletion tests/remote/api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import base64
import importlib
import importlib.util
import os
import shutil
Expand Down
1 change: 0 additions & 1 deletion tests/remote/test_authentication.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib
import importlib.util
from typing import TYPE_CHECKING, ClassVar, cast
from unittest import mock
Expand Down
Loading