Skip to content
Draft
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 .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "submodules/va_spec"]
path = submodules/va_spec
url = https://github.com/ga4gh/va-spec
branch = 1.0.0-ballot.2025-03
branch = issue-319
9 changes: 7 additions & 2 deletions src/ga4gh/va_spec/aac_2017/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def validate_strength(cls, v: MappableConcept | None) -> MappableConcept | None:
:return: Validated strength value
"""
return validate_mappable_concept(
v, System.AMP_ASCO_CAP, AMP_ASCO_CAP_LEVELS, mc_is_required=False
v,
System.AMP_ASCO_CAP,
valid_codes=AMP_ASCO_CAP_LEVELS,
mc_is_required=False,
)

@field_validator("classification")
Expand All @@ -75,7 +78,9 @@ def validate_classification(cls, v: MappableConcept) -> MappableConcept:
:raises ValueError: If invalid classification values are provided
:return: Validated classification value
"""
return validate_mappable_concept(v, System.AMP_ASCO_CAP, AMP_ASCO_CAP_TIERS)
return validate_mappable_concept(
v, System.AMP_ASCO_CAP, valid_codes=AMP_ASCO_CAP_TIERS
)


class VariantDiagnosticStudyStatement(BaseModel, AmpAscoCapValidatorMixin):
Expand Down
8 changes: 2 additions & 6 deletions src/ga4gh/va_spec/acmg_2015/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

from .models import (
ACMG_CLASSIFICATIONS,
EVIDENCE_OUTCOME_VALUES,
AcmgClassification,
EvidenceOutcome,
VariantPathogenicityFunctionalImpactEvidenceLine,
VariantPathogenicityEvidenceLine,
VariantPathogenicityStatement,
)

__all__ = [
"ACMG_CLASSIFICATIONS",
"EVIDENCE_OUTCOME_VALUES",
"AcmgClassification",
"EvidenceOutcome",
"VariantPathogenicityFunctionalImpactEvidenceLine",
"VariantPathogenicityEvidenceLine",
"VariantPathogenicityStatement",
]
89 changes: 45 additions & 44 deletions src/ga4gh/va_spec/acmg_2015/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,6 @@
from pydantic import BaseModel, Field, field_validator, model_validator


class EvidenceOutcome(str, Enum):
"""Define constraints for evidence outcome values"""

PS3 = "PS3"
PS3_MODERATE = "PS3_moderate"
PS3_SUPPORTING = "PS3_supporting"
PS3_NOT_MET = "PS3_not_met"
BS3 = "BS3"
BS3_MODERATE = "BS3_moderate"
BS3_SUPPORTING = "BS3_supporting"
BS3_NOT_MET = "BS3_not_met"


EVIDENCE_OUTCOME_VALUES = [v.value for v in EvidenceOutcome.__members__.values()]


class AcmgClassification(str, Enum):
"""Define constraints for ACMG classifications"""

Expand All @@ -53,27 +37,57 @@ class AcmgClassification(str, Enum):
ACMG_CLASSIFICATIONS = [v.value for v in AcmgClassification.__members__.values()]


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.
class VariantPathogenicityEvidenceLine(BaseModel, EvidenceLineValidatorMixin):
"""An Evidence Line that describes how information about the specific criterion
evidence for the variant was assessed as evidence for or against the variant's
pathogenicity.
"""

targetProposition: VariantPathogenicityProposition | None = Field(
None,
description="A Variant Pathogenicity Proposition against which functional impact information was assessed, in determining the strength and direction of support this information provides as evidence.",
description="A Variant Pathogenicity Proposition against which specific information was assessed, in determining the strength and direction of support this information provides as evidence.",
)
strengthOfEvidenceProvided: MappableConcept | None = Field(
None,
description="The strength of support that an Evidence Line is determined to provide for or against the proposed pathogenicity of the assessed variant. Strength is evaluated relative to the direction indicated by the 'directionOfEvidenceProvided' attribute. The indicated enumeration constrains the nested MappableConcept.primaryCoding > Coding.code attribute when capturing evidence strength. Conditional requirement: if directionOfEvidenceProvided is either 'supports' or 'disputes', then this attribute is required. If it is 'none', then this attribute is not allowed.",
)
specifiedBy: Method | iriReference = Field(
...,
description="The guidelines that were followed to interpret variant functional impact information as evidence for or against the assessed variant's pathogenicity.",
description="The guidelines that were followed to assess variant information as evidence for or against the assessed variant's pathogenicity.",
)

class Criterion(str, Enum):
"""Define ACMG 2015 criterion values"""

PVS1 = "PVS1"
PS1 = "PS1"
PS2 = "PS2"
PS3 = "PS3"
PS4 = "PS4"
PM1 = "PM1"
PM2 = "PM2"
PM3 = "PM3"
PM4 = "PM4"
PM5 = "PM5"
PM6 = "PM6"
PP1 = "PP1"
PP2 = "PP2"
PP3 = "PP3"
PP4 = "PP4"
PP5 = "PP5"
BA1 = "BA1"
BS1 = "BS1"
BS2 = "BS2"
BS3 = "BS3"
BS4 = "BS4"
BP1 = "BP1"
BP2 = "BP2"
BP3 = "BP3"
BP4 = "BP4"
BP5 = "BP5"
BP6 = "BP6"
BP7 = "BP7"

@field_validator("strengthOfEvidenceProvided")
@classmethod
def validate_strength_of_evidence_provided(
Expand All @@ -86,24 +100,12 @@ def validate_strength_of_evidence_provided(
:return: Validated strengthOfEvidenceProvided value
"""
return validate_mappable_concept(
v, System.ACMG, STRENGTH_OF_EVIDENCE_PROVIDED_VALUES, mc_is_required=False
v,
System.ACMG,
valid_codes=STRENGTH_OF_EVIDENCE_PROVIDED_VALUES,
mc_is_required=False,
)

@field_validator("specifiedBy")
@classmethod
def validate_specified_by(cls, v: Method | iriReference) -> Method | iriReference:
"""Validate specifiedBy

:param v: specifiedBy
:raises ValueError: If invalid specifiedBy values are provided
:return: Validated specifiedBy value
"""
if isinstance(v, Method) and not v.reportedIn:
err_msg = "`reportedIn` is required."
raise ValueError(err_msg)

return v

@model_validator(mode="before")
def validate_evidence_outcome(cls, values: dict) -> dict: # noqa: N805
"""Validate ``evidenceOutcome`` property if it exists
Expand All @@ -113,9 +115,8 @@ def validate_evidence_outcome(cls, values: dict) -> dict: # noqa: N805
:return: Validated input values. If ``evidenceOutcome`` exists, then it will be
validated and converted to a ``MappableConcept``
"""
return cls._validate_evidence_outcome(
values, System.ACMG, EVIDENCE_OUTCOME_VALUES
)
acmg_code_pattern = r"^((?:PVS1)(?:_(?:not_met|(?:strong|moderate|supporting)))?|(?:PS[1-4]|BS[1-4])(?:_(?:not_met|(?:very_strong|moderate|supporting)))?|BA1(?:_not_met)?|(?:PM[1-6])(?:_(?:not_met|(?:very_strong|strong|supporting)))?|(PP[1-5]|BP[1-7])(?:_(?:not_met|very_strong|strong|moderate))?)$"
return cls._validate_evidence_outcome(values, System.ACMG, acmg_code_pattern)


class VariantPathogenicityStatement(BaseModel, StatementValidatorMixin):
Expand Down Expand Up @@ -148,7 +149,7 @@ def validate_strength(cls, v: MappableConcept | None) -> MappableConcept | None:
:return: Validated strength value
"""
return validate_mappable_concept(
v, System.ACMG, STRENGTHS, mc_is_required=False
v, System.ACMG, valid_codes=STRENGTHS, mc_is_required=False
)

@field_validator("classification")
Expand Down
2 changes: 2 additions & 0 deletions src/ga4gh/va_spec/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
StudyGroup,
StudyResult,
SubjectVariantProposition,
TumorVariantFrequencyStudyResult,
VariantDiagnosticProposition,
VariantOncogenicityProposition,
VariantPathogenicityProposition,
Expand Down Expand Up @@ -85,4 +86,5 @@
"VariantPathogenicityProposition",
"VariantPrognosticProposition",
"VariantTherapeuticResponseProposition",
"TumorVariantFrequencyStudyResult",
]
77 changes: 68 additions & 9 deletions src/ga4gh/va_spec/base/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,44 @@ class CohortAlleleFrequencyStudyResult(_StudyResult, BaseModelForbidExtra):
)


class TumorVariantFrequencyStudyResult(_StudyResult, BaseModelForbidExtra):
"""A Study Result that reports measures related to the frequency of an variant
across different tumor types.
"""

type: Literal["TumorVariantFrequencyStudyResult"] = Field(
"TumorVariantFrequencyStudyResult",
description="MUST be 'TumorVariantFrequencyStudyResult'.",
)
sourceDataSet: DataSet | None = Field(
None,
description="The dataset from which data in the Tumor Variant Frequency Study Result was taken.",
)
focusVariant: Allele | CategoricalVariant | iriReference = Field(
...,
description="The variant for which frequency data is reported in the Study Result",
)
affectedSampleCount: int = Field(
..., description="The number of tumor samples that contain the focus variant"
)
totalSampleCount: int = Field(
...,
description="The total number of tumor samples included in the dataset",
)
affectedFrequency: float = Field(
...,
description="The frequency of tumor samples that include the focus variant.",
)
sampleGroup: StudyGroup | None = Field(
None,
description="The set of samples about which the frequency data was generated.",
)
subGroupFrequency: list[TumorVariantFrequencyStudyResult] | None = Field(
None,
description="A list of Tumor Variant Frequency Study Result objects describing subsets of the sample group currently being described. Subgroups can be further subdivided into more subcohorts. This enables, for example, the description of frequency data within samples with a narrower categorical variant than the root focus variant, or samples with a specific tumors type",
)


class ExperimentalVariantFunctionalImpactStudyResult(
_StudyResult, BaseModelForbidExtra
):
Expand Down Expand Up @@ -352,7 +390,7 @@ class Method(Entity, BaseModelForbidExtra):
type: Literal["Method"] = Field(
CoreType.METHOD.value, description=f"MUST be '{CoreType.METHOD.value}'."
)
subtype: MappableConcept | None = Field(
methodType: str | None = Field(
None,
description="A specific type of method that a Method instance represents (e.g. 'Variant Interpretation Guideline', or 'Experimental Protocol').",
)
Expand All @@ -374,7 +412,7 @@ class Contribution(Entity, BaseModelForbidExtra):
contributor: Agent | None = Field(
None, description="The agent that made the contribution."
)
activityType: MappableConcept | None = Field(
activityType: str | None = Field(
None,
description="The specific type of activity performed or role played by an agent in making the contribution (e.g. for a publication, agents may contribute as a primary author, editor, figure designer, data generator, etc.). Values of this property may be framed as activities, or as contribution roles (e.g. using terms from the Contribution Role Ontology (CRO)).",
)
Expand All @@ -391,7 +429,7 @@ class Document(Entity, BaseModelForbidExtra):
type: Literal["Document"] = Field(
CoreType.DOCUMENT.value, description=f"Must be '{CoreType.DOCUMENT.value}'"
)
subtype: MappableConcept | None = Field(
documentType: str | None = Field(
None,
description="A specific type of document that a Document instance represents (e.g. 'publication', 'patent', 'pathology report')",
)
Expand Down Expand Up @@ -427,7 +465,7 @@ class Agent(Entity, BaseModelForbidExtra):
CoreType.AGENT.value, description=f"MUST be '{CoreType.AGENT.value}'."
)
name: str | None = Field(None, description="The given name of the Agent.")
subtype: MappableConcept | None = Field(
agentType: str | None = Field(
None,
description="A specific type of agent the Agent object represents. Recommended subtypes include codes for `person`, `organization`, or `software`.",
)
Expand All @@ -451,7 +489,7 @@ class DataSet(Entity, BaseModelForbidExtra):
type: Literal["DataSet"] = Field(
CoreType.DATA_SET.value, description=f"MUST be '{CoreType.DATA_SET.value}'."
)
subtype: MappableConcept | None = Field(
datasetType: str | None = Field(
None,
description="A specific type of data set the DataSet instance represents (e.g. a 'clinical data set', a 'sequencing data set', a 'gene expression data set', a 'genome annotation data set')",
)
Expand Down Expand Up @@ -673,23 +711,44 @@ class EvidenceLineValidatorMixin:

@staticmethod
def _validate_evidence_outcome(
values: dict, system: System, codes: list[str]
values: dict, system: System, code_pattern: 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``
:param system: System that should be used for ``primaryCoding.system``
:param code_pattern: The regex pattern that should be used for
``primaryCoding.code``
: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)
validate_mappable_concept(
mc, system, code_pattern=code_pattern, mc_is_required=False
)
return values

@field_validator("specifiedBy")
@classmethod
def validate_specified_by(cls, v: Method | iriReference) -> Method | iriReference:
"""Validate specifiedBy

:param v: specifiedBy
:raises ValueError: If invalid specifiedBy values are provided
:return: Validated specifiedBy value
"""
if isinstance(v, Method):
if not v.reportedIn:
err_msg = "`reportedIn` is required."
raise ValueError(err_msg)

cls.Criterion(v.methodType)

return v

@model_validator(mode="after")
def evidence_line_validator(cls, model: BaseModel) -> BaseModel: # noqa: N805
"""Validate that the model is a ``EvidenceLine``.
Expand Down
17 changes: 14 additions & 3 deletions src/ga4gh/va_spec/base/validators.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
"""Shared validator functions"""

import re

from ga4gh.core.models import MappableConcept
from ga4gh.va_spec.base.enums import System


def validate_mappable_concept(
mc: MappableConcept | None,
valid_system: System,
valid_codes: list[str],
valid_codes: list[str] | None = None,
code_pattern: str | None = None,
mc_is_required: bool = False,
) -> MappableConcept | None:
"""Validate GKS Core Mappable Concept object

:param mc: Mappable Concept object
:param valid_system: The system that should be used
:param valid_codes: The codes that should be used
:param valid_codes: The codes that should be used for ``primaryCoding.code``
:param code_pattern: The regex pattern that should be used for
``primaryCoding.code``
:param mc_is_required: Whether or not `mc` is required
:raises ValueError: If `mc` is invalid
:return: Validated mappable concept
Expand All @@ -30,8 +35,14 @@ def validate_mappable_concept(
err_msg = f"`primaryCoding.system` must be '{valid_system.value}'."
raise ValueError(err_msg)

if mc.primaryCoding.code.root not in valid_codes:
if valid_codes is not None and mc.primaryCoding.code.root not in valid_codes:
err_msg = f"`primaryCoding.code` must be one of {valid_codes}."
raise ValueError(err_msg)

if code_pattern is not None and not re.match(
code_pattern, mc.primaryCoding.code.root
):
err_msg = f"`primaryCoding.code` does not match regex pattern {code_pattern}"
raise ValueError(err_msg)

return mc
Loading