Skip to content
Merged
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
1 change: 0 additions & 1 deletion src/mavedb/lib/validation/constants/score_set.py

This file was deleted.

42 changes: 20 additions & 22 deletions src/mavedb/view_models/score_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
42 changes: 35 additions & 7 deletions tests/view_models/test_score_set.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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))