Skip to content

Commit 096b851

Browse files
authored
Merge pull request #414 from VariantEffect/feature/bencap/413/not-specified-score-range
Support for Score Ranges with an Unspecified Classification
2 parents 60d4e0d + 254c338 commit 096b851

File tree

3 files changed

+55
-30
lines changed

3 files changed

+55
-30
lines changed

src/mavedb/lib/validation/constants/score_set.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/mavedb/view_models/score_set.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
from __future__ import annotations
33

44
from datetime import date
5-
from typing import Any, Collection, Dict, Optional, Sequence
5+
from typing import Any, Collection, Dict, Optional, Sequence, Literal
66

77
from humps import camelize
88
from pydantic import root_validator
99

1010
from mavedb.lib.validation import urn_re
11-
from mavedb.lib.validation.constants.score_set import default_ranges
1211
from mavedb.lib.validation.exceptions import ValidationError
1312
from mavedb.lib.validation.utilities import inf_or_float, is_null
1413
from mavedb.models.enums.mapping_state import MappingState
@@ -55,23 +54,13 @@ class Config:
5554
class ScoreRange(BaseModel):
5655
label: str
5756
description: Optional[str]
58-
classification: str
57+
classification: Literal["normal", "abnormal", "not_specified"]
5958
# Purposefully vague type hint because of some odd JSON Schema generation behavior.
6059
# Typing this as tuple[Union[float, None], Union[float, None]] will generate an invalid
6160
# jsonschema, and fail all tests that access the schema. This may be fixed in pydantic v2,
6261
# but it's unclear. Even just typing it as Tuple[Any, Any] will generate an invalid schema!
6362
range: list[Any] # really: tuple[Union[float, None], Union[float, None]]
6463

65-
@validator("classification")
66-
def range_classification_value_is_accepted(cls, field_value: str):
67-
classification = field_value.strip().lower()
68-
if classification not in default_ranges:
69-
raise ValidationError(
70-
f"Unexpected classification value(s): {classification}. Permitted values: {default_ranges}"
71-
)
72-
73-
return classification
74-
7564
@validator("range")
7665
def ranges_are_not_backwards(cls, field_value: tuple[Any]):
7766
if len(field_value) != 2:
@@ -89,7 +78,7 @@ def ranges_are_not_backwards(cls, field_value: tuple[Any]):
8978

9079

9180
class ScoreRanges(BaseModel):
92-
wt_score: float
81+
wt_score: Optional[float]
9382
ranges: list[ScoreRange] # type: ignore
9483

9584

@@ -209,17 +198,16 @@ def score_range_labels_must_be_unique(cls, field_value: Optional[ScoreRanges]):
209198
return field_value
210199

211200
@validator("score_ranges")
212-
def ranges_contain_normal_and_abnormal(cls, field_value: Optional[ScoreRanges]):
201+
def score_range_normal_classification_exists_if_wild_type_score_provided(cls, field_value: Optional[ScoreRanges]):
213202
if field_value is None:
214203
return None
215204

216-
ranges = set([range_model.classification for range_model in field_value.ranges])
217-
if not set(default_ranges).issubset(ranges):
218-
raise ValidationError(
219-
"Both `normal` and `abnormal` ranges must be provided.",
220-
# Raise this error inside the first classification provided by the model.
221-
custom_loc=["body", "scoreRanges", "ranges", 0, "classification"],
222-
)
205+
if field_value.wt_score is not None:
206+
if not any([range_model.classification == "normal" for range_model in field_value.ranges]):
207+
raise ValidationError(
208+
"A wild type score has been provided, but no normal classification range exists.",
209+
custom_loc=["body", "scoreRanges", "wtScore"],
210+
)
223211

224212
return field_value
225213

@@ -264,6 +252,16 @@ def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreRanges]):
264252
normal_ranges = [
265253
range_model.range for range_model in field_value.ranges if range_model.classification == "normal"
266254
]
255+
256+
if normal_ranges and field_value.wt_score is None:
257+
raise ValidationError(
258+
"A normal range has been provided, but no wild type score has been provided.",
259+
custom_loc=["body", "scoreRanges", "wtScore"],
260+
)
261+
262+
if field_value.wt_score is None:
263+
return field_value
264+
267265
for range in normal_ranges:
268266
if field_value.wt_score >= inf_or_float(range[0], lower=True) and field_value.wt_score < inf_or_float(
269267
range[1], lower=False

tests/view_models/test_score_set.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pytest
22
from fastapi.encoders import jsonable_encoder
33

4-
from mavedb.lib.validation.constants.score_set import default_ranges
54
from mavedb.view_models.publication_identifier import PublicationIdentifierCreate
65
from mavedb.view_models.score_set import ScoreSetCreate, ScoreSetModify
76
from mavedb.view_models.target_gene import TargetGeneCreate
@@ -440,20 +439,35 @@ def test_cannot_create_score_set_with_wild_type_outside_normal_range():
440439
)
441440

442441

443-
@pytest.mark.parametrize("present_name", default_ranges)
444-
def test_cannot_create_score_set_without_default_range(present_name):
442+
def test_cannot_create_score_set_with_wild_type_score_and_no_normal_range():
443+
wt_score = -0.5
445444
score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy()
446445
score_set_test["score_ranges"] = {
447-
"wt_score": -1.5,
446+
"wt_score": wt_score,
448447
"ranges": [
449-
{"label": "range_2", "classification": f"{present_name}", "range": (-3, -1)},
448+
{"label": "range_1", "classification": "abnormal", "range": (-1, 0)},
450449
],
451450
}
452451

453452
with pytest.raises(ValueError) as exc_info:
454453
ScoreSetModify(**jsonable_encoder(score_set_test))
455454

456-
assert "Both `normal` and `abnormal` ranges must be provided." in str(exc_info.value)
455+
assert "A wild type score has been provided, but no normal classification range exists." in str(exc_info.value)
456+
457+
458+
def test_cannot_create_score_set_with_normal_range_and_no_wild_type_score():
459+
score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy()
460+
score_set_test["score_ranges"] = {
461+
"wt_score": None,
462+
"ranges": [
463+
{"label": "range_1", "classification": "normal", "range": (-1, 0)},
464+
],
465+
}
466+
467+
with pytest.raises(ValueError) as exc_info:
468+
ScoreSetModify(**jsonable_encoder(score_set_test))
469+
470+
assert "A normal range has been provided, but no wild type score has been provided." in str(exc_info.value)
457471

458472

459473
def test_cannot_create_score_set_without_default_ranges():
@@ -468,4 +482,18 @@ def test_cannot_create_score_set_without_default_ranges():
468482
with pytest.raises(ValueError) as exc_info:
469483
ScoreSetModify(**jsonable_encoder(score_set_test))
470484

471-
assert "Unexpected classification value(s): other. Permitted values: ['normal', 'abnormal']" in str(exc_info.value)
485+
assert "unexpected value; permitted: 'normal', 'abnormal', 'not_specified'" in str(exc_info.value)
486+
487+
488+
@pytest.mark.parametrize("classification", ["normal", "abnormal", "not_specified"])
489+
def test_can_create_score_set_with_any_range_classification(classification):
490+
wt_score = -0.5 if classification == "normal" else None
491+
score_set_test = TEST_MINIMAL_SEQ_SCORESET.copy()
492+
score_set_test["score_ranges"] = {
493+
"wt_score": wt_score,
494+
"ranges": [
495+
{"label": "range_1", "classification": classification, "range": (-1, 0)},
496+
],
497+
}
498+
499+
ScoreSetModify(**jsonable_encoder(score_set_test))

0 commit comments

Comments
 (0)