diff --git a/ixmp4/data/db/optimization/parameter/model.py b/ixmp4/data/db/optimization/parameter/model.py index 3199675d..bb052ea7 100644 --- a/ixmp4/data/db/optimization/parameter/model.py +++ b/ixmp4/data/db/optimization/parameter/model.py @@ -4,6 +4,7 @@ from sqlalchemy.orm import validates from ixmp4 import db +from ixmp4.core.exceptions import OptimizationDataValidationError from ixmp4.data import types from ixmp4.data.abstract import optimization as abstract @@ -14,6 +15,7 @@ class Parameter(base.BaseModel): # NOTE: These might be mixin-able, but would require some abstraction NotFound: ClassVar = abstract.Parameter.NotFound NotUnique: ClassVar = abstract.Parameter.NotUnique + DataInvalid: ClassVar = OptimizationDataValidationError DeletionPrevented: ClassVar = abstract.Parameter.DeletionPrevented # constrained_to_indexsets: ClassVar[list[str] | None] = None @@ -28,7 +30,7 @@ def validate_data(self, key, data: dict[str, Any]): del data_to_validate["values"] del data_to_validate["units"] _ = utils.validate_data( - key=key, + host=self, data=data_to_validate, columns=self.columns, ) diff --git a/ixmp4/data/db/optimization/parameter/repository.py b/ixmp4/data/db/optimization/parameter/repository.py index f4cbb85f..699cfcf4 100644 --- a/ixmp4/data/db/optimization/parameter/repository.py +++ b/ixmp4/data/db/optimization/parameter/repository.py @@ -3,6 +3,7 @@ import pandas as pd from ixmp4 import db +from ixmp4.core.exceptions import OptimizationItemUsageError from ixmp4.data.abstract import optimization as abstract from ixmp4.data.auth.decorators import guard from ixmp4.data.db.unit import Unit @@ -20,6 +21,8 @@ class ParameterRepository( ): model_class = Parameter + UsageError = OptimizationItemUsageError + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.docs = ParameterDocsRepository(*args, **kwargs) @@ -112,16 +115,20 @@ def create( if isinstance(constrained_to_indexsets, str): constrained_to_indexsets = list(constrained_to_indexsets) if column_names and len(column_names) != len(constrained_to_indexsets): - raise ValueError( + raise self.UsageError( + f"While processing Parameter {name}: \n" "`constrained_to_indexsets` and `column_names` not equal in length! " "Please provide the same number of entries for both!" ) # TODO: activate something like this if each column must be indexed by a unique # indexset # if len(constrained_to_indexsets) != len(set(constrained_to_indexsets)): - # raise ValueError("Each dimension must be constrained to a unique indexset!") # noqa + # raise self.UsageError("Each dimension must be constrained to a unique indexset!") # noqa if column_names and len(column_names) != len(set(column_names)): - raise ValueError("The given `column_names` are not unique!") + raise self.UsageError( + f"While processing Parameter {name}: \n" + "The given `column_names` are not unique!" + ) parameter = super().create( run_id=run_id, @@ -149,13 +156,19 @@ def tabulate(self, *args, **kwargs) -> pd.DataFrame: @guard("edit") def add_data(self, parameter_id: int, data: dict[str, Any] | pd.DataFrame) -> None: if isinstance(data, dict): - data = pd.DataFrame.from_dict(data=data) + try: + data = pd.DataFrame.from_dict(data=data) + except ValueError as e: + raise Parameter.DataInvalid(str(e)) from e + parameter = self.get_by_id(id=parameter_id) missing_columns = set(["values", "units"]) - set(data.columns) - assert ( - not missing_columns - ), f"Parameter.data must include the column(s): {', '.join(missing_columns)}!" + if missing_columns: + raise OptimizationItemUsageError( + "Parameter.data must include the column(s): " + f"{', '.join(missing_columns)}!" + ) # Can use a set for now, need full column if we care about order for unit_name in set(data["units"]): diff --git a/tests/core/test_optimization_parameter.py b/tests/core/test_optimization_parameter.py index e0f92768..8602ddf1 100644 --- a/tests/core/test_optimization_parameter.py +++ b/tests/core/test_optimization_parameter.py @@ -3,6 +3,10 @@ import ixmp4 from ixmp4.core import IndexSet, Parameter +from ixmp4.core.exceptions import ( + OptimizationDataValidationError, + OptimizationItemUsageError, +) from ..utils import assert_unordered_equality, create_indexsets_for_run @@ -60,7 +64,7 @@ def test_create_parameter(self, platform: ixmp4.Platform): ) # Test mismatch in constrained_to_indexsets and column_names raises - with pytest.raises(ValueError, match="not equal in length"): + with pytest.raises(OptimizationItemUsageError, match="not equal in length"): _ = run.optimization.parameters.create( "Parameter 2", constrained_to_indexsets=[indexset.name], @@ -76,7 +80,9 @@ def test_create_parameter(self, platform: ixmp4.Platform): assert parameter_2.columns[0].name == "Column 1" # Test duplicate column_names raise - with pytest.raises(ValueError, match="`column_names` are not unique"): + with pytest.raises( + OptimizationItemUsageError, match="`column_names` are not unique" + ): _ = run.optimization.parameters.create( name="Parameter 3", constrained_to_indexsets=[indexset.name, indexset.name], @@ -149,7 +155,7 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): ) with pytest.raises( - AssertionError, match=r"must include the column\(s\): values!" + OptimizationItemUsageError, match=r"must include the column\(s\): values!" ): parameter_2.add( pd.DataFrame( @@ -162,7 +168,7 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): ) with pytest.raises( - AssertionError, match=r"must include the column\(s\): units!" + OptimizationItemUsageError, match=r"must include the column\(s\): units!" ): parameter_2.add( data=pd.DataFrame( @@ -176,7 +182,10 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): # By converting data to pd.DataFrame, we automatically enforce equal length # of new columns, raises All arrays must be of the same length otherwise: - with pytest.raises(ValueError, match="All arrays must be of the same length"): + with pytest.raises( + OptimizationDataValidationError, + match="All arrays must be of the same length", + ): parameter_2.add( data={ indexset.name: ["foo", "foo"], @@ -186,7 +195,9 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): }, ) - with pytest.raises(ValueError, match="contains duplicate rows"): + with pytest.raises( + OptimizationDataValidationError, match="contains duplicate rows" + ): parameter_2.add( data={ indexset.name: ["foo", "foo"], diff --git a/tests/data/test_optimization_parameter.py b/tests/data/test_optimization_parameter.py index 7365da34..dac17e86 100644 --- a/tests/data/test_optimization_parameter.py +++ b/tests/data/test_optimization_parameter.py @@ -2,6 +2,10 @@ import pytest import ixmp4 +from ixmp4.core.exceptions import ( + OptimizationDataValidationError, + OptimizationItemUsageError, +) from ixmp4.data.abstract import Parameter from ..utils import assert_unordered_equality, create_indexsets_for_run @@ -60,7 +64,7 @@ def test_create_parameter(self, platform: ixmp4.Platform): ) # Test mismatch in constrained_to_indexsets and column_names raises - with pytest.raises(ValueError, match="not equal in length"): + with pytest.raises(OptimizationItemUsageError, match="not equal in length"): _ = platform.backend.optimization.parameters.create( run_id=run.id, name="Parameter 2", @@ -78,7 +82,9 @@ def test_create_parameter(self, platform: ixmp4.Platform): assert parameter_2.columns[0].name == "Column 1" # Test duplicate column_names raise - with pytest.raises(ValueError, match="`column_names` are not unique"): + with pytest.raises( + OptimizationItemUsageError, match="`column_names` are not unique" + ): _ = platform.backend.optimization.parameters.create( run_id=run.id, name="Parameter 3", @@ -161,7 +167,7 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): ) with pytest.raises( - AssertionError, match=r"must include the column\(s\): values!" + OptimizationItemUsageError, match=r"must include the column\(s\): values!" ): platform.backend.optimization.parameters.add_data( parameter_id=parameter_2.id, @@ -175,7 +181,7 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): ) with pytest.raises( - AssertionError, match=r"must include the column\(s\): units!" + OptimizationItemUsageError, match=r"must include the column\(s\): units!" ): platform.backend.optimization.parameters.add_data( parameter_id=parameter_2.id, @@ -190,7 +196,10 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): # By converting data to pd.DataFrame, we automatically enforce equal length # of new columns, raises All arrays must be of the same length otherwise: - with pytest.raises(ValueError, match="All arrays must be of the same length"): + with pytest.raises( + OptimizationDataValidationError, + match="All arrays must be of the same length", + ): platform.backend.optimization.parameters.add_data( parameter_id=parameter_2.id, data={ @@ -201,7 +210,9 @@ def test_parameter_add_data(self, platform: ixmp4.Platform): }, ) - with pytest.raises(ValueError, match="contains duplicate rows"): + with pytest.raises( + OptimizationDataValidationError, match="contains duplicate rows" + ): platform.backend.optimization.parameters.add_data( parameter_id=parameter_2.id, data={