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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
"ga4gh.vrs==2.*",
"ga4gh.cat_vrs~=0.4.0",
"ga4gh.cat_vrs~=0.5.0",
"pydantic==2.*"
]

Expand Down
16 changes: 10 additions & 6 deletions src/ga4gh/va_spec/aac_2017/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
from ga4gh.core.models import MappableConcept, iriReference
from ga4gh.va_spec.base.core import (
Method,
Statement,
StatementValidatorMixin,
VariantDiagnosticProposition,
VariantPrognosticProposition,
VariantTherapeuticResponseProposition,
)
from ga4gh.va_spec.base.enums import System
from ga4gh.va_spec.base.validators import validate_mappable_concept
from pydantic import (
BaseModel,
Field,
field_validator,
)
Expand Down Expand Up @@ -46,8 +47,11 @@ class Classification(str, Enum):
AMP_ASCO_CAP_TIERS = [v.value for v in Classification.__members__.values()]


class _ValidatorMixin:
"""Mixin class for reusable AMP/ASCO/CAP field validators"""
class AmpAscoCapValidatorMixin(StatementValidatorMixin):
"""Mixin class for reusable AMP/ASCO/CAP field validators

Should be used with classes that inherit from Pydantic BaseModel
"""

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


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


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


class VariantTherapeuticResponseStudyStatement(Statement, _ValidatorMixin):
class VariantTherapeuticResponseStudyStatement(BaseModel, AmpAscoCapValidatorMixin):
"""A statement reporting a conclusion from a single study about whether a variant is
associated with a therapeutic response (positive or negative) - based on
interpretation of the study's results.
Expand Down
39 changes: 21 additions & 18 deletions src/ga4gh/va_spec/acmg_2015/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

from ga4gh.core.models import MappableConcept, iriReference
from ga4gh.va_spec.base.core import (
EvidenceLine,
EvidenceLineValidatorMixin,
Method,
Statement,
StatementValidatorMixin,
VariantPathogenicityProposition,
)
from ga4gh.va_spec.base.enums import (
Expand All @@ -18,8 +18,10 @@
STRENGTHS,
System,
)
from ga4gh.va_spec.base.validators import validate_mappable_concept
from pydantic import Field, field_validator
from ga4gh.va_spec.base.validators import (
validate_mappable_concept,
)
from pydantic import BaseModel, Field, field_validator, model_validator


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


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

return v

@field_validator("evidenceOutcome")
@classmethod
def validate_evidence_outcome(
cls, v: MappableConcept | None
) -> MappableConcept | None:
"""Validate evidenceOutcome
@model_validator(mode="before")
def validate_evidence_outcome(cls, values: dict) -> dict: # noqa: N805
"""Validate ``evidenceOutcome`` property if it exists

:param v: evidenceOutcome
:raises ValueError: If invalid evidenceOutcome values are provided
:return: Validated evidenceOutcome value
:param values: Input values
:raises ValueError: If ``evidenceOutcome`` exists and is invalid
:return: Validated input values. If ``evidenceOutcome`` exists, then it will be
validated and converted to a ``MappableConcept``
"""
return validate_mappable_concept(
v, System.ACMG, EVIDENCE_OUTCOME_VALUES, mc_is_required=False
return cls._validate_evidence_outcome(
values, System.ACMG, EVIDENCE_OUTCOME_VALUES
)


class VariantPathogenicityStatement(Statement):
class VariantPathogenicityStatement(BaseModel, StatementValidatorMixin):
"""A Statement describing the role of a variant in causing an inherited condition."""

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

supported_systems = [System.ACMG.value, System.ACMG.value]
supported_systems = [System.ACMG.value, System.CLIN_GEN.value]
if v.primaryCoding.system not in supported_systems:
err_msg = f"`primaryCoding.system` must be one of: {supported_systems}."
raise ValueError(err_msg)

if v.primaryCoding.system == System.ACMG:
if v.primaryCoding.code.root not in ACMG_CLASSIFICATIONS:
Expand Down
4 changes: 4 additions & 0 deletions src/ga4gh/va_spec/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
)
from .domain_entities import Condition, ConditionSet, Therapeutic, TherapyGroup
from .enums import (
CCV_CLASSIFICATIONS,
CLIN_GEN_CLASSIFICATIONS,
STRENGTH_OF_EVIDENCE_PROVIDED_VALUES,
STRENGTHS,
CcvClassification,
ClinGenClassification,
DiagnosticPredicate,
MembershipOperator,
Expand All @@ -42,7 +44,9 @@

__all__ = [
"Agent",
"CCV_CLASSIFICATIONS",
"CLIN_GEN_CLASSIFICATIONS",
"CcvClassification",
"ClinGenClassification",
"ClinGenClassification",
"ClinicalVariantProposition",
Expand Down
104 changes: 92 additions & 12 deletions src/ga4gh/va_spec/base/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@
from ga4gh.va_spec.base.enums import (
DiagnosticPredicate,
PrognosticPredicate,
System,
TherapeuticResponsePredicate,
)
from ga4gh.va_spec.base.validators import validate_mappable_concept
from ga4gh.vrs.models import Allele, MolecularVariation
from pydantic import (
BaseModel,
ConfigDict,
Field,
RootModel,
StringConstraints,
ValidationError,
field_validator,
model_validator,
)

StatementType = TypeVar("StatementType", bound="Statement")
StatementType = TypeVar("StatementType")
EvidenceLineType = TypeVar("EvidenceLineType")

#########################################
# Abstract Core Classes
Expand Down Expand Up @@ -484,7 +489,7 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra):
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.",
)
hasEvidenceItems: (
list[StudyResult | StatementType | EvidenceLine | iriReference] | None
list[StudyResult | StatementType | EvidenceLineType | iriReference] | None
) = Field(
None,
description="An individual piece of information that was evaluated as evidence in building the argument represented by an Evidence Line.",
Expand All @@ -509,7 +514,7 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra):
@field_validator("hasEvidenceItems", mode="before")
def validate_has_evidence_items(
cls, # noqa: N805
v: list[StudyResult, StatementType, EvidenceLine, iriReference] | None,
v: list | None,
) -> list | None:
"""Ensure hasEvidenceItems is correct type

Expand Down Expand Up @@ -539,15 +544,12 @@ def validate_has_evidence_items(
obj_
for _, obj_ in vars(imported_module).items()
if inspect.isclass(obj_)
and issubclass(obj_, Statement)
and obj_ is not Statement
and issubclass(obj_, BaseModel)
and obj_.__name__.endswith(("Statement", "EvidenceLine"))
]
)

has_evidence_items_models.extend(
[Statement, StudyResult, EvidenceLine, iriReference]
)

has_evidence_items_models.extend([Statement, StudyResult, EvidenceLine])
for evidence_item in v:
if isinstance(evidence_item, dict):
found_model = False
Expand All @@ -561,10 +563,13 @@ def validate_has_evidence_items(
found_model = True
break
if not found_model:
err_msg = "Unable to find valid model"
err_msg = "Unable to find valid model for `hasEvidenceItems`"
raise ValueError(err_msg)
elif isinstance(evidence_item, str):
evidence_items.append(iriReference(root=evidence_item))
else:
evidence_items.append(evidence_item)
err_msg = "Unable to find valid model for `hasEvidenceItems`"
raise ValueError(err_msg)
return evidence_items


Expand All @@ -580,9 +585,17 @@ class Statement(InformationEntity, BaseModelForbidExtra):
type: Literal["Statement"] = Field(
CoreType.STATEMENT.value, description=f"MUST be '{CoreType.STATEMENT.value}'."
)
proposition: Proposition = Field(
proposition: (
ExperimentalVariantFunctionalImpactProposition
| VariantDiagnosticProposition
| VariantOncogenicityProposition
| VariantPathogenicityProposition
| VariantPrognosticProposition
| VariantTherapeuticResponseProposition
) = Field(
...,
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.",
discriminator="type",
)
direction: Direction = Field(
...,
Expand Down Expand Up @@ -624,3 +637,70 @@ class StudyGroup(Entity, BaseModelForbidExtra):
None,
description="A feature or role shared by all members of the StudyGroup, representing a criterion for membership in the group.",
)


class StatementValidatorMixin:
"""Mixin class for reusable Statement model validators

Should be used with classes that inherit from Pydantic BaseModel
"""

model_config = ConfigDict(extra="allow")

@model_validator(mode="after")
def statement_validator(cls, model: BaseModel) -> BaseModel: # noqa: N805
"""Validate that the model is a ``Statement``.

:param model: Pydantic BaseModel to validate
:raises ValueError: If ``model`` does not validate against a ``Statement``
:return: Validated model
"""
try:
Statement(**model.model_dump())
except ValidationError as e:
err_msg = f"Must be a `Statement`: {e}"
raise ValueError(err_msg) from e
return model


class EvidenceLineValidatorMixin:
"""Mixin class for reusable EvidenceLine model validators

Should be used with classes that inherit from Pydantic BaseModel
"""

model_config = ConfigDict(extra="allow")

@staticmethod
def _validate_evidence_outcome(
values: dict, system: System, codes: list[str]
) -> dict:
"""Validate ``evidenceOutcome`` property if it exists

:param values: Input values
:param system: System that should be used in ``MappableConcept``
:param codes: Codes that should be used in ``MappableConcept``
:raises ValueError: If ``evidenceOutcome`` exists and is invalid
:return: Validated input values. If ``evidenceOutcome`` exists, then it will be
validated and converted to a ``MappableConcept``
"""
if "evidenceOutcome" in values:
mc = MappableConcept(**values["evidenceOutcome"])
values["evidenceOutcome"] = mc
validate_mappable_concept(mc, system, codes, mc_is_required=False)
return values

@model_validator(mode="after")
def evidence_line_validator(cls, model: BaseModel) -> BaseModel: # noqa: N805
"""Validate that the model is a ``EvidenceLine``.

:param model: Pydantic BaseModel to validate
:raises ValueError: If ``model`` does not validate against a ``EvidenceLine``
:return: Validated model
"""
try:
EvidenceLine(**model.model_dump())
except ValidationError as e:
err_msg = f"Must be an `EvidenceLine`: {e}"
raise ValueError(err_msg) from e
return model
13 changes: 13 additions & 0 deletions src/ga4gh/va_spec/base/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ class ClinGenClassification(str, Enum):
CLIN_GEN_CLASSIFICATIONS = [v.value for v in ClinGenClassification.__members__.values()]


class CcvClassification(str, Enum):
"""Define constraints for CCV classifications"""

ONCOGENIC = "oncogenic"
LIKELY_ONCOGENIC = "likely oncogenic"
UNCERTAIN_SIGNIFICANCE = "uncertain significance"
LIKELY_BENIGN = "likely benign"
BENIGN = "benign"


CCV_CLASSIFICATIONS = [v.value for v in CcvClassification.__members__.values()]


class System(str, Enum):
"""Define constraints for systems"""

Expand Down
Loading