Skip to content

Commit 13157ea

Browse files
authored
Merge pull request #439 from VariantEffect/feature/bencap/438/add-odds-path-to-score-range-data-model
Add OddsPath Property to ScoreRange Data Model
2 parents 195ab49 + fe5b29d commit 13157ea

File tree

8 files changed

+558
-82
lines changed

8 files changed

+558
-82
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Literal, Optional
2+
from pydantic import validator
3+
4+
from mavedb.view_models import record_type_validator, set_record_type
5+
from mavedb.view_models.base.base import BaseModel
6+
7+
8+
class OddsPathBase(BaseModel):
9+
ratio: float
10+
evidence: Optional[
11+
Literal[
12+
"BS3_STRONG",
13+
"BS3_MODERATE",
14+
"BS3_SUPPORTING",
15+
"INDETERMINATE",
16+
"PS3_VERY_STRONG",
17+
"PS3_STRONG",
18+
"PS3_MODERATE",
19+
"PS3_SUPPORTING",
20+
]
21+
] = None
22+
23+
24+
class OddsPathModify(OddsPathBase):
25+
@validator("ratio")
26+
def ratio_must_be_positive(cls, value: float) -> float:
27+
if value < 0:
28+
raise ValueError("OddsPath value must be greater than or equal to 0")
29+
30+
return value
31+
32+
33+
class OddsPathCreate(OddsPathModify):
34+
pass
35+
36+
37+
class SavedOddsPath(OddsPathBase):
38+
record_type: str = None # type: ignore
39+
40+
_record_type_factory = record_type_validator()(set_record_type)
41+
42+
43+
class OddsPath(SavedOddsPath):
44+
pass
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from typing import Optional, Literal, Any, Sequence
2+
from pydantic import validator
3+
4+
from mavedb.lib.validation.exceptions import ValidationError
5+
from mavedb.lib.validation.utilities import inf_or_float
6+
from mavedb.view_models import record_type_validator, set_record_type
7+
from mavedb.view_models.base.base import BaseModel
8+
from mavedb.view_models.publication_identifier import PublicationIdentifierBase
9+
from mavedb.view_models.odds_path import OddsPathCreate, OddsPathBase, OddsPathModify, SavedOddsPath, OddsPath
10+
11+
12+
### Range model
13+
14+
15+
class ScoreRangeBase(BaseModel):
16+
label: str
17+
description: Optional[str]
18+
classification: Literal["normal", "abnormal", "not_specified"]
19+
# Purposefully vague type hint because of some odd JSON Schema generation behavior.
20+
# Typing this as tuple[Union[float, None], Union[float, None]] will generate an invalid
21+
# jsonschema, and fail all tests that access the schema. This may be fixed in pydantic v2,
22+
# but it's unclear. Even just typing it as Tuple[Any, Any] will generate an invalid schema!
23+
range: list[Any] # really: tuple[Union[float, None], Union[float, None]]
24+
odds_path: Optional[OddsPathBase] = None
25+
26+
27+
class ScoreRangeModify(ScoreRangeBase):
28+
odds_path: Optional[OddsPathModify] = None
29+
30+
@validator("range")
31+
def ranges_are_not_backwards(cls, field_value: tuple[Any]):
32+
if len(field_value) != 2:
33+
raise ValidationError("Only a lower and upper bound are allowed.")
34+
35+
field_value[0] = inf_or_float(field_value[0], True) if field_value[0] is not None else None
36+
field_value[1] = inf_or_float(field_value[1], False) if field_value[1] is not None else None
37+
38+
if inf_or_float(field_value[0], True) > inf_or_float(field_value[1], False):
39+
raise ValidationError("The lower bound of the score range may not be larger than the upper bound.")
40+
elif inf_or_float(field_value[0], True) == inf_or_float(field_value[1], False):
41+
raise ValidationError("The lower and upper bound of the score range may not be the same.")
42+
43+
return field_value
44+
45+
46+
class ScoreRangeCreate(ScoreRangeModify):
47+
odds_path: Optional[OddsPathCreate] = None
48+
49+
50+
class SavedScoreRange(ScoreRangeBase):
51+
record_type: str = None # type: ignore
52+
53+
odds_path: Optional[SavedOddsPath] = None
54+
55+
_record_type_factory = record_type_validator()(set_record_type)
56+
57+
58+
class ScoreRange(SavedScoreRange):
59+
odds_path: Optional[OddsPath] = None
60+
61+
62+
### Ranges wrapper
63+
64+
65+
class ScoreSetRangesBase(BaseModel):
66+
wt_score: Optional[float] = None
67+
ranges: Sequence[ScoreRangeBase]
68+
odds_path_source: Optional[Sequence[PublicationIdentifierBase]] = None
69+
70+
71+
class ScoreSetRangesModify(ScoreSetRangesBase):
72+
ranges: Sequence[ScoreRangeModify]
73+
74+
75+
class ScoreSetRangesCreate(ScoreSetRangesModify):
76+
ranges: Sequence[ScoreRangeCreate]
77+
78+
79+
class SavedScoreSetRanges(ScoreSetRangesBase):
80+
record_type: str = None # type: ignore
81+
82+
ranges: Sequence[SavedScoreRange]
83+
84+
_record_type_factory = record_type_validator()(set_record_type)
85+
86+
87+
class ScoreSetRanges(SavedScoreSetRanges):
88+
ranges: Sequence[ScoreRange]

src/mavedb/view_models/score_set.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from __future__ import annotations
33

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

77
from humps import camelize
88
from pydantic import root_validator
@@ -27,6 +27,7 @@
2727
PublicationIdentifierCreate,
2828
SavedPublicationIdentifier,
2929
)
30+
from mavedb.view_models.score_range import SavedScoreSetRanges, ScoreSetRangesCreate, ScoreSetRanges
3031
from mavedb.view_models.target_gene import (
3132
SavedTargetGene,
3233
ShortTargetGene,
@@ -50,37 +51,6 @@ class Config:
5051
arbitrary_types_allowed = True
5152

5253

53-
class ScoreRange(BaseModel):
54-
label: str
55-
description: Optional[str]
56-
classification: Literal["normal", "abnormal", "not_specified"]
57-
# Purposefully vague type hint because of some odd JSON Schema generation behavior.
58-
# Typing this as tuple[Union[float, None], Union[float, None]] will generate an invalid
59-
# jsonschema, and fail all tests that access the schema. This may be fixed in pydantic v2,
60-
# but it's unclear. Even just typing it as Tuple[Any, Any] will generate an invalid schema!
61-
range: list[Any] # really: tuple[Union[float, None], Union[float, None]]
62-
63-
@validator("range")
64-
def ranges_are_not_backwards(cls, field_value: tuple[Any]):
65-
if len(field_value) != 2:
66-
raise ValidationError("Only a lower and upper bound are allowed.")
67-
68-
field_value[0] = inf_or_float(field_value[0], True) if field_value[0] is not None else None
69-
field_value[1] = inf_or_float(field_value[1], False) if field_value[1] is not None else None
70-
71-
if inf_or_float(field_value[0], True) > inf_or_float(field_value[1], False):
72-
raise ValidationError("The lower bound of the score range may not be larger than the upper bound.")
73-
elif inf_or_float(field_value[0], True) == inf_or_float(field_value[1], False):
74-
raise ValidationError("The lower and upper bound of the score range may not be the same.")
75-
76-
return field_value
77-
78-
79-
class ScoreRanges(BaseModel):
80-
wt_score: Optional[float]
81-
ranges: list[ScoreRange] # type: ignore
82-
83-
8454
class ScoreSetGetter(PublicationIdentifiersGetter):
8555
def get(self, key: Any, default: Any = ...) -> Any:
8656
if key == "meta_analyzes_score_set_urns":
@@ -110,7 +80,7 @@ class ScoreSetModify(ScoreSetBase):
11080
secondary_publication_identifiers: Optional[list[PublicationIdentifierCreate]]
11181
doi_identifiers: Optional[list[DoiIdentifierCreate]]
11282
target_genes: list[TargetGeneCreate]
113-
score_ranges: Optional[ScoreRanges]
83+
score_ranges: Optional[ScoreSetRangesCreate]
11484

11585
@validator("title", "short_description", "abstract_text", "method_text")
11686
def validate_field_is_non_empty(cls, v):
@@ -198,7 +168,7 @@ def target_accession_base_editor_targets_are_consistent(cls, field_value, values
198168
return field_value
199169

200170
@validator("score_ranges")
201-
def score_range_labels_must_be_unique(cls, field_value: Optional[ScoreRanges]):
171+
def score_range_labels_must_be_unique(cls, field_value: Optional[ScoreSetRangesCreate]):
202172
if field_value is None:
203173
return None
204174

@@ -217,7 +187,9 @@ def score_range_labels_must_be_unique(cls, field_value: Optional[ScoreRanges]):
217187
return field_value
218188

219189
@validator("score_ranges")
220-
def score_range_normal_classification_exists_if_wild_type_score_provided(cls, field_value: Optional[ScoreRanges]):
190+
def score_range_normal_classification_exists_if_wild_type_score_provided(
191+
cls, field_value: Optional[ScoreSetRangesCreate]
192+
):
221193
if field_value is None:
222194
return None
223195

@@ -231,7 +203,7 @@ def score_range_normal_classification_exists_if_wild_type_score_provided(cls, fi
231203
return field_value
232204

233205
@validator("score_ranges")
234-
def ranges_do_not_overlap(cls, field_value: Optional[ScoreRanges]):
206+
def ranges_do_not_overlap(cls, field_value: Optional[ScoreSetRangesCreate]):
235207
def test_overlap(tp1, tp2) -> bool:
236208
# Always check the tuple with the lowest lower bound. If we do not check
237209
# overlaps in this manner, checking the overlap of (0,1) and (1,2) will
@@ -264,7 +236,7 @@ def test_overlap(tp1, tp2) -> bool:
264236
return field_value
265237

266238
@validator("score_ranges")
267-
def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreRanges]):
239+
def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreSetRangesCreate]):
268240
if field_value is None:
269241
return None
270242

@@ -292,6 +264,35 @@ def wild_type_score_in_normal_range(cls, field_value: Optional[ScoreRanges]):
292264
custom_loc=["body", "scoreRanges", "wtScore"],
293265
)
294266

267+
@root_validator()
268+
def validate_score_range_odds_path_source_in_publication_identifiers(cls, values):
269+
score_ranges: Optional[ScoreSetRangesCreate] = values.get("score_ranges")
270+
if score_ranges is None or score_ranges.odds_path_source is None:
271+
return values
272+
273+
if not score_ranges.odds_path_source:
274+
return values
275+
276+
for idx, pub in enumerate(score_ranges.odds_path_source):
277+
primary_publication_identifiers = (
278+
values.get("primary_publication_identifiers", [])
279+
if values.get("primary_publication_identifiers")
280+
else []
281+
)
282+
secondary_publication_identifiers = (
283+
values.get("secondary_publication_identifiers", [])
284+
if values.get("secondary_publication_identifiers")
285+
else []
286+
)
287+
if pub not in [*primary_publication_identifiers, *secondary_publication_identifiers]:
288+
raise ValidationError(
289+
f"Odds path source publication identifier at index {idx} is not defined in score set publications. "
290+
"To use a publication identifier in the odds path source, it must be defined in the primary or secondary publication identifiers.",
291+
custom_loc=["body", "scoreRanges", "oddsPath", "source", idx],
292+
)
293+
294+
return values
295+
295296

296297
class ScoreSetCreate(ScoreSetModify):
297298
"""View model for creating a new score set."""
@@ -414,7 +415,7 @@ class SavedScoreSet(ScoreSetBase):
414415
dataset_columns: Dict
415416
external_links: Dict[str, ExternalLink]
416417
contributors: list[Contributor]
417-
score_ranges: Optional[ScoreRanges]
418+
score_ranges: Optional[SavedScoreSetRanges]
418419
score_calibrations: Optional[dict[str, Calibration]]
419420

420421
_record_type_factory = record_type_validator()(set_record_type)
@@ -452,6 +453,7 @@ class ScoreSet(SavedScoreSet):
452453
processing_errors: Optional[dict]
453454
mapping_state: Optional[MappingState]
454455
mapping_errors: Optional[dict]
456+
score_ranges: Optional[ScoreSetRanges]
455457

456458

457459
class ScoreSetWithVariants(ScoreSet):
@@ -484,6 +486,7 @@ class ScoreSetPublicDump(SavedScoreSet):
484486
processing_errors: Optional[Dict]
485487
mapping_state: Optional[MappingState]
486488
mapping_errors: Optional[Dict]
489+
score_ranges: Optional[ScoreSetRanges]
487490

488491

489492
# ruff: noqa: E402

0 commit comments

Comments
 (0)