Skip to content

Commit 841b791

Browse files
authored
fix!: community models should not inherit from Statement / EvidenceLine (#22)
close #21 * Use model validator instead of inheriting from `Statement` or `EvidenceLine` * `VariantOncogenicityFunctionalImpactEvidenceLine` and `VariantPathogenicityFunctionalImpactEvidenceLine` should NOT inherit from `EvidenceLine` * `VariantOncogenicityStudyStatement`, `VariantPathogenicityStatement`, `VariantDiagnosticStudyStatement`, `VariantPrognosticStudyStatement`, and `VariantTherapeuticResponseStudyStatement` should NOT inherit from `Statement`
1 parent b824721 commit 841b791

File tree

8 files changed

+406
-60
lines changed

8 files changed

+406
-60
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ requires-python = ">=3.10"
3232
dynamic = ["version"]
3333
dependencies = [
3434
"ga4gh.vrs==2.*",
35-
"ga4gh.cat_vrs~=0.4.0",
35+
"ga4gh.cat_vrs~=0.5.0",
3636
"pydantic==2.*"
3737
]
3838

src/ga4gh/va_spec/aac_2017/models.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
from ga4gh.core.models import MappableConcept, iriReference
1010
from ga4gh.va_spec.base.core import (
1111
Method,
12-
Statement,
12+
StatementValidatorMixin,
1313
VariantDiagnosticProposition,
1414
VariantPrognosticProposition,
1515
VariantTherapeuticResponseProposition,
1616
)
1717
from ga4gh.va_spec.base.enums import System
1818
from ga4gh.va_spec.base.validators import validate_mappable_concept
1919
from pydantic import (
20+
BaseModel,
2021
Field,
2122
field_validator,
2223
)
@@ -46,8 +47,11 @@ class Classification(str, Enum):
4647
AMP_ASCO_CAP_TIERS = [v.value for v in Classification.__members__.values()]
4748

4849

49-
class _ValidatorMixin:
50-
"""Mixin class for reusable AMP/ASCO/CAP field validators"""
50+
class AmpAscoCapValidatorMixin(StatementValidatorMixin):
51+
"""Mixin class for reusable AMP/ASCO/CAP field validators
52+
53+
Should be used with classes that inherit from Pydantic BaseModel
54+
"""
5155

5256
@field_validator("strength")
5357
@classmethod
@@ -74,7 +78,7 @@ def validate_classification(cls, v: MappableConcept) -> MappableConcept:
7478
return validate_mappable_concept(v, System.AMP_ASCO_CAP, AMP_ASCO_CAP_TIERS)
7579

7680

77-
class VariantDiagnosticStudyStatement(Statement, _ValidatorMixin):
81+
class VariantDiagnosticStudyStatement(BaseModel, AmpAscoCapValidatorMixin):
7882
"""A statement reporting a conclusion from a single study about whether a variant is
7983
associated with a disease (a diagnostic inclusion criterion), or absence of a
8084
disease (diagnostic exclusion criterion) - based on interpretation of the study's
@@ -99,7 +103,7 @@ class VariantDiagnosticStudyStatement(Statement, _ValidatorMixin):
99103
)
100104

101105

102-
class VariantPrognosticStudyStatement(Statement, _ValidatorMixin):
106+
class VariantPrognosticStudyStatement(BaseModel, AmpAscoCapValidatorMixin):
103107
"""A statement reporting a conclusion from a single study about whether a variant is
104108
associated with a disease prognosis - based on interpretation of the study's
105109
results.
@@ -123,7 +127,7 @@ class VariantPrognosticStudyStatement(Statement, _ValidatorMixin):
123127
)
124128

125129

126-
class VariantTherapeuticResponseStudyStatement(Statement, _ValidatorMixin):
130+
class VariantTherapeuticResponseStudyStatement(BaseModel, AmpAscoCapValidatorMixin):
127131
"""A statement reporting a conclusion from a single study about whether a variant is
128132
associated with a therapeutic response (positive or negative) - based on
129133
interpretation of the study's results.

src/ga4gh/va_spec/acmg_2015/models.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
from ga4gh.core.models import MappableConcept, iriReference
99
from ga4gh.va_spec.base.core import (
10-
EvidenceLine,
10+
EvidenceLineValidatorMixin,
1111
Method,
12-
Statement,
12+
StatementValidatorMixin,
1313
VariantPathogenicityProposition,
1414
)
1515
from ga4gh.va_spec.base.enums import (
@@ -18,8 +18,10 @@
1818
STRENGTHS,
1919
System,
2020
)
21-
from ga4gh.va_spec.base.validators import validate_mappable_concept
22-
from pydantic import Field, field_validator
21+
from ga4gh.va_spec.base.validators import (
22+
validate_mappable_concept,
23+
)
24+
from pydantic import BaseModel, Field, field_validator, model_validator
2325

2426

2527
class EvidenceOutcome(str, Enum):
@@ -51,7 +53,9 @@ class AcmgClassification(str, Enum):
5153
ACMG_CLASSIFICATIONS = [v.value for v in AcmgClassification.__members__.values()]
5254

5355

54-
class VariantPathogenicityFunctionalImpactEvidenceLine(EvidenceLine):
56+
class VariantPathogenicityFunctionalImpactEvidenceLine(
57+
BaseModel, EvidenceLineValidatorMixin
58+
):
5559
"""An Evidence Line that describes how information about the functional impact of a
5660
variant on a gene or gene product was interpreted as evidence for or against the
5761
variant's pathogenicity.
@@ -100,23 +104,21 @@ def validate_specified_by(cls, v: Method | iriReference) -> Method | iriReferenc
100104

101105
return v
102106

103-
@field_validator("evidenceOutcome")
104-
@classmethod
105-
def validate_evidence_outcome(
106-
cls, v: MappableConcept | None
107-
) -> MappableConcept | None:
108-
"""Validate evidenceOutcome
107+
@model_validator(mode="before")
108+
def validate_evidence_outcome(cls, values: dict) -> dict: # noqa: N805
109+
"""Validate ``evidenceOutcome`` property if it exists
109110
110-
:param v: evidenceOutcome
111-
:raises ValueError: If invalid evidenceOutcome values are provided
112-
:return: Validated evidenceOutcome value
111+
:param values: Input values
112+
:raises ValueError: If ``evidenceOutcome`` exists and is invalid
113+
:return: Validated input values. If ``evidenceOutcome`` exists, then it will be
114+
validated and converted to a ``MappableConcept``
113115
"""
114-
return validate_mappable_concept(
115-
v, System.ACMG, EVIDENCE_OUTCOME_VALUES, mc_is_required=False
116+
return cls._validate_evidence_outcome(
117+
values, System.ACMG, EVIDENCE_OUTCOME_VALUES
116118
)
117119

118120

119-
class VariantPathogenicityStatement(Statement):
121+
class VariantPathogenicityStatement(BaseModel, StatementValidatorMixin):
120122
"""A Statement describing the role of a variant in causing an inherited condition."""
121123

122124
proposition: VariantPathogenicityProposition | None = Field(
@@ -162,9 +164,10 @@ def validate_classification(cls, v: MappableConcept) -> MappableConcept:
162164
err_msg = "`primaryCoding` is required."
163165
raise ValueError(err_msg)
164166

165-
supported_systems = [System.ACMG.value, System.ACMG.value]
167+
supported_systems = [System.ACMG.value, System.CLIN_GEN.value]
166168
if v.primaryCoding.system not in supported_systems:
167169
err_msg = f"`primaryCoding.system` must be one of: {supported_systems}."
170+
raise ValueError(err_msg)
168171

169172
if v.primaryCoding.system == System.ACMG:
170173
if v.primaryCoding.code.root not in ACMG_CLASSIFICATIONS:

src/ga4gh/va_spec/base/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
)
2828
from .domain_entities import Condition, ConditionSet, Therapeutic, TherapyGroup
2929
from .enums import (
30+
CCV_CLASSIFICATIONS,
3031
CLIN_GEN_CLASSIFICATIONS,
3132
STRENGTH_OF_EVIDENCE_PROVIDED_VALUES,
3233
STRENGTHS,
34+
CcvClassification,
3335
ClinGenClassification,
3436
DiagnosticPredicate,
3537
MembershipOperator,
@@ -42,7 +44,9 @@
4244

4345
__all__ = [
4446
"Agent",
47+
"CCV_CLASSIFICATIONS",
4548
"CLIN_GEN_CLASSIFICATIONS",
49+
"CcvClassification",
4650
"ClinGenClassification",
4751
"ClinGenClassification",
4852
"ClinicalVariantProposition",

src/ga4gh/va_spec/base/core.py

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@
2020
from ga4gh.va_spec.base.enums import (
2121
DiagnosticPredicate,
2222
PrognosticPredicate,
23+
System,
2324
TherapeuticResponsePredicate,
2425
)
26+
from ga4gh.va_spec.base.validators import validate_mappable_concept
2527
from ga4gh.vrs.models import Allele, MolecularVariation
2628
from pydantic import (
29+
BaseModel,
2730
ConfigDict,
2831
Field,
2932
RootModel,
3033
StringConstraints,
3134
ValidationError,
3235
field_validator,
36+
model_validator,
3337
)
3438

35-
StatementType = TypeVar("StatementType", bound="Statement")
39+
StatementType = TypeVar("StatementType")
40+
EvidenceLineType = TypeVar("EvidenceLineType")
3641

3742
#########################################
3843
# Abstract Core Classes
@@ -484,7 +489,7 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra):
484489
description="The possible fact against which evidence items contained in an Evidence Line were collectively evaluated, in determining the overall strength and direction of support they provide. For example, in an ACMG Guideline-based assessment of variant pathogenicity, the support provided by distinct lines of evidence are assessed against a target proposition that the variant is pathogenic for a specific disease.",
485490
)
486491
hasEvidenceItems: (
487-
list[StudyResult | StatementType | EvidenceLine | iriReference] | None
492+
list[StudyResult | StatementType | EvidenceLineType | iriReference] | None
488493
) = Field(
489494
None,
490495
description="An individual piece of information that was evaluated as evidence in building the argument represented by an Evidence Line.",
@@ -509,7 +514,7 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra):
509514
@field_validator("hasEvidenceItems", mode="before")
510515
def validate_has_evidence_items(
511516
cls, # noqa: N805
512-
v: list[StudyResult, StatementType, EvidenceLine, iriReference] | None,
517+
v: list | None,
513518
) -> list | None:
514519
"""Ensure hasEvidenceItems is correct type
515520
@@ -539,15 +544,12 @@ def validate_has_evidence_items(
539544
obj_
540545
for _, obj_ in vars(imported_module).items()
541546
if inspect.isclass(obj_)
542-
and issubclass(obj_, Statement)
543-
and obj_ is not Statement
547+
and issubclass(obj_, BaseModel)
548+
and obj_.__name__.endswith(("Statement", "EvidenceLine"))
544549
]
545550
)
546551

547-
has_evidence_items_models.extend(
548-
[Statement, StudyResult, EvidenceLine, iriReference]
549-
)
550-
552+
has_evidence_items_models.extend([Statement, StudyResult, EvidenceLine])
551553
for evidence_item in v:
552554
if isinstance(evidence_item, dict):
553555
found_model = False
@@ -561,10 +563,13 @@ def validate_has_evidence_items(
561563
found_model = True
562564
break
563565
if not found_model:
564-
err_msg = "Unable to find valid model"
566+
err_msg = "Unable to find valid model for `hasEvidenceItems`"
565567
raise ValueError(err_msg)
568+
elif isinstance(evidence_item, str):
569+
evidence_items.append(iriReference(root=evidence_item))
566570
else:
567-
evidence_items.append(evidence_item)
571+
err_msg = "Unable to find valid model for `hasEvidenceItems`"
572+
raise ValueError(err_msg)
568573
return evidence_items
569574

570575

@@ -580,9 +585,17 @@ class Statement(InformationEntity, BaseModelForbidExtra):
580585
type: Literal["Statement"] = Field(
581586
CoreType.STATEMENT.value, description=f"MUST be '{CoreType.STATEMENT.value}'."
582587
)
583-
proposition: Proposition = Field(
588+
proposition: (
589+
ExperimentalVariantFunctionalImpactProposition
590+
| VariantDiagnosticProposition
591+
| VariantOncogenicityProposition
592+
| VariantPathogenicityProposition
593+
| VariantPrognosticProposition
594+
| VariantTherapeuticResponseProposition
595+
) = Field(
584596
...,
585597
description="A possible fact, the validity of which is assessed and reported by the Statement. A Statement can put forth the proposition as being true, false, or uncertain, and may provide an assessment of the level of confidence/evidence supporting this claim.",
598+
discriminator="type",
586599
)
587600
direction: Direction = Field(
588601
...,
@@ -624,3 +637,70 @@ class StudyGroup(Entity, BaseModelForbidExtra):
624637
None,
625638
description="A feature or role shared by all members of the StudyGroup, representing a criterion for membership in the group.",
626639
)
640+
641+
642+
class StatementValidatorMixin:
643+
"""Mixin class for reusable Statement model validators
644+
645+
Should be used with classes that inherit from Pydantic BaseModel
646+
"""
647+
648+
model_config = ConfigDict(extra="allow")
649+
650+
@model_validator(mode="after")
651+
def statement_validator(cls, model: BaseModel) -> BaseModel: # noqa: N805
652+
"""Validate that the model is a ``Statement``.
653+
654+
:param model: Pydantic BaseModel to validate
655+
:raises ValueError: If ``model`` does not validate against a ``Statement``
656+
:return: Validated model
657+
"""
658+
try:
659+
Statement(**model.model_dump())
660+
except ValidationError as e:
661+
err_msg = f"Must be a `Statement`: {e}"
662+
raise ValueError(err_msg) from e
663+
return model
664+
665+
666+
class EvidenceLineValidatorMixin:
667+
"""Mixin class for reusable EvidenceLine model validators
668+
669+
Should be used with classes that inherit from Pydantic BaseModel
670+
"""
671+
672+
model_config = ConfigDict(extra="allow")
673+
674+
@staticmethod
675+
def _validate_evidence_outcome(
676+
values: dict, system: System, codes: list[str]
677+
) -> dict:
678+
"""Validate ``evidenceOutcome`` property if it exists
679+
680+
:param values: Input values
681+
:param system: System that should be used in ``MappableConcept``
682+
:param codes: Codes that should be used in ``MappableConcept``
683+
:raises ValueError: If ``evidenceOutcome`` exists and is invalid
684+
:return: Validated input values. If ``evidenceOutcome`` exists, then it will be
685+
validated and converted to a ``MappableConcept``
686+
"""
687+
if "evidenceOutcome" in values:
688+
mc = MappableConcept(**values["evidenceOutcome"])
689+
values["evidenceOutcome"] = mc
690+
validate_mappable_concept(mc, system, codes, mc_is_required=False)
691+
return values
692+
693+
@model_validator(mode="after")
694+
def evidence_line_validator(cls, model: BaseModel) -> BaseModel: # noqa: N805
695+
"""Validate that the model is a ``EvidenceLine``.
696+
697+
:param model: Pydantic BaseModel to validate
698+
:raises ValueError: If ``model`` does not validate against a ``EvidenceLine``
699+
:return: Validated model
700+
"""
701+
try:
702+
EvidenceLine(**model.model_dump())
703+
except ValidationError as e:
704+
err_msg = f"Must be an `EvidenceLine`: {e}"
705+
raise ValueError(err_msg) from e
706+
return model

src/ga4gh/va_spec/base/enums.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ class ClinGenClassification(str, Enum):
7575
CLIN_GEN_CLASSIFICATIONS = [v.value for v in ClinGenClassification.__members__.values()]
7676

7777

78+
class CcvClassification(str, Enum):
79+
"""Define constraints for CCV classifications"""
80+
81+
ONCOGENIC = "oncogenic"
82+
LIKELY_ONCOGENIC = "likely oncogenic"
83+
UNCERTAIN_SIGNIFICANCE = "uncertain significance"
84+
LIKELY_BENIGN = "likely benign"
85+
BENIGN = "benign"
86+
87+
88+
CCV_CLASSIFICATIONS = [v.value for v in CcvClassification.__members__.values()]
89+
90+
7891
class System(str, Enum):
7992
"""Define constraints for systems"""
8093

0 commit comments

Comments
 (0)