22from __future__ import annotations
33
44from datetime import date
5- from typing import Any , Collection , Dict , Optional , Sequence , Literal
5+ from typing import Any , Collection , Dict , Optional , Sequence
66
77from humps import camelize
88from pydantic import root_validator
2727 PublicationIdentifierCreate ,
2828 SavedPublicationIdentifier ,
2929)
30+ from mavedb .view_models .score_range import SavedScoreSetRanges , ScoreSetRangesCreate , ScoreSetRanges
3031from 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-
8454class 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
296297class 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
457459class 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