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 .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.1.0-snapshot.2026-06
branch = 1.1.0-ballot.2026-09
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ keywords = ["bioinformatics", "ga4gh", "genomics", "variation"]
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
"ga4gh.vrs~=2.4.0-a3",
"ga4gh.cat_vrs~=0.8.0-a3",
"pydantic>=2.0,<3.0",
"ga4gh.vrs~=2.4.0-a5",
"ga4gh.cat_vrs~=0.8.0-a5",
"pydantic>=2.12,<3.0",
"typing_extensions",
]

Expand Down
100 changes: 55 additions & 45 deletions src/ga4gh/va_spec/aac_2017/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
from typing_extensions import Self

from ga4gh.core.metadata import Maturity
from ga4gh.core.models import BaseModelForbidExtra, MappableConcept, iriReference
from ga4gh.core.models import (
BaseModelForbidExtra,
MappableConcept,
iriReference,
)
from ga4gh.va_spec.aac_2017.metadata import AAC2017MetadataMixin
from ga4gh.va_spec.base.core import (
Direction,
Expand Down Expand Up @@ -62,14 +66,15 @@ class AmpAscoCapEvidenceLineStrength(str, Enum):


class AmpAscoCapEvidenceLine(AAC2017MetadataMixin, EvidenceLine):
"""Evidence line for AMP/ASCO/CAP"""
"""General evidence line for AMP/ASCO/CAP"""

_maturity: ClassVar[Maturity] = Maturity.DRAFT

targetProposition: (
VariantPrognosticProposition
| VariantDiagnosticProposition
| VariantTherapeuticResponseProposition
| iriReference
)

@field_validator("strengthOfEvidenceProvided", mode="after")
Expand All @@ -90,7 +95,7 @@ def validate_strength_of_evidence_provided(
class _PrognosticEvidenceLineObject(AmpAscoCapEvidenceLine):
"""Internal prognostic evidence line for AMP/ASCO/CAP"""

targetProposition: VariantPrognosticProposition
targetProposition: VariantPrognosticProposition | iriReference


class PrognosticEvidenceLine(
Expand All @@ -104,7 +109,7 @@ class PrognosticEvidenceLine(
class _DiagnosticEvidenceLineObject(AmpAscoCapEvidenceLine):
"""Internal diagnostic evidence line for AMP/ASCO/CAP"""

targetProposition: VariantDiagnosticProposition
targetProposition: VariantDiagnosticProposition | iriReference


class DiagnosticEvidenceLine(
Expand All @@ -118,7 +123,7 @@ class DiagnosticEvidenceLine(
class _TherapeuticEvidenceLineObject(AmpAscoCapEvidenceLine):
"""Internal therapeutic evidence line for AMP/ASCO/CAP"""

targetProposition: VariantTherapeuticResponseProposition
targetProposition: VariantTherapeuticResponseProposition | iriReference


class TherapeuticEvidenceLine(
Expand Down Expand Up @@ -204,59 +209,67 @@ class VariantClinicalSignificanceStatement(

_maturity: ClassVar[Maturity] = Maturity.DRAFT

proposition: VariantClinicalSignificanceProposition
strength: MappableConcept | None = Field(
proposition: VariantClinicalSignificanceProposition | iriReference
strength: MappableConcept | iriReference | None = Field(
default=None,
description="The strength of support that the Statement is determined to provide for or against the Variant Clinical Significance Proposition for the assessed variant, based on the curation and reporting conventions of the AMP/ASCO/CAP 2017 Guidelines.",
)
classification: MappableConcept = Field(
classification: MappableConcept | iriReference = Field(
...,
description="A single term or phrase classifying the subject variant based on the outcome of direction and strength assessments of the Statement's Proposition, using terms from the AMP/ASCO/CAP 2017 Guidelines.",
description="A single term or phrase classifying the subject variant based on the result of direction and strength assessments of the Statement's Proposition, using terms from the AMP/ASCO/CAP 2017 Guidelines.",
)
specifiedBy: Method | iriReference

@model_validator(mode="before")
@classmethod
def validate_tier_evidence_lines(cls, values: dict) -> dict:
"""Validate tier I and II evidence-line types before base coercion."""
if not isinstance(values, dict):
return values

classification = values.get("classification")
if not isinstance(classification, dict):
return values

primary_coding = classification.get("primaryCoding")
if not isinstance(primary_coding, dict) or primary_coding.get("code") not in {
AmpAscoCapClassificationCode.TIER_1,
AmpAscoCapClassificationCode.TIER_2,
}:
return values

approved_el_classes = (
DiagnosticEvidenceLine,
PrognosticEvidenceLine,
TherapeuticEvidenceLine,
iriReference,
)
for evidence_line in values.get("hasEvidenceLines") or []:
for approved_el_cls in approved_el_classes:
try:
approved_el_cls.model_validate(evidence_line)
break
except Exception: # noqa: S112
continue
else:
msg = "`hasEvidenceLines` must be one of: `DiagnosticEvidenceLine`, `PrognosticEvidenceLine`, `TherapeuticEvidenceLine`, or `iriReference`"
raise ValueError(msg)

return values

@model_validator(mode="after")
def validate_statement(self) -> Self:
"""Validate VariantClinicalSignificanceStatement"""

def _validate_evidence_lines(
classification_code: AmpAscoCapClassificationCode,
has_evidence_lines: list,
) -> None:
"""Validate allowed evidence lines given classification code"""
approved_el_classes = [
DiagnosticEvidenceLine,
PrognosticEvidenceLine,
TherapeuticEvidenceLine,
]
if classification_code in {
AmpAscoCapClassificationCode.TIER_1,
AmpAscoCapClassificationCode.TIER_2,
}:
for evidence_line in has_evidence_lines:
if hasattr(evidence_line, "root"):
el_input = evidence_line.root
elif hasattr(evidence_line, "model_dump"):
el_input = evidence_line.model_dump()
else:
el_input = evidence_line

for approved_el_cls in approved_el_classes:
try:
approved_el_cls.model_validate(el_input)
break
except Exception: # noqa: S112
continue
else:
msg = "`hasEvidenceLines` must be one of: `DiagnosticEvidenceLine`, `PrognosticEvidenceLine`, or `TherapeuticEvidenceLine`"
raise ValueError(msg)
if isinstance(self.classification, iriReference) or isinstance(
self.strength, iriReference
):
return self

def _validate_amp_asco_cap_classification_constraints(
classification_code: AmpAscoCapClassificationCode,
classification_name: str | None,
direction: str,
strength_code: MappableConcept | None,
has_evidence_lines: list,
) -> None:
"""Validate that a classification code enforces required values for
strength, name, direction, and when applicable allowed evidence line types.
Expand All @@ -283,8 +296,6 @@ def _validate_amp_asco_cap_classification_constraints(
msg = f"`direction` must be: {expected.direction.value}"
raise ValueError(msg)

_validate_evidence_lines(classification_code, has_evidence_lines)

# Validate strength system. The actual value will be validated in
# `_validate_amp_asco_cap_classification_constraints`
validate_mappable_concept(
Expand All @@ -307,7 +318,6 @@ def _validate_amp_asco_cap_classification_constraints(
self.classification.name,
self.direction,
self.strength,
self.hasEvidenceLines or [],
)

return self
Loading
Loading