diff --git a/src/mavedb/lib/validation/constants/score_set.py b/src/mavedb/lib/validation/constants/score_set.py deleted file mode 100644 index eaf9aa27f..000000000 --- a/src/mavedb/lib/validation/constants/score_set.py +++ /dev/null @@ -1 +0,0 @@ -default_ranges = ["normal", "abnormal"] diff --git a/src/mavedb/view_models/score_set.py b/src/mavedb/view_models/score_set.py index a577d10ca..8bc19c2d5 100644 --- a/src/mavedb/view_models/score_set.py +++ b/src/mavedb/view_models/score_set.py @@ -2,13 +2,12 @@ from __future__ import annotations from datetime import date -from typing import Any, Collection, Dict, Optional, Sequence +from typing import Any, Collection, Dict, Optional, Sequence, Literal from humps import camelize from pydantic import root_validator from mavedb.lib.validation import urn_re -from mavedb.lib.validation.constants.score_set import default_ranges from mavedb.lib.validation.exceptions import ValidationError from mavedb.lib.validation.utilities import inf_or_float, is_null from mavedb.models.enums.mapping_state import MappingState @@ -55,23 +54,13 @@ class Config: class ScoreRange(BaseModel): label: str description: Optional[str] - classification: str + classification: Literal["normal", "abnormal", "not_specified"] # Purposefully vague type hint because of some odd JSON Schema generation behavior. # Typing this as tuple[Union[float, None], Union[float, None]] will generate an invalid # jsonschema, and fail all tests that access the schema. This may be fixed in pydantic v2, # but it's unclear. Even just typing it as Tuple[Any, Any] will generate an invalid schema! range: list[Any] # really: tuple[Union[float, None], Union[float, None]] - @validator("classification") - def range_classification_value_is_accepted(cls, field_value: str): - classification = field_value.strip().lower() - if classification not in default_ranges: - raise ValidationError( - f"Unexpected classification value(s): {classification}. Permitted values: {default_ranges}" - ) - - return classification - @validator("range") def ranges_are_not_backwards(cls, field_value: tuple[Any]): if len(field_value) != 2: @@ -89,7 +78,7 @@ def ranges_are_not_backwards(cls, field_value: tuple[Any]): class ScoreRanges(BaseModel): - wt_score: float + wt_score: Optional[float] ranges: list[ScoreRange] # type: ignore @@ -209,17 +198,16 @@ def score_range_labels_must_be_unique(cls, field_value: Optional[ScoreRanges]): return field_value @validator("score_ranges") - def ranges_contain_normal_and_abnormal(cls, field_value: Optional[ScoreRanges]): + def score_range_normal_classification_exists_if_wild_type_score_provided(cls, field_value: Optional[ScoreRanges]): if field_value is None: return None - ranges = set([range_model.classification for range_model in field_value.ranges]) - if not set(default_ranges).issubset(ranges): - raise ValidationError( - "Both `normal` and `abnormal` ranges must be provided.", - # Raise this error inside the first classification provided by the model. - custom_loc=["body", "scoreRanges", "ranges", 0, "classification"], - ) + if field_value.wt_score is not None: + if not any([range_model.classification == "normal" for range_model in field_value.ranges]): + raise ValidationError( + "A wild type score has been provided, but no normal classification range exists.", + custom_loc=["body", "scoreRanges", "wtScore"], + ) return field_value @@ -264,6 +252,16 @@ def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreRanges]): normal_ranges = [ range_model.range for range_model in field_value.ranges if range_model.classification == "normal" ] + + if normal_ranges and field_value.wt_score is None: + raise ValidationError( + "A normal range has been provided, but no wild type score has been provided.", + custom_loc=["body", "scoreRanges", "wtScore"], + ) + + if field_value.wt_score is None: + return field_value + for range in normal_ranges: if field_value.wt_score >= inf_or_float(range[0], lower=True) and field_value.wt_score < inf_or_float( range[1], lower=False diff --git a/tests/view_models/test_score_set.py b/tests/view_models/test_score_set.py index c55d53494..a47c32420 100644 --- a/tests/view_models/test_score_set.py +++ b/tests/view_models/test_score_set.py @@ -1,7 +1,6 @@ import pytest from fastapi.encoders import jsonable_encoder -from mavedb.lib.validation.constants.score_set import default_ranges from mavedb.view_models.publication_identifier import PublicationIdentifierCreate from mavedb.view_models.score_set import ScoreSetCreate, ScoreSetModify from mavedb.view_models.target_gene import TargetGeneCreate @@ -440,20 +439,35 @@ def test_cannot_create_score_set_with_wild_type_outside_normal_range(): ) -@pytest.mark.parametrize("present_name", default_ranges) -def test_cannot_create_score_set_without_default_range(present_name): +def test_cannot_create_score_set_with_wild_type_score_and_no_normal_range(): + wt_score = -0.5 score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy() score_set_test["score_ranges"] = { - "wt_score": -1.5, + "wt_score": wt_score, "ranges": [ - {"label": "range_2", "classification": f"{present_name}", "range": (-3, -1)}, + {"label": "range_1", "classification": "abnormal", "range": (-1, 0)}, ], } with pytest.raises(ValueError) as exc_info: ScoreSetModify(**jsonable_encoder(score_set_test)) - assert "Both `normal` and `abnormal` ranges must be provided." in str(exc_info.value) + assert "A wild type score has been provided, but no normal classification range exists." in str(exc_info.value) + + +def test_cannot_create_score_set_with_normal_range_and_no_wild_type_score(): + score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy() + score_set_test["score_ranges"] = { + "wt_score": None, + "ranges": [ + {"label": "range_1", "classification": "normal", "range": (-1, 0)}, + ], + } + + with pytest.raises(ValueError) as exc_info: + ScoreSetModify(**jsonable_encoder(score_set_test)) + + assert "A normal range has been provided, but no wild type score has been provided." in str(exc_info.value) def test_cannot_create_score_set_without_default_ranges(): @@ -468,4 +482,18 @@ def test_cannot_create_score_set_without_default_ranges(): with pytest.raises(ValueError) as exc_info: ScoreSetModify(**jsonable_encoder(score_set_test)) - assert "Unexpected classification value(s): other. Permitted values: ['normal', 'abnormal']" in str(exc_info.value) + assert "unexpected value; permitted: 'normal', 'abnormal', 'not_specified'" in str(exc_info.value) + + +@pytest.mark.parametrize("classification", ["normal", "abnormal", "not_specified"]) +def test_can_create_score_set_with_any_range_classification(classification): + wt_score = -0.5 if classification == "normal" else None + score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy() + score_set_test["score_ranges"] = { + "wt_score": wt_score, + "ranges": [ + {"label": "range_1", "classification": classification, "range": (-1, 0)}, + ], + } + + ScoreSetModify(**jsonable_encoder(score_set_test))